summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--config/preloaded-classes-denylist1
-rw-r--r--core/api/current.txt2
-rw-r--r--core/api/test-current.txt4
-rw-r--r--core/java/android/app/ActivityManagerInternal.java9
-rw-r--r--core/java/android/app/ActivityThread.java2
-rw-r--r--core/java/android/app/KeyguardManager.java3
-rw-r--r--core/java/android/app/LoadedApk.java4
-rw-r--r--core/java/android/app/Notification.java36
-rw-r--r--core/java/android/app/NotificationManager.java170
-rw-r--r--core/java/android/app/admin/DevicePolicyManager.java18
-rw-r--r--core/java/android/app/backup/BackupManagerInternal.java40
-rw-r--r--core/java/android/app/backup/IBackupManager.aidl32
-rw-r--r--core/java/android/companion/CompanionDeviceService.java33
-rw-r--r--core/java/android/companion/virtual/VirtualDeviceInternal.java8
-rw-r--r--core/java/android/companion/virtual/VirtualDeviceManager.java5
-rw-r--r--core/java/android/content/ClipData.java16
-rw-r--r--core/java/android/content/Intent.java62
-rw-r--r--core/java/android/content/pm/PackageManager.java2
-rw-r--r--core/java/android/content/res/ApkAssets.java58
-rw-r--r--core/java/android/content/res/ResourceTimer.java56
-rw-r--r--core/java/android/hardware/contexthub/HubEndpoint.java23
-rw-r--r--core/java/android/hardware/contexthub/IContextHubEndpoint.aidl6
-rw-r--r--core/java/android/hardware/fingerprint/FingerprintSensorPropertiesInternal.java9
-rw-r--r--core/java/android/hardware/input/IInputManager.aidl5
-rw-r--r--core/java/android/hardware/input/InputGestureData.java121
-rw-r--r--core/java/android/hardware/input/InputManager.java24
-rw-r--r--core/java/android/hardware/input/KeyGestureEvent.java26
-rw-r--r--core/java/android/os/Debug.java8
-rw-r--r--core/java/android/os/GraphicsEnvironment.java4
-rw-r--r--core/java/android/os/Parcel.java8
-rw-r--r--core/java/android/os/PowerManager.java1
-rw-r--r--core/java/android/os/ServiceManager.java2
-rw-r--r--core/java/android/os/ServiceManagerNative.java15
-rw-r--r--core/java/android/os/flags.aconfig8
-rw-r--r--core/java/android/provider/Settings.java9
-rw-r--r--core/java/android/security/flags.aconfig10
-rw-r--r--core/java/android/service/dreams/flags.aconfig7
-rw-r--r--core/java/android/text/Layout.java15
-rw-r--r--core/java/android/view/DragEvent.java30
-rw-r--r--core/java/android/view/InputEventReceiver.java2
-rw-r--r--core/java/android/view/PointerIcon.java12
-rw-r--r--core/java/android/view/ViewRootImpl.java10
-rw-r--r--core/java/android/view/accessibility/AccessibilityManager.java3
-rw-r--r--core/java/android/widget/TextView.java28
-rw-r--r--core/java/android/window/DisplayAreaOrganizer.java8
-rw-r--r--core/java/android/window/flags/windowing_frontend.aconfig11
-rw-r--r--core/java/android/window/flags/windowing_sdk.aconfig11
-rw-r--r--core/java/com/android/internal/os/BatteryStatsHistory.java131
-rw-r--r--core/java/com/android/internal/security/VerityUtils.java17
-rw-r--r--core/java/com/android/internal/statusbar/IStatusBarService.aidl3
-rw-r--r--core/java/com/android/internal/widget/LockPatternUtils.java52
-rw-r--r--core/java/com/android/internal/widget/LockscreenCredential.java4
-rw-r--r--core/java/com/android/internal/widget/NotificationExpandButton.java48
-rw-r--r--core/java/com/android/internal/widget/NotificationProgressBar.java631
-rw-r--r--core/java/com/android/internal/widget/NotificationProgressDrawable.java389
-rw-r--r--core/jni/android_content_res_ApkAssets.cpp66
-rw-r--r--core/jni/android_hardware_UsbDeviceConnection.cpp22
-rw-r--r--core/jni/android_os_Debug.cpp12
-rw-r--r--core/jni/android_view_InputEventReceiver.cpp5
-rw-r--r--core/proto/android/providers/settings/secure.proto1
-rw-r--r--core/res/res/drawable/notification_progress.xml1
-rw-r--r--core/res/res/values-round-watch/dimens.xml2
-rw-r--r--core/res/res/values/attrs.xml22
-rw-r--r--core/res/res/values/config.xml3
-rw-r--r--core/res/res/values/dimens.xml2
-rw-r--r--core/res/res/values/strings.xml2
-rw-r--r--core/res/res/values/symbols.xml3
-rw-r--r--core/res/res/xml/sms_short_codes.xml2
-rw-r--r--core/tests/coretests/src/android/app/NotificationManagerTest.java213
-rw-r--r--core/tests/coretests/src/android/app/NotificationTest.java77
-rw-r--r--core/tests/coretests/src/android/os/OWNERS3
-rw-r--r--core/tests/coretests/src/android/os/ParcelTest.java6
-rw-r--r--core/tests/coretests/src/com/android/internal/widget/NotificationProgressBarTest.java603
-rw-r--r--graphics/java/android/graphics/Paint.java125
-rw-r--r--graphics/java/android/graphics/fonts/FontVariationAxis.java17
-rw-r--r--keystore/java/android/security/keystore/KeyStoreManager.java6
-rw-r--r--libs/WindowManager/Shell/AndroidManifest.xml1
-rw-r--r--libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java10
-rw-r--r--libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeTransitionSource.kt9
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/animation/SizeChangeAnimation.java288
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/appzoomout/AppZoomOut.java33
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/appzoomout/AppZoomOutController.java162
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/appzoomout/AppZoomOutDisplayAreaOrganizer.java157
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayController.java30
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayLayout.java70
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMComponent.java4
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java42
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java21
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java13
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeDragAndDropTransitionHandler.kt5
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt14
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeShellCommandHandler.kt5
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt68
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTaskPosition.kt40
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt111
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt5
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt63
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt10
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ToggleResizeDesktopTaskTransitionHandler.kt10
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/desktopwallpaperactivity/DesktopWallpaperActivityTokenProvider.kt14
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragUtils.java5
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java23
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java38
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentsAnimationRunner.aidl6
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java20
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java47
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultSurfaceAnimator.java89
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java26
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java51
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java31
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java2
-rw-r--r--libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientation.kt1
-rw-r--r--libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/nonmatchparent/BottomHalfEnterPipToOtherOrientation.kt1
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/appzoomout/AppZoomOutControllerTest.java87
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayControllerTests.java5
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayLayoutTest.java36
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeEventLoggerTest.kt539
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt62
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt125
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt138
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt5
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropControllerTest.java5
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/GlobalDragListenerTest.kt6
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipSchedulerTest.java13
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentsTransitionHandlerTest.java4
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt54
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTestsBase.kt15
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java56
-rw-r--r--libs/androidfw/ApkAssets.cpp9
-rw-r--r--libs/androidfw/AssetsProvider.cpp82
-rw-r--r--libs/androidfw/Idmap.cpp21
-rw-r--r--libs/androidfw/ResourceTypes.cpp78
-rw-r--r--libs/androidfw/Util.cpp25
-rw-r--r--libs/androidfw/ZipUtils.cpp34
-rw-r--r--libs/androidfw/include/androidfw/ApkAssets.h2
-rw-r--r--libs/androidfw/include/androidfw/AssetsProvider.h25
-rw-r--r--libs/androidfw/include/androidfw/Idmap.h32
-rw-r--r--libs/androidfw/include/androidfw/ResourceTypes.h48
-rw-r--r--libs/androidfw/include/androidfw/misc.h6
-rw-r--r--libs/androidfw/misc.cpp69
-rw-r--r--libs/androidfw/tests/Idmap_test.cpp27
-rw-r--r--libs/appfunctions/api/current.txt3
-rw-r--r--libs/appfunctions/java/com/android/extensions/appfunctions/AppFunctionService.java44
-rw-r--r--libs/hwui/aconfig/hwui_flags.aconfig7
-rw-r--r--libs/hwui/hwui/DrawTextFunctor.h16
-rw-r--r--libs/hwui/hwui/MinikinUtils.cpp8
-rw-r--r--libs/hwui/hwui/Typeface.cpp83
-rw-r--r--libs/hwui/hwui/Typeface.h30
-rw-r--r--libs/hwui/jni/Paint.cpp5
-rw-r--r--libs/hwui/jni/Typeface.cpp40
-rw-r--r--libs/hwui/jni/text/TextShaper.cpp2
-rw-r--r--libs/hwui/renderthread/VulkanManager.cpp4
-rw-r--r--libs/hwui/tests/common/TestUtils.cpp7
-rw-r--r--libs/hwui/tests/unit/TypefaceTests.cpp252
-rw-r--r--media/TEST_MAPPING5
-rw-r--r--media/java/android/media/IAudioService.aidl2
-rw-r--r--media/java/android/media/MediaRoute2Info.java3
-rw-r--r--media/java/android/media/MediaRouter2.java4
-rw-r--r--media/java/android/media/MediaRouter2Manager.java4
-rw-r--r--media/java/android/media/flags/media_better_together.aconfig10
-rw-r--r--mime/Android.bp37
-rw-r--r--mime/jarjar-rules-alt.txt1
-rw-r--r--mime/jarjar-rules.txt2
-rw-r--r--native/android/tests/system_health/OWNERS1
-rw-r--r--packages/FusedLocation/src/com/android/location/fused/FusedLocationProvider.java16
-rw-r--r--packages/InputDevices/res/raw/keyboard_layout_romanian.kcm93
-rw-r--r--packages/InputDevices/res/raw/keyboard_layout_serbian_and_montenegrin_cyrillic.kcm95
-rw-r--r--packages/InputDevices/res/raw/keyboard_layout_serbian_and_montenegrin_latin.kcm95
-rw-r--r--packages/PrintSpooler/Android.bp15
-rw-r--r--packages/PrintSpooler/flags/flags.aconfig9
-rw-r--r--packages/PrintSpooler/src/com/android/printspooler/model/PrintSpoolerService.java7
-rw-r--r--packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/DataChangeReason.kt2
-rw-r--r--packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGetterApi.kt1
-rw-r--r--packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGetterCodecs.kt1
-rw-r--r--packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceSetterApi.kt4
-rw-r--r--packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PersistentPreference.kt18
-rw-r--r--packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceCoordinate.kt (renamed from packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceCoordinate.kt)4
-rw-r--r--packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenBindingHelper.kt23
-rw-r--r--packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListRepository.kt6
-rw-r--r--packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppListRepositoryTest.kt20
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/applications/AppUtils.java1
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java21
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidDeviceManager.java8
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/bluetooth/LeAudioProfile.java74
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/dream/DreamBackend.java58
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/enterprise/ActionDisabledByAdminControllerFactory.java19
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/notification/data/repository/FakeZenModeRepository.kt10
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/notification/modes/TestModeBuilder.java18
-rw-r--r--packages/SettingsLib/tests/robotests/src/com/android/settingslib/applications/ApplicationsStateRoboTest.java20
-rw-r--r--packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HearingAidDeviceManagerTest.java39
-rw-r--r--packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/modes/ZenModeTest.java50
-rw-r--r--packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java1
-rw-r--r--packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java1
-rw-r--r--packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupAgent.java27
-rw-r--r--packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java3
-rw-r--r--packages/SettingsProvider/test/src/com/android/providers/settings/SettingsBackupAgentTest.java72
-rw-r--r--packages/SystemUI/Android.bp1
-rw-r--r--packages/SystemUI/aconfig/systemui.aconfig45
-rw-r--r--packages/SystemUI/compose/core/src/com/android/compose/gesture/NestedDraggable.kt139
-rw-r--r--packages/SystemUI/compose/core/tests/AndroidManifest.xml4
-rw-r--r--packages/SystemUI/compose/core/tests/src/com/android/compose/gesture/NestedDraggableTest.kt240
-rw-r--r--packages/SystemUI/compose/core/tests/src/com/android/compose/gesture/TestOverscrollEffect.kt20
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt14
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt4
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromDreamToCommunalTransition.kt9
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToSplitShadeTransition.kt2
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToCommunalTransition.kt13
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToSplitShadeTransition.kt2
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromNotificationsShadeToQuickSettingsShadeTransition.kt2
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToNotificationsShadeTransition.kt2
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToQuickSettingsShadeTransition.kt2
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToShadeTransition.kt2
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt52
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt14
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeAnimation.kt8
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt100
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt6
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt12
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/state/TransitionState.kt4
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/reveal/ContainerReveal.kt2
-rw-r--r--packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt109
-rw-r--r--packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt24
-rw-r--r--packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt204
-rw-r--r--packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/TransitionDslTest.kt10
-rw-r--r--packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockController.kt1
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/DragToInteractAnimationControllerTest.java2
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegateTest.java2
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandlerTest.java2
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/MenuViewTest.java2
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/utils/TestUtils.java5
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractorTest.kt126
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingRepositoryTest.kt22
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractorTest.kt44
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorTest.kt28
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorParameterizedTest.kt (renamed from packages/SystemUI/tests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorParameterizedTest.kt)103
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorTest.kt (renamed from packages/SystemUI/tests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorTest.kt)0
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/source/TestShortcuts.kt9
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutCustomizationViewModelTest.kt46
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/DoNotDisturbQuickAffordanceConfigTest.kt13
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorTest.kt7
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToPrimaryBouncerTransitionViewModelTest.kt44
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModelTest.kt44
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/EditModeButtonViewModelTest.kt14
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/InternetTileNewImplTest.kt45
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/ScreenRecordTileTest.java34
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractorTest.kt6
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt46
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt27
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java9
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractorTest.kt20
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeExpandedStateInteractorTest.kt77
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java8
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationShadeDepthControllerTest.kt4
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/StatusBarSignalPolicyTest.kt21
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt92
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt97
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/layout/StatusBarContentInsetsProviderTest.kt2
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/layout/ui/viewmodel/StatusBarContentInsetsViewModelTest.kt77
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt35
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractorTest.kt49
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerTest.java2
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt14
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java26
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.kt7
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerTest.kt (renamed from packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerViaRepoTest.kt)30
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerViaListenerTest.kt694
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/FakeHomeStatusBarViewModel.kt6
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelImplTest.kt31
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt22
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModelTest.kt181
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/window/StatusBarWindowControllerImplTest.kt8
-rw-r--r--packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockConfig.kt4
-rw-r--r--packages/SystemUI/plugin_core/processor/src/com/android/systemui/plugins/processor/ProtectedPluginProcessor.kt12
-rw-r--r--packages/SystemUI/res/drawable/ic_media_connecting_status_container.xml199
-rw-r--r--packages/SystemUI/res/drawable/ic_media_pause_button.xml135
-rw-r--r--packages/SystemUI/res/drawable/ic_media_pause_button_container.xml135
-rw-r--r--packages/SystemUI/res/drawable/ic_media_play_button.xml124
-rw-r--r--packages/SystemUI/res/drawable/ic_media_play_button_container.xml135
-rw-r--r--packages/SystemUI/res/layout/volume_dialog.xml1
-rw-r--r--packages/SystemUI/res/layout/volume_ringer_button.xml5
-rw-r--r--packages/SystemUI/res/values/dimens.xml1
-rw-r--r--packages/SystemUI/res/values/strings.xml3
-rw-r--r--packages/SystemUI/shared/src/com/android/systemui/shared/system/RecentsAnimationListener.java3
-rw-r--r--packages/SystemUI/src/com/android/keyguard/KeyguardDisplayManager.java34
-rw-r--r--packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java9
-rw-r--r--packages/SystemUI/src/com/android/systemui/SystemUIInitializer.java6
-rw-r--r--packages/SystemUI/src/com/android/systemui/accessibility/FullscreenMagnificationController.java207
-rw-r--r--packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuController.java4
-rw-r--r--packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuInfoRepository.java20
-rw-r--r--packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerController.java4
-rw-r--r--packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java10
-rw-r--r--packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorImpl.kt25
-rw-r--r--packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractor.kt17
-rw-r--r--packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingRepository.kt13
-rw-r--r--packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegate.kt38
-rw-r--r--packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogUiEvent.kt8
-rw-r--r--packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModel.kt35
-rw-r--r--packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItem.kt3
-rw-r--r--packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractor.kt46
-rw-r--r--packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactory.kt6
-rw-r--r--packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalTransitionKeys.kt1
-rw-r--r--packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java4
-rw-r--r--packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractor.kt21
-rw-r--r--packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractor.kt10
-rw-r--r--packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt23
-rw-r--r--packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java3
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/InputGestureMaps.kt5
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/source/SystemShortcutsSource.kt19
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/ShortcutCustomizationDialogStarter.kt8
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutCustomizer.kt92
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutCustomizationViewModel.kt23
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt8
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/BlurConfig.kt6
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/PrimaryBouncerTransition.kt3
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToPrimaryBouncerTransitionViewModel.kt25
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToPrimaryBouncerTransitionViewModel.kt3
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DozingToPrimaryBouncerTransitionViewModel.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToPrimaryBouncerTransitionViewModel.kt3
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModel.kt26
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToPrimaryBouncerTransitionViewModel.kt3
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToAodTransitionViewModel.kt3
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToDozingTransitionViewModel.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGlanceableHubTransitionViewModel.kt3
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModel.kt3
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModel.kt3
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToOccludedTransitionViewModel.kt3
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/Media3ActionFactory.kt31
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaActions.kt31
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaViewController.kt15
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/toolbar/EditModeButtonViewModel.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenRecordTile.java110
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/ScreenRecordDetailsViewModel.kt61
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt9
-rw-r--r--packages/SystemUI/src/com/android/systemui/scene/data/repository/SceneContainerRepository.kt37
-rw-r--r--packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt59
-rw-r--r--packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt32
-rw-r--r--packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java10
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayAwareModule.kt7
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayChangeLatencyTracker.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/ShadeExpansionStateManager.kt16
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt8
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/ShadeStateTraceLogger.kt70
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/ShadeTraceLogger.kt9
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/StartShadeModule.kt5
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/domain/interactor/FakeShadeExpandedStateInteractor.kt33
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractor.kt17
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeExpandedStateInteractor.kt118
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/LightRevealScrim.kt7
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java7
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt17
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java22
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java3
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModel.kt11
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModel.kt11
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/binder/OngoingActivityChipBinder.kt106
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/binder/OngoingActivityChipViewBinding.kt34
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChip.kt208
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChips.kt47
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/OngoingActivityChipModel.kt5
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/view/ChipChronometer.kt23
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarModule.kt19
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/layout/ui/viewmodel/StatusBarContentInsetsViewModel.kt51
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/layout/ui/viewmodel/StatusBarContentInsetsViewModelStore.kt99
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt25
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorLogger.kt9
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractor.kt55
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationListInteractor.kt9
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/model/TopPinnedState.kt36
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconManager.kt6
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationLogger.java6
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ChannelEditorListView.kt61
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java7
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/MagicActionBackgroundDrawable.kt55
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java12
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java12
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt11
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java3
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java18
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationStatsLoggerImpl.kt6
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt10
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java2
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java8
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimState.java6
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java10
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt134
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/StatusBarUseReposForCallChip.kt53
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt8
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/HomeStatusBarViewBinder.kt60
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/StatusBarOperatorNameViewBinder.kt7
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/composable/StatusBarRoot.kt48
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModel.kt66
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyStateInflater.kt11
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/ui/StatusBarUiLayerModule.kt23
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModel.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/util/kotlin/Utils.kt25
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/binder/VolumeDialogRingerViewBinder.kt64
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogViewBinder.kt1
-rw-r--r--packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt29
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/accessibility/FullscreenMagnificationControllerTest.java237
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationControllerTest.java2
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java2
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorTest.kt25
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegateTest.kt109
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactoryTest.kt2
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/IconManagerTest.kt25
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java14
-rw-r--r--packages/SystemUI/tests/utils/src/android/internal/statusbar/FakeStatusBarService.kt2
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/animation/ActivityTransitionAnimatorKosmos.kt4
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/FakeAudioSharingRepository.kt11
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorKosmos.kt3
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/FakeDisplayRepository.kt3
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorFactory.kt6
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorKosmos.kt2
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/transitions/FakeBouncerTransition.kt1
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/GeneralKosmos.kt4
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/toolbar/EditModeButtonViewModelKosmos.kt7
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/startable/SceneContainerStartableKosmos.kt2
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeTestUtil.kt1
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractorKosmos.kt13
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeInteractorKosmos.kt6
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/layout/StatusBarContentInsetsProviderKosmos.kt17
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/layout/ui/viewmodel/StatusBarContentInsetsViewModelKosmos.kt35
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt2
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelKosmos.kt4
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/ConfigurationControllerKosmos.kt3
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/data/repository/ZenModeRepositoryKosmos.kt2
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/volume/MediaOutputKosmos.kt2
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/AudioRepositoryKosmos.kt2
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/VolumeDialogKosmos.kt31
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/dagger/VolumeDialogComponentKosmos.kt41
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/data/repository/VolumeDialogVisibilityRepositoryKosmos.kt2
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogCallbacksInteractorKosmos.kt4
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/VolumeDialogRingerViewBinderKosmos.kt24
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/data/repository/VolumeDialogRingerFeedbackRepositoryKosmos.kt2
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/domain/VolumeDialogRingerInteractorKosmos.kt4
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/settings/domain/VolumeDialogSettingsButtonInteractorKosmos.kt33
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/settings/ui/binder/VolumeDialogSettingsButtonViewBinderKosmos.kt23
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/settings/ui/viewmodel/VolumeDialogSettingsButtonViewModelKosmos.kt37
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/dagger/VolumeDialogSliderComponentKosmos.kt84
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogOverscrollViewBinderKosmos.kt23
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderHapticsViewBinderKosmos.kt33
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinderKosmos.kt29
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSlidersViewBinderKosmos.kt23
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogOverscrollViewModelKosmos.kt26
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderIconProviderKosmos.kt31
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderInputEventsViewModelKosmos.kt29
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderViewModelKosmos.kt34
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSlidersViewModelKosmos.kt31
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogViewBinderKosmos.kt39
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ui/utils/JankListenerFactoryKosmos.kt22
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ui/viewmodel/VolumeDialogViewModelKosmos.kt37
-rw-r--r--packages/Vcn/service-b/Android.bp4
-rw-r--r--packages/Vcn/service-b/service-utils/android/util/LocalLog.java148
-rw-r--r--packages/Vcn/service-b/service-utils/com/android/internal/util/WakeupMessage.java142
-rw-r--r--packages/Vcn/service-b/service-vcn-platform-jarjar-rules.txt3
-rw-r--r--ravenwood/Android.bp35
-rw-r--r--ravenwood/CleanSpec.mk45
-rwxr-xr-xravenwood/tools/hoststubgen/scripts/dump-jar2
-rw-r--r--ravenwood/tools/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt8
-rw-r--r--ravenwood/tools/hoststubgen/src/com/android/hoststubgen/filters/TextFileFilterPolicyParser.kt373
-rwxr-xr-xravenwood/tools/hoststubgen/test-tiny-framework/diff-and-update-golden.sh7
-rwxr-xr-xravenwood/tools/hoststubgen/test-tiny-framework/tiny-framework-dump-test.py5
-rw-r--r--services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java6
-rw-r--r--services/backup/java/com/android/server/backup/BackupManagerService.java69
-rw-r--r--services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java2
-rw-r--r--services/core/java/com/android/server/BinaryTransparencyService.java6
-rw-r--r--services/core/java/com/android/server/am/ActivityManagerService.java85
-rw-r--r--services/core/java/com/android/server/am/AppProfiler.java6
-rw-r--r--services/core/java/com/android/server/am/OomAdjuster.java14
-rw-r--r--services/core/java/com/android/server/am/ProcessStateController.java37
-rw-r--r--services/core/java/com/android/server/am/UserController.java85
-rw-r--r--services/core/java/com/android/server/audio/AudioService.java21
-rw-r--r--services/core/java/com/android/server/input/InputGestureManager.java25
-rw-r--r--services/core/java/com/android/server/input/InputManagerService.java10
-rw-r--r--services/core/java/com/android/server/input/KeyGestureController.java74
-rw-r--r--services/core/java/com/android/server/inputmethod/ImeVisibilityStateComputer.java5
-rw-r--r--services/core/java/com/android/server/location/contexthub/ContextHubEndpointBroker.java176
-rw-r--r--services/core/java/com/android/server/location/fudger/LocationFudger.java11
-rw-r--r--services/core/java/com/android/server/locksettings/LockSettingsService.java12
-rw-r--r--services/core/java/com/android/server/locksettings/UnifiedProfilePasswordCache.java5
-rw-r--r--services/core/java/com/android/server/locksettings/recoverablekeystore/KeySyncTask.java4
-rw-r--r--services/core/java/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManager.java2
-rw-r--r--services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverySessionStorage.java7
-rw-r--r--services/core/java/com/android/server/media/MediaRouterService.java8
-rw-r--r--services/core/java/com/android/server/media/TEST_MAPPING5
-rw-r--r--services/core/java/com/android/server/media/quality/MediaQualityService.java68
-rw-r--r--services/core/java/com/android/server/notification/GroupHelper.java34
-rw-r--r--services/core/java/com/android/server/notification/NotificationDelegate.java5
-rw-r--r--services/core/java/com/android/server/notification/NotificationManagerService.java35
-rw-r--r--services/core/java/com/android/server/notification/NotificationRecord.java81
-rw-r--r--services/core/java/com/android/server/notification/PreferencesHelper.java97
-rw-r--r--services/core/java/com/android/server/notification/flags.aconfig10
-rw-r--r--services/core/java/com/android/server/pm/InstallPackageHelper.java75
-rw-r--r--services/core/java/com/android/server/pm/PackageInstallerSession.java81
-rw-r--r--services/core/java/com/android/server/pm/PackageManagerServiceUtils.java16
-rw-r--r--services/core/java/com/android/server/pm/UserManagerService.java3
-rw-r--r--services/core/java/com/android/server/power/PowerManagerService.java18
-rw-r--r--services/core/java/com/android/server/power/ThermalManagerService.java313
-rw-r--r--services/core/java/com/android/server/power/feature/PowerManagerFlags.java10
-rw-r--r--services/core/java/com/android/server/power/feature/power_flags.aconfig11
-rw-r--r--services/core/java/com/android/server/power/hint/HintManagerService.java4
-rw-r--r--services/core/java/com/android/server/resources/ResourcesManagerShellCommand.java1
-rw-r--r--services/core/java/com/android/server/rollback/RollbackStore.java8
-rw-r--r--services/core/java/com/android/server/selinux/SelinuxAuditLogBuilder.java12
-rw-r--r--services/core/java/com/android/server/selinux/SelinuxAuditLogsCollector.java29
-rw-r--r--services/core/java/com/android/server/statusbar/StatusBarManagerService.java13
-rw-r--r--services/core/java/com/android/server/timezonedetector/NotifyingTimeZoneChangeListener.java649
-rw-r--r--services/core/java/com/android/server/timezonedetector/TimeZoneChangeListener.java24
-rw-r--r--services/core/java/com/android/server/timezonedetector/TimeZoneDetectorStrategyImpl.java37
-rw-r--r--services/core/java/com/android/server/vibrator/BasicToPwleSegmentAdapter.java5
-rw-r--r--services/core/java/com/android/server/wm/ActivityRecord.java46
-rw-r--r--services/core/java/com/android/server/wm/ActivityTaskSupervisor.java2
-rw-r--r--services/core/java/com/android/server/wm/AppCompatController.java4
-rw-r--r--services/core/java/com/android/server/wm/AppCompatLetterboxPolicy.java2
-rw-r--r--services/core/java/com/android/server/wm/AppCompatOrientationOverrides.java2
-rw-r--r--services/core/java/com/android/server/wm/AppCompatOrientationPolicy.java6
-rw-r--r--services/core/java/com/android/server/wm/AppCompatOverrides.java8
-rw-r--r--services/core/java/com/android/server/wm/DisplayArea.java2
-rw-r--r--services/core/java/com/android/server/wm/DisplayAreaPolicy.java8
-rw-r--r--services/core/java/com/android/server/wm/DisplayContent.java56
-rw-r--r--services/core/java/com/android/server/wm/DragDropController.java5
-rw-r--r--services/core/java/com/android/server/wm/DragState.java76
-rw-r--r--services/core/java/com/android/server/wm/PageSizeMismatchDialog.java8
-rw-r--r--services/core/java/com/android/server/wm/PersisterQueue.java28
-rw-r--r--services/core/java/com/android/server/wm/SnapshotPersistQueue.java8
-rw-r--r--services/core/java/com/android/server/wm/StartingData.java7
-rw-r--r--services/core/java/com/android/server/wm/Task.java16
-rw-r--r--services/core/java/com/android/server/wm/Transition.java9
-rw-r--r--services/core/java/com/android/server/wm/WindowContainer.java21
-rw-r--r--services/core/java/com/android/server/wm/WindowManagerService.java17
-rw-r--r--services/core/java/com/android/server/wm/WindowState.java24
-rw-r--r--services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java23
-rw-r--r--services/java/com/android/server/SystemServer.java20
-rw-r--r--services/print/java/com/android/server/print/flags.aconfig2
-rw-r--r--services/robotests/backup/src/com/android/server/backup/BackupManagerServiceRoboTest.java7
-rw-r--r--services/tests/mockingservicestests/src/com/android/server/am/ActivityManagerServiceTest.java12
-rw-r--r--services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java37
-rw-r--r--services/tests/mockingservicestests/src/com/android/server/backup/BackupManagerServiceTest.java18
-rw-r--r--services/tests/mockingservicestests/src/com/android/server/power/ThermalManagerServiceMockingTest.java10
-rw-r--r--services/tests/performancehinttests/src/com/android/server/power/hint/HintManagerServiceTest.java28
-rw-r--r--services/tests/powerservicetests/src/com/android/server/power/PowerManagerServiceTest.java26
-rw-r--r--services/tests/powerstatstests/Android.bp3
-rw-r--r--services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryTraceTest.java105
-rw-r--r--services/tests/selinux/src/com/android/server/selinux/SelinuxAuditLogsBuilderTest.java36
-rw-r--r--services/tests/selinux/src/com/android/server/selinux/SelinuxAuditLogsCollectorTest.java11
-rw-r--r--services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java6
-rw-r--r--services/tests/servicestests/src/com/android/server/am/ActivityManagerTest.java23
-rw-r--r--services/tests/servicestests/src/com/android/server/am/UserControllerTest.java28
-rw-r--r--services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest1.java16
-rw-r--r--services/tests/servicestests/src/com/android/server/power/ThermalManagerServiceTest.java124
-rw-r--r--services/tests/timetests/src/com/android/server/timezonedetector/NotifyingTimeZoneChangeListenerTest.java527
-rw-r--r--services/tests/timetests/src/com/android/server/timezonedetector/TimeZoneDetectorStrategyImplTest.java286
-rw-r--r--services/tests/uiservicestests/src/com/android/server/notification/GroupHelperTest.java128
-rw-r--r--services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java79
-rw-r--r--services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java257
-rw-r--r--services/tests/vibrator/src/com/android/server/vibrator/BasicToPwleSegmentAdapterTest.java158
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/AppCompatOrientationOverridesTest.java2
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/AppCompatOrientationPolicyTest.java2
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java12
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/DragDropControllerTests.java255
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/PersisterQueueTests.java11
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java103
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java2
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java7
-rw-r--r--telecomm/java/android/telecom/TelecomManager.java29
-rw-r--r--telecomm/java/com/android/internal/telecom/ITelecomService.aidl6
-rw-r--r--tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/open/MainActivityStartsSecondaryWithAlwaysExpandTest.kt18
-rw-r--r--tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/splitscreen/EnterSystemSplitTest.kt3
-rw-r--r--tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromFixedOrientationTest.kt2
-rw-r--r--tests/Input/AndroidManifest.xml2
-rw-r--r--tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt27
-rw-r--r--tests/Input/src/com/android/test/input/CaptureEventActivity.kt85
-rw-r--r--tests/Input/src/com/android/test/input/UinputRecordingIntegrationTests.kt1
-rw-r--r--tests/PlatformCompatGating/src/com/android/tests/gating/PlatformCompatPermissionsTest.java25
-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/integration-tests/DumpTest/components_full_proto.txt2
-rw-r--r--tools/aapt2/readme.md6
-rw-r--r--tools/aapt2/util/Util.cpp2
-rw-r--r--tools/systemfeatures/src/com/android/systemfeatures/SystemFeaturesGenerator.kt2
-rw-r--r--tools/systemfeatures/tests/golden/RoFeatures.java.gen2
-rw-r--r--tools/systemfeatures/tests/golden/RoNoFeatures.java.gen2
-rw-r--r--tools/systemfeatures/tests/golden/RwFeatures.java.gen2
-rw-r--r--tools/systemfeatures/tests/golden/RwNoFeatures.java.gen2
-rw-r--r--tools/systemfeatures/tests/src/SystemFeaturesGeneratorTest.java22
596 files changed, 16582 insertions, 5768 deletions
diff --git a/config/preloaded-classes-denylist b/config/preloaded-classes-denylist
index e3e929cb00d9..a6a1d1680b7b 100644
--- a/config/preloaded-classes-denylist
+++ b/config/preloaded-classes-denylist
@@ -1,5 +1,4 @@
android.content.AsyncTaskLoader$LoadTask
-android.media.MediaCodecInfo$CodecCapabilities$FeatureList
android.net.ConnectivityThread$Singleton
android.os.FileObserver
android.os.NullVibrator
diff --git a/core/api/current.txt b/core/api/current.txt
index 6707c15de682..b7f7a7f9e779 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -16896,6 +16896,7 @@ package android.graphics {
method public android.graphics.Paint.FontMetricsInt getFontMetricsInt();
method @FlaggedApi("com.android.text.flags.fix_line_height_for_locale") public void getFontMetricsIntForLocale(@NonNull android.graphics.Paint.FontMetricsInt);
method public float getFontSpacing();
+ method @FlaggedApi("com.android.text.flags.typeface_redesign_readonly") @Nullable public String getFontVariationOverride();
method public String getFontVariationSettings();
method public int getHinting();
method public float getLetterSpacing();
@@ -16974,6 +16975,7 @@ package android.graphics {
method public void setFilterBitmap(boolean);
method public void setFlags(int);
method public void setFontFeatureSettings(String);
+ method @FlaggedApi("com.android.text.flags.typeface_redesign_readonly") public boolean setFontVariationOverride(@Nullable String);
method public boolean setFontVariationSettings(String);
method public void setHinting(int);
method public void setLetterSpacing(float);
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index a988acf1f4a9..a352d9d2ea06 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -3415,6 +3415,10 @@ package android.telecom {
method public void onBindClient(@Nullable android.content.Intent);
}
+ public class TelecomManager {
+ method @FlaggedApi("com.android.server.telecom.flags.voip_call_monitor_refactor") public boolean hasForegroundServiceDelegation(@Nullable android.telecom.PhoneAccountHandle);
+ }
+
}
package android.telephony {
diff --git a/core/java/android/app/ActivityManagerInternal.java b/core/java/android/app/ActivityManagerInternal.java
index 999db18a1229..6151b8e2ef0a 100644
--- a/core/java/android/app/ActivityManagerInternal.java
+++ b/core/java/android/app/ActivityManagerInternal.java
@@ -142,6 +142,15 @@ public abstract class ActivityManagerInternal {
String processName, String abiOverride, int uid, Runnable crashHandler);
/**
+ * Called when a user is being deleted. This can happen during normal device usage
+ * or just at startup, when partially removed users are purged. Any state persisted by the
+ * ActivityManager should be purged now.
+ *
+ * @param userId The user being cleaned up.
+ */
+ public abstract void onUserRemoving(@UserIdInt int userId);
+
+ /**
* Called when a user has been deleted. This can happen during normal device usage
* or just at startup, when partially removed users are purged. Any state persisted by the
* ActivityManager should be purged now.
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java
index 1f3e6559a695..717a2acb4b4a 100644
--- a/core/java/android/app/ActivityThread.java
+++ b/core/java/android/app/ActivityThread.java
@@ -105,7 +105,6 @@ import android.content.pm.ServiceInfo;
import android.content.res.AssetManager;
import android.content.res.CompatibilityInfo;
import android.content.res.Configuration;
-import android.content.res.ResourceTimer;
import android.content.res.Resources;
import android.content.res.ResourcesImpl;
import android.content.res.loader.ResourcesLoader;
@@ -5254,7 +5253,6 @@ public final class ActivityThread extends ClientTransactionHandler
Resources.dumpHistory(pw, "");
pw.flush();
- ResourceTimer.dumpTimers(info.fd.getFileDescriptor(), "-refresh");
if (info.finishCallback != null) {
info.finishCallback.sendResult(null);
}
diff --git a/core/java/android/app/KeyguardManager.java b/core/java/android/app/KeyguardManager.java
index 67f7bee4028e..b5ac4e78c7ad 100644
--- a/core/java/android/app/KeyguardManager.java
+++ b/core/java/android/app/KeyguardManager.java
@@ -70,7 +70,6 @@ import com.android.internal.widget.VerifyCredentialResponse;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.nio.charset.Charset;
-import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.Executor;
@@ -1064,7 +1063,7 @@ public class KeyguardManager {
Log.e(TAG, "Save lock exception", e);
success = false;
} finally {
- Arrays.fill(password, (byte) 0);
+ LockPatternUtils.zeroize(password);
}
return success;
}
diff --git a/core/java/android/app/LoadedApk.java b/core/java/android/app/LoadedApk.java
index 3d85ea6a1fca..ffd235f91e09 100644
--- a/core/java/android/app/LoadedApk.java
+++ b/core/java/android/app/LoadedApk.java
@@ -1129,6 +1129,10 @@ public final class LoadedApk {
@UnsupportedAppUsage
public ClassLoader getClassLoader() {
+ ClassLoader ret = mClassLoader;
+ if (ret != null) {
+ return ret;
+ }
synchronized (mLock) {
if (mClassLoader == null) {
createOrUpdateClassLoaderLocked(null /*addedPaths*/);
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java
index 5176aee9051f..c2ce7d511681 100644
--- a/core/java/android/app/Notification.java
+++ b/core/java/android/app/Notification.java
@@ -1968,6 +1968,13 @@ public class Notification implements Parcelable
@SystemApi
public static final int SEMANTIC_ACTION_CONVERSATION_IS_PHISHING = 12;
+ /**
+ * {@link #extras} key to a boolean defining if this action requires special visual
+ * treatment.
+ * @hide
+ */
+ public static final String EXTRA_IS_MAGIC = "android.extra.IS_MAGIC";
+
private final Bundle mExtras;
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
private Icon mIcon;
@@ -5984,6 +5991,15 @@ public class Notification implements Parcelable
}
setHeaderlessVerticalMargins(contentView, p, hasSecondLine);
+ // Update margins to leave space for the top line (but not for headerless views like
+ // HUNS, which use a different layout that already accounts for that).
+ if (Flags.notificationsRedesignTemplates() && !p.mHeaderless) {
+ int margin = getContentMarginTop(mContext,
+ R.dimen.notification_2025_content_margin_top);
+ contentView.setViewLayoutMargin(R.id.notification_main_column,
+ RemoteViews.MARGIN_TOP, margin, TypedValue.COMPLEX_UNIT_PX);
+ }
+
return contentView;
}
@@ -6207,7 +6223,7 @@ public class Notification implements Parcelable
int textColor = Colors.flattenAlpha(getPrimaryTextColor(p), pillColor);
contentView.setInt(R.id.expand_button, "setDefaultTextColor", textColor);
contentView.setInt(R.id.expand_button, "setDefaultPillColor", pillColor);
- // Use different highlighted colors for e.g. unopened groups
+ // Use different highlighted colors for conversations' unread count
if (p.mHighlightExpander) {
pillColor = Colors.flattenAlpha(
getColors(p).getTertiaryFixedDimAccentColor(), bgColor);
@@ -6456,16 +6472,6 @@ public class Notification implements Parcelable
big.setColorStateList(R.id.snooze_button, "setImageTintList", actionColor);
big.setColorStateList(R.id.bubble_button, "setImageTintList", actionColor);
- // Update margins to leave space for the top line (but not for HUNs, which use a
- // different layout that already accounts for that).
- if (Flags.notificationsRedesignTemplates()
- && p.mViewType != StandardTemplateParams.VIEW_TYPE_HEADS_UP) {
- int margin = getContentMarginTop(mContext,
- R.dimen.notification_2025_content_margin_top);
- big.setViewLayoutMargin(R.id.notification_main_column, RemoteViews.MARGIN_TOP,
- margin, TypedValue.COMPLEX_UNIT_PX);
- }
-
boolean validRemoteInput = false;
// In the UI, contextual actions appear separately from the standard actions, so we
@@ -6807,8 +6813,6 @@ public class Notification implements Parcelable
public RemoteViews makeNotificationGroupHeader() {
return makeNotificationHeader(mParams.reset()
.viewType(StandardTemplateParams.VIEW_TYPE_GROUP_HEADER)
- // Highlight group expander until the group is first opened
- .highlightExpander(Flags.notificationsRedesignTemplates())
.fillTextsFrom(this));
}
@@ -6984,14 +6988,12 @@ public class Notification implements Parcelable
* @param useRegularSubtext uses the normal subtext set if there is one available. Otherwise
* a new subtext is created consisting of the content of the
* notification.
- * @param highlightExpander whether the expander should use the highlighted colors
* @hide
*/
- public RemoteViews makeLowPriorityContentView(boolean useRegularSubtext,
- boolean highlightExpander) {
+ public RemoteViews makeLowPriorityContentView(boolean useRegularSubtext) {
StandardTemplateParams p = mParams.reset()
.viewType(StandardTemplateParams.VIEW_TYPE_MINIMIZED)
- .highlightExpander(highlightExpander)
+ .highlightExpander(false)
.fillTextsFrom(this);
if (!useRegularSubtext || TextUtils.isEmpty(p.mSubText)) {
p.summaryText(createSummaryText());
diff --git a/core/java/android/app/NotificationManager.java b/core/java/android/app/NotificationManager.java
index 08bd854525ec..aede8aa70ede 100644
--- a/core/java/android/app/NotificationManager.java
+++ b/core/java/android/app/NotificationManager.java
@@ -17,6 +17,7 @@
package android.app;
import static android.Manifest.permission.POST_NOTIFICATIONS;
+import static android.app.NotificationChannel.DEFAULT_CHANNEL_ID;
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
import static android.service.notification.Flags.notificationClassification;
@@ -50,6 +51,7 @@ import android.os.Binder;
import android.os.Build;
import android.os.Bundle;
import android.os.IBinder;
+import android.os.IpcDataCache;
import android.os.Parcel;
import android.os.Parcelable;
import android.os.RemoteException;
@@ -71,6 +73,8 @@ import android.util.LruCache;
import android.util.Slog;
import android.util.proto.ProtoOutputStream;
+import com.android.internal.annotations.VisibleForTesting;
+
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.time.InstantSource;
@@ -1202,12 +1206,20 @@ public class NotificationManager {
* package (see {@link Context#createPackageContext(String, int)}).</p>
*/
public NotificationChannel getNotificationChannel(String channelId) {
- INotificationManager service = service();
- try {
- return service.getNotificationChannel(mContext.getOpPackageName(),
- mContext.getUserId(), mContext.getPackageName(), channelId);
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ if (Flags.nmBinderPerfCacheChannels()) {
+ return getChannelFromList(channelId,
+ mNotificationChannelListCache.query(new NotificationChannelQuery(
+ mContext.getOpPackageName(),
+ mContext.getPackageName(),
+ mContext.getUserId())));
+ } else {
+ INotificationManager service = service();
+ try {
+ return service.getNotificationChannel(mContext.getOpPackageName(),
+ mContext.getUserId(), mContext.getPackageName(), channelId);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
}
}
@@ -1222,13 +1234,21 @@ public class NotificationManager {
*/
public @Nullable NotificationChannel getNotificationChannel(@NonNull String channelId,
@NonNull String conversationId) {
- INotificationManager service = service();
- try {
- return service.getConversationNotificationChannel(mContext.getOpPackageName(),
- mContext.getUserId(), mContext.getPackageName(), channelId, true,
- conversationId);
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ if (Flags.nmBinderPerfCacheChannels()) {
+ return getConversationChannelFromList(channelId, conversationId,
+ mNotificationChannelListCache.query(new NotificationChannelQuery(
+ mContext.getOpPackageName(),
+ mContext.getPackageName(),
+ mContext.getUserId())));
+ } else {
+ INotificationManager service = service();
+ try {
+ return service.getConversationNotificationChannel(mContext.getOpPackageName(),
+ mContext.getUserId(), mContext.getPackageName(), channelId, true,
+ conversationId);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
}
}
@@ -1241,15 +1261,62 @@ public class NotificationManager {
* {@link Context#createPackageContext(String, int)}).</p>
*/
public List<NotificationChannel> getNotificationChannels() {
- INotificationManager service = service();
- try {
- return service.getNotificationChannels(mContext.getOpPackageName(),
- mContext.getPackageName(), mContext.getUserId()).getList();
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ if (Flags.nmBinderPerfCacheChannels()) {
+ return mNotificationChannelListCache.query(new NotificationChannelQuery(
+ mContext.getOpPackageName(),
+ mContext.getPackageName(),
+ mContext.getUserId()));
+ } else {
+ INotificationManager service = service();
+ try {
+ return service.getNotificationChannels(mContext.getOpPackageName(),
+ mContext.getPackageName(), mContext.getUserId()).getList();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
}
}
+ // channel list assumed to be associated with the appropriate package & user id already.
+ private static NotificationChannel getChannelFromList(String channelId,
+ List<NotificationChannel> channels) {
+ if (channels == null) {
+ return null;
+ }
+ if (channelId == null) {
+ channelId = DEFAULT_CHANNEL_ID;
+ }
+ for (NotificationChannel channel : channels) {
+ if (channelId.equals(channel.getId())) {
+ return channel;
+ }
+ }
+ return null;
+ }
+
+ private static NotificationChannel getConversationChannelFromList(String channelId,
+ String conversationId, List<NotificationChannel> channels) {
+ if (channels == null) {
+ return null;
+ }
+ if (channelId == null) {
+ channelId = DEFAULT_CHANNEL_ID;
+ }
+ if (conversationId == null) {
+ return getChannelFromList(channelId, channels);
+ }
+ NotificationChannel parent = null;
+ for (NotificationChannel channel : channels) {
+ if (conversationId.equals(channel.getConversationId())
+ && channelId.equals(channel.getParentChannelId())) {
+ return channel;
+ } else if (channelId.equals(channel.getId())) {
+ parent = channel;
+ }
+ }
+ return parent;
+ }
+
/**
* Deletes the given notification channel.
*
@@ -1328,6 +1395,71 @@ public class NotificationManager {
}
}
+ private static final String NOTIFICATION_CHANNEL_CACHE_API = "getNotificationChannel";
+ private static final String NOTIFICATION_CHANNEL_LIST_CACHE_NAME = "getNotificationChannels";
+ private static final int NOTIFICATION_CHANNEL_CACHE_SIZE = 10;
+
+ private final IpcDataCache.QueryHandler<NotificationChannelQuery, List<NotificationChannel>>
+ mNotificationChannelListQueryHandler = new IpcDataCache.QueryHandler<>() {
+ @Override
+ public List<NotificationChannel> apply(NotificationChannelQuery query) {
+ INotificationManager service = service();
+ try {
+ return service.getNotificationChannels(query.callingPkg,
+ query.targetPkg, query.userId).getList();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
+ public boolean shouldBypassCache(@NonNull NotificationChannelQuery query) {
+ // Other locations should also not be querying the cache in the first place if
+ // the flag is not enabled, but this is an extra precaution.
+ if (!Flags.nmBinderPerfCacheChannels()) {
+ Log.wtf(TAG,
+ "shouldBypassCache called when nm_binder_perf_cache_channels off");
+ return true;
+ }
+ return false;
+ }
+ };
+
+ private final IpcDataCache<NotificationChannelQuery, List<NotificationChannel>>
+ mNotificationChannelListCache =
+ new IpcDataCache<>(NOTIFICATION_CHANNEL_CACHE_SIZE, IpcDataCache.MODULE_SYSTEM,
+ NOTIFICATION_CHANNEL_CACHE_API, NOTIFICATION_CHANNEL_LIST_CACHE_NAME,
+ mNotificationChannelListQueryHandler);
+
+ private record NotificationChannelQuery(
+ String callingPkg,
+ String targetPkg,
+ int userId) {}
+
+ /**
+ * @hide
+ */
+ public static void invalidateNotificationChannelCache() {
+ if (Flags.nmBinderPerfCacheChannels()) {
+ IpcDataCache.invalidateCache(IpcDataCache.MODULE_SYSTEM,
+ NOTIFICATION_CHANNEL_CACHE_API);
+ } else {
+ // if we are here, we have failed to flag something
+ Log.wtf(TAG, "invalidateNotificationChannelCache called without flag");
+ }
+ }
+
+ /**
+ * For testing only: running tests with a cache requires marking the cache's property for
+ * testing, as test APIs otherwise cannot invalidate the cache. This must be called after
+ * calling PropertyInvalidatedCache.setTestMode(true).
+ * @hide
+ */
+ @VisibleForTesting
+ public void setChannelCacheToTestMode() {
+ mNotificationChannelListCache.testPropertyName();
+ }
+
/**
* @hide
*/
diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java
index a2fddb045179..c50452157d74 100644
--- a/core/java/android/app/admin/DevicePolicyManager.java
+++ b/core/java/android/app/admin/DevicePolicyManager.java
@@ -4354,20 +4354,24 @@ public class DevicePolicyManager {
}
/**
- * Indicates that app functions are not controlled by policy.
+ * Indicates that {@link android.app.appfunctions.AppFunctionManager} is not controlled by
+ * policy.
*
* <p>If no admin set this policy, it means appfunctions are enabled.
*/
@FlaggedApi(android.app.appfunctions.flags.Flags.FLAG_ENABLE_APP_FUNCTION_MANAGER)
public static final int APP_FUNCTIONS_NOT_CONTROLLED_BY_POLICY = 0;
- /** Indicates that app functions are controlled and disabled by a policy. */
+ /** Indicates that {@link android.app.appfunctions.AppFunctionManager} is controlled and
+ * disabled by policy, i.e. no apps in the current user are allowed to expose app functions.
+ */
@FlaggedApi(android.app.appfunctions.flags.Flags.FLAG_ENABLE_APP_FUNCTION_MANAGER)
public static final int APP_FUNCTIONS_DISABLED = 1;
/**
- * Indicates that app functions are controlled and disabled by a policy for cross profile
- * interactions only.
+ * Indicates that {@link android.app.appfunctions.AppFunctionManager} is controlled and
+ * disabled by a policy for cross profile interactions only, i.e. app functions exposed by apps
+ * in the current user can only be invoked within the same user.
*
* <p>This is different from {@link #APP_FUNCTIONS_DISABLED} in that it only disables cross
* profile interactions (even if the caller has permissions required to interact across users).
@@ -4388,7 +4392,9 @@ public class DevicePolicyManager {
public @interface AppFunctionsPolicy {}
/**
- * Sets the app functions policy which controls app functions operations on the device.
+ * Sets the {@link android.app.appfunctions.AppFunctionManager} policy which controls app
+ * functions operations on the device. An app function is a piece of functionality that apps
+ * expose to the system for cross-app orchestration.
*
* <p>This function can only be called by a device owner, a profile owner or holders of the
* permission {@link android.Manifest.permission#MANAGE_DEVICE_POLICY_APP_FUNCTIONS}.
@@ -4414,7 +4420,7 @@ public class DevicePolicyManager {
}
/**
- * Returns the current app functions policy.
+ * Returns the current {@link android.app.appfunctions.AppFunctionManager} policy.
*
* <p>The returned policy will be the current resolved policy rather than the policy set by the
* calling admin.
diff --git a/core/java/android/app/backup/BackupManagerInternal.java b/core/java/android/app/backup/BackupManagerInternal.java
new file mode 100644
index 000000000000..ceb5ae01f177
--- /dev/null
+++ b/core/java/android/app/backup/BackupManagerInternal.java
@@ -0,0 +1,40 @@
+/*
+ * 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 android.app.backup;
+
+import android.annotation.UserIdInt;
+import android.os.IBinder;
+
+/**
+ * Local system service interface for {@link com.android.server.backup.BackupManagerService}.
+ *
+ * @hide Only for use within the system server.
+ */
+public interface BackupManagerInternal {
+
+ /**
+ * Notifies the Backup Manager Service that an agent has become available. This
+ * method is only invoked by the Activity Manager.
+ */
+ void agentConnectedForUser(String packageName, @UserIdInt int userId, IBinder agent);
+
+ /**
+ * Notify the Backup Manager Service that an agent has unexpectedly gone away.
+ * This method is only invoked by the Activity Manager.
+ */
+ void agentDisconnectedForUser(String packageName, @UserIdInt int userId);
+}
diff --git a/core/java/android/app/backup/IBackupManager.aidl b/core/java/android/app/backup/IBackupManager.aidl
index 041c2a7c09f4..5d01d72c35a0 100644
--- a/core/java/android/app/backup/IBackupManager.aidl
+++ b/core/java/android/app/backup/IBackupManager.aidl
@@ -93,38 +93,6 @@ interface IBackupManager {
IBackupObserver observer);
/**
- * Notifies the Backup Manager Service that an agent has become available. This
- * method is only invoked by the Activity Manager.
- *
- * If {@code userId} is different from the calling user id, then the caller must hold the
- * android.permission.INTERACT_ACROSS_USERS_FULL permission.
- *
- * @param userId User id for which an agent has become available.
- */
- void agentConnectedForUser(int userId, String packageName, IBinder agent);
-
- /**
- * {@link android.app.backup.IBackupManager.agentConnected} for the calling user id.
- */
- void agentConnected(String packageName, IBinder agent);
-
- /**
- * Notify the Backup Manager Service that an agent has unexpectedly gone away.
- * This method is only invoked by the Activity Manager.
- *
- * If {@code userId} is different from the calling user id, then the caller must hold the
- * android.permission.INTERACT_ACROSS_USERS_FULL permission.
- *
- * @param userId User id for which an agent has unexpectedly gone away.
- */
- void agentDisconnectedForUser(int userId, String packageName);
-
- /**
- * {@link android.app.backup.IBackupManager.agentDisconnected} for the calling user id.
- */
- void agentDisconnected(String packageName);
-
- /**
* Notify the Backup Manager Service that an application being installed will
* need a data-restore pass. This method is only invoked by the Package Manager.
*
diff --git a/core/java/android/companion/CompanionDeviceService.java b/core/java/android/companion/CompanionDeviceService.java
index 316d129bd6b9..971d402569c0 100644
--- a/core/java/android/companion/CompanionDeviceService.java
+++ b/core/java/android/companion/CompanionDeviceService.java
@@ -62,10 +62,11 @@ import java.util.concurrent.Executor;
*
* <p>
* If the companion application has requested observing device presence (see
- * {@link CompanionDeviceManager#startObservingDevicePresence(String)}) the system will
- * <a href="https://developer.android.com/guide/components/bound-services"> bind the service</a>
- * when it detects the device nearby (for BLE devices) or when the device is connected
- * (for Bluetooth devices).
+ * {@link CompanionDeviceManager#stopObservingDevicePresence(ObservingDevicePresenceRequest)})
+ * the system will <a href="https://developer.android.com/guide/components/bound-services">
+ * bind the service</a> when one of the {@link DevicePresenceEvent#EVENT_BLE_APPEARED},
+ * {@link DevicePresenceEvent#EVENT_BT_CONNECTED},
+ * {@link DevicePresenceEvent#EVENT_SELF_MANAGED_APPEARED} event is notified.
*
* <p>
* The system binding {@link CompanionDeviceService} elevates the priority of the process that
@@ -102,15 +103,25 @@ public abstract class CompanionDeviceService extends Service {
/**
* An intent action for a service to be bound whenever this app's companion device(s)
- * are nearby.
+ * are nearby or self-managed device(s) report app appeared.
*
- * <p>The app will be kept alive for as long as the device is nearby or companion app reports
- * appeared.
- * If the app is not running at the time device gets connected, the app will be woken up.</p>
+ * <p>The app will be kept bound by the system when one of the
+ * {@link DevicePresenceEvent#EVENT_BLE_APPEARED},
+ * {@link DevicePresenceEvent#EVENT_BT_CONNECTED},
+ * {@link DevicePresenceEvent#EVENT_SELF_MANAGED_APPEARED} event is notified.
*
- * <p>Shortly after the device goes out of range or the companion app reports disappeared,
- * the service will be unbound, and the app will be eligible for cleanup, unless any other
- * user-visible components are running.</p>
+ * If the app is not running when one of the
+ * {@link DevicePresenceEvent#EVENT_BLE_APPEARED},
+ * {@link DevicePresenceEvent#EVENT_BT_CONNECTED},
+ * {@link DevicePresenceEvent#EVENT_SELF_MANAGED_APPEARED} event is notified, the app will be
+ * kept bound by the system.</p>
+ *
+ * <p>Shortly, the service will be unbound if both
+ * {@link DevicePresenceEvent#EVENT_BLE_DISAPPEARED} and
+ * {@link DevicePresenceEvent#EVENT_BT_DISCONNECTED} are notified, or
+ * {@link DevicePresenceEvent#EVENT_SELF_MANAGED_DISAPPEARED} event is notified.
+ * The app will be eligible for cleanup, unless any other user-visible components are
+ * running.</p>
*
* If running in background is not essential for the devices that this app can manage,
* app should avoid declaring this service.</p>
diff --git a/core/java/android/companion/virtual/VirtualDeviceInternal.java b/core/java/android/companion/virtual/VirtualDeviceInternal.java
index 42c74414ecd9..311e24ba6254 100644
--- a/core/java/android/companion/virtual/VirtualDeviceInternal.java
+++ b/core/java/android/companion/virtual/VirtualDeviceInternal.java
@@ -83,7 +83,6 @@ import java.util.function.IntConsumer;
public class VirtualDeviceInternal {
private final Context mContext;
- private final IVirtualDeviceManager mService;
private final IVirtualDevice mVirtualDevice;
private final Object mActivityListenersLock = new Object();
@GuardedBy("mActivityListenersLock")
@@ -206,7 +205,6 @@ public class VirtualDeviceInternal {
Context context,
int associationId,
VirtualDeviceParams params) throws RemoteException {
- mService = service;
mContext = context.getApplicationContext();
mVirtualDevice = service.createVirtualDevice(
new Binder(),
@@ -217,11 +215,7 @@ public class VirtualDeviceInternal {
mSoundEffectListener);
}
- VirtualDeviceInternal(
- IVirtualDeviceManager service,
- Context context,
- IVirtualDevice virtualDevice) {
- mService = service;
+ VirtualDeviceInternal(Context context, IVirtualDevice virtualDevice) {
mContext = context.getApplicationContext();
mVirtualDevice = virtualDevice;
try {
diff --git a/core/java/android/companion/virtual/VirtualDeviceManager.java b/core/java/android/companion/virtual/VirtualDeviceManager.java
index ed2fd99c55c5..73ea9f0462d5 100644
--- a/core/java/android/companion/virtual/VirtualDeviceManager.java
+++ b/core/java/android/companion/virtual/VirtualDeviceManager.java
@@ -577,9 +577,8 @@ public final class VirtualDeviceManager {
}
/** @hide */
- public VirtualDevice(IVirtualDeviceManager service, Context context,
- IVirtualDevice virtualDevice) {
- mVirtualDeviceInternal = new VirtualDeviceInternal(service, context, virtualDevice);
+ public VirtualDevice(Context context, IVirtualDevice virtualDevice) {
+ mVirtualDeviceInternal = new VirtualDeviceInternal(context, virtualDevice);
}
/**
diff --git a/core/java/android/content/ClipData.java b/core/java/android/content/ClipData.java
index e271cf4f60ec..4e292d0b7d18 100644
--- a/core/java/android/content/ClipData.java
+++ b/core/java/android/content/ClipData.java
@@ -221,6 +221,12 @@ public class ClipData implements Parcelable {
// if the data is obtained from {@link #copyForTransferWithActivityInfo}
private ActivityInfo mActivityInfo;
+ private boolean mTokenVerificationEnabled;
+
+ void setTokenVerificationEnabled() {
+ mTokenVerificationEnabled = true;
+ }
+
/**
* A builder for a ClipData Item.
*/
@@ -398,7 +404,9 @@ public class ClipData implements Parcelable {
* Retrieve the raw Intent contained in this Item.
*/
public Intent getIntent() {
- Intent.maybeMarkAsMissingCreatorToken(mIntent);
+ if (mTokenVerificationEnabled) {
+ Intent.maybeMarkAsMissingCreatorToken(mIntent);
+ }
return mIntent;
}
@@ -1353,6 +1361,12 @@ public class ClipData implements Parcelable {
}
}
+ void setTokenVerificationEnabled() {
+ for (int i = 0; i < mItems.size(); ++i) {
+ mItems.get(i).setTokenVerificationEnabled();
+ }
+ }
+
@Override
public int describeContents() {
return 0;
diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java
index e3e10388754c..01e24d81a7cd 100644
--- a/core/java/android/content/Intent.java
+++ b/core/java/android/content/Intent.java
@@ -12394,14 +12394,30 @@ public class Intent implements Parcelable, Cloneable {
* @hide
*/
public void collectExtraIntentKeys() {
+ collectExtraIntentKeys(false);
+ }
+
+ /**
+ * Collects keys in the extra bundle whose value are intents.
+ * With these keys collected on the client side, the system server would only unparcel values
+ * of these keys and create IntentCreatorToken for them.
+ * This method could also be called from the system server side as a catch all safty net in case
+ * these keys are not collected on the client side. In that case, call it with forceUnparcel set
+ * to true since everything is parceled on the system server side.
+ *
+ * @param forceUnparcel if it is true, unparcel everything to determine if an object is an
+ * intent. Otherwise, do not unparcel anything.
+ * @hide
+ */
+ public void collectExtraIntentKeys(boolean forceUnparcel) {
if (preventIntentRedirect()) {
- collectNestedIntentKeysRecur(new ArraySet<>());
+ collectNestedIntentKeysRecur(new ArraySet<>(), forceUnparcel);
}
}
- private void collectNestedIntentKeysRecur(Set<Intent> visited) {
+ private void collectNestedIntentKeysRecur(Set<Intent> visited, boolean forceUnparcel) {
addExtendedFlags(EXTENDED_FLAG_NESTED_INTENT_KEYS_COLLECTED);
- if (mExtras != null && !mExtras.isParcelled() && !mExtras.isEmpty()) {
+ if (mExtras != null && (forceUnparcel || !mExtras.isParcelled()) && !mExtras.isEmpty()) {
for (String key : mExtras.keySet()) {
Object value;
try {
@@ -12410,23 +12426,25 @@ public class Intent implements Parcelable, Cloneable {
// It is okay to not collect a parceled intent since it would have been
// coming from another process and collected by its containing intent already
// in that process.
- if (!mExtras.isValueParceled(key)) {
+ if (forceUnparcel || !mExtras.isValueParceled(key)) {
value = mExtras.get(key);
} else {
value = null;
}
} catch (BadParcelableException e) {
- // This probably would never happen. But just in case, simply ignore it since
- // it is not an intent anyway.
+ // This may still happen if the keys are collected on the system server side, in
+ // which case, we will try to unparcel everything. If this happens, simply
+ // ignore it since it is not an intent anyway.
value = null;
}
if (value instanceof Intent intent) {
handleNestedIntent(intent, visited, new NestedIntentKey(
- NestedIntentKey.NESTED_INTENT_KEY_TYPE_EXTRA_PARCEL, key, 0));
+ NestedIntentKey.NESTED_INTENT_KEY_TYPE_EXTRA_PARCEL, key, 0),
+ forceUnparcel);
} else if (value instanceof Parcelable[] parcelables) {
- handleParcelableArray(parcelables, key, visited);
+ handleParcelableArray(parcelables, key, visited, forceUnparcel);
} else if (value instanceof ArrayList<?> parcelables) {
- handleParcelableList(parcelables, key, visited);
+ handleParcelableList(parcelables, key, visited, forceUnparcel);
}
}
}
@@ -12436,13 +12454,15 @@ public class Intent implements Parcelable, Cloneable {
Intent intent = mClipData.getItemAt(i).mIntent;
if (intent != null && !visited.contains(intent)) {
handleNestedIntent(intent, visited, new NestedIntentKey(
- NestedIntentKey.NESTED_INTENT_KEY_TYPE_CLIP_DATA, null, i));
+ NestedIntentKey.NESTED_INTENT_KEY_TYPE_CLIP_DATA, null, i),
+ forceUnparcel);
}
}
}
}
- private void handleNestedIntent(Intent intent, Set<Intent> visited, NestedIntentKey key) {
+ private void handleNestedIntent(Intent intent, Set<Intent> visited, NestedIntentKey key,
+ boolean forceUnparcel) {
if (mCreatorTokenInfo == null) {
mCreatorTokenInfo = new CreatorTokenInfo();
}
@@ -12452,24 +12472,28 @@ public class Intent implements Parcelable, Cloneable {
mCreatorTokenInfo.mNestedIntentKeys.add(key);
if (!visited.contains(intent)) {
visited.add(intent);
- intent.collectNestedIntentKeysRecur(visited);
+ intent.collectNestedIntentKeysRecur(visited, forceUnparcel);
}
}
- private void handleParcelableArray(Parcelable[] parcelables, String key, Set<Intent> visited) {
+ private void handleParcelableArray(Parcelable[] parcelables, String key, Set<Intent> visited,
+ boolean forceUnparcel) {
for (int i = 0; i < parcelables.length; i++) {
if (parcelables[i] instanceof Intent intent && !visited.contains(intent)) {
handleNestedIntent(intent, visited, new NestedIntentKey(
- NestedIntentKey.NESTED_INTENT_KEY_TYPE_EXTRA_PARCEL_ARRAY, key, i));
+ NestedIntentKey.NESTED_INTENT_KEY_TYPE_EXTRA_PARCEL_ARRAY, key, i),
+ forceUnparcel);
}
}
}
- private void handleParcelableList(ArrayList<?> parcelables, String key, Set<Intent> visited) {
+ private void handleParcelableList(ArrayList<?> parcelables, String key, Set<Intent> visited,
+ boolean forceUnparcel) {
for (int i = 0; i < parcelables.size(); i++) {
if (parcelables.get(i) instanceof Intent intent && !visited.contains(intent)) {
handleNestedIntent(intent, visited, new NestedIntentKey(
- NestedIntentKey.NESTED_INTENT_KEY_TYPE_EXTRA_PARCEL_LIST, key, i));
+ NestedIntentKey.NESTED_INTENT_KEY_TYPE_EXTRA_PARCEL_LIST, key, i),
+ forceUnparcel);
}
}
}
@@ -12482,6 +12506,9 @@ public class Intent implements Parcelable, Cloneable {
if (intent.mExtras != null) {
intent.mExtras.enableTokenVerification();
}
+ if (intent.mClipData != null) {
+ intent.mClipData.setTokenVerificationEnabled();
+ }
};
/** @hide */
@@ -12493,6 +12520,9 @@ public class Intent implements Parcelable, Cloneable {
// mark trusted creator token present.
mExtras.enableTokenVerification();
}
+ if (mClipData != null) {
+ mClipData.setTokenVerificationEnabled();
+ }
}
/** @hide */
diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java
index c16582f19c9b..8c7e93a834b7 100644
--- a/core/java/android/content/pm/PackageManager.java
+++ b/core/java/android/content/pm/PackageManager.java
@@ -4649,6 +4649,7 @@ public abstract class PackageManager {
* the Android Keystore backed by an isolated execution environment. The version indicates
* which features are implemented in the isolated execution environment:
* <ul>
+ * <li>400: Inclusion of module information (via tag MODULE_HASH) in the attestation record.
* <li>300: Ability to include a second IMEI in the ID attestation record, see
* {@link android.app.admin.DevicePolicyManager#ID_TYPE_IMEI}.
* <li>200: Hardware support for Curve 25519 (including both Ed25519 signature generation and
@@ -4682,6 +4683,7 @@ public abstract class PackageManager {
* StrongBox</a>. If this feature has a version, the version number indicates which features are
* implemented in StrongBox:
* <ul>
+ * <li>400: Inclusion of module information (via tag MODULE_HASH) in the attestation record.
* <li>300: Ability to include a second IMEI in the ID attestation record, see
* {@link android.app.admin.DevicePolicyManager#ID_TYPE_IMEI}.
* <li>200: No new features for StrongBox (the Android Keystore environment backed by an
diff --git a/core/java/android/content/res/ApkAssets.java b/core/java/android/content/res/ApkAssets.java
index b938aac811fd..075457885586 100644
--- a/core/java/android/content/res/ApkAssets.java
+++ b/core/java/android/content/res/ApkAssets.java
@@ -25,7 +25,6 @@ import android.content.res.loader.ResourcesProvider;
import android.ravenwood.annotation.RavenwoodClassLoadHook;
import android.ravenwood.annotation.RavenwoodKeepWholeClass;
import android.text.TextUtils;
-import android.util.Log;
import com.android.internal.annotations.GuardedBy;
@@ -51,7 +50,6 @@ import java.util.Objects;
@RavenwoodKeepWholeClass
@RavenwoodClassLoadHook(RavenwoodClassLoadHook.LIBANDROID_LOADING_HOOK)
public final class ApkAssets {
- private static final boolean DEBUG = false;
/**
* The apk assets contains framework resource values specified by the system.
@@ -136,17 +134,6 @@ public final class ApkAssets {
@Nullable
private final AssetsProvider mAssets;
- @NonNull
- private String mName;
-
- private static final int UPTODATE_FALSE = 0;
- private static final int UPTODATE_TRUE = 1;
- private static final int UPTODATE_ALWAYS_TRUE = 2;
-
- // Start with the only value that may change later and would force a native call to
- // double check it.
- private int mPreviousUpToDateResult = UPTODATE_TRUE;
-
/**
* Creates a new ApkAssets instance from the given path on disk.
*
@@ -317,7 +304,7 @@ public final class ApkAssets {
private ApkAssets(@FormatType int format, @NonNull String path, @PropertyFlags int flags,
@Nullable AssetsProvider assets) throws IOException {
- this(format, flags, assets, path);
+ this(format, flags, assets);
Objects.requireNonNull(path, "path");
mNativePtr = nativeLoad(format, path, flags, assets);
mStringBlock = new StringBlock(nativeGetStringBlock(mNativePtr), true /*useSparse*/);
@@ -326,7 +313,7 @@ public final class ApkAssets {
private ApkAssets(@FormatType int format, @NonNull FileDescriptor fd,
@NonNull String friendlyName, @PropertyFlags int flags, @Nullable AssetsProvider assets)
throws IOException {
- this(format, flags, assets, friendlyName);
+ this(format, flags, assets);
Objects.requireNonNull(fd, "fd");
Objects.requireNonNull(friendlyName, "friendlyName");
mNativePtr = nativeLoadFd(format, fd, friendlyName, flags, assets);
@@ -336,7 +323,7 @@ public final class ApkAssets {
private ApkAssets(@FormatType int format, @NonNull FileDescriptor fd,
@NonNull String friendlyName, long offset, long length, @PropertyFlags int flags,
@Nullable AssetsProvider assets) throws IOException {
- this(format, flags, assets, friendlyName);
+ this(format, flags, assets);
Objects.requireNonNull(fd, "fd");
Objects.requireNonNull(friendlyName, "friendlyName");
mNativePtr = nativeLoadFdOffsets(format, fd, friendlyName, offset, length, flags, assets);
@@ -344,17 +331,16 @@ public final class ApkAssets {
}
private ApkAssets(@PropertyFlags int flags, @Nullable AssetsProvider assets) {
- this(FORMAT_APK, flags, assets, "empty");
+ this(FORMAT_APK, flags, assets);
mNativePtr = nativeLoadEmpty(flags, assets);
mStringBlock = null;
}
private ApkAssets(@FormatType int format, @PropertyFlags int flags,
- @Nullable AssetsProvider assets, @NonNull String name) {
+ @Nullable AssetsProvider assets) {
mFlags = flags;
mAssets = assets;
mIsOverlay = format == FORMAT_IDMAP;
- if (DEBUG) mName = name;
}
@UnsupportedAppUsage
@@ -367,7 +353,7 @@ public final class ApkAssets {
/** @hide */
public @NonNull String getDebugName() {
synchronized (this) {
- return nativeGetDebugName(mNativePtr);
+ return mNativePtr == 0 ? "<destroyed>" : nativeGetDebugName(mNativePtr);
}
}
@@ -435,41 +421,13 @@ public final class ApkAssets {
}
}
- private static double intervalMs(long beginNs, long endNs) {
- return (endNs - beginNs) / 1000000.0;
- }
-
/**
* Returns false if the underlying APK was changed since this ApkAssets was loaded.
*/
public boolean isUpToDate() {
- // This function is performance-critical - it's called multiple times on every Resources
- // object creation, and on few other cache accesses - so it's important to avoid the native
- // call when we know for sure what it will return (which is the case for both ALWAYS_TRUE
- // and FALSE).
- if (mPreviousUpToDateResult != UPTODATE_TRUE) {
- return mPreviousUpToDateResult == UPTODATE_ALWAYS_TRUE;
- }
- final long beforeTs, afterLockTs, afterNativeTs, afterUnlockTs;
- if (DEBUG) beforeTs = System.nanoTime();
- final int res;
synchronized (this) {
- if (DEBUG) afterLockTs = System.nanoTime();
- res = nativeIsUpToDate(mNativePtr);
- if (DEBUG) afterNativeTs = System.nanoTime();
- }
- if (DEBUG) {
- afterUnlockTs = System.nanoTime();
- if (afterUnlockTs - beforeTs >= 10L * 1000000) {
- Log.d("ApkAssets", "isUpToDate(" + mName + ") took "
- + intervalMs(beforeTs, afterUnlockTs)
- + " ms: " + intervalMs(beforeTs, afterLockTs)
- + " / " + intervalMs(afterLockTs, afterNativeTs)
- + " / " + intervalMs(afterNativeTs, afterUnlockTs));
- }
+ return nativeIsUpToDate(mNativePtr);
}
- mPreviousUpToDateResult = res;
- return res != UPTODATE_FALSE;
}
public boolean isSystem() {
@@ -529,7 +487,7 @@ public final class ApkAssets {
private static native @NonNull String nativeGetAssetPath(long ptr);
private static native @NonNull String nativeGetDebugName(long ptr);
private static native long nativeGetStringBlock(long ptr);
- @CriticalNative private static native int nativeIsUpToDate(long ptr);
+ @CriticalNative private static native boolean nativeIsUpToDate(long ptr);
private static native long nativeOpenXml(long ptr, @NonNull String fileName) throws IOException;
private static native @Nullable OverlayableInfo nativeGetOverlayableInfo(long ptr,
String overlayableName) throws IOException;
diff --git a/core/java/android/content/res/ResourceTimer.java b/core/java/android/content/res/ResourceTimer.java
index 2d1bf4d9d296..d51f64ce8106 100644
--- a/core/java/android/content/res/ResourceTimer.java
+++ b/core/java/android/content/res/ResourceTimer.java
@@ -17,10 +17,13 @@
package android.content.res;
import android.annotation.NonNull;
+import android.annotation.Nullable;
+
import android.app.AppProtoEnums;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
+import android.os.ParcelFileDescriptor;
import android.os.Process;
import android.os.SystemClock;
import android.text.TextUtils;
@@ -30,7 +33,6 @@ import com.android.internal.annotations.GuardedBy;
import com.android.internal.util.FastPrintWriter;
import com.android.internal.util.FrameworkStatsLog;
-import java.io.FileDescriptor;
import java.io.FileOutputStream;
import java.io.PrintWriter;
import java.util.Arrays;
@@ -275,40 +277,38 @@ public final class ResourceTimer {
* Update the metrics information and dump it.
* @hide
*/
- public static void dumpTimers(@NonNull FileDescriptor fd, String... args) {
- try (PrintWriter pw = new FastPrintWriter(new FileOutputStream(fd))) {
- pw.println("\nDumping ResourceTimers");
-
- final boolean enabled;
- synchronized (sLock) {
- enabled = sEnabled && sConfig != null;
- }
- if (!enabled) {
+ public static void dumpTimers(@NonNull ParcelFileDescriptor pfd, @Nullable String[] args) {
+ FileOutputStream fout = new FileOutputStream(pfd.getFileDescriptor());
+ PrintWriter pw = new FastPrintWriter(fout);
+ synchronized (sLock) {
+ if (!sEnabled || (sConfig == null)) {
pw.println(" Timers are not enabled in this process");
+ pw.flush();
return;
}
+ }
- // Look for the --refresh switch. If the switch is present, then sTimers is updated.
- // Otherwise, the current value of sTimers is displayed.
- boolean refresh = Arrays.asList(args).contains("-refresh");
-
- synchronized (sLock) {
- update(refresh);
- long runtime = sLastUpdated - sProcessStart;
- pw.format(" config runtime=%d proc=%s\n", runtime, Process.myProcessName());
- for (int i = 0; i < sTimers.length; i++) {
- Timer t = sTimers[i];
- if (t.count != 0) {
- String name = sConfig.timers[i];
- pw.format(" stats timer=%s cnt=%d avg=%d min=%d max=%d pval=%s "
- + "largest=%s\n",
- name, t.count, t.total / t.count, t.mintime, t.maxtime,
- packedString(t.percentile),
- packedString(t.largest));
- }
+ // Look for the --refresh switch. If the switch is present, then sTimers is updated.
+ // Otherwise, the current value of sTimers is displayed.
+ boolean refresh = Arrays.asList(args).contains("-refresh");
+
+ synchronized (sLock) {
+ update(refresh);
+ long runtime = sLastUpdated - sProcessStart;
+ pw.format(" config runtime=%d proc=%s\n", runtime, Process.myProcessName());
+ for (int i = 0; i < sTimers.length; i++) {
+ Timer t = sTimers[i];
+ if (t.count != 0) {
+ String name = sConfig.timers[i];
+ pw.format(" stats timer=%s cnt=%d avg=%d min=%d max=%d pval=%s "
+ + "largest=%s\n",
+ name, t.count, t.total / t.count, t.mintime, t.maxtime,
+ packedString(t.percentile),
+ packedString(t.largest));
}
}
}
+ pw.flush();
}
// Enable (or disabled) the runtime timers. Note that timers are disabled by default. This
diff --git a/core/java/android/hardware/contexthub/HubEndpoint.java b/core/java/android/hardware/contexthub/HubEndpoint.java
index 71702d996883..25cdc508fdce 100644
--- a/core/java/android/hardware/contexthub/HubEndpoint.java
+++ b/core/java/android/hardware/contexthub/HubEndpoint.java
@@ -137,6 +137,8 @@ public class HubEndpoint {
serviceDescriptor,
mLifecycleCallback.onSessionOpenRequest(
initiator, serviceDescriptor)));
+ } else {
+ invokeCallbackFinished();
}
}
@@ -163,6 +165,8 @@ public class HubEndpoint {
+ result.getReason());
rejectSession(sessionId);
}
+
+ invokeCallbackFinished();
}
private void acceptSession(
@@ -249,7 +253,12 @@ public class HubEndpoint {
activeSession.setOpened();
if (mLifecycleCallback != null) {
mLifecycleCallbackExecutor.execute(
- () -> mLifecycleCallback.onSessionOpened(activeSession));
+ () -> {
+ mLifecycleCallback.onSessionOpened(activeSession);
+ invokeCallbackFinished();
+ });
+ } else {
+ invokeCallbackFinished();
}
}
@@ -278,7 +287,10 @@ public class HubEndpoint {
synchronized (mLock) {
mActiveSessions.remove(sessionId);
}
+ invokeCallbackFinished();
});
+ } else {
+ invokeCallbackFinished();
}
}
@@ -323,8 +335,17 @@ public class HubEndpoint {
e.rethrowFromSystemServer();
}
}
+ invokeCallbackFinished();
});
}
+
+ private void invokeCallbackFinished() {
+ try {
+ mServiceToken.onCallbackFinished();
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ }
+ }
};
/** Binder returned from system service, non-null while registered. */
diff --git a/core/java/android/hardware/contexthub/IContextHubEndpoint.aidl b/core/java/android/hardware/contexthub/IContextHubEndpoint.aidl
index 44f80c819e83..eb1255c06094 100644
--- a/core/java/android/hardware/contexthub/IContextHubEndpoint.aidl
+++ b/core/java/android/hardware/contexthub/IContextHubEndpoint.aidl
@@ -94,4 +94,10 @@ interface IContextHubEndpoint {
*/
@EnforcePermission("ACCESS_CONTEXT_HUB")
void sendMessageDeliveryStatus(int sessionId, int messageSeqNumber, byte errorCode);
+
+ /**
+ * Invoked when a callback from IContextHubEndpointCallback finishes.
+ */
+ @EnforcePermission("ACCESS_CONTEXT_HUB")
+ void onCallbackFinished();
}
diff --git a/core/java/android/hardware/fingerprint/FingerprintSensorPropertiesInternal.java b/core/java/android/hardware/fingerprint/FingerprintSensorPropertiesInternal.java
index d84d29292ed5..8fbe05c4e9eb 100644
--- a/core/java/android/hardware/fingerprint/FingerprintSensorPropertiesInternal.java
+++ b/core/java/android/hardware/fingerprint/FingerprintSensorPropertiesInternal.java
@@ -121,15 +121,20 @@ public class FingerprintSensorPropertiesInternal extends SensorPropertiesInterna
/**
* Returns if sensor type is ultrasonic Udfps
- * @return true if sensor is ultrasonic Udfps, false otherwise
*/
public boolean isUltrasonicUdfps() {
return sensorType == TYPE_UDFPS_ULTRASONIC;
}
/**
+ * Returns if sensor type is optical Udfps
+ */
+ public boolean isOpticalUdfps() {
+ return sensorType == TYPE_UDFPS_OPTICAL;
+ }
+
+ /**
* Returns if sensor type is side-FPS
- * @return true if sensor is side-fps, false otherwise
*/
public boolean isAnySidefpsType() {
switch (sensorType) {
diff --git a/core/java/android/hardware/input/IInputManager.aidl b/core/java/android/hardware/input/IInputManager.aidl
index ed510e467f82..2bb28a1b6b0b 100644
--- a/core/java/android/hardware/input/IInputManager.aidl
+++ b/core/java/android/hardware/input/IInputManager.aidl
@@ -266,6 +266,11 @@ interface IInputManager {
@PermissionManuallyEnforced
@JavaPassthrough(annotation="@android.annotation.RequiresPermission(value = "
+ "android.Manifest.permission.MANAGE_KEY_GESTURES)")
+ AidlInputGestureData getInputGesture(int userId, in AidlInputGestureData.Trigger trigger);
+
+ @PermissionManuallyEnforced
+ @JavaPassthrough(annotation="@android.annotation.RequiresPermission(value = "
+ + "android.Manifest.permission.MANAGE_KEY_GESTURES)")
int addCustomInputGesture(int userId, in AidlInputGestureData data);
@PermissionManuallyEnforced
diff --git a/core/java/android/hardware/input/InputGestureData.java b/core/java/android/hardware/input/InputGestureData.java
index f41550f6061e..75c652c973e4 100644
--- a/core/java/android/hardware/input/InputGestureData.java
+++ b/core/java/android/hardware/input/InputGestureData.java
@@ -48,27 +48,7 @@ public final class InputGestureData {
/** Returns the trigger information for this input gesture */
public Trigger getTrigger() {
- switch (mInputGestureData.trigger.getTag()) {
- case AidlInputGestureData.Trigger.Tag.key: {
- AidlInputGestureData.KeyTrigger trigger = mInputGestureData.trigger.getKey();
- if (trigger == null) {
- throw new RuntimeException("InputGestureData is corrupted, null key trigger!");
- }
- return createKeyTrigger(trigger.keycode, trigger.modifierState);
- }
- case AidlInputGestureData.Trigger.Tag.touchpadGesture: {
- AidlInputGestureData.TouchpadGestureTrigger trigger =
- mInputGestureData.trigger.getTouchpadGesture();
- if (trigger == null) {
- throw new RuntimeException(
- "InputGestureData is corrupted, null touchpad trigger!");
- }
- return createTouchpadTrigger(trigger.gestureType);
- }
- default:
- throw new RuntimeException("InputGestureData is corrupted, invalid trigger type!");
-
- }
+ return createTriggerFromAidlTrigger(mInputGestureData.trigger);
}
/** Returns the action to perform for this input gesture */
@@ -147,18 +127,7 @@ public final class InputGestureData {
"No app launch data for system action launch application");
}
AidlInputGestureData data = new AidlInputGestureData();
- data.trigger = new AidlInputGestureData.Trigger();
- if (mTrigger instanceof KeyTrigger keyTrigger) {
- data.trigger.setKey(new AidlInputGestureData.KeyTrigger());
- data.trigger.getKey().keycode = keyTrigger.getKeycode();
- data.trigger.getKey().modifierState = keyTrigger.getModifierState();
- } else if (mTrigger instanceof TouchpadTrigger touchpadTrigger) {
- data.trigger.setTouchpadGesture(new AidlInputGestureData.TouchpadGestureTrigger());
- data.trigger.getTouchpadGesture().gestureType =
- touchpadTrigger.getTouchpadGestureType();
- } else {
- throw new IllegalArgumentException("Invalid trigger type!");
- }
+ data.trigger = mTrigger.getAidlTrigger();
data.gestureType = mKeyGestureType;
if (mAppLaunchData != null) {
if (mAppLaunchData instanceof AppLaunchData.CategoryData categoryData) {
@@ -198,6 +167,7 @@ public final class InputGestureData {
}
public interface Trigger {
+ AidlInputGestureData.Trigger getAidlTrigger();
}
/** Creates a input gesture trigger based on a key press */
@@ -210,85 +180,128 @@ public final class InputGestureData {
return new TouchpadTrigger(touchpadGestureType);
}
+ public static Trigger createTriggerFromAidlTrigger(AidlInputGestureData.Trigger aidlTrigger) {
+ switch (aidlTrigger.getTag()) {
+ case AidlInputGestureData.Trigger.Tag.key: {
+ AidlInputGestureData.KeyTrigger trigger = aidlTrigger.getKey();
+ if (trigger == null) {
+ throw new RuntimeException("aidlTrigger is corrupted, null key trigger!");
+ }
+ return new KeyTrigger(trigger);
+ }
+ case AidlInputGestureData.Trigger.Tag.touchpadGesture: {
+ AidlInputGestureData.TouchpadGestureTrigger trigger =
+ aidlTrigger.getTouchpadGesture();
+ if (trigger == null) {
+ throw new RuntimeException(
+ "aidlTrigger is corrupted, null touchpad trigger!");
+ }
+ return new TouchpadTrigger(trigger);
+ }
+ default:
+ throw new RuntimeException("aidlTrigger is corrupted, invalid trigger type!");
+
+ }
+ }
+
/** Key based input gesture trigger */
public static class KeyTrigger implements Trigger {
- private static final int SHORTCUT_META_MASK =
- KeyEvent.META_META_ON | KeyEvent.META_CTRL_ON | KeyEvent.META_ALT_ON
- | KeyEvent.META_SHIFT_ON;
- private final int mKeycode;
- private final int mModifierState;
+
+ AidlInputGestureData.KeyTrigger mAidlKeyTrigger;
+
+ private KeyTrigger(@NonNull AidlInputGestureData.KeyTrigger aidlKeyTrigger) {
+ mAidlKeyTrigger = aidlKeyTrigger;
+ }
private KeyTrigger(int keycode, int modifierState) {
if (keycode <= KeyEvent.KEYCODE_UNKNOWN || keycode > KeyEvent.getMaxKeyCode()) {
throw new IllegalArgumentException("Invalid keycode = " + keycode);
}
- mKeycode = keycode;
- mModifierState = modifierState;
+ mAidlKeyTrigger = new AidlInputGestureData.KeyTrigger();
+ mAidlKeyTrigger.keycode = keycode;
+ mAidlKeyTrigger.modifierState = modifierState;
}
public int getKeycode() {
- return mKeycode;
+ return mAidlKeyTrigger.keycode;
}
public int getModifierState() {
- return mModifierState;
+ return mAidlKeyTrigger.modifierState;
+ }
+
+ public AidlInputGestureData.Trigger getAidlTrigger() {
+ AidlInputGestureData.Trigger trigger = new AidlInputGestureData.Trigger();
+ trigger.setKey(mAidlKeyTrigger);
+ return trigger;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof KeyTrigger that)) return false;
- return mKeycode == that.mKeycode && mModifierState == that.mModifierState;
+ return Objects.equals(mAidlKeyTrigger, that.mAidlKeyTrigger);
}
@Override
public int hashCode() {
- return Objects.hash(mKeycode, mModifierState);
+ return mAidlKeyTrigger.hashCode();
}
@Override
public String toString() {
return "KeyTrigger{" +
- "mKeycode=" + KeyEvent.keyCodeToString(mKeycode) +
- ", mModifierState=" + mModifierState +
+ "mKeycode=" + KeyEvent.keyCodeToString(mAidlKeyTrigger.keycode) +
+ ", mModifierState=" + mAidlKeyTrigger.modifierState +
'}';
}
}
/** Touchpad based input gesture trigger */
public static class TouchpadTrigger implements Trigger {
- private final int mTouchpadGestureType;
+ AidlInputGestureData.TouchpadGestureTrigger mAidlTouchpadTrigger;
+
+ private TouchpadTrigger(
+ @NonNull AidlInputGestureData.TouchpadGestureTrigger aidlTouchpadTrigger) {
+ mAidlTouchpadTrigger = aidlTouchpadTrigger;
+ }
private TouchpadTrigger(int touchpadGestureType) {
if (touchpadGestureType != TOUCHPAD_GESTURE_TYPE_THREE_FINGER_TAP) {
throw new IllegalArgumentException(
"Invalid touchpadGestureType = " + touchpadGestureType);
}
- mTouchpadGestureType = touchpadGestureType;
+ mAidlTouchpadTrigger = new AidlInputGestureData.TouchpadGestureTrigger();
+ mAidlTouchpadTrigger.gestureType = touchpadGestureType;
}
public int getTouchpadGestureType() {
- return mTouchpadGestureType;
+ return mAidlTouchpadTrigger.gestureType;
+ }
+
+ public AidlInputGestureData.Trigger getAidlTrigger() {
+ AidlInputGestureData.Trigger trigger = new AidlInputGestureData.Trigger();
+ trigger.setTouchpadGesture(mAidlTouchpadTrigger);
+ return trigger;
}
@Override
public String toString() {
return "TouchpadTrigger{" +
- "mTouchpadGestureType=" + mTouchpadGestureType +
+ "mTouchpadGestureType=" + mAidlTouchpadTrigger.gestureType +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- TouchpadTrigger that = (TouchpadTrigger) o;
- return mTouchpadGestureType == that.mTouchpadGestureType;
+ if (!(o instanceof TouchpadTrigger that)) return false;
+ return Objects.equals(mAidlTouchpadTrigger, that.mAidlTouchpadTrigger);
}
@Override
public int hashCode() {
- return Objects.hashCode(mTouchpadGestureType);
+ return mAidlTouchpadTrigger.hashCode();
}
}
diff --git a/core/java/android/hardware/input/InputManager.java b/core/java/android/hardware/input/InputManager.java
index 10224c1be788..cf41e138047a 100644
--- a/core/java/android/hardware/input/InputManager.java
+++ b/core/java/android/hardware/input/InputManager.java
@@ -1480,6 +1480,30 @@ public final class InputManager {
mGlobal.unregisterKeyGestureEventHandler(handler);
}
+ /**
+ * Find an input gesture mapped to a particular trigger.
+ *
+ * @param trigger to find the input gesture for
+ * @return input gesture mapped to the provided trigger, {@code null} if none found
+ *
+ * @hide
+ */
+ @RequiresPermission(Manifest.permission.MANAGE_KEY_GESTURES)
+ @UserHandleAware
+ @Nullable
+ public InputGestureData getInputGesture(@NonNull InputGestureData.Trigger trigger) {
+ try {
+ AidlInputGestureData result = mIm.getInputGesture(mContext.getUserId(),
+ trigger.getAidlTrigger());
+ if (result == null) {
+ return null;
+ }
+ return new InputGestureData(result);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
/** Adds a new custom input gesture
*
* @param inputGestureData gesture data to add as custom gesture
diff --git a/core/java/android/hardware/input/KeyGestureEvent.java b/core/java/android/hardware/input/KeyGestureEvent.java
index cb1e0161441f..4025242fd208 100644
--- a/core/java/android/hardware/input/KeyGestureEvent.java
+++ b/core/java/android/hardware/input/KeyGestureEvent.java
@@ -43,6 +43,9 @@ public final class KeyGestureEvent {
private static final int LOG_EVENT_UNSPECIFIED =
FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__UNSPECIFIED;
+ // Used as a placeholder to identify if a gesture is reserved for system
+ public static final int KEY_GESTURE_TYPE_SYSTEM_RESERVED = -1;
+
// These values should not change and values should not be re-used as this data is persisted to
// long term storage and must be kept backwards compatible.
public static final int KEY_GESTURE_TYPE_UNSPECIFIED = 0;
@@ -144,6 +147,7 @@ public final class KeyGestureEvent {
public static final int ACTION_GESTURE_COMPLETE = 2;
@IntDef(prefix = "KEY_GESTURE_TYPE_", value = {
+ KEY_GESTURE_TYPE_SYSTEM_RESERVED,
KEY_GESTURE_TYPE_UNSPECIFIED,
KEY_GESTURE_TYPE_HOME,
KEY_GESTURE_TYPE_RECENT_APPS,
@@ -232,6 +236,26 @@ public final class KeyGestureEvent {
public @interface KeyGestureType {
}
+ /**
+ * Returns whether the key gesture type passed as argument is allowed for visible background
+ * users.
+ *
+ * @hide
+ */
+ public static boolean isVisibleBackgrounduserAllowedGesture(int keyGestureType) {
+ switch (keyGestureType) {
+ case KEY_GESTURE_TYPE_SLEEP:
+ case KEY_GESTURE_TYPE_WAKEUP:
+ case KEY_GESTURE_TYPE_LAUNCH_ASSISTANT:
+ case KEY_GESTURE_TYPE_LAUNCH_VOICE_ASSISTANT:
+ case KEY_GESTURE_TYPE_VOLUME_MUTE:
+ case KEY_GESTURE_TYPE_RECENT_APPS:
+ case KEY_GESTURE_TYPE_APP_SWITCH:
+ return false;
+ }
+ return true;
+ }
+
public KeyGestureEvent(@NonNull AidlKeyGestureEvent keyGestureEvent) {
this.mKeyGestureEvent = keyGestureEvent;
}
@@ -645,6 +669,8 @@ public final class KeyGestureEvent {
private static String keyGestureTypeToString(@KeyGestureType int value) {
switch (value) {
+ case KEY_GESTURE_TYPE_SYSTEM_RESERVED:
+ return "KEY_GESTURE_TYPE_SYSTEM_RESERVED";
case KEY_GESTURE_TYPE_UNSPECIFIED:
return "KEY_GESTURE_TYPE_UNSPECIFIED";
case KEY_GESTURE_TYPE_HOME:
diff --git a/core/java/android/os/Debug.java b/core/java/android/os/Debug.java
index 2bc6ab5a18e9..d5640221e6bd 100644
--- a/core/java/android/os/Debug.java
+++ b/core/java/android/os/Debug.java
@@ -2783,4 +2783,12 @@ public final class Debug
*/
public static native boolean logAllocatorStats();
+ /**
+ * Return the amount of memory (in kB) allocated by kernel drivers through CMA.
+ * @return a non-negative value or -1 on error.
+ *
+ * @hide
+ */
+ public static native long getKernelCmaUsageKb();
+
}
diff --git a/core/java/android/os/GraphicsEnvironment.java b/core/java/android/os/GraphicsEnvironment.java
index 12080ca511b2..45a7afa014a1 100644
--- a/core/java/android/os/GraphicsEnvironment.java
+++ b/core/java/android/os/GraphicsEnvironment.java
@@ -706,11 +706,11 @@ public class GraphicsEnvironment {
* @param context
*/
public void showAngleInUseDialogBox(Context context) {
- if (!shouldShowAngleInUseDialogBox(context)) {
+ if (!mShouldUseAngle) {
return;
}
- if (!mShouldUseAngle) {
+ if (!shouldShowAngleInUseDialogBox(context)) {
return;
}
diff --git a/core/java/android/os/Parcel.java b/core/java/android/os/Parcel.java
index 0879118ff856..4aa74621bd62 100644
--- a/core/java/android/os/Parcel.java
+++ b/core/java/android/os/Parcel.java
@@ -593,11 +593,11 @@ public final class Parcel {
*/
public final void recycle() {
if (mRecycled) {
- Log.wtf(TAG, "Recycle called on unowned Parcel. (recycle twice?) Here: "
+ String error = "Recycle called on unowned Parcel. (recycle twice?) Here: "
+ Log.getStackTraceString(new Throwable())
- + " Original recycle call (if DEBUG_RECYCLE): ", mStack);
-
- return;
+ + " Original recycle call (if DEBUG_RECYCLE): ";
+ Log.wtf(TAG, error, mStack);
+ throw new IllegalStateException(error, mStack);
}
mRecycled = true;
diff --git a/core/java/android/os/PowerManager.java b/core/java/android/os/PowerManager.java
index 1801df048b3e..2a5666cbe83c 100644
--- a/core/java/android/os/PowerManager.java
+++ b/core/java/android/os/PowerManager.java
@@ -615,6 +615,7 @@ public final class PowerManager {
WAKE_REASON_WAKE_KEY,
WAKE_REASON_WAKE_MOTION,
WAKE_REASON_HDMI,
+ WAKE_REASON_LID,
WAKE_REASON_DISPLAY_GROUP_ADDED,
WAKE_REASON_DISPLAY_GROUP_TURNED_ON,
WAKE_REASON_UNFOLD_DEVICE,
diff --git a/core/java/android/os/ServiceManager.java b/core/java/android/os/ServiceManager.java
index 9085fe09bdaa..a58fea891851 100644
--- a/core/java/android/os/ServiceManager.java
+++ b/core/java/android/os/ServiceManager.java
@@ -278,7 +278,7 @@ public final class ServiceManager {
return service;
} else {
return Binder.allowBlocking(
- getIServiceManager().checkService(name).getServiceWithMetadata().service);
+ getIServiceManager().checkService2(name).getServiceWithMetadata().service);
}
} catch (RemoteException e) {
Log.e(TAG, "error in checkService", e);
diff --git a/core/java/android/os/ServiceManagerNative.java b/core/java/android/os/ServiceManagerNative.java
index 7ea521ec5dd4..a5aa1b3efcd2 100644
--- a/core/java/android/os/ServiceManagerNative.java
+++ b/core/java/android/os/ServiceManagerNative.java
@@ -62,16 +62,23 @@ class ServiceManagerProxy implements IServiceManager {
@UnsupportedAppUsage
public IBinder getService(String name) throws RemoteException {
// Same as checkService (old versions of servicemanager had both methods).
- return checkService(name).getServiceWithMetadata().service;
+ return checkService2(name).getServiceWithMetadata().service;
}
public Service getService2(String name) throws RemoteException {
// Same as checkService (old versions of servicemanager had both methods).
- return checkService(name);
+ return checkService2(name);
}
- public Service checkService(String name) throws RemoteException {
- return mServiceManager.checkService(name);
+ // TODO(b/355394904): This function has been deprecated, please use checkService2 instead.
+ @UnsupportedAppUsage
+ public IBinder checkService(String name) throws RemoteException {
+ // Same as checkService (old versions of servicemanager had both methods).
+ return checkService2(name).getServiceWithMetadata().service;
+ }
+
+ public Service checkService2(String name) throws RemoteException {
+ return mServiceManager.checkService2(name);
}
public void addService(String name, IBinder service, boolean allowIsolated, int dumpPriority)
diff --git a/core/java/android/os/flags.aconfig b/core/java/android/os/flags.aconfig
index e24f08b7dfe5..8b8369890d1b 100644
--- a/core/java/android/os/flags.aconfig
+++ b/core/java/android/os/flags.aconfig
@@ -110,6 +110,14 @@ flag {
}
flag {
+ name: "allow_thermal_hal_skin_forecast"
+ is_exported: true
+ namespace: "game"
+ description: "Enable thermal HAL skin temperature forecast to be used by headroom API"
+ bug: "383211885"
+}
+
+flag {
name: "allow_thermal_headroom_thresholds"
is_exported: true
namespace: "game"
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 11dddfb24ad5..baaaa464a4cf 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -10525,6 +10525,15 @@ public final class Settings {
public static final String SCREENSAVER_ACTIVATE_ON_SLEEP = "screensaver_activate_on_sleep";
/**
+ * If screensavers are enabled, whether the screensaver should be
+ * automatically launched when the device is stationary and upright.
+ * @hide
+ */
+ @Readable
+ public static final String SCREENSAVER_ACTIVATE_ON_POSTURED =
+ "screensaver_activate_on_postured";
+
+ /**
* If screensavers are enabled, the default screensaver component.
* @hide
*/
diff --git a/core/java/android/security/flags.aconfig b/core/java/android/security/flags.aconfig
index ebb6fb451699..4a9e945e62a9 100644
--- a/core/java/android/security/flags.aconfig
+++ b/core/java/android/security/flags.aconfig
@@ -42,6 +42,16 @@ flag {
}
flag {
+ name: "secure_array_zeroization"
+ namespace: "platform_security"
+ description: "Enable secure array zeroization"
+ bug: "320392352"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+flag {
name: "deprecate_fsv_sig"
namespace: "hardware_backed_security"
description: "Feature flag for deprecating .fsv_sig"
diff --git a/core/java/android/service/dreams/flags.aconfig b/core/java/android/service/dreams/flags.aconfig
index dfc11dcb5427..d3a230d1335d 100644
--- a/core/java/android/service/dreams/flags.aconfig
+++ b/core/java/android/service/dreams/flags.aconfig
@@ -77,3 +77,10 @@ flag {
purpose: PURPOSE_BUGFIX
}
}
+
+flag {
+ name: "allow_dream_when_postured"
+ namespace: "systemui"
+ description: "Allow dreaming when device is stationary and upright"
+ bug: "383208131"
+}
diff --git a/core/java/android/text/Layout.java b/core/java/android/text/Layout.java
index d53b98c65f9a..3c53506990d1 100644
--- a/core/java/android/text/Layout.java
+++ b/core/java/android/text/Layout.java
@@ -16,7 +16,6 @@
package android.text;
-import static com.android.graphics.hwui.flags.Flags.highContrastTextLuminance;
import static com.android.text.flags.Flags.FLAG_FIX_LINE_HEIGHT_FOR_LOCALE;
import static com.android.text.flags.Flags.FLAG_LETTER_SPACING_JUSTIFICATION;
import static com.android.text.flags.Flags.FLAG_USE_BOUNDS_FOR_WIDTH;
@@ -670,15 +669,11 @@ public abstract class Layout {
// High-contrast text mode
// Determine if the text is black-on-white or white-on-black, so we know what blendmode will
// give the highest contrast and most realistic text color.
- // This equation should match the one in libs/hwui/hwui/DrawTextFunctor.h
- if (highContrastTextLuminance()) {
- var lab = new double[3];
- ColorUtils.colorToLAB(color, lab);
- return lab[0] < 50.0;
- } else {
- int channelSum = Color.red(color) + Color.green(color) + Color.blue(color);
- return channelSum < (128 * 3);
- }
+ // LINT.IfChange(hct_darken)
+ var lab = new double[3];
+ ColorUtils.colorToLAB(color, lab);
+ return lab[0] < 50.0;
+ // LINT.ThenChange(/libs/hwui/hwui/DrawTextFunctor.h:hct_darken)
}
private boolean isJustificationRequired(int lineNum) {
diff --git a/core/java/android/view/DragEvent.java b/core/java/android/view/DragEvent.java
index b65e3ebc3871..77af312eac4a 100644
--- a/core/java/android/view/DragEvent.java
+++ b/core/java/android/view/DragEvent.java
@@ -157,6 +157,11 @@ public class DragEvent implements Parcelable {
private float mOffsetY;
/**
+ * The id of the display where the `mX` and `mY` of this event belongs to.
+ */
+ private int mDisplayId;
+
+ /**
* The View#DRAG_FLAG_* flags used to start the current drag, only provided if the target window
* has the {@link WindowManager.LayoutParams#PRIVATE_FLAG_INTERCEPT_GLOBAL_DRAG_AND_DROP} flag
* and is only sent with {@link #ACTION_DRAG_STARTED} and {@link #ACTION_DROP}.
@@ -297,14 +302,15 @@ public class DragEvent implements Parcelable {
private DragEvent() {
}
- private void init(int action, float x, float y, float offsetX, float offsetY, int flags,
- ClipDescription description, ClipData data, SurfaceControl dragSurface,
+ private void init(int action, float x, float y, float offsetX, float offsetY, int displayId,
+ int flags, ClipDescription description, ClipData data, SurfaceControl dragSurface,
IDragAndDropPermissions dragAndDropPermissions, Object localState, boolean result) {
mAction = action;
mX = x;
mY = y;
mOffsetX = offsetX;
mOffsetY = offsetY;
+ mDisplayId = displayId;
mFlags = flags;
mClipDescription = description;
mClipData = data;
@@ -315,20 +321,20 @@ public class DragEvent implements Parcelable {
}
static DragEvent obtain() {
- return DragEvent.obtain(0, 0f, 0f, 0f, 0f, 0, null, null, null, null, null, false);
+ return DragEvent.obtain(0, 0f, 0f, 0f, 0f, 0, 0, null, null, null, null, null, false);
}
/** @hide */
public static DragEvent obtain(int action, float x, float y, float offsetX, float offsetY,
- int flags, Object localState, ClipDescription description, ClipData data,
+ int displayId, int flags, Object localState, ClipDescription description, ClipData data,
SurfaceControl dragSurface, IDragAndDropPermissions dragAndDropPermissions,
boolean result) {
final DragEvent ev;
synchronized (gRecyclerLock) {
if (gRecyclerTop == null) {
ev = new DragEvent();
- ev.init(action, x, y, offsetX, offsetY, flags, description, data, dragSurface,
- dragAndDropPermissions, localState, result);
+ ev.init(action, x, y, offsetX, offsetY, displayId, flags, description, data,
+ dragSurface, dragAndDropPermissions, localState, result);
return ev;
}
ev = gRecyclerTop;
@@ -339,7 +345,7 @@ public class DragEvent implements Parcelable {
ev.mRecycled = false;
ev.mNext = null;
- ev.init(action, x, y, offsetX, offsetY, flags, description, data, dragSurface,
+ ev.init(action, x, y, offsetX, offsetY, displayId, flags, description, data, dragSurface,
dragAndDropPermissions, localState, result);
return ev;
@@ -349,8 +355,9 @@ public class DragEvent implements Parcelable {
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
public static DragEvent obtain(DragEvent source) {
return obtain(source.mAction, source.mX, source.mY, source.mOffsetX, source.mOffsetY,
- source.mFlags, source.mLocalState, source.mClipDescription, source.mClipData,
- source.mDragSurface, source.mDragAndDropPermissions, source.mDragResult);
+ source.mDisplayId, source.mFlags, source.mLocalState, source.mClipDescription,
+ source.mClipData, source.mDragSurface, source.mDragAndDropPermissions,
+ source.mDragResult);
}
/**
@@ -398,6 +405,11 @@ public class DragEvent implements Parcelable {
return mOffsetY;
}
+ /** @hide */
+ public int getDisplayId() {
+ return mDisplayId;
+ }
+
/**
* Returns the {@link android.content.ClipData} object sent to the system as part of the call
* to
diff --git a/core/java/android/view/InputEventReceiver.java b/core/java/android/view/InputEventReceiver.java
index 2cc05b0bc4b0..1c36eaf99afa 100644
--- a/core/java/android/view/InputEventReceiver.java
+++ b/core/java/android/view/InputEventReceiver.java
@@ -177,7 +177,7 @@ public abstract class InputEventReceiver {
* drag
* if true, the window associated with this input channel has just lost drag
*/
- public void onDragEvent(boolean isExiting, float x, float y) {
+ public void onDragEvent(boolean isExiting, float x, float y, int displayId) {
}
/**
diff --git a/core/java/android/view/PointerIcon.java b/core/java/android/view/PointerIcon.java
index b21e85aeeb6a..da3a817f0341 100644
--- a/core/java/android/view/PointerIcon.java
+++ b/core/java/android/view/PointerIcon.java
@@ -514,10 +514,14 @@ public final class PointerIcon implements Parcelable {
final TypedArray a = resources.obtainAttributes(
parser, com.android.internal.R.styleable.PointerIcon);
bitmapRes = a.getResourceId(com.android.internal.R.styleable.PointerIcon_bitmap, 0);
- hotSpotX = a.getDimension(com.android.internal.R.styleable.PointerIcon_hotSpotX, 0)
- * pointerScale;
- hotSpotY = a.getDimension(com.android.internal.R.styleable.PointerIcon_hotSpotY, 0)
- * pointerScale;
+ // Cast the hotspot dimensions to int before scaling to match the scaling logic of
+ // the bitmap, whose intrinsic size is also an int before it is scaled.
+ final int unscaledHotSpotX =
+ (int) a.getDimension(com.android.internal.R.styleable.PointerIcon_hotSpotX, 0);
+ final int unscaledHotSpotY =
+ (int) a.getDimension(com.android.internal.R.styleable.PointerIcon_hotSpotY, 0);
+ hotSpotX = unscaledHotSpotX * pointerScale;
+ hotSpotY = unscaledHotSpotY * pointerScale;
a.recycle();
} catch (Exception ex) {
throw new IllegalArgumentException("Exception parsing pointer icon resource.", ex);
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index 8ef0b0eebb8c..36671b901a6b 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -10561,13 +10561,13 @@ public final class ViewRootImpl implements ViewParent,
}
@Override
- public void onDragEvent(boolean isExiting, float x, float y) {
+ public void onDragEvent(boolean isExiting, float x, float y, int displayId) {
// force DRAG_EXITED_EVENT if appropriate
DragEvent event = DragEvent.obtain(
- isExiting ? DragEvent.ACTION_DRAG_EXITED : DragEvent.ACTION_DRAG_LOCATION,
- x, y, 0 /* offsetX */, 0 /* offsetY */, 0 /* flags */, null/* localState */,
- null/* description */, null /* data */, null /* dragSurface */,
- null /* dragAndDropPermissions */, false /* result */);
+ isExiting ? DragEvent.ACTION_DRAG_EXITED : DragEvent.ACTION_DRAG_LOCATION, x, y,
+ 0 /* offsetX */, 0 /* offsetY */, displayId, 0 /* flags */,
+ null/* localState */, null/* description */, null /* data */,
+ null /* dragSurface */, null /* dragAndDropPermissions */, false /* result */);
dispatchDragEvent(event);
}
diff --git a/core/java/android/view/accessibility/AccessibilityManager.java b/core/java/android/view/accessibility/AccessibilityManager.java
index 544f42b9acfa..64277b14098d 100644
--- a/core/java/android/view/accessibility/AccessibilityManager.java
+++ b/core/java/android/view/accessibility/AccessibilityManager.java
@@ -157,6 +157,9 @@ public final class AccessibilityManager {
/** @hide */
public static final int AUTOCLICK_CURSOR_AREA_SIZE_MAX = 100;
+ /** @hide */
+ public static final int AUTOCLICK_CURSOR_AREA_INCREMENT_SIZE = 20;
+
/**
* Activity action: Launch UI to manage which accessibility service or feature is assigned
* to the navigation bar Accessibility button.
diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java
index 71a832d84f08..99fe0cbdca25 100644
--- a/core/java/android/widget/TextView.java
+++ b/core/java/android/widget/TextView.java
@@ -18,7 +18,6 @@ package android.widget;
import static android.Manifest.permission.INTERACT_ACROSS_USERS_FULL;
import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
-import static android.graphics.Paint.NEW_FONT_VARIATION_MANAGEMENT;
import static android.view.ContentInfo.FLAG_CONVERT_TO_PLAIN_TEXT;
import static android.view.ContentInfo.SOURCE_AUTOFILL;
import static android.view.ContentInfo.SOURCE_CLIPBOARD;
@@ -5544,13 +5543,32 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
return true;
}
- final boolean useFontVariationStore = Flags.typefaceRedesignReadonly()
- && CompatChanges.isChangeEnabled(NEW_FONT_VARIATION_MANAGEMENT);
boolean effective;
- if (useFontVariationStore) {
+ if (Flags.typefaceRedesignReadonly()) {
if (mFontWeightAdjustment != 0
&& mFontWeightAdjustment != Configuration.FONT_WEIGHT_ADJUSTMENT_UNDEFINED) {
- mTextPaint.setFontVariationSettings(fontVariationSettings, mFontWeightAdjustment);
+ List<FontVariationAxis> axes = FontVariationAxis.fromFontVariationSettingsForList(
+ fontVariationSettings);
+ if (axes == null) {
+ return false; // invalid format of the font variation settings.
+ }
+ boolean wghtAdjusted = false;
+ for (int i = 0; i < axes.size(); ++i) {
+ FontVariationAxis axis = axes.get(i);
+ if (axis.getOpenTypeTagValue() == 0x77676874 /* wght */) {
+ axes.set(i, new FontVariationAxis("wght",
+ Math.clamp(axis.getStyleValue() + mFontWeightAdjustment,
+ FontStyle.FONT_WEIGHT_MIN, FontStyle.FONT_WEIGHT_MAX)));
+ wghtAdjusted = true;
+ }
+ }
+ if (!wghtAdjusted) {
+ axes.add(new FontVariationAxis("wght",
+ Math.clamp(400 + mFontWeightAdjustment,
+ FontStyle.FONT_WEIGHT_MIN, FontStyle.FONT_WEIGHT_MAX)));
+ }
+ mTextPaint.setFontVariationSettings(
+ FontVariationAxis.toFontVariationSettings(axes));
} else {
mTextPaint.setFontVariationSettings(fontVariationSettings);
}
diff --git a/core/java/android/window/DisplayAreaOrganizer.java b/core/java/android/window/DisplayAreaOrganizer.java
index 84ce247264f6..bd711fc79083 100644
--- a/core/java/android/window/DisplayAreaOrganizer.java
+++ b/core/java/android/window/DisplayAreaOrganizer.java
@@ -121,6 +121,14 @@ public class DisplayAreaOrganizer extends WindowOrganizer {
public static final int FEATURE_WINDOWING_LAYER = FEATURE_SYSTEM_FIRST + 9;
/**
+ * Display area for rendering app zoom out. When there are multiple layers on the screen,
+ * we want to render these layers based on a depth model. Here we zoom out the layer behind,
+ * whether it's an app or the homescreen.
+ * @hide
+ */
+ public static final int FEATURE_APP_ZOOM_OUT = FEATURE_SYSTEM_FIRST + 10;
+
+ /**
* The last boundary of display area for system features
*/
public static final int FEATURE_SYSTEM_LAST = 10_000;
diff --git a/core/java/android/window/flags/windowing_frontend.aconfig b/core/java/android/window/flags/windowing_frontend.aconfig
index de3e0d3faf43..7a1078f8718f 100644
--- a/core/java/android/window/flags/windowing_frontend.aconfig
+++ b/core/java/android/window/flags/windowing_frontend.aconfig
@@ -415,6 +415,17 @@ flag {
}
flag {
+ name: "keep_app_window_hide_while_locked"
+ namespace: "windowing_frontend"
+ description: "Do not let app window visible while device is locked"
+ is_fixed_read_only: true
+ bug: "378088391"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+flag {
name: "port_window_size_animation"
namespace: "windowing_frontend"
description: "Port window-resize animation from legacy to shell"
diff --git a/core/java/android/window/flags/windowing_sdk.aconfig b/core/java/android/window/flags/windowing_sdk.aconfig
index b38feeef290b..f178b0ed2043 100644
--- a/core/java/android/window/flags/windowing_sdk.aconfig
+++ b/core/java/android/window/flags/windowing_sdk.aconfig
@@ -167,3 +167,14 @@ flag {
purpose: PURPOSE_BUGFIX
}
}
+
+flag {
+ namespace: "windowing_sdk"
+ name: "use_self_sync_transaction_for_layer"
+ description: "Always use this.getSyncTransaction for assignLayer"
+ bug: "388127825"
+ is_fixed_read_only: true
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
diff --git a/core/java/com/android/internal/os/BatteryStatsHistory.java b/core/java/com/android/internal/os/BatteryStatsHistory.java
index c9c4be1e2c93..dc440e36ca0d 100644
--- a/core/java/com/android/internal/os/BatteryStatsHistory.java
+++ b/core/java/com/android/internal/os/BatteryStatsHistory.java
@@ -19,6 +19,7 @@ package com.android.internal.os;
import static android.os.BatteryStats.HistoryItem.EVENT_FLAG_FINISH;
import static android.os.BatteryStats.HistoryItem.EVENT_FLAG_START;
import static android.os.BatteryStats.HistoryItem.EVENT_STATE_CHANGE;
+import static android.os.Trace.TRACE_TAG_SYSTEM_SERVER;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -215,6 +216,7 @@ public class BatteryStatsHistory {
private final ArraySet<PowerStats.Descriptor> mWrittenPowerStatsDescriptors = new ArraySet<>();
private byte mLastHistoryStepLevel = 0;
private boolean mMutable = true;
+ private int mIteratorCookie;
private final BatteryStatsHistory mWritableHistory;
private static class BatteryHistoryFile implements Comparable<BatteryHistoryFile> {
@@ -289,6 +291,7 @@ public class BatteryStatsHistory {
}
void load() {
+ Trace.asyncTraceBegin(TRACE_TAG_SYSTEM_SERVER, "BatteryStatsHistory.load", 0);
mDirectory.mkdirs();
if (!mDirectory.exists()) {
Slog.wtf(TAG, "HistoryDir does not exist:" + mDirectory.getPath());
@@ -325,8 +328,11 @@ public class BatteryStatsHistory {
}
} finally {
unlock();
+ Trace.asyncTraceEnd(TRACE_TAG_SYSTEM_SERVER, "BatteryStatsHistory.load", 0);
}
});
+ } else {
+ Trace.asyncTraceEnd(TRACE_TAG_SYSTEM_SERVER, "BatteryStatsHistory.load", 0);
}
}
@@ -418,6 +424,7 @@ public class BatteryStatsHistory {
}
void writeToParcel(Parcel out, boolean useBlobs) {
+ Trace.traceBegin(TRACE_TAG_SYSTEM_SERVER, "BatteryStatsHistory.writeToParcel");
lock();
try {
final long start = SystemClock.uptimeMillis();
@@ -443,6 +450,7 @@ public class BatteryStatsHistory {
}
} finally {
unlock();
+ Trace.traceEnd(TRACE_TAG_SYSTEM_SERVER);
}
}
@@ -482,34 +490,39 @@ public class BatteryStatsHistory {
}
private void cleanup() {
- if (mDirectory == null) {
- return;
- }
-
- if (!tryLock()) {
- mCleanupNeeded = true;
- return;
- }
-
- mCleanupNeeded = false;
+ Trace.traceBegin(TRACE_TAG_SYSTEM_SERVER, "BatteryStatsHistory.cleanup");
try {
- // if free disk space is less than 100MB, delete oldest history file.
- if (!hasFreeDiskSpace(mDirectory)) {
- BatteryHistoryFile oldest = mHistoryFiles.remove(0);
- oldest.atomicFile.delete();
+ if (mDirectory == null) {
+ return;
+ }
+
+ if (!tryLock()) {
+ mCleanupNeeded = true;
+ return;
}
- // if there is more history stored than allowed, delete oldest history files.
- int size = getSize();
- while (size > mMaxHistorySize) {
- BatteryHistoryFile oldest = mHistoryFiles.get(0);
- int length = (int) oldest.atomicFile.getBaseFile().length();
- oldest.atomicFile.delete();
- mHistoryFiles.remove(0);
- size -= length;
+ mCleanupNeeded = false;
+ try {
+ // if free disk space is less than 100MB, delete oldest history file.
+ if (!hasFreeDiskSpace(mDirectory)) {
+ BatteryHistoryFile oldest = mHistoryFiles.remove(0);
+ oldest.atomicFile.delete();
+ }
+
+ // if there is more history stored than allowed, delete oldest history files.
+ int size = getSize();
+ while (size > mMaxHistorySize) {
+ BatteryHistoryFile oldest = mHistoryFiles.get(0);
+ int length = (int) oldest.atomicFile.getBaseFile().length();
+ oldest.atomicFile.delete();
+ mHistoryFiles.remove(0);
+ size -= length;
+ }
+ } finally {
+ unlock();
}
} finally {
- unlock();
+ Trace.traceEnd(TRACE_TAG_SYSTEM_SERVER);
}
}
}
@@ -710,13 +723,18 @@ public class BatteryStatsHistory {
* in the system directory, so it is not safe while actively writing history.
*/
public BatteryStatsHistory copy() {
- synchronized (this) {
- // Make a copy of battery history to avoid concurrent modification.
- Parcel historyBufferCopy = Parcel.obtain();
- historyBufferCopy.appendFrom(mHistoryBuffer, 0, mHistoryBuffer.dataSize());
+ Trace.traceBegin(TRACE_TAG_SYSTEM_SERVER, "BatteryStatsHistory.copy");
+ try {
+ synchronized (this) {
+ // Make a copy of battery history to avoid concurrent modification.
+ Parcel historyBufferCopy = Parcel.obtain();
+ historyBufferCopy.appendFrom(mHistoryBuffer, 0, mHistoryBuffer.dataSize());
- return new BatteryStatsHistory(historyBufferCopy, mSystemDir, 0, 0, null, null, null,
- null, mEventLogger, this);
+ return new BatteryStatsHistory(historyBufferCopy, mSystemDir, 0, 0, null, null,
+ null, null, mEventLogger, this);
+ }
+ } finally {
+ Trace.traceEnd(TRACE_TAG_SYSTEM_SERVER);
}
}
@@ -826,7 +844,7 @@ public class BatteryStatsHistory {
*/
@NonNull
public BatteryStatsHistoryIterator iterate(long startTimeMs, long endTimeMs) {
- if (mMutable) {
+ if (mMutable || mIteratorCookie != 0) {
return copy().iterate(startTimeMs, endTimeMs);
}
@@ -837,7 +855,12 @@ public class BatteryStatsHistory {
mCurrentParcel = null;
mCurrentParcelEnd = 0;
mParcelIndex = 0;
- return new BatteryStatsHistoryIterator(this, startTimeMs, endTimeMs);
+ BatteryStatsHistoryIterator iterator = new BatteryStatsHistoryIterator(
+ this, startTimeMs, endTimeMs);
+ mIteratorCookie = System.identityHashCode(iterator);
+ Trace.asyncTraceBegin(TRACE_TAG_SYSTEM_SERVER, "BatteryStatsHistory.iterate",
+ mIteratorCookie);
+ return iterator;
}
/**
@@ -848,6 +871,9 @@ public class BatteryStatsHistory {
if (mHistoryDir != null) {
mHistoryDir.unlock();
}
+ Trace.asyncTraceEnd(TRACE_TAG_SYSTEM_SERVER, "BatteryStatsHistory.iterate",
+ mIteratorCookie);
+ mIteratorCookie = 0;
}
/**
@@ -949,28 +975,33 @@ public class BatteryStatsHistory {
* @return true if success, false otherwise.
*/
public boolean readFileToParcel(Parcel out, AtomicFile file) {
- byte[] raw = null;
+ Trace.traceBegin(TRACE_TAG_SYSTEM_SERVER, "BatteryStatsHistory.read");
try {
- final long start = SystemClock.uptimeMillis();
- raw = file.readFully();
- if (DEBUG) {
- Slog.d(TAG, "readFileToParcel:" + file.getBaseFile().getPath()
- + " duration ms:" + (SystemClock.uptimeMillis() - start));
+ byte[] raw = null;
+ try {
+ final long start = SystemClock.uptimeMillis();
+ raw = file.readFully();
+ if (DEBUG) {
+ Slog.d(TAG, "readFileToParcel:" + file.getBaseFile().getPath()
+ + " duration ms:" + (SystemClock.uptimeMillis() - start));
+ }
+ } catch (Exception e) {
+ Slog.e(TAG, "Error reading file " + file.getBaseFile().getPath(), e);
+ return false;
}
- } catch (Exception e) {
- Slog.e(TAG, "Error reading file " + file.getBaseFile().getPath(), e);
- return false;
- }
- out.unmarshall(raw, 0, raw.length);
- out.setDataPosition(0);
- if (!verifyVersion(out)) {
- return false;
+ out.unmarshall(raw, 0, raw.length);
+ out.setDataPosition(0);
+ if (!verifyVersion(out)) {
+ return false;
+ }
+ // skip monotonic time field.
+ out.readLong();
+ // skip monotonic size field
+ out.readLong();
+ return true;
+ } finally {
+ Trace.traceEnd(TRACE_TAG_SYSTEM_SERVER);
}
- // skip monotonic time field.
- out.readLong();
- // skip monotonic size field
- out.readLong();
- return true;
}
/**
diff --git a/core/java/com/android/internal/security/VerityUtils.java b/core/java/com/android/internal/security/VerityUtils.java
index 7f7ea8b28546..37500766a4ac 100644
--- a/core/java/com/android/internal/security/VerityUtils.java
+++ b/core/java/com/android/internal/security/VerityUtils.java
@@ -36,7 +36,6 @@ import com.android.internal.org.bouncycastle.cms.SignerInformationVerifier;
import com.android.internal.org.bouncycastle.cms.jcajce.JcaSimpleSignerInfoVerifierBuilder;
import com.android.internal.org.bouncycastle.operator.OperatorCreationException;
-import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
@@ -53,12 +52,6 @@ import java.security.cert.X509Certificate;
public abstract class VerityUtils {
private static final String TAG = "VerityUtils";
- /**
- * File extension of the signature file. For example, foo.apk.fsv_sig is the signature file of
- * foo.apk.
- */
- public static final String FSVERITY_SIGNATURE_FILE_EXTENSION = ".fsv_sig";
-
/** SHA256 hash size. */
private static final int HASH_SIZE_BYTES = 32;
@@ -67,16 +60,6 @@ public abstract class VerityUtils {
|| SystemProperties.getInt("ro.apk_verity.mode", 0) == 2;
}
- /** Returns true if the given file looks like containing an fs-verity signature. */
- public static boolean isFsveritySignatureFile(File file) {
- return file.getName().endsWith(FSVERITY_SIGNATURE_FILE_EXTENSION);
- }
-
- /** Returns the fs-verity signature file path of the given file. */
- public static String getFsveritySignatureFilePath(String filePath) {
- return filePath + FSVERITY_SIGNATURE_FILE_EXTENSION;
- }
-
/** Enables fs-verity for the file without signature. */
public static void setUpFsverity(@NonNull String filePath) throws IOException {
int errno = enableFsverityNative(filePath);
diff --git a/core/java/com/android/internal/statusbar/IStatusBarService.aidl b/core/java/com/android/internal/statusbar/IStatusBarService.aidl
index f14e1f63cdf6..ec0954d5590a 100644
--- a/core/java/com/android/internal/statusbar/IStatusBarService.aidl
+++ b/core/java/com/android/internal/statusbar/IStatusBarService.aidl
@@ -239,4 +239,7 @@ interface IStatusBarService
/** Unbundle a categorized notification */
void unbundleNotification(String key);
+
+ /** Rebundle an (un)categorized notification */
+ void rebundleNotification(String key);
}
diff --git a/core/java/com/android/internal/widget/LockPatternUtils.java b/core/java/com/android/internal/widget/LockPatternUtils.java
index 39ddea614ee4..74707703f5f2 100644
--- a/core/java/com/android/internal/widget/LockPatternUtils.java
+++ b/core/java/com/android/internal/widget/LockPatternUtils.java
@@ -65,6 +65,7 @@ import android.util.SparseLongArray;
import android.view.InputDevice;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.ArrayUtils;
import com.android.server.LocalServices;
import com.google.android.collect.Lists;
@@ -75,6 +76,7 @@ import java.nio.charset.StandardCharsets;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
@@ -292,6 +294,56 @@ public class LockPatternUtils {
}
+ /**
+ * This exists temporarily due to trunk-stable policies.
+ * Please use ArrayUtils directly if you can.
+ */
+ public static byte[] newNonMovableByteArray(int length) {
+ if (!android.security.Flags.secureArrayZeroization()) {
+ return new byte[length];
+ }
+ return ArrayUtils.newNonMovableByteArray(length);
+ }
+
+ /**
+ * This exists temporarily due to trunk-stable policies.
+ * Please use ArrayUtils directly if you can.
+ */
+ public static char[] newNonMovableCharArray(int length) {
+ if (!android.security.Flags.secureArrayZeroization()) {
+ return new char[length];
+ }
+ return ArrayUtils.newNonMovableCharArray(length);
+ }
+
+ /**
+ * This exists temporarily due to trunk-stable policies.
+ * Please use ArrayUtils directly if you can.
+ */
+ public static void zeroize(byte[] array) {
+ if (!android.security.Flags.secureArrayZeroization()) {
+ if (array != null) {
+ Arrays.fill(array, (byte) 0);
+ }
+ return;
+ }
+ ArrayUtils.zeroize(array);
+ }
+
+ /**
+ * This exists temporarily due to trunk-stable policies.
+ * Please use ArrayUtils directly if you can.
+ */
+ public static void zeroize(char[] array) {
+ if (!android.security.Flags.secureArrayZeroization()) {
+ if (array != null) {
+ Arrays.fill(array, (char) 0);
+ }
+ return;
+ }
+ ArrayUtils.zeroize(array);
+ }
+
@UnsupportedAppUsage
public DevicePolicyManager getDevicePolicyManager() {
if (mDevicePolicyManager == null) {
diff --git a/core/java/com/android/internal/widget/LockscreenCredential.java b/core/java/com/android/internal/widget/LockscreenCredential.java
index 54b9a225f944..92ce990c67df 100644
--- a/core/java/com/android/internal/widget/LockscreenCredential.java
+++ b/core/java/com/android/internal/widget/LockscreenCredential.java
@@ -246,7 +246,7 @@ public class LockscreenCredential implements Parcelable, AutoCloseable {
*/
public void zeroize() {
if (mCredential != null) {
- Arrays.fill(mCredential, (byte) 0);
+ LockPatternUtils.zeroize(mCredential);
mCredential = null;
}
}
@@ -346,7 +346,7 @@ public class LockscreenCredential implements Parcelable, AutoCloseable {
byte[] sha1 = MessageDigest.getInstance("SHA-1").digest(saltedPassword);
byte[] md5 = MessageDigest.getInstance("MD5").digest(saltedPassword);
- Arrays.fill(saltedPassword, (byte) 0);
+ LockPatternUtils.zeroize(saltedPassword);
return HexEncoding.encodeToString(ArrayUtils.concat(sha1, md5));
} catch (NoSuchAlgorithmException e) {
throw new AssertionError("Missing digest algorithm: ", e);
diff --git a/core/java/com/android/internal/widget/NotificationExpandButton.java b/core/java/com/android/internal/widget/NotificationExpandButton.java
index 80bc4fd89c8d..dd12f69a56fb 100644
--- a/core/java/com/android/internal/widget/NotificationExpandButton.java
+++ b/core/java/com/android/internal/widget/NotificationExpandButton.java
@@ -56,8 +56,6 @@ public class NotificationExpandButton extends FrameLayout {
private int mDefaultTextColor;
private int mHighlightPillColor;
private int mHighlightTextColor;
- // Track whether this ever had mExpanded = true, so that we don't highlight it anymore.
- private boolean mWasExpanded = false;
public NotificationExpandButton(Context context) {
this(context, null, 0, 0);
@@ -136,7 +134,6 @@ public class NotificationExpandButton extends FrameLayout {
int contentDescriptionId;
if (mExpanded) {
if (notificationsRedesignTemplates()) {
- mWasExpanded = true;
drawableId = R.drawable.ic_notification_2025_collapse;
} else {
drawableId = R.drawable.ic_collapse_notification;
@@ -156,8 +153,6 @@ public class NotificationExpandButton extends FrameLayout {
if (!notificationsRedesignTemplates()) {
// changing the expanded state can affect the number display
updateNumber();
- } else {
- updateColors();
}
}
@@ -197,43 +192,22 @@ public class NotificationExpandButton extends FrameLayout {
);
}
- /**
- * Use highlight colors for the expander for groups (when the number is showing) that haven't
- * been opened before, as long as the colors are available.
- */
- private boolean shouldBeHighlighted() {
- return !mWasExpanded && shouldShowNumber()
- && mHighlightPillColor != 0 && mHighlightTextColor != 0;
- }
-
private void updateColors() {
- if (notificationsRedesignTemplates()) {
- if (shouldBeHighlighted()) {
+ if (shouldShowNumber()) {
+ if (mHighlightPillColor != 0) {
mPillDrawable.setTintList(ColorStateList.valueOf(mHighlightPillColor));
- mIconView.setColorFilter(mHighlightTextColor);
+ }
+ mIconView.setColorFilter(mHighlightTextColor);
+ if (mHighlightTextColor != 0) {
mNumberView.setTextColor(mHighlightTextColor);
- } else {
- mPillDrawable.setTintList(ColorStateList.valueOf(mDefaultPillColor));
- mIconView.setColorFilter(mDefaultTextColor);
- mNumberView.setTextColor(mDefaultTextColor);
}
} else {
- if (shouldShowNumber()) {
- if (mHighlightPillColor != 0) {
- mPillDrawable.setTintList(ColorStateList.valueOf(mHighlightPillColor));
- }
- mIconView.setColorFilter(mHighlightTextColor);
- if (mHighlightTextColor != 0) {
- mNumberView.setTextColor(mHighlightTextColor);
- }
- } else {
- if (mDefaultPillColor != 0) {
- mPillDrawable.setTintList(ColorStateList.valueOf(mDefaultPillColor));
- }
- mIconView.setColorFilter(mDefaultTextColor);
- if (mDefaultTextColor != 0) {
- mNumberView.setTextColor(mDefaultTextColor);
- }
+ if (mDefaultPillColor != 0) {
+ mPillDrawable.setTintList(ColorStateList.valueOf(mDefaultPillColor));
+ }
+ mIconView.setColorFilter(mDefaultTextColor);
+ if (mDefaultTextColor != 0) {
+ mNumberView.setTextColor(mDefaultTextColor);
}
}
}
diff --git a/core/java/com/android/internal/widget/NotificationProgressBar.java b/core/java/com/android/internal/widget/NotificationProgressBar.java
index 8cd7843fe1d9..904b73f41e70 100644
--- a/core/java/com/android/internal/widget/NotificationProgressBar.java
+++ b/core/java/com/android/internal/widget/NotificationProgressBar.java
@@ -23,6 +23,7 @@ import android.content.Context;
import android.content.res.ColorStateList;
import android.content.res.TypedArray;
import android.graphics.Canvas;
+import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
@@ -31,6 +32,7 @@ import android.graphics.drawable.LayerDrawable;
import android.os.Bundle;
import android.util.AttributeSet;
import android.util.Log;
+import android.util.Pair;
import android.view.RemotableViewMethod;
import android.widget.ProgressBar;
import android.widget.RemoteViews;
@@ -40,14 +42,15 @@ import androidx.annotation.ColorInt;
import com.android.internal.R;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.Preconditions;
-import com.android.internal.widget.NotificationProgressDrawable.Part;
-import com.android.internal.widget.NotificationProgressDrawable.Point;
-import com.android.internal.widget.NotificationProgressDrawable.Segment;
+import com.android.internal.widget.NotificationProgressDrawable.DrawablePart;
+import com.android.internal.widget.NotificationProgressDrawable.DrawablePoint;
+import com.android.internal.widget.NotificationProgressDrawable.DrawableSegment;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
import java.util.SortedSet;
import java.util.TreeSet;
@@ -56,18 +59,26 @@ import java.util.TreeSet;
* represent Notification ProgressStyle progress, such as for ridesharing and navigation.
*/
@RemoteViews.RemoteView
-public final class NotificationProgressBar extends ProgressBar {
+public final class NotificationProgressBar extends ProgressBar implements
+ NotificationProgressDrawable.BoundsChangeListener {
private static final String TAG = "NotificationProgressBar";
+ private static final boolean DEBUG = false;
private NotificationProgressDrawable mNotificationProgressDrawable;
+ private final Rect mProgressDrawableBounds = new Rect();
private NotificationProgressModel mProgressModel;
@Nullable
- private List<Part> mProgressDrawableParts = null;
+ private List<Part> mParts = null;
+
+ // List of drawable parts before segment splitting by process.
+ @Nullable
+ private List<DrawablePart> mProgressDrawableParts = null;
@Nullable
private Drawable mTracker = null;
+ private boolean mHasTrackerIcon = false;
/** @see R.styleable#NotificationProgressBar_trackerHeight */
private final int mTrackerHeight;
@@ -76,7 +87,13 @@ public final class NotificationProgressBar extends ProgressBar {
private final Matrix mMatrix = new Matrix();
private Matrix mTrackerDrawMatrix = null;
- private float mScale = 0;
+ private float mProgressFraction = 0;
+ /**
+ * The location of progress on the stretched and rescaled progress bar, in fraction. Used for
+ * calculating the tracker position. If stretching and rescaling is not needed, ==
+ * mProgressFraction.
+ */
+ private float mAdjustedProgressFraction = 0;
/** Indicates whether mTrackerPos needs to be recalculated before the tracker is drawn. */
private boolean mTrackerPosIsDirty = false;
@@ -96,20 +113,21 @@ public final class NotificationProgressBar extends ProgressBar {
int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
- final TypedArray a = context.obtainStyledAttributes(
- attrs, R.styleable.NotificationProgressBar, defStyleAttr, defStyleRes);
+ final TypedArray a = context.obtainStyledAttributes(attrs,
+ R.styleable.NotificationProgressBar, defStyleAttr, defStyleRes);
saveAttributeDataForStyleable(context, R.styleable.NotificationProgressBar, attrs, a,
defStyleAttr,
defStyleRes);
try {
mNotificationProgressDrawable = getNotificationProgressDrawable();
+ mNotificationProgressDrawable.setBoundsChangeListener(this);
} catch (IllegalStateException ex) {
Log.e(TAG, "Can't get NotificationProgressDrawable", ex);
}
// Supports setting the tracker in xml, but ProgressStyle notifications set/override it
- // via {@code setProgressTrackerIcon}.
+ // via {@code #setProgressTrackerIcon}.
final Drawable tracker = a.getDrawable(R.styleable.NotificationProgressBar_tracker);
setTracker(tracker);
@@ -126,8 +144,7 @@ public final class NotificationProgressBar extends ProgressBar {
*/
@RemotableViewMethod
public void setProgressModel(@Nullable Bundle bundle) {
- Preconditions.checkArgument(bundle != null,
- "Bundle shouldn't be null");
+ Preconditions.checkArgument(bundle != null, "Bundle shouldn't be null");
mProgressModel = NotificationProgressModel.fromBundle(bundle);
final boolean isIndeterminate = mProgressModel.isIndeterminate();
@@ -137,20 +154,25 @@ public final class NotificationProgressBar extends ProgressBar {
final int indeterminateColor = mProgressModel.getIndeterminateColor();
setIndeterminateTintList(ColorStateList.valueOf(indeterminateColor));
} else {
+ // TODO: b/372908709 - maybe don't rerun the entire calculation every time the
+ // progress model is updated? For example, if the segments and parts aren't changed,
+ // there is no need to call `processAndConvertToViewParts` again.
+
final int progress = mProgressModel.getProgress();
final int progressMax = mProgressModel.getProgressMax();
- mProgressDrawableParts = processAndConvertToDrawableParts(mProgressModel.getSegments(),
+
+ mParts = processAndConvertToViewParts(mProgressModel.getSegments(),
mProgressModel.getPoints(),
progress,
- progressMax,
- mProgressModel.isStyledByProgress());
-
- if (mNotificationProgressDrawable != null) {
- mNotificationProgressDrawable.setParts(mProgressDrawableParts);
- }
+ progressMax);
setMax(progressMax);
setProgress(progress);
+
+ if (mNotificationProgressDrawable != null
+ && mNotificationProgressDrawable.getBounds().width() != 0) {
+ updateDrawableParts();
+ }
}
}
@@ -200,9 +222,7 @@ public final class NotificationProgressBar extends ProgressBar {
} else {
progressTrackerDrawable = null;
}
- return () -> {
- setTracker(progressTrackerDrawable);
- };
+ return () -> setTracker(progressTrackerDrawable);
}
private void setTracker(@Nullable Drawable tracker) {
@@ -226,8 +246,14 @@ public final class NotificationProgressBar extends ProgressBar {
final boolean trackerSizeChanged = trackerSizeChanged(tracker, mTracker);
mTracker = tracker;
- if (mNotificationProgressDrawable != null) {
- mNotificationProgressDrawable.setHasTrackerIcon(mTracker != null);
+ final boolean hasTrackerIcon = (mTracker != null);
+ if (mHasTrackerIcon != hasTrackerIcon) {
+ mHasTrackerIcon = hasTrackerIcon;
+ if (mNotificationProgressDrawable != null
+ && mNotificationProgressDrawable.getBounds().width() != 0
+ && mProgressModel.isStyledByProgress()) {
+ updateDrawableParts();
+ }
}
configureTrackerBounds();
@@ -293,6 +319,8 @@ public final class NotificationProgressBar extends ProgressBar {
mTrackerDrawMatrix.postTranslate(Math.round(dx), Math.round(dy));
}
+ // This updates the visual position of the progress indicator, i.e., the tracker. It doesn't
+ // update the NotificationProgressDrawable, which is updated by {@code #setProgressModel}.
@Override
public synchronized void setProgress(int progress) {
super.setProgress(progress);
@@ -300,6 +328,8 @@ public final class NotificationProgressBar extends ProgressBar {
onMaybeVisualProgressChanged();
}
+ // This updates the visual position of the progress indicator, i.e., the tracker. It doesn't
+ // update the NotificationProgressDrawable, which is updated by {@code #setProgressModel}.
@Override
public void setProgress(int progress, boolean animate) {
// Animation isn't supported by NotificationProgressBar.
@@ -308,6 +338,8 @@ public final class NotificationProgressBar extends ProgressBar {
onMaybeVisualProgressChanged();
}
+ // This updates the visual position of the progress indicator, i.e., the tracker. It doesn't
+ // update the NotificationProgressDrawable, which is updated by {@code #setProgressModel}.
@Override
public synchronized void setMin(int min) {
super.setMin(min);
@@ -315,6 +347,8 @@ public final class NotificationProgressBar extends ProgressBar {
onMaybeVisualProgressChanged();
}
+ // This updates the visual position of the progress indicator, i.e., the tracker. It doesn't
+ // update the NotificationProgressDrawable, which is updated by {@code #setProgressModel}.
@Override
public synchronized void setMax(int max) {
super.setMax(max);
@@ -323,10 +357,10 @@ public final class NotificationProgressBar extends ProgressBar {
}
private void onMaybeVisualProgressChanged() {
- float scale = getScale();
- if (mScale == scale) return;
+ float progressFraction = getProgressFraction();
+ if (mProgressFraction == progressFraction) return;
- mScale = scale;
+ mProgressFraction = progressFraction;
mTrackerPosIsDirty = true;
invalidate();
}
@@ -350,8 +384,7 @@ public final class NotificationProgressBar extends ProgressBar {
super.drawableStateChanged();
final Drawable tracker = mTracker;
- if (tracker != null && tracker.isStateful()
- && tracker.setState(getDrawableState())) {
+ if (tracker != null && tracker.isStateful() && tracker.setState(getDrawableState())) {
invalidateDrawable(tracker);
}
}
@@ -372,6 +405,65 @@ public final class NotificationProgressBar extends ProgressBar {
updateTrackerAndBarPos(w, h);
}
+ @Override
+ public void onDrawableBoundsChanged() {
+ final Rect progressDrawableBounds = mNotificationProgressDrawable.getBounds();
+
+ if (mProgressDrawableBounds.equals(progressDrawableBounds)) return;
+
+ if (mProgressDrawableBounds.width() != progressDrawableBounds.width()) {
+ updateDrawableParts();
+ }
+
+ mProgressDrawableBounds.set(progressDrawableBounds);
+ }
+
+ private void updateDrawableParts() {
+ if (DEBUG) {
+ Log.d(TAG, "updateDrawableParts() called. mNotificationProgressDrawable = "
+ + mNotificationProgressDrawable + ", mParts = " + mParts);
+ }
+
+ if (mNotificationProgressDrawable == null) return;
+ if (mParts == null) return;
+
+ final float width = mNotificationProgressDrawable.getBounds().width();
+ if (width == 0) {
+ if (mProgressDrawableParts != null) {
+ if (DEBUG) {
+ Log.d(TAG, "Clearing mProgressDrawableParts");
+ }
+ mProgressDrawableParts.clear();
+ mNotificationProgressDrawable.setParts(mProgressDrawableParts);
+ }
+ return;
+ }
+
+ mProgressDrawableParts = processAndConvertToDrawableParts(
+ mParts,
+ width,
+ mNotificationProgressDrawable.getSegSegGap(),
+ mNotificationProgressDrawable.getSegPointGap(),
+ mNotificationProgressDrawable.getPointRadius(),
+ mHasTrackerIcon
+ );
+ Pair<List<DrawablePart>, Float> p = maybeStretchAndRescaleSegments(
+ mParts,
+ mProgressDrawableParts,
+ mNotificationProgressDrawable.getSegmentMinWidth(),
+ mNotificationProgressDrawable.getPointRadius(),
+ getProgressFraction(),
+ width,
+ mProgressModel.isStyledByProgress(),
+ mHasTrackerIcon ? 0F : mNotificationProgressDrawable.getSegSegGap());
+
+ if (DEBUG) {
+ Log.d(TAG, "Updating NotificationProgressDrawable parts");
+ }
+ mNotificationProgressDrawable.setParts(p.first);
+ mAdjustedProgressFraction = p.second / width;
+ }
+
private void updateTrackerAndBarPos(int w, int h) {
final int paddedHeight = h - mPaddingTop - mPaddingBottom;
final Drawable bar = getCurrentDrawable();
@@ -402,11 +494,11 @@ public final class NotificationProgressBar extends ProgressBar {
}
if (tracker != null) {
- setTrackerPos(w, tracker, mScale, trackerOffsetY);
+ setTrackerPos(w, tracker, mAdjustedProgressFraction, trackerOffsetY);
}
}
- private float getScale() {
+ private float getProgressFraction() {
int min = getMin();
int max = getMax();
int range = max - min;
@@ -416,19 +508,19 @@ public final class NotificationProgressBar extends ProgressBar {
/**
* Updates the tracker drawable bounds.
*
- * @param w Width of the view, including padding
- * @param tracker Drawable used for the tracker
- * @param scale Current progress between 0 and 1
- * @param offsetY Vertical offset for centering. If set to
- * {@link Integer#MIN_VALUE}, the current offset will be used.
+ * @param w Width of the view, including padding
+ * @param tracker Drawable used for the tracker
+ * @param progressFraction Current progress between 0 and 1
+ * @param offsetY Vertical offset for centering. If set to
+ * {@link Integer#MIN_VALUE}, the current offset will be used.
*/
- private void setTrackerPos(int w, Drawable tracker, float scale, int offsetY) {
+ private void setTrackerPos(int w, Drawable tracker, float progressFraction, int offsetY) {
int available = w - mPaddingLeft - mPaddingRight;
final int trackerWidth = tracker.getIntrinsicWidth();
final int trackerHeight = tracker.getIntrinsicHeight();
available -= ((mTrackerHeight <= 0) ? trackerWidth : mTrackerWidth);
- final int trackerPos = (int) (scale * available + 0.5f);
+ final int trackerPos = (int) (progressFraction * available + 0.5f);
final int top, bottom;
if (offsetY == Integer.MIN_VALUE) {
@@ -448,8 +540,8 @@ public final class NotificationProgressBar extends ProgressBar {
if (background != null) {
final int bkgOffsetX = mPaddingLeft;
final int bkgOffsetY = mPaddingTop;
- background.setHotspotBounds(left + bkgOffsetX, top + bkgOffsetY,
- right + bkgOffsetX, bottom + bkgOffsetY);
+ background.setHotspotBounds(left + bkgOffsetX, top + bkgOffsetY, right + bkgOffsetX,
+ bottom + bkgOffsetY);
}
// Canvas will be translated, so 0,0 is where we start drawing
@@ -482,7 +574,7 @@ public final class NotificationProgressBar extends ProgressBar {
if (mTracker == null) return;
if (mTrackerPosIsDirty) {
- setTrackerPos(getWidth(), mTracker, mScale, Integer.MIN_VALUE);
+ setTrackerPos(getWidth(), mTracker, mAdjustedProgressFraction, Integer.MIN_VALUE);
}
final int saveCount = canvas.save();
@@ -531,7 +623,7 @@ public final class NotificationProgressBar extends ProgressBar {
final Drawable tracker = mTracker;
if (tracker != null) {
- setTrackerPos(getWidth(), tracker, mScale, Integer.MIN_VALUE);
+ setTrackerPos(getWidth(), tracker, mAdjustedProgressFraction, Integer.MIN_VALUE);
// Since we draw translated, the drawable's bounds that it signals
// for invalidation won't be the actual bounds we want invalidated,
@@ -541,16 +633,14 @@ public final class NotificationProgressBar extends ProgressBar {
}
/**
- * Processes the ProgressStyle data and convert to list of {@code
- * NotificationProgressDrawable.Part}.
+ * Processes the ProgressStyle data and convert to a list of {@code Part}.
*/
@VisibleForTesting
- public static List<Part> processAndConvertToDrawableParts(
+ public static List<Part> processAndConvertToViewParts(
List<ProgressStyle.Segment> segments,
List<ProgressStyle.Point> points,
int progress,
- int progressMax,
- boolean isStyledByProgress
+ int progressMax
) {
if (segments.isEmpty()) {
throw new IllegalArgumentException("List of segments shouldn't be empty");
@@ -571,6 +661,7 @@ public final class NotificationProgressBar extends ProgressBar {
if (progress < 0 || progress > progressMax) {
throw new IllegalArgumentException("Invalid progress : " + progress);
}
+
for (ProgressStyle.Point point : points) {
final int pos = point.getPosition();
if (pos < 0 || pos > progressMax) {
@@ -583,23 +674,21 @@ public final class NotificationProgressBar extends ProgressBar {
final Map<Integer, ProgressStyle.Point> positionToPointMap = generatePositionToPointMap(
points);
final SortedSet<Integer> sortedPos = generateSortedPositionSet(startToSegmentMap,
- positionToPointMap, progress, isStyledByProgress);
+ positionToPointMap);
- final Map<Integer, ProgressStyle.Segment> startToSplitSegmentMap =
- splitSegmentsByPointsAndProgress(
- startToSegmentMap, sortedPos, progressMax);
+ final Map<Integer, ProgressStyle.Segment> startToSplitSegmentMap = splitSegmentsByPoints(
+ startToSegmentMap, sortedPos, progressMax);
- return convertToDrawableParts(startToSplitSegmentMap, positionToPointMap, sortedPos,
- progress, progressMax,
- isStyledByProgress);
+ return convertToViewParts(startToSplitSegmentMap, positionToPointMap, sortedPos,
+ progressMax);
}
// Any segment with a point on it gets split by the point.
- // If isStyledByProgress is true, also split the segment with the progress value in its range.
- private static Map<Integer, ProgressStyle.Segment> splitSegmentsByPointsAndProgress(
+ private static Map<Integer, ProgressStyle.Segment> splitSegmentsByPoints(
Map<Integer, ProgressStyle.Segment> startToSegmentMap,
SortedSet<Integer> sortedPos,
- int progressMax) {
+ int progressMax
+ ) {
int prevSegStart = 0;
for (Integer pos : sortedPos) {
if (pos == 0 || pos == progressMax) continue;
@@ -610,8 +699,7 @@ public final class NotificationProgressBar extends ProgressBar {
final ProgressStyle.Segment prevSeg = startToSegmentMap.get(prevSegStart);
final ProgressStyle.Segment leftSeg = new ProgressStyle.Segment(
- pos - prevSegStart).setColor(
- prevSeg.getColor());
+ pos - prevSegStart).setColor(prevSeg.getColor());
final ProgressStyle.Segment rightSeg = new ProgressStyle.Segment(
prevSegStart + prevSeg.getLength() - pos).setColor(prevSeg.getColor());
@@ -624,32 +712,21 @@ public final class NotificationProgressBar extends ProgressBar {
return startToSegmentMap;
}
- private static List<Part> convertToDrawableParts(
+ private static List<Part> convertToViewParts(
Map<Integer, ProgressStyle.Segment> startToSegmentMap,
Map<Integer, ProgressStyle.Point> positionToPointMap,
SortedSet<Integer> sortedPos,
- int progress,
- int progressMax,
- boolean isStyledByProgress
+ int progressMax
) {
List<Part> parts = new ArrayList<>();
- boolean styleRemainingParts = false;
for (Integer pos : sortedPos) {
if (positionToPointMap.containsKey(pos)) {
final ProgressStyle.Point point = positionToPointMap.get(pos);
- final int color = maybeGetFadedColor(point.getColor(), styleRemainingParts);
- parts.add(new Point(null, color, styleRemainingParts));
- }
- // We want the Point at the current progress to be filled (not faded), but a Segment
- // starting at this progress to be faded.
- if (isStyledByProgress && !styleRemainingParts && pos == progress) {
- styleRemainingParts = true;
+ parts.add(new Point(point.getColor()));
}
if (startToSegmentMap.containsKey(pos)) {
final ProgressStyle.Segment seg = startToSegmentMap.get(pos);
- final int color = maybeGetFadedColor(seg.getColor(), styleRemainingParts);
- parts.add(new Segment(
- (float) seg.getLength() / progressMax, color, styleRemainingParts));
+ parts.add(new Segment((float) seg.getLength() / progressMax, seg.getColor()));
}
}
@@ -660,11 +737,24 @@ public final class NotificationProgressBar extends ProgressBar {
private static int maybeGetFadedColor(@ColorInt int color, boolean fade) {
if (!fade) return color;
- return NotificationProgressDrawable.getFadedColor(color);
+ return getFadedColor(color);
+ }
+
+ /**
+ * Get a color with an opacity that's 40% of the input color.
+ */
+ @ColorInt
+ static int getFadedColor(@ColorInt int color) {
+ return Color.argb(
+ (int) (Color.alpha(color) * 0.4f + 0.5f),
+ Color.red(color),
+ Color.green(color),
+ Color.blue(color));
}
private static Map<Integer, ProgressStyle.Segment> generateStartToSegmentMap(
- List<ProgressStyle.Segment> segments) {
+ List<ProgressStyle.Segment> segments
+ ) {
final Map<Integer, ProgressStyle.Segment> startToSegmentMap = new HashMap<>();
int currentStart = 0; // Initial start position is 0
@@ -681,7 +771,8 @@ public final class NotificationProgressBar extends ProgressBar {
}
private static Map<Integer, ProgressStyle.Point> generatePositionToPointMap(
- List<ProgressStyle.Point> points) {
+ List<ProgressStyle.Point> points
+ ) {
final Map<Integer, ProgressStyle.Point> positionToPointMap = new HashMap<>();
for (ProgressStyle.Point point : points) {
@@ -693,14 +784,392 @@ public final class NotificationProgressBar extends ProgressBar {
private static SortedSet<Integer> generateSortedPositionSet(
Map<Integer, ProgressStyle.Segment> startToSegmentMap,
- Map<Integer, ProgressStyle.Point> positionToPointMap, int progress,
- boolean isStyledByProgress) {
+ Map<Integer, ProgressStyle.Point> positionToPointMap
+ ) {
final SortedSet<Integer> sortedPos = new TreeSet<>(startToSegmentMap.keySet());
sortedPos.addAll(positionToPointMap.keySet());
- if (isStyledByProgress) {
- sortedPos.add(progress);
- }
return sortedPos;
}
+
+ /**
+ * Processes the list of {@code Part} and convert to a list of {@code DrawablePart}.
+ */
+ @VisibleForTesting
+ public static List<DrawablePart> processAndConvertToDrawableParts(
+ List<Part> parts,
+ float totalWidth,
+ float segSegGap,
+ float segPointGap,
+ float pointRadius,
+ boolean hasTrackerIcon
+ ) {
+ List<DrawablePart> drawableParts = new ArrayList<>();
+
+ // generally, we will start drawing at (x, y) and end at (x+w, y)
+ float x = (float) 0;
+
+ final int nParts = parts.size();
+ for (int iPart = 0; iPart < nParts; iPart++) {
+ final Part part = parts.get(iPart);
+ final Part prevPart = iPart == 0 ? null : parts.get(iPart - 1);
+ final Part nextPart = iPart + 1 == nParts ? null : parts.get(iPart + 1);
+ if (part instanceof Segment segment) {
+ final float segWidth = segment.mFraction * totalWidth;
+ // Advance the start position to account for a point immediately prior.
+ final float startOffset = getSegStartOffset(prevPart, pointRadius, segPointGap, x);
+ final float start = x + startOffset;
+ // Retract the end position to account for the padding and a point immediately
+ // after.
+ final float endOffset = getSegEndOffset(segment, nextPart, pointRadius, segPointGap,
+ segSegGap, x + segWidth, totalWidth, hasTrackerIcon);
+ final float end = x + segWidth - endOffset;
+
+ drawableParts.add(new DrawableSegment(start, end, segment.mColor, segment.mFaded));
+
+ segment.mStart = x;
+ segment.mEnd = x + segWidth;
+
+ // Advance the current position to account for the segment's fraction of the total
+ // width (ignoring offset and padding)
+ x += segWidth;
+ } else if (part instanceof Point point) {
+ final float pointWidth = 2 * pointRadius;
+ float start = x - pointRadius;
+ if (start < 0) start = 0;
+ float end = start + pointWidth;
+ if (end > totalWidth) {
+ end = totalWidth;
+ if (totalWidth > pointWidth) start = totalWidth - pointWidth;
+ }
+
+ drawableParts.add(new DrawablePoint(start, end, point.mColor));
+ }
+ }
+
+ return drawableParts;
+ }
+
+ private static float getSegStartOffset(Part prevPart, float pointRadius, float segPointGap,
+ float startX) {
+ if (!(prevPart instanceof Point)) return 0F;
+ final float pointOffset = (startX < pointRadius) ? (pointRadius - startX) : 0;
+ return pointOffset + pointRadius + segPointGap;
+ }
+
+ private static float getSegEndOffset(Segment seg, Part nextPart, float pointRadius,
+ float segPointGap, float segSegGap, float endX, float totalWidth,
+ boolean hasTrackerIcon) {
+ if (nextPart == null) return 0F;
+ if (nextPart instanceof Segment nextSeg) {
+ if (!seg.mFaded && nextSeg.mFaded) {
+ // @see Segment#mFaded
+ return hasTrackerIcon ? 0F : segSegGap;
+ }
+ return segSegGap;
+ }
+
+ final float pointWidth = 2 * pointRadius;
+ final float pointOffset = (endX + pointRadius > totalWidth && totalWidth > pointWidth)
+ ? (endX + pointRadius - totalWidth) : 0;
+ return segPointGap + pointRadius + pointOffset;
+ }
+
+ /**
+ * Processes the list of {@code DrawablePart} data and convert to a pair of:
+ * - list of processed {@code DrawablePart}.
+ * - location of progress on the stretched and rescaled progress bar.
+ */
+ @VisibleForTesting
+ public static Pair<List<DrawablePart>, Float> maybeStretchAndRescaleSegments(
+ List<Part> parts,
+ List<DrawablePart> drawableParts,
+ float segmentMinWidth,
+ float pointRadius,
+ float progressFraction,
+ float totalWidth,
+ boolean isStyledByProgress,
+ float progressGap
+ ) {
+ final List<DrawableSegment> drawableSegments = drawableParts
+ .stream()
+ .filter(DrawableSegment.class::isInstance)
+ .map(DrawableSegment.class::cast)
+ .toList();
+ float totalExcessWidth = 0;
+ float totalPositiveExcessWidth = 0;
+ for (DrawableSegment drawableSegment : drawableSegments) {
+ final float excessWidth = drawableSegment.getWidth() - segmentMinWidth;
+ totalExcessWidth += excessWidth;
+ if (excessWidth > 0) totalPositiveExcessWidth += excessWidth;
+ }
+
+ // All drawable segments are above minimum width. No need to stretch and rescale.
+ if (totalExcessWidth == totalPositiveExcessWidth) {
+ return maybeSplitDrawableSegmentsByProgress(
+ parts,
+ drawableParts,
+ progressFraction,
+ totalWidth,
+ isStyledByProgress,
+ progressGap);
+ }
+
+ if (totalExcessWidth < 0) {
+ // TODO: b/372908709 - throw an error so that the caller can catch and go to fallback
+ // option. (instead of return.)
+ Log.w(TAG, "Not enough width to satisfy the minimum width for segments.");
+ return maybeSplitDrawableSegmentsByProgress(
+ parts,
+ drawableParts,
+ progressFraction,
+ totalWidth,
+ isStyledByProgress,
+ progressGap);
+ }
+
+ final int nParts = drawableParts.size();
+ float startOffset = 0;
+ for (int iPart = 0; iPart < nParts; iPart++) {
+ final DrawablePart drawablePart = drawableParts.get(iPart);
+ if (drawablePart instanceof DrawableSegment drawableSegment) {
+ final float origDrawableSegmentWidth = drawableSegment.getWidth();
+
+ float drawableSegmentWidth = segmentMinWidth;
+ // Allocate the totalExcessWidth to the segments above minimum, proportionally to
+ // their initial excessWidth.
+ if (origDrawableSegmentWidth > segmentMinWidth) {
+ drawableSegmentWidth +=
+ totalExcessWidth * (origDrawableSegmentWidth - segmentMinWidth)
+ / totalPositiveExcessWidth;
+ }
+
+ final float widthDiff = drawableSegmentWidth - drawableSegment.getWidth();
+
+ // Adjust drawable segments to new widths
+ drawableSegment.setStart(drawableSegment.getStart() + startOffset);
+ drawableSegment.setEnd(
+ drawableSegment.getStart() + origDrawableSegmentWidth + widthDiff);
+
+ // Also adjust view segments to new width. (For view segments, only start is
+ // needed?)
+ // Check that segments and drawableSegments are of the same size?
+ final Segment segment = (Segment) parts.get(iPart);
+ final float origSegmentWidth = segment.getWidth();
+ segment.mStart = segment.mStart + startOffset;
+ segment.mEnd = segment.mStart + origSegmentWidth + widthDiff;
+
+ // Increase startOffset for the subsequent segments.
+ startOffset += widthDiff;
+ } else if (drawablePart instanceof DrawablePoint drawablePoint) {
+ drawablePoint.setStart(drawablePoint.getStart() + startOffset);
+ drawablePoint.setEnd(drawablePoint.getStart() + 2 * pointRadius);
+ }
+ }
+
+ return maybeSplitDrawableSegmentsByProgress(
+ parts,
+ drawableParts,
+ progressFraction,
+ totalWidth,
+ isStyledByProgress,
+ progressGap);
+ }
+
+ /**
+ * Find the location of progress on the stretched and rescaled progress bar.
+ * If isStyledByProgress is true, also split the drawable segment with the progress value in its
+ * range. Style the drawable parts after process with reduced opacity and segment height.
+ */
+ private static Pair<List<DrawablePart>, Float> maybeSplitDrawableSegmentsByProgress(
+ // Needed to get the original segment start and end positions in pixels.
+ List<Part> parts,
+ List<DrawablePart> drawableParts,
+ float progressFraction,
+ float totalWidth,
+ boolean isStyledByProgress,
+ float progressGap
+ ) {
+ if (progressFraction == 1) return new Pair<>(drawableParts, totalWidth);
+
+ int iPartFirstSegmentToStyle = -1;
+ int iPartSegmentToSplit = -1;
+ float rescaledProgressX = 0;
+ float startFraction = 0;
+ final int nParts = parts.size();
+ for (int iPart = 0; iPart < nParts; iPart++) {
+ final Part part = parts.get(iPart);
+ if (!(part instanceof Segment)) continue;
+ final Segment segment = (Segment) part;
+ if (startFraction == progressFraction) {
+ iPartFirstSegmentToStyle = iPart;
+ rescaledProgressX = segment.mStart;
+ break;
+ } else if (startFraction < progressFraction
+ && progressFraction < startFraction + segment.mFraction) {
+ iPartSegmentToSplit = iPart;
+ rescaledProgressX = segment.mStart
+ + (progressFraction - startFraction) / segment.mFraction
+ * segment.getWidth();
+ break;
+ }
+ startFraction += segment.mFraction;
+ }
+
+ if (!isStyledByProgress) return new Pair<>(drawableParts, rescaledProgressX);
+
+ List<DrawablePart> splitDrawableParts = new ArrayList<>();
+ boolean styleRemainingParts = false;
+ for (int iPart = 0; iPart < nParts; iPart++) {
+ final DrawablePart drawablePart = drawableParts.get(iPart);
+ if (drawablePart instanceof DrawablePoint drawablePoint) {
+ final int color = maybeGetFadedColor(drawablePoint.getColor(), styleRemainingParts);
+ splitDrawableParts.add(
+ new DrawablePoint(drawablePoint.getStart(), drawablePoint.getEnd(), color));
+ }
+ if (iPart == iPartFirstSegmentToStyle) styleRemainingParts = true;
+ if (drawablePart instanceof DrawableSegment drawableSegment) {
+ if (iPart == iPartSegmentToSplit) {
+ if (rescaledProgressX <= drawableSegment.getStart()) {
+ styleRemainingParts = true;
+ final int color = maybeGetFadedColor(drawableSegment.getColor(), true);
+ splitDrawableParts.add(new DrawableSegment(drawableSegment.getStart(),
+ drawableSegment.getEnd(), color, true));
+ } else if (drawableSegment.getStart() < rescaledProgressX
+ && rescaledProgressX < drawableSegment.getEnd()) {
+ splitDrawableParts.add(new DrawableSegment(drawableSegment.getStart(),
+ rescaledProgressX - progressGap, drawableSegment.getColor()));
+ final int color = maybeGetFadedColor(drawableSegment.getColor(), true);
+ splitDrawableParts.add(
+ new DrawableSegment(rescaledProgressX, drawableSegment.getEnd(),
+ color, true));
+ styleRemainingParts = true;
+ } else {
+ splitDrawableParts.add(new DrawableSegment(drawableSegment.getStart(),
+ drawableSegment.getEnd(), drawableSegment.getColor()));
+ styleRemainingParts = true;
+ }
+ } else {
+ final int color = maybeGetFadedColor(drawableSegment.getColor(),
+ styleRemainingParts);
+ splitDrawableParts.add(new DrawableSegment(drawableSegment.getStart(),
+ drawableSegment.getEnd(), color, styleRemainingParts));
+ }
+ }
+ }
+
+ return new Pair<>(splitDrawableParts, rescaledProgressX);
+ }
+
+ /**
+ * A part of the progress bar, which is either a {@link Segment} with non-zero length, or a
+ * {@link Point} with zero length.
+ */
+ // TODO: b/372908709 - maybe this should be made private? Only test the final
+ // NotificationDrawable.Parts.
+ public interface Part {
+ }
+
+ /**
+ * A segment is a part of the progress bar with non-zero length. For example, it can
+ * represent a portion in a navigation journey with certain traffic condition.
+ */
+ public static final class Segment implements Part {
+ private final float mFraction;
+ @ColorInt
+ private final int mColor;
+ /**
+ * Whether the segment is faded or not.
+ * <p>
+ * <pre>
+ * When mFaded is set to true, a combination of the following is done to the segment:
+ * 1. The drawing color is mColor with opacity updated to 40%.
+ * 2. The gap between faded and non-faded segments is:
+ * - the segment-segment gap, when there is no tracker icon
+ * - 0, when there is tracker icon
+ * </pre>
+ * </p>
+ */
+ private final boolean mFaded;
+
+ /** Start position (in pixels) */
+ private float mStart;
+ /** End position (in pixels */
+ private float mEnd;
+
+ public Segment(float fraction, @ColorInt int color) {
+ this(fraction, color, false);
+ }
+
+ public Segment(float fraction, @ColorInt int color, boolean faded) {
+ mFraction = fraction;
+ mColor = color;
+ mFaded = faded;
+ }
+
+ /** Returns the calculated drawing width of the part */
+ public float getWidth() {
+ return mEnd - mStart;
+ }
+
+ @Override
+ public String toString() {
+ return "Segment(fraction=" + this.mFraction + ", color=" + this.mColor + ", faded="
+ + this.mFaded + "), mStart = " + this.mStart + ", mEnd = " + this.mEnd;
+ }
+
+ // Needed for unit tests
+ @Override
+ public boolean equals(@androidx.annotation.Nullable Object other) {
+ if (this == other) return true;
+
+ if (other == null || getClass() != other.getClass()) return false;
+
+ Segment that = (Segment) other;
+ if (Float.compare(this.mFraction, that.mFraction) != 0) return false;
+ if (this.mColor != that.mColor) return false;
+ return this.mFaded == that.mFaded;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mFraction, mColor, mFaded);
+ }
+ }
+
+ /**
+ * A point is a part of the progress bar with zero length. Points are designated points within a
+ * progress bar to visualize distinct stages or milestones. For example, a stop in a multi-stop
+ * ride-share journey.
+ */
+ public static final class Point implements Part {
+ @ColorInt
+ private final int mColor;
+
+ public Point(@ColorInt int color) {
+ mColor = color;
+ }
+
+ @Override
+ public String toString() {
+ return "Point(color=" + this.mColor + ")";
+ }
+
+ // Needed for unit tests.
+ @Override
+ public boolean equals(@androidx.annotation.Nullable Object other) {
+ if (this == other) return true;
+
+ if (other == null || getClass() != other.getClass()) return false;
+
+ Point that = (Point) other;
+
+ return this.mColor == that.mColor;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mColor);
+ }
+ }
}
diff --git a/core/java/com/android/internal/widget/NotificationProgressDrawable.java b/core/java/com/android/internal/widget/NotificationProgressDrawable.java
index 8629a1c95202..4ece81c24edc 100644
--- a/core/java/com/android/internal/widget/NotificationProgressDrawable.java
+++ b/core/java/com/android/internal/widget/NotificationProgressDrawable.java
@@ -21,7 +21,6 @@ import android.content.res.Resources;
import android.content.res.Resources.Theme;
import android.content.res.TypedArray;
import android.graphics.Canvas;
-import android.graphics.Color;
import android.graphics.ColorFilter;
import android.graphics.Paint;
import android.graphics.PixelFormat;
@@ -49,22 +48,24 @@ import java.util.Objects;
/**
* This is used by NotificationProgressBar for displaying a custom background. It composes of
- * segments, which have non-zero length, and points, which have zero length.
+ * segments, which have non-zero length varying drawing width, and points, which have zero length
+ * and fixed size for drawing.
*
- * @see Segment
- * @see Point
+ * @see DrawableSegment
+ * @see DrawablePoint
*/
public final class NotificationProgressDrawable extends Drawable {
private static final String TAG = "NotifProgressDrawable";
+ @Nullable
+ private BoundsChangeListener mBoundsChangeListener = null;
+
private State mState;
private boolean mMutated;
- private final ArrayList<Part> mParts = new ArrayList<>();
- private boolean mHasTrackerIcon;
+ private final ArrayList<DrawablePart> mParts = new ArrayList<>();
private final RectF mSegRectF = new RectF();
- private final Rect mPointRect = new Rect();
private final RectF mPointRectF = new RectF();
private final Paint mFillPaint = new Paint();
@@ -80,33 +81,37 @@ public final class NotificationProgressDrawable extends Drawable {
}
/**
- * <p>Set the segment default color for the drawable.</p>
- * <p>Note: changing this property will affect all instances of a drawable loaded from a
- * resource. It is recommended to invoke {@link #mutate()} before changing this property.</p>
- *
- * @param color The color of the stroke
- * @see #mutate()
+ * Returns the gap between two segments.
*/
- public void setSegmentDefaultColor(@ColorInt int color) {
- mState.setSegmentColor(color);
+ public float getSegSegGap() {
+ return mState.mSegSegGap;
}
/**
- * <p>Set the point rect default color for the drawable.</p>
- * <p>Note: changing this property will affect all instances of a drawable loaded from a
- * resource. It is recommended to invoke {@link #mutate()} before changing this property.</p>
- *
- * @param color The color of the point rect
- * @see #mutate()
+ * Returns the gap between a segment and a point.
+ */
+ public float getSegPointGap() {
+ return mState.mSegPointGap;
+ }
+
+ /**
+ * Returns the gap between a segment and a point.
*/
- public void setPointRectDefaultColor(@ColorInt int color) {
- mState.setPointRectColor(color);
+ public float getSegmentMinWidth() {
+ return mState.mSegmentMinWidth;
+ }
+
+ /**
+ * Returns the radius for the points.
+ */
+ public float getPointRadius() {
+ return mState.mPointRadius;
}
/**
* Set the segments and points that constitute the drawable.
*/
- public void setParts(List<Part> parts) {
+ public void setParts(List<DrawablePart> parts) {
mParts.clear();
mParts.addAll(parts);
@@ -116,51 +121,22 @@ public final class NotificationProgressDrawable extends Drawable {
/**
* Set the segments and points that constitute the drawable.
*/
- public void setParts(@NonNull Part... parts) {
+ public void setParts(@NonNull DrawablePart... parts) {
setParts(Arrays.asList(parts));
}
- /**
- * Set whether a tracker is drawn on top of this NotificationProgressDrawable.
- */
- public void setHasTrackerIcon(boolean hasTrackerIcon) {
- if (mHasTrackerIcon != hasTrackerIcon) {
- mHasTrackerIcon = hasTrackerIcon;
- invalidateSelf();
- }
- }
-
@Override
public void draw(@NonNull Canvas canvas) {
- final float pointRadius =
- mState.mPointRadius; // how big the point icon will be, halved
-
- // generally, we will start drawing at (x, y) and end at (x+w, y)
- float x = (float) getBounds().left;
+ final float pointRadius = mState.mPointRadius;
+ final float left = (float) getBounds().left;
final float centerY = (float) getBounds().centerY();
- final float totalWidth = (float) getBounds().width();
- float segPointGap = mState.mSegPointGap;
final int numParts = mParts.size();
for (int iPart = 0; iPart < numParts; iPart++) {
- final Part part = mParts.get(iPart);
- final Part prevPart = iPart == 0 ? null : mParts.get(iPart - 1);
- final Part nextPart = iPart + 1 == numParts ? null : mParts.get(iPart + 1);
- if (part instanceof Segment segment) {
- final float segWidth = segment.mFraction * totalWidth;
- // Advance the start position to account for a point immediately prior.
- final float startOffset = getSegStartOffset(prevPart, pointRadius, segPointGap, x);
- final float start = x + startOffset;
- // Retract the end position to account for the padding and a point immediately
- // after.
- final float endOffset = getSegEndOffset(segment, nextPart, pointRadius, segPointGap,
- mState.mSegSegGap, x + segWidth, totalWidth, mHasTrackerIcon);
- final float end = x + segWidth - endOffset;
-
- // Advance the current position to account for the segment's fraction of the total
- // width (ignoring offset and padding)
- x += segWidth;
-
+ final DrawablePart part = mParts.get(iPart);
+ final float start = left + part.mStart;
+ final float end = left + part.mEnd;
+ if (part instanceof DrawableSegment segment) {
// No space left to draw the segment
if (start > end) continue;
@@ -168,67 +144,23 @@ public final class NotificationProgressDrawable extends Drawable {
: mState.mSegmentHeight / 2F;
final float cornerRadius = mState.mSegmentCornerRadius;
- mFillPaint.setColor(segment.mColor != Color.TRANSPARENT ? segment.mColor
- : (segment.mFaded ? mState.mFadedSegmentColor : mState.mSegmentColor));
+ mFillPaint.setColor(segment.mColor);
mSegRectF.set(start, centerY - radiusY, end, centerY + radiusY);
canvas.drawRoundRect(mSegRectF, cornerRadius, cornerRadius, mFillPaint);
- } else if (part instanceof Point point) {
- final float pointWidth = 2 * pointRadius;
- float start = x - pointRadius;
- if (start < 0) start = 0;
- float end = start + pointWidth;
- if (end > totalWidth) {
- end = totalWidth;
- if (totalWidth > pointWidth) start = totalWidth - pointWidth;
- }
- mPointRect.set((int) start, (int) (centerY - pointRadius), (int) end,
- (int) (centerY + pointRadius));
-
- if (point.mIcon != null) {
- point.mIcon.setBounds(mPointRect);
- point.mIcon.draw(canvas);
- } else {
- // TODO: b/367804171 - actually use a vector asset for the default point
- // rather than drawing it as a box?
- mPointRectF.set(start, centerY - pointRadius, end, centerY + pointRadius);
- final float inset = mState.mPointRectInset;
- final float cornerRadius = mState.mPointRectCornerRadius;
- mPointRectF.inset(inset, inset);
-
- mFillPaint.setColor(point.mColor != Color.TRANSPARENT ? point.mColor
- : (point.mFaded ? mState.mFadedPointRectColor
- : mState.mPointRectColor));
-
- canvas.drawRoundRect(mPointRectF, cornerRadius, cornerRadius, mFillPaint);
- }
- }
- }
- }
+ } else if (part instanceof DrawablePoint point) {
+ // TODO: b/367804171 - actually use a vector asset for the default point
+ // rather than drawing it as a box?
+ mPointRectF.set(start, centerY - pointRadius, end, centerY + pointRadius);
+ final float inset = mState.mPointRectInset;
+ final float cornerRadius = mState.mPointRectCornerRadius;
+ mPointRectF.inset(inset, inset);
- private static float getSegStartOffset(Part prevPart, float pointRadius, float segPointGap,
- float startX) {
- if (!(prevPart instanceof Point)) return 0F;
- final float pointOffset = (startX < pointRadius) ? (pointRadius - startX) : 0;
- return pointOffset + pointRadius + segPointGap;
- }
+ mFillPaint.setColor(point.mColor);
- private static float getSegEndOffset(Segment seg, Part nextPart, float pointRadius,
- float segPointGap,
- float segSegGap, float endX, float totalWidth, boolean hasTrackerIcon) {
- if (nextPart == null) return 0F;
- if (nextPart instanceof Segment nextSeg) {
- if (!seg.mFaded && nextSeg.mFaded) {
- // @see Segment#mFaded
- return hasTrackerIcon ? 0F : segSegGap;
+ canvas.drawRoundRect(mPointRectF, cornerRadius, cornerRadius, mFillPaint);
}
- return segSegGap;
}
-
- final float pointWidth = 2 * pointRadius;
- final float pointOffset = (endX + pointRadius > totalWidth && totalWidth > pointWidth)
- ? (endX + pointRadius - totalWidth) : 0;
- return segPointGap + pointRadius + pointOffset;
}
@Override
@@ -260,6 +192,19 @@ public final class NotificationProgressDrawable extends Drawable {
return PixelFormat.UNKNOWN;
}
+ public void setBoundsChangeListener(BoundsChangeListener listener) {
+ mBoundsChangeListener = listener;
+ }
+
+ @Override
+ protected void onBoundsChange(Rect bounds) {
+ super.onBoundsChange(bounds);
+
+ if (mBoundsChangeListener != null) {
+ mBoundsChangeListener.onDrawableBoundsChanged();
+ }
+ }
+
@Override
public void inflate(@NonNull Resources r, @NonNull XmlPullParser parser,
@NonNull AttributeSet attrs, @Nullable Resources.Theme theme)
@@ -384,6 +329,8 @@ public final class NotificationProgressDrawable extends Drawable {
// Extract the theme attributes, if any.
state.mThemeAttrsSegments = a.extractThemeAttrs();
+ state.mSegmentMinWidth = a.getDimension(
+ R.styleable.NotificationProgressDrawableSegments_minWidth, state.mSegmentMinWidth);
state.mSegmentHeight = a.getDimension(
R.styleable.NotificationProgressDrawableSegments_height, state.mSegmentHeight);
state.mFadedSegmentHeight = a.getDimension(
@@ -392,9 +339,6 @@ public final class NotificationProgressDrawable extends Drawable {
state.mSegmentCornerRadius = a.getDimension(
R.styleable.NotificationProgressDrawableSegments_cornerRadius,
state.mSegmentCornerRadius);
- final int color = a.getColor(R.styleable.NotificationProgressDrawableSegments_color,
- state.mSegmentColor);
- setSegmentDefaultColor(color);
}
private void updatePointsFromTypedArray(TypedArray a) {
@@ -413,9 +357,6 @@ public final class NotificationProgressDrawable extends Drawable {
state.mPointRectCornerRadius = a.getDimension(
R.styleable.NotificationProgressDrawablePoints_cornerRadius,
state.mPointRectCornerRadius);
- final int color = a.getColor(R.styleable.NotificationProgressDrawablePoints_color,
- state.mPointRectColor);
- setPointRectDefaultColor(color);
}
static int resolveDensity(@Nullable Resources r, int parentDensity) {
@@ -464,63 +405,57 @@ public final class NotificationProgressDrawable extends Drawable {
}
/**
- * A part of the progress bar, which is either a S{@link Segment} with non-zero length, or a
- * {@link Point} with zero length.
+ * Listener to receive updates about drawable bounds changing
*/
- public interface Part {
+ public interface BoundsChangeListener {
+ /** Called when bounds have changed */
+ void onDrawableBoundsChanged();
}
/**
- * A segment is a part of the progress bar with non-zero length. For example, it can
- * represent a portion in a navigation journey with certain traffic condition.
- *
+ * A part of the progress drawable, which is either a {@link DrawableSegment} with non-zero
+ * length and varying drawing width, or a {@link DrawablePoint} with zero length and fixed size
+ * for drawing.
*/
- public static final class Segment implements Part {
- private final float mFraction;
- @ColorInt private final int mColor;
- /** Whether the segment is faded or not.
- * <p>
- * <pre>
- * When mFaded is set to true, a combination of the following is done to the segment:
- * 1. The drawing color is mColor with opacity updated to 40%.
- * 2. The gap between faded and non-faded segments is:
- * - the segment-segment gap, when there is no tracker icon
- * - 0, when there is tracker icon
- * </pre>
- * </p>
- */
- private final boolean mFaded;
-
- public Segment(float fraction) {
- this(fraction, Color.TRANSPARENT);
+ public abstract static class DrawablePart {
+ // TODO: b/372908709 - maybe rename start/end to left/right, to be consistent with the
+ // bounds rect.
+ /** Start position for drawing (in pixels) */
+ protected float mStart;
+ /** End position for drawing (in pixels) */
+ protected float mEnd;
+ /** Drawing color. */
+ @ColorInt protected final int mColor;
+
+ protected DrawablePart(float start, float end, @ColorInt int color) {
+ mStart = start;
+ mEnd = end;
+ mColor = color;
}
- public Segment(float fraction, @ColorInt int color) {
- this(fraction, color, false);
+ public float getStart() {
+ return this.mStart;
}
- public Segment(float fraction, @ColorInt int color, boolean faded) {
- mFraction = fraction;
- mColor = color;
- mFaded = faded;
+ public void setStart(float start) {
+ mStart = start;
}
- public float getFraction() {
- return this.mFraction;
+ public float getEnd() {
+ return this.mEnd;
}
- public int getColor() {
- return this.mColor;
+ public void setEnd(float end) {
+ mEnd = end;
}
- public boolean getFaded() {
- return this.mFaded;
+ /** Returns the calculated drawing width of the part */
+ public float getWidth() {
+ return mEnd - mStart;
}
- @Override
- public String toString() {
- return "Segment(fraction=" + this.mFraction + ", color=" + this.mColor + ", faded="
- + this.mFaded + ')';
+ public int getColor() {
+ return this.mColor;
}
// Needed for unit tests
@@ -530,80 +465,79 @@ public final class NotificationProgressDrawable extends Drawable {
if (other == null || getClass() != other.getClass()) return false;
- Segment that = (Segment) other;
- if (Float.compare(this.mFraction, that.mFraction) != 0) return false;
- if (this.mColor != that.mColor) return false;
- return this.mFaded == that.mFaded;
+ DrawablePart that = (DrawablePart) other;
+ if (Float.compare(this.mStart, that.mStart) != 0) return false;
+ if (Float.compare(this.mEnd, that.mEnd) != 0) return false;
+ return this.mColor == that.mColor;
}
@Override
public int hashCode() {
- return Objects.hash(mFraction, mColor, mFaded);
+ return Objects.hash(mStart, mEnd, mColor);
}
}
/**
- * A point is a part of the progress bar with zero length. Points are designated points within a
- * progressbar to visualize distinct stages or milestones. For example, a stop in a multi-stop
- * ride-share journey.
+ * A segment is a part of the progress bar with non-zero length. For example, it can
+ * represent a portion in a navigation journey with certain traffic condition.
+ * <p>
+ * The start and end positions for drawing a segment are assumed to have been adjusted for
+ * the Points and gaps neighboring the segment.
+ * </p>
*/
- public static final class Point implements Part {
- @Nullable
- private final Drawable mIcon;
- @ColorInt private final int mColor;
+ public static final class DrawableSegment extends DrawablePart {
+ /**
+ * Whether the segment is faded or not.
+ * <p>
+ * Faded segments and non-faded segments are drawn with different heights.
+ * </p>
+ */
private final boolean mFaded;
- public Point(@Nullable Drawable icon) {
- this(icon, Color.TRANSPARENT, false);
- }
-
- public Point(@Nullable Drawable icon, @ColorInt int color) {
- this(icon, color, false);
-
+ public DrawableSegment(float start, float end, int color) {
+ this(start, end, color, false);
}
- public Point(@Nullable Drawable icon, @ColorInt int color, boolean faded) {
- mIcon = icon;
- mColor = color;
+ public DrawableSegment(float start, float end, int color, boolean faded) {
+ super(start, end, color);
mFaded = faded;
}
- @Nullable
- public Drawable getIcon() {
- return this.mIcon;
- }
-
- public int getColor() {
- return this.mColor;
- }
-
- public boolean getFaded() {
- return this.mFaded;
- }
-
@Override
public String toString() {
- return "Point(icon=" + this.mIcon + ", color=" + this.mColor + ", faded=" + this.mFaded
- + ")";
+ return "Segment(start=" + this.mStart + ", end=" + this.mEnd + ", color=" + this.mColor
+ + ", faded=" + this.mFaded + ')';
}
// Needed for unit tests.
@Override
public boolean equals(@Nullable Object other) {
- if (this == other) return true;
-
- if (other == null || getClass() != other.getClass()) return false;
+ if (!super.equals(other)) return false;
- Point that = (Point) other;
-
- if (!Objects.equals(this.mIcon, that.mIcon)) return false;
- if (this.mColor != that.mColor) return false;
+ DrawableSegment that = (DrawableSegment) other;
return this.mFaded == that.mFaded;
}
@Override
public int hashCode() {
- return Objects.hash(mIcon, mColor, mFaded);
+ return Objects.hash(super.hashCode(), mFaded);
+ }
+ }
+
+ /**
+ * A point is a part of the progress bar with zero length. Points are designated points within a
+ * progress bar to visualize distinct stages or milestones. For example, a stop in a multi-stop
+ * ride-share journey.
+ */
+ public static final class DrawablePoint extends DrawablePart {
+ public DrawablePoint(float start, float end, int color) {
+ super(start, end, color);
+ }
+
+ @Override
+ public String toString() {
+ return "Point(start=" + this.mStart + ", end=" + this.mEnd + ", color=" + this.mColor
+ + ")";
}
}
@@ -628,16 +562,14 @@ public final class NotificationProgressDrawable extends Drawable {
int mChangingConfigurations;
float mSegSegGap = 0.0f;
float mSegPointGap = 0.0f;
+ float mSegmentMinWidth = 0.0f;
float mSegmentHeight;
float mFadedSegmentHeight;
float mSegmentCornerRadius;
- int mSegmentColor;
- int mFadedSegmentColor;
+ // how big the point icon will be, halved
float mPointRadius;
float mPointRectInset;
float mPointRectCornerRadius;
- int mPointRectColor;
- int mFadedPointRectColor;
int[] mThemeAttrs;
int[] mThemeAttrsSegments;
@@ -652,16 +584,13 @@ public final class NotificationProgressDrawable extends Drawable {
mChangingConfigurations = orig.mChangingConfigurations;
mSegSegGap = orig.mSegSegGap;
mSegPointGap = orig.mSegPointGap;
+ mSegmentMinWidth = orig.mSegmentMinWidth;
mSegmentHeight = orig.mSegmentHeight;
mFadedSegmentHeight = orig.mFadedSegmentHeight;
mSegmentCornerRadius = orig.mSegmentCornerRadius;
- mSegmentColor = orig.mSegmentColor;
- mFadedSegmentColor = orig.mFadedSegmentColor;
mPointRadius = orig.mPointRadius;
mPointRectInset = orig.mPointRectInset;
mPointRectCornerRadius = orig.mPointRectCornerRadius;
- mPointRectColor = orig.mPointRectColor;
- mFadedPointRectColor = orig.mFadedPointRectColor;
mThemeAttrs = orig.mThemeAttrs;
mThemeAttrsSegments = orig.mThemeAttrsSegments;
@@ -674,6 +603,18 @@ public final class NotificationProgressDrawable extends Drawable {
}
private void applyDensityScaling(int sourceDensity, int targetDensity) {
+ if (mSegSegGap > 0) {
+ mSegSegGap = scaleFromDensity(
+ mSegSegGap, sourceDensity, targetDensity);
+ }
+ if (mSegPointGap > 0) {
+ mSegPointGap = scaleFromDensity(
+ mSegPointGap, sourceDensity, targetDensity);
+ }
+ if (mSegmentMinWidth > 0) {
+ mSegmentMinWidth = scaleFromDensity(
+ mSegmentMinWidth, sourceDensity, targetDensity);
+ }
if (mSegmentHeight > 0) {
mSegmentHeight = scaleFromDensity(
mSegmentHeight, sourceDensity, targetDensity);
@@ -740,28 +681,6 @@ public final class NotificationProgressDrawable extends Drawable {
applyDensityScaling(sourceDensity, targetDensity);
}
}
-
- public void setSegmentColor(int color) {
- mSegmentColor = color;
- mFadedSegmentColor = getFadedColor(color);
- }
-
- public void setPointRectColor(int color) {
- mPointRectColor = color;
- mFadedPointRectColor = getFadedColor(color);
- }
- }
-
- /**
- * Get a color with an opacity that's 25% of the input color.
- */
- @ColorInt
- static int getFadedColor(@ColorInt int color) {
- return Color.argb(
- (int) (Color.alpha(color) * 0.4f + 0.5f),
- Color.red(color),
- Color.green(color),
- Color.blue(color));
}
@Override
diff --git a/core/jni/android_content_res_ApkAssets.cpp b/core/jni/android_content_res_ApkAssets.cpp
index e6364a96bd9f..1e7bfe32ba79 100644
--- a/core/jni/android_content_res_ApkAssets.cpp
+++ b/core/jni/android_content_res_ApkAssets.cpp
@@ -111,8 +111,9 @@ static void DeleteGuardedApkAssets(Guarded<AssetManager2::ApkAssetsPtr>& apk_ass
class LoaderAssetsProvider : public AssetsProvider {
public:
static std::unique_ptr<AssetsProvider> Create(JNIEnv* env, jobject assets_provider) {
- return std::unique_ptr<AssetsProvider>{
- assets_provider ? new LoaderAssetsProvider(env, assets_provider) : nullptr};
+ return (!assets_provider) ? EmptyAssetsProvider::Create()
+ : std::unique_ptr<AssetsProvider>(new LoaderAssetsProvider(
+ env, assets_provider));
}
bool ForEachFile(const std::string& /* root_path */,
@@ -128,8 +129,8 @@ class LoaderAssetsProvider : public AssetsProvider {
return debug_name_;
}
- UpToDate IsUpToDate() const override {
- return UpToDate::Always;
+ bool IsUpToDate() const override {
+ return true;
}
~LoaderAssetsProvider() override {
@@ -211,7 +212,7 @@ class LoaderAssetsProvider : public AssetsProvider {
auto string_result = static_cast<jstring>(env->CallObjectMethod(
assets_provider_, gAssetsProviderOffsets.toString));
ScopedUtfChars str(env, string_result);
- debug_name_ = std::string(str.c_str());
+ debug_name_ = std::string(str.c_str(), str.size());
}
// The global reference to the AssetsProvider
@@ -242,10 +243,10 @@ static jlong NativeLoad(JNIEnv* env, jclass /*clazz*/, const format_type_t forma
apk_assets = ApkAssets::LoadOverlay(path.c_str(), property_flags);
break;
case FORMAT_ARSC:
- apk_assets = ApkAssets::LoadTable(AssetsProvider::CreateAssetFromFile(path.c_str()),
- MultiAssetsProvider::Create(std::move(loader_assets)),
- property_flags);
- break;
+ apk_assets = ApkAssets::LoadTable(AssetsProvider::CreateAssetFromFile(path.c_str()),
+ std::move(loader_assets),
+ property_flags);
+ break;
case FORMAT_DIRECTORY: {
auto assets = MultiAssetsProvider::Create(std::move(loader_assets),
DirectoryAssetsProvider::Create(path.c_str()));
@@ -315,11 +316,10 @@ static jlong NativeLoadFromFd(JNIEnv* env, jclass /*clazz*/, const format_type_t
break;
}
case FORMAT_ARSC:
- apk_assets = ApkAssets::LoadTable(AssetsProvider::CreateAssetFromFd(std::move(dup_fd),
- nullptr /* path */),
- MultiAssetsProvider::Create(std::move(loader_assets)),
- property_flags);
- break;
+ apk_assets = ApkAssets::LoadTable(
+ AssetsProvider::CreateAssetFromFd(std::move(dup_fd), nullptr /* path */),
+ std::move(loader_assets), property_flags);
+ break;
default:
const std::string error_msg = base::StringPrintf("Unsupported format type %d", format);
jniThrowException(env, "java/lang/IllegalArgumentException", error_msg.c_str());
@@ -386,15 +386,12 @@ static jlong NativeLoadFromFdOffset(JNIEnv* env, jclass /*clazz*/, const format_
break;
}
case FORMAT_ARSC:
- apk_assets =
- ApkAssets::LoadTable(AssetsProvider::CreateAssetFromFd(std::move(dup_fd),
- nullptr /* path */,
- static_cast<off64_t>(offset),
- static_cast<off64_t>(
- length)),
- MultiAssetsProvider::Create(std::move(loader_assets)),
- property_flags);
- break;
+ apk_assets = ApkAssets::LoadTable(
+ AssetsProvider::CreateAssetFromFd(std::move(dup_fd), nullptr /* path */,
+ static_cast<off64_t>(offset),
+ static_cast<off64_t>(length)),
+ std::move(loader_assets), property_flags);
+ break;
default:
const std::string error_msg = base::StringPrintf("Unsupported format type %d", format);
jniThrowException(env, "java/lang/IllegalArgumentException", error_msg.c_str());
@@ -411,16 +408,13 @@ static jlong NativeLoadFromFdOffset(JNIEnv* env, jclass /*clazz*/, const format_
}
static jlong NativeLoadEmpty(JNIEnv* env, jclass /*clazz*/, jint flags, jobject assets_provider) {
- auto apk_assets = ApkAssets::Load(MultiAssetsProvider::Create(
- LoaderAssetsProvider::Create(env, assets_provider)),
- flags);
- if (apk_assets == nullptr) {
- const std::string error_msg =
- base::StringPrintf("Failed to load empty assets with provider %p",
- (void*)assets_provider);
- jniThrowException(env, "java/io/IOException", error_msg.c_str());
- return 0;
- }
+ auto apk_assets = ApkAssets::Load(LoaderAssetsProvider::Create(env, assets_provider), flags);
+ if (apk_assets == nullptr) {
+ const std::string error_msg =
+ base::StringPrintf("Failed to load empty assets with provider %p", (void*)assets_provider);
+ jniThrowException(env, "java/io/IOException", error_msg.c_str());
+ return 0;
+ }
return CreateGuardedApkAssets(std::move(apk_assets));
}
@@ -449,10 +443,10 @@ static jlong NativeGetStringBlock(JNIEnv* /*env*/, jclass /*clazz*/, jlong ptr)
return reinterpret_cast<jlong>(apk_assets->GetLoadedArsc()->GetStringPool());
}
-static jint NativeIsUpToDate(CRITICAL_JNI_PARAMS_COMMA jlong ptr) {
+static jboolean NativeIsUpToDate(CRITICAL_JNI_PARAMS_COMMA jlong ptr) {
auto scoped_apk_assets = ScopedLock(ApkAssetsFromLong(ptr));
auto apk_assets = scoped_apk_assets->get();
- return (jint)apk_assets->IsUpToDate();
+ return apk_assets->IsUpToDate() ? JNI_TRUE : JNI_FALSE;
}
static jlong NativeOpenXml(JNIEnv* env, jclass /*clazz*/, jlong ptr, jstring file_name) {
@@ -564,7 +558,7 @@ static const JNINativeMethod gApkAssetsMethods[] = {
{"nativeGetDebugName", "(J)Ljava/lang/String;", (void*)NativeGetDebugName},
{"nativeGetStringBlock", "(J)J", (void*)NativeGetStringBlock},
// @CriticalNative
- {"nativeIsUpToDate", "(J)I", (void*)NativeIsUpToDate},
+ {"nativeIsUpToDate", "(J)Z", (void*)NativeIsUpToDate},
{"nativeOpenXml", "(JLjava/lang/String;)J", (void*)NativeOpenXml},
{"nativeGetOverlayableInfo", "(JLjava/lang/String;)Landroid/content/om/OverlayableInfo;",
(void*)NativeGetOverlayableInfo},
diff --git a/core/jni/android_hardware_UsbDeviceConnection.cpp b/core/jni/android_hardware_UsbDeviceConnection.cpp
index b1221ee38db3..68ef3d424d12 100644
--- a/core/jni/android_hardware_UsbDeviceConnection.cpp
+++ b/core/jni/android_hardware_UsbDeviceConnection.cpp
@@ -165,19 +165,25 @@ android_hardware_UsbDeviceConnection_control_request(JNIEnv *env, jobject thiz,
return -1;
}
- jbyte* bufferBytes = NULL;
- if (buffer) {
- bufferBytes = (jbyte*)env->GetPrimitiveArrayCritical(buffer, NULL);
+ bool is_dir_in = (requestType & USB_ENDPOINT_DIR_MASK) == USB_DIR_IN;
+ std::unique_ptr<jbyte[]> bufferBytes(new (std::nothrow) jbyte[length]);
+ if (!bufferBytes) {
+ jniThrowException(env, "java/lang/OutOfMemoryError", NULL);
+ return -1;
+ }
+
+ if (!is_dir_in && buffer) {
+ env->GetByteArrayRegion(buffer, start, length, bufferBytes.get());
}
- jint result = usb_device_control_transfer(device, requestType, request,
- value, index, bufferBytes + start, length, timeout);
+ jint bytes_transferred = usb_device_control_transfer(device, requestType, request,
+ value, index, bufferBytes.get(), length, timeout);
- if (bufferBytes) {
- env->ReleasePrimitiveArrayCritical(buffer, bufferBytes, 0);
+ if (bytes_transferred > 0 && is_dir_in) {
+ env->SetByteArrayRegion(buffer, start, bytes_transferred, bufferBytes.get());
}
- return result;
+ return bytes_transferred;
}
static jint
diff --git a/core/jni/android_os_Debug.cpp b/core/jni/android_os_Debug.cpp
index 3c2dccd451d4..9ef17e82c38e 100644
--- a/core/jni/android_os_Debug.cpp
+++ b/core/jni/android_os_Debug.cpp
@@ -729,6 +729,17 @@ static jlong android_os_Debug_getGpuPrivateMemoryKb(JNIEnv* env, jobject clazz)
return gpuPrivateMem / 1024;
}
+static jlong android_os_Debug_getKernelCmaUsageKb(JNIEnv* env, jobject clazz) {
+ jlong totalKernelCmaUsageKb = -1;
+ uint64_t size;
+
+ if (meminfo::ReadKernelCmaUsageKb(&size)) {
+ totalKernelCmaUsageKb = size;
+ }
+
+ return totalKernelCmaUsageKb;
+}
+
static jlong android_os_Debug_getDmabufMappedSizeKb(JNIEnv* env, jobject clazz) {
jlong dmabufPss = 0;
std::vector<dmabufinfo::DmaBuffer> dmabufs;
@@ -836,6 +847,7 @@ static const JNINativeMethod gMethods[] = {
{"getGpuTotalUsageKb", "()J", (void*)android_os_Debug_getGpuTotalUsageKb},
{"isVmapStack", "()Z", (void*)android_os_Debug_isVmapStack},
{"logAllocatorStats", "()Z", (void*)android_os_Debug_logAllocatorStats},
+ {"getKernelCmaUsageKb", "()J", (void*)android_os_Debug_getKernelCmaUsageKb},
};
int register_android_os_Debug(JNIEnv *env)
diff --git a/core/jni/android_view_InputEventReceiver.cpp b/core/jni/android_view_InputEventReceiver.cpp
index 3a1e8835c8db..6272fb1947c1 100644
--- a/core/jni/android_view_InputEventReceiver.cpp
+++ b/core/jni/android_view_InputEventReceiver.cpp
@@ -441,7 +441,8 @@ status_t NativeInputEventReceiver::consumeEvents(JNIEnv* env,
}
env->CallVoidMethod(receiverObj.get(), gInputEventReceiverClassInfo.onDragEvent,
jboolean(dragEvent->isExiting()), dragEvent->getX(),
- dragEvent->getY());
+ dragEvent->getY(),
+ static_cast<jint>(dragEvent->getDisplayId().val()));
finishInputEvent(seq, /*handled=*/true);
continue;
}
@@ -643,7 +644,7 @@ int register_android_view_InputEventReceiver(JNIEnv* env) {
GetMethodIDOrDie(env, gInputEventReceiverClassInfo.clazz, "onPointerCaptureEvent",
"(Z)V");
gInputEventReceiverClassInfo.onDragEvent =
- GetMethodIDOrDie(env, gInputEventReceiverClassInfo.clazz, "onDragEvent", "(ZFF)V");
+ GetMethodIDOrDie(env, gInputEventReceiverClassInfo.clazz, "onDragEvent", "(ZFFI)V");
gInputEventReceiverClassInfo.onTouchModeChanged =
GetMethodIDOrDie(env, gInputEventReceiverClassInfo.clazz, "onTouchModeChanged", "(Z)V");
gInputEventReceiverClassInfo.onBatchedInputEventPending =
diff --git a/core/proto/android/providers/settings/secure.proto b/core/proto/android/providers/settings/secure.proto
index 4f7ba9388a1d..5d0b340ac839 100644
--- a/core/proto/android/providers/settings/secure.proto
+++ b/core/proto/android/providers/settings/secure.proto
@@ -580,6 +580,7 @@ message SecureSettingsProto {
optional SettingProto activate_on_dock = 3 [ (android.privacy).dest = DEST_AUTOMATIC ];
optional SettingProto activate_on_sleep = 4 [ (android.privacy).dest = DEST_AUTOMATIC ];
optional SettingProto default_component = 5 [ (android.privacy).dest = DEST_AUTOMATIC ];
+ optional SettingProto activate_on_postured = 6 [ (android.privacy).dest = DEST_AUTOMATIC ];
}
optional Screensaver screensaver = 47;
diff --git a/core/res/res/drawable/notification_progress.xml b/core/res/res/drawable/notification_progress.xml
index 5d272fb00e34..ff5450ee106f 100644
--- a/core/res/res/drawable/notification_progress.xml
+++ b/core/res/res/drawable/notification_progress.xml
@@ -24,6 +24,7 @@
android:segPointGap="@dimen/notification_progress_segPoint_gap">
<segments
android:color="?attr/colorProgressBackgroundNormal"
+ android:minWidth="@dimen/notification_progress_segments_min_width"
android:height="@dimen/notification_progress_segments_height"
android:fadedHeight="@dimen/notification_progress_segments_faded_height"
android:cornerRadius="@dimen/notification_progress_segments_corner_radius"/>
diff --git a/core/res/res/values-round-watch/dimens.xml b/core/res/res/values-round-watch/dimens.xml
index f288b41fb556..59ee554798bc 100644
--- a/core/res/res/values-round-watch/dimens.xml
+++ b/core/res/res/values-round-watch/dimens.xml
@@ -26,6 +26,6 @@
<item name="input_extract_action_button_height" type="dimen">32dp</item>
<item name="input_extract_action_icon_padding" type="dimen">5dp</item>
- <item name="global_actions_vertical_padding_percentage" type="fraction">20.8%</item>
+ <item name="global_actions_vertical_padding_percentage" type="fraction">21.8%</item>
<item name="global_actions_horizontal_padding_percentage" type="fraction">5.2%</item>
</resources>
diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml
index 728c856f5855..8372aecf0d27 100644
--- a/core/res/res/values/attrs.xml
+++ b/core/res/res/values/attrs.xml
@@ -7572,25 +7572,31 @@
<!-- NotificationProgressDrawable class -->
<!-- ================================== -->
- <!-- Drawable used to render a segmented bar, with segments and points. -->
+ <!-- Drawable used to render a notification progress bar, with segments and points. -->
<!-- @hide internal use only -->
<declare-styleable name="NotificationProgressDrawable">
- <!-- Default color for the parts. -->
+ <!-- The gap between two segments. -->
<attr name="segSegGap" format="dimension" />
+ <!-- The gap between a segment and a point. -->
<attr name="segPointGap" format="dimension" />
</declare-styleable>
<!-- Used to config the segments of a NotificationProgressDrawable. -->
<!-- @hide internal use only -->
<declare-styleable name="NotificationProgressDrawableSegments">
- <!-- Height of the solid segments -->
+ <!-- TODO: b/372908709 - maybe move this to NotificationProgressBar, because that's the only
+ place this is used actually. Same for NotificationProgressDrawable.segSegGap/segPointGap
+ above. -->
+ <!-- Minimum required drawing width. The drawing width refers to the width after
+ the original segments have been adjusted for the neighboring Points and gaps. This is
+ enforced by stretching the segments that are too short. -->
+ <attr name="minWidth" format="dimension" />
+ <!-- Height of the solid segments. -->
<attr name="height" />
- <!-- Height of the faded segments -->
- <attr name="fadedHeight" format="dimension"/>
+ <!-- Height of the faded segments. -->
+ <attr name="fadedHeight" format="dimension" />
<!-- Corner radius of the segment rect. -->
<attr name="cornerRadius" format="dimension" />
- <!-- Default color of the segment. -->
- <attr name="color" />
</declare-styleable>
<!-- Used to config the points of a NotificationProgressDrawable. -->
@@ -7602,8 +7608,6 @@
<attr name="inset" />
<!-- Corner radius of the point rect. -->
<attr name="cornerRadius"/>
- <!-- Default color of the point rect. -->
- <attr name="color" />
</declare-styleable>
<!-- ========================== -->
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index ce9a0c636d62..e14cffd72b0c 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -2766,6 +2766,9 @@
<bool name="config_dreamsActivatedOnDockByDefault">true</bool>
<!-- If supported and enabled, are dreams activated when asleep and charging? (by default) -->
<bool name="config_dreamsActivatedOnSleepByDefault">false</bool>
+ <!-- If supported and enabled, are dreams enabled while device is stationary and upright?
+ (by default) -->
+ <bool name="config_dreamsActivatedOnPosturedByDefault">false</bool>
<!-- ComponentName of the default dream (Settings.Secure.DEFAULT_SCREENSAVER_COMPONENT) -->
<string name="config_dreamsDefaultComponent" translatable="false">com.android.deskclock/com.android.deskclock.Screensaver</string>
<!-- ComponentNames of the dreams that we should hide -->
diff --git a/core/res/res/values/dimens.xml b/core/res/res/values/dimens.xml
index 4f7351c7cc4d..d6b8704a978b 100644
--- a/core/res/res/values/dimens.xml
+++ b/core/res/res/values/dimens.xml
@@ -899,6 +899,8 @@
<dimen name="notification_progress_segSeg_gap">4dp</dimen>
<!-- The gap between a segment and a point in the notification progress bar -->
<dimen name="notification_progress_segPoint_gap">4dp</dimen>
+ <!-- The minimum required drawing width of the notification progress bar segments -->
+ <dimen name="notification_progress_segments_min_width">16dp</dimen>
<!-- The height of the notification progress bar segments -->
<dimen name="notification_progress_segments_height">6dp</dimen>
<!-- The height of the notification progress bar faded segments -->
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index debc5e9a0dce..fa4c21de682e 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -6646,6 +6646,8 @@ ul.</string>
<string name="satellite_notification_title">Auto connected to satellite</string>
<!-- Notification summary when satellite service is auto connected. [CHAR LIMIT=NONE] -->
<string name="satellite_notification_summary">You can send and receive messages without a mobile or Wi-Fi network</string>
+ <!-- Notification summary when satellite service connected with data service supported. [CHAR LIMIT=NONE] -->
+ <string name="satellite_notification_summary_with_data">You can send and receive messages and use limited data by satellite</string>
<!-- Notification title when satellite service can be manually enabled. -->
<string name="satellite_notification_manual_title">Use satellite messaging?</string>
<!-- Notification summary when satellite service can be manually enabled. [CHAR LIMIT=NONE] -->
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index f89ca44cce30..68008e57094d 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -2330,6 +2330,7 @@
<java-symbol type="bool" name="config_dreamsEnabledOnBattery" />
<java-symbol type="bool" name="config_dreamsActivatedOnDockByDefault" />
<java-symbol type="bool" name="config_dreamsActivatedOnSleepByDefault" />
+ <java-symbol type="bool" name="config_dreamsActivatedOnPosturedByDefault" />
<java-symbol type="integer" name="config_dreamsBatteryLevelMinimumWhenPowered" />
<java-symbol type="integer" name="config_dreamsBatteryLevelMinimumWhenNotPowered" />
<java-symbol type="integer" name="config_dreamsBatteryLevelDrainCutoff" />
@@ -3950,6 +3951,7 @@
<java-symbol type="dimen" name="notification_progress_tracker_height" />
<java-symbol type="dimen" name="notification_progress_segSeg_gap" />
<java-symbol type="dimen" name="notification_progress_segPoint_gap" />
+ <java-symbol type="dimen" name="notification_progress_segments_min_width" />
<java-symbol type="dimen" name="notification_progress_segments_height" />
<java-symbol type="dimen" name="notification_progress_segments_faded_height" />
<java-symbol type="dimen" name="notification_progress_segments_corner_radius" />
@@ -5650,6 +5652,7 @@
<!-- System notification for satellite service -->
<java-symbol type="string" name="satellite_notification_title" />
<java-symbol type="string" name="satellite_notification_summary" />
+ <java-symbol type="string" name="satellite_notification_summary_with_data" />
<java-symbol type="string" name="satellite_notification_manual_title" />
<java-symbol type="string" name="satellite_notification_manual_summary" />
<java-symbol type="string" name="satellite_notification_open_message" />
diff --git a/core/res/res/xml/sms_short_codes.xml b/core/res/res/xml/sms_short_codes.xml
index 06cd44e6544d..9b3a6cba5f23 100644
--- a/core/res/res/xml/sms_short_codes.xml
+++ b/core/res/res/xml/sms_short_codes.xml
@@ -111,7 +111,7 @@
<shortcode country="cz" premium="90\\d{5}|90\\d{3}" free="116\\d{3}" />
<!-- Germany: 4-5 digits plus 1232xxx (premium codes from http://www.vodafone.de/infofaxe/537.pdf and http://premiumdienste.eplus.de/pdf/kodex.pdf), plus EU. To keep the premium regex from being too large, it only includes payment processors that have been used by SMS malware, with the regular pattern matching the other premium short codes. -->
- <shortcode country="de" pattern="\\d{4,5}|1232\\d{3}" premium="11(?:111|833)|1232(?:013|021|060|075|286|358)|118(?:44|80|86)|20[25]00|220(?:21|22|88|99)|221(?:14|21)|223(?:44|53|77)|224[13]0|225(?:20|59|90)|226(?:06|10|20|26|30|40|56|70)|227(?:07|33|39|66|76|78|79|88|99)|228(?:08|11|66|77)|23300|30030|3[12347]000|330(?:33|55|66)|33(?:233|331|366|533)|34(?:34|567)|37000|40(?:040|123|444|[3568]00)|41(?:010|414)|44(?:000|044|344|44[24]|544)|50005|50100|50123|50555|51000|52(?:255|783)|54(?:100|2542)|55(?:077|[24]00|222|333|55|[12369]55)|56(?:789|886)|60800|6[13]000|66(?:[12348]66|566|766|777|88|999)|68888|70(?:07|123|777)|76766|77(?:007|070|222|444|[567]77)|80(?:008|123|888)|82(?:002|[378]00|323|444|472|474|488|727)|83(?:005|[169]00|333|830)|84(?:141|300|32[34]|343|488|499|777|888)|85888|86(?:188|566|640|644|650|677|868|888)|870[24]9|871(?:23|[49]9)|872(?:1[0-8]|49|99)|87499|875(?:49|55|99)|876(?:0[1367]|1[1245678]|54|99)|877(?:00|99)|878(?:15|25|3[567]|8[12])|87999|880(?:08|44|55|77|99)|88688|888(?:03|10|8|89)|8899|90(?:009|999)|99999" free="116\\d{3}|81214|81215|47529|70296|83782|3011|73240|72438" />
+ <shortcode country="de" pattern="\\d{4,5}|1232\\d{3}" premium="11(?:111|833)|1232(?:013|021|060|075|286|358)|118(?:44|80|86)|20[25]00|220(?:21|22|88|99)|221(?:14|21)|223(?:44|53|77)|224[13]0|225(?:20|59|90)|226(?:06|10|20|26|30|40|56|70)|227(?:07|33|39|66|76|78|79|88|99)|228(?:08|11|66|77)|23300|30030|3[12347]000|330(?:33|55|66)|33(?:233|331|366|533)|34(?:34|567)|37000|40(?:040|123|444|[3568]00)|41(?:010|414)|44(?:000|044|344|44[24]|544)|50005|50100|50123|50555|51000|52(?:255|783)|54(?:100|2542)|55(?:077|[24]00|222|333|55|[12369]55)|56(?:789|886)|60800|6[13]000|66(?:[12348]66|566|766|777|88|999)|68888|70(?:07|123|777)|76766|77(?:007|070|222|444|[567]77)|80(?:008|123|888)|82(?:002|[378]00|323|444|472|474|488|727)|83(?:005|[169]00|333|830)|84(?:141|300|32[34]|343|488|499|777|888)|85888|86(?:188|566|640|644|650|677|868|888)|870[24]9|871(?:23|[49]9)|872(?:1[0-8]|49|99)|87499|875(?:49|55|99)|876(?:0[1367]|1[1245678]|54|99)|877(?:00|99)|878(?:15|25|3[567]|8[12])|87999|880(?:08|44|55|77|99)|88688|888(?:03|10|8|89)|8899|90(?:009|999)|99999" free="116\\d{3}|81214|81215|47529|70296|83782|3011|73240|72438|70997" />
<!-- Denmark: see http://iprs.webspacecommerce.com/Denmark-Premium-Rate-Numbers -->
<shortcode country="dk" pattern="\\d{4,5}" premium="1\\d{3}" free="116\\d{3}|4665" />
diff --git a/core/tests/coretests/src/android/app/NotificationManagerTest.java b/core/tests/coretests/src/android/app/NotificationManagerTest.java
index 6538ce85457c..3d6e1225bd92 100644
--- a/core/tests/coretests/src/android/app/NotificationManagerTest.java
+++ b/core/tests/coretests/src/android/app/NotificationManagerTest.java
@@ -16,6 +16,8 @@
package android.app;
+import static com.google.common.truth.Truth.assertThat;
+
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
@@ -25,8 +27,12 @@ import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
import android.content.Context;
+import android.content.ContextWrapper;
+import android.content.pm.ParceledListSlice;
+import android.os.UserHandle;
import android.platform.test.annotations.EnableFlags;
import android.platform.test.annotations.Presubmit;
import android.platform.test.flag.junit.SetFlagsRule;
@@ -35,6 +41,7 @@ import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
+import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
@@ -42,6 +49,7 @@ import org.junit.runner.RunWith;
import java.time.Instant;
import java.time.InstantSource;
+import java.util.List;
@RunWith(AndroidJUnit4.class)
@SmallTest
@@ -50,14 +58,24 @@ public class NotificationManagerTest {
@Rule
public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
- private Context mContext;
private NotificationManagerWithMockService mNotificationManager;
private final FakeClock mClock = new FakeClock();
+ private PackageTestableContext mContext;
+
@Before
public void setUp() {
- mContext = ApplicationProvider.getApplicationContext();
+ mContext = new PackageTestableContext(ApplicationProvider.getApplicationContext());
mNotificationManager = new NotificationManagerWithMockService(mContext, mClock);
+
+ // Caches must be in test mode in order to be used in tests.
+ PropertyInvalidatedCache.setTestMode(true);
+ mNotificationManager.setChannelCacheToTestMode();
+ }
+
+ @After
+ public void tearDown() {
+ PropertyInvalidatedCache.setTestMode(false);
}
@Test
@@ -243,12 +261,161 @@ public class NotificationManagerTest {
anyInt(), any(), anyInt());
}
+ @Test
+ @EnableFlags(Flags.FLAG_NM_BINDER_PERF_CACHE_CHANNELS)
+ public void getNotificationChannel_cachedUntilInvalidated() throws Exception {
+ // Invalidate the cache first because the cache won't do anything until then
+ NotificationManager.invalidateNotificationChannelCache();
+
+ // It doesn't matter what the returned contents are, as long as we return a channel.
+ // This setup must set up getNotificationChannels(), as that's the method called.
+ when(mNotificationManager.mBackendService.getNotificationChannels(any(), any(),
+ anyInt())).thenReturn(new ParceledListSlice<>(List.of(exampleChannel())));
+
+ // ask for the same channel 100 times without invalidating the cache
+ for (int i = 0; i < 100; i++) {
+ NotificationChannel unused = mNotificationManager.getNotificationChannel("id");
+ }
+
+ // invalidate the cache; then ask again
+ NotificationManager.invalidateNotificationChannelCache();
+ NotificationChannel unused = mNotificationManager.getNotificationChannel("id");
+
+ verify(mNotificationManager.mBackendService, times(2))
+ .getNotificationChannels(any(), any(), anyInt());
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_NM_BINDER_PERF_CACHE_CHANNELS)
+ public void getNotificationChannel_sameApp_oneCall() throws Exception {
+ NotificationManager.invalidateNotificationChannelCache();
+
+ NotificationChannel c1 = new NotificationChannel("id1", "name1",
+ NotificationManager.IMPORTANCE_DEFAULT);
+ NotificationChannel c2 = new NotificationChannel("id2", "name2",
+ NotificationManager.IMPORTANCE_NONE);
+
+ when(mNotificationManager.mBackendService.getNotificationChannels(any(), any(),
+ anyInt())).thenReturn(new ParceledListSlice<>(List.of(c1, c2)));
+
+ assertThat(mNotificationManager.getNotificationChannel("id1")).isEqualTo(c1);
+ assertThat(mNotificationManager.getNotificationChannel("id2")).isEqualTo(c2);
+ assertThat(mNotificationManager.getNotificationChannel("id3")).isNull();
+
+ verify(mNotificationManager.mBackendService, times(1))
+ .getNotificationChannels(any(), any(), anyInt());
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_NM_BINDER_PERF_CACHE_CHANNELS)
+ public void getNotificationChannels_cachedUntilInvalidated() throws Exception {
+ NotificationManager.invalidateNotificationChannelCache();
+ when(mNotificationManager.mBackendService.getNotificationChannels(any(), any(),
+ anyInt())).thenReturn(new ParceledListSlice<>(List.of(exampleChannel())));
+
+ // ask for channels 100 times without invalidating the cache
+ for (int i = 0; i < 100; i++) {
+ List<NotificationChannel> unused = mNotificationManager.getNotificationChannels();
+ }
+
+ // invalidate the cache; then ask again
+ NotificationManager.invalidateNotificationChannelCache();
+ List<NotificationChannel> res = mNotificationManager.getNotificationChannels();
+
+ verify(mNotificationManager.mBackendService, times(2))
+ .getNotificationChannels(any(), any(), anyInt());
+ assertThat(res).containsExactlyElementsIn(List.of(exampleChannel()));
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_NM_BINDER_PERF_CACHE_CHANNELS)
+ public void getNotificationChannel_channelAndConversationLookup() throws Exception {
+ NotificationManager.invalidateNotificationChannelCache();
+
+ // Full list of channels: c1; conv1 = child of c1; c2 is unrelated
+ NotificationChannel c1 = new NotificationChannel("id", "name",
+ NotificationManager.IMPORTANCE_DEFAULT);
+ NotificationChannel conv1 = new NotificationChannel("", "name_conversation",
+ NotificationManager.IMPORTANCE_DEFAULT);
+ conv1.setConversationId("id", "id_conversation");
+ NotificationChannel c2 = new NotificationChannel("other", "name2",
+ NotificationManager.IMPORTANCE_DEFAULT);
+
+ when(mNotificationManager.mBackendService.getNotificationChannels(any(), any(), anyInt()))
+ .thenReturn(new ParceledListSlice<>(List.of(c1, conv1, c2)));
+
+ // Lookup for channel c1 and c2: returned as expected
+ assertThat(mNotificationManager.getNotificationChannel("id")).isEqualTo(c1);
+ assertThat(mNotificationManager.getNotificationChannel("other")).isEqualTo(c2);
+
+ // Lookup for conv1 should return conv1
+ assertThat(mNotificationManager.getNotificationChannel("id", "id_conversation")).isEqualTo(
+ conv1);
+
+ // Lookup for a different conversation channel that doesn't exist, whose parent channel id
+ // is "id", should return c1
+ assertThat(mNotificationManager.getNotificationChannel("id", "nonexistent")).isEqualTo(c1);
+
+ // Lookup of a nonexistent channel is null
+ assertThat(mNotificationManager.getNotificationChannel("id3")).isNull();
+
+ // All of that should have been one call to getNotificationChannels()
+ verify(mNotificationManager.mBackendService, times(1))
+ .getNotificationChannels(any(), any(), anyInt());
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_NM_BINDER_PERF_CACHE_CHANNELS)
+ public void getNotificationChannel_differentPackages() throws Exception {
+ NotificationManager.invalidateNotificationChannelCache();
+ final String pkg1 = "one";
+ final String pkg2 = "two";
+ final int userId = 0;
+ final int userId1 = 1;
+
+ // multiple channels with the same ID, but belonging to different packages/users
+ NotificationChannel channel1 = new NotificationChannel("id", "name1",
+ NotificationManager.IMPORTANCE_DEFAULT);
+ NotificationChannel channel2 = channel1.copy();
+ channel2.setName("name2");
+ NotificationChannel channel3 = channel1.copy();
+ channel3.setName("name3");
+
+ when(mNotificationManager.mBackendService.getNotificationChannels(any(), eq(pkg1),
+ eq(userId))).thenReturn(new ParceledListSlice<>(List.of(channel1)));
+ when(mNotificationManager.mBackendService.getNotificationChannels(any(), eq(pkg2),
+ eq(userId))).thenReturn(new ParceledListSlice<>(List.of(channel2)));
+ when(mNotificationManager.mBackendService.getNotificationChannels(any(), eq(pkg1),
+ eq(userId1))).thenReturn(new ParceledListSlice<>(List.of(channel3)));
+
+ // set our context to pretend to be from package 1 and userId 0
+ mContext.setParameters(pkg1, pkg1, userId);
+ assertThat(mNotificationManager.getNotificationChannel("id")).isEqualTo(channel1);
+
+ // now package 2
+ mContext.setParameters(pkg2, pkg2, userId);
+ assertThat(mNotificationManager.getNotificationChannel("id")).isEqualTo(channel2);
+
+ // now pkg1 for a different user
+ mContext.setParameters(pkg1, pkg1, userId1);
+ assertThat(mNotificationManager.getNotificationChannel("id")).isEqualTo(channel3);
+
+ // Those should have been three different calls
+ verify(mNotificationManager.mBackendService, times(3))
+ .getNotificationChannels(any(), any(), anyInt());
+ }
+
private Notification exampleNotification() {
return new Notification.Builder(mContext, "channel")
.setSmallIcon(android.R.drawable.star_big_on)
.build();
}
+ private NotificationChannel exampleChannel() {
+ return new NotificationChannel("id", "channel_name",
+ NotificationManager.IMPORTANCE_DEFAULT);
+ }
+
private static class NotificationManagerWithMockService extends NotificationManager {
private final INotificationManager mBackendService;
@@ -264,6 +431,48 @@ public class NotificationManagerTest {
}
}
+ // Helper context wrapper class where we can control just the return values of getPackageName,
+ // getOpPackageName, and getUserId (used in getNotificationChannels).
+ private static class PackageTestableContext extends ContextWrapper {
+ private String mPackage;
+ private String mOpPackage;
+ private Integer mUserId;
+
+ PackageTestableContext(Context base) {
+ super(base);
+ }
+
+ void setParameters(String packageName, String opPackageName, int userId) {
+ mPackage = packageName;
+ mOpPackage = opPackageName;
+ mUserId = userId;
+ }
+
+ @Override
+ public String getPackageName() {
+ if (mPackage != null) return mPackage;
+ return super.getPackageName();
+ }
+
+ @Override
+ public String getOpPackageName() {
+ if (mOpPackage != null) return mOpPackage;
+ return super.getOpPackageName();
+ }
+
+ @Override
+ public int getUserId() {
+ if (mUserId != null) return mUserId;
+ return super.getUserId();
+ }
+
+ @Override
+ public UserHandle getUser() {
+ if (mUserId != null) return UserHandle.of(mUserId);
+ return super.getUser();
+ }
+ }
+
private static class FakeClock implements InstantSource {
private long mNowMillis = 441644400000L;
diff --git a/core/tests/coretests/src/android/app/NotificationTest.java b/core/tests/coretests/src/android/app/NotificationTest.java
index ca6ad6fae46e..7be6950fb613 100644
--- a/core/tests/coretests/src/android/app/NotificationTest.java
+++ b/core/tests/coretests/src/android/app/NotificationTest.java
@@ -2504,6 +2504,21 @@ public class NotificationTest {
@Test
@EnableFlags(Flags.FLAG_API_RICH_ONGOING)
+ public void progressStyle_setProgressSegments() {
+ final List<Notification.ProgressStyle.Segment> segments = List.of(
+ new Notification.ProgressStyle.Segment(100).setColor(Color.WHITE),
+ new Notification.ProgressStyle.Segment(50).setColor(Color.RED),
+ new Notification.ProgressStyle.Segment(50).setColor(Color.BLUE)
+ );
+
+ final Notification.ProgressStyle progressStyle1 = new Notification.ProgressStyle();
+ progressStyle1.setProgressSegments(segments);
+
+ assertThat(progressStyle1.getProgressSegments()).isEqualTo(segments);
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_API_RICH_ONGOING)
public void progressStyle_addProgressPoint_dropsNegativePoints() {
// GIVEN
final Notification.ProgressStyle progressStyle = new Notification.ProgressStyle();
@@ -2532,6 +2547,21 @@ public class NotificationTest {
@Test
@EnableFlags(Flags.FLAG_API_RICH_ONGOING)
+ public void progressStyle_setProgressPoints() {
+ final List<Notification.ProgressStyle.Point> points = List.of(
+ new Notification.ProgressStyle.Point(0).setColor(Color.WHITE),
+ new Notification.ProgressStyle.Point(50).setColor(Color.RED),
+ new Notification.ProgressStyle.Point(100).setColor(Color.BLUE)
+ );
+
+ final Notification.ProgressStyle progressStyle1 = new Notification.ProgressStyle();
+ progressStyle1.setProgressPoints(points);
+
+ assertThat(progressStyle1.getProgressPoints()).isEqualTo(points);
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_API_RICH_ONGOING)
public void progressStyle_createProgressModel_ignoresPointsExceedingMax() {
// GIVEN
final Notification.ProgressStyle progressStyle = new Notification.ProgressStyle();
@@ -2673,11 +2703,58 @@ public class NotificationTest {
@Test
@EnableFlags(Flags.FLAG_API_RICH_ONGOING)
+ public void progressStyle_setProgressIndeterminate() {
+ final Notification.ProgressStyle progressStyle1 = new Notification.ProgressStyle();
+ progressStyle1.setProgressIndeterminate(true);
+ assertThat(progressStyle1.isProgressIndeterminate()).isTrue();
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_API_RICH_ONGOING)
public void progressStyle_styledByProgress_defaultValueTrue() {
final Notification.ProgressStyle progressStyle1 = new Notification.ProgressStyle();
assertThat(progressStyle1.isStyledByProgress()).isTrue();
}
+
+ @Test
+ @EnableFlags(Flags.FLAG_API_RICH_ONGOING)
+ public void progressStyle_setStyledByProgress() {
+ final Notification.ProgressStyle progressStyle1 = new Notification.ProgressStyle();
+ progressStyle1.setStyledByProgress(false);
+ assertThat(progressStyle1.isStyledByProgress()).isFalse();
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_API_RICH_ONGOING)
+ public void progressStyle_point() {
+ final int id = 1;
+ final int position = 10;
+ final int color = Color.RED;
+
+ final Notification.ProgressStyle.Point point =
+ new Notification.ProgressStyle.Point(position).setId(id).setColor(color);
+
+ assertEquals(id, point.getId());
+ assertEquals(position, point.getPosition());
+ assertEquals(color, point.getColor());
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_API_RICH_ONGOING)
+ public void progressStyle_segment() {
+ final int id = 1;
+ final int length = 100;
+ final int color = Color.RED;
+
+ final Notification.ProgressStyle.Segment segment =
+ new Notification.ProgressStyle.Segment(length).setId(id).setColor(color);
+
+ assertEquals(id, segment.getId());
+ assertEquals(length, segment.getLength());
+ assertEquals(color, segment.getColor());
+ }
+
private void assertValid(Notification.Colors c) {
// Assert that all colors are populated
assertThat(c.getBackgroundColor()).isNotEqualTo(Notification.COLOR_INVALID);
diff --git a/core/tests/coretests/src/android/os/OWNERS b/core/tests/coretests/src/android/os/OWNERS
index c45080fb5e26..5fd4ffc7329a 100644
--- a/core/tests/coretests/src/android/os/OWNERS
+++ b/core/tests/coretests/src/android/os/OWNERS
@@ -10,6 +10,9 @@ per-file PowerManager*.java = file:/services/core/java/com/android/server/power/
# PerformanceHintManager
per-file PerformanceHintManagerTest.java = file:/ADPF_OWNERS
+# SystemHealthManager
+per-file SystemHealthManagerUnitTest.java = file:/ADPF_OWNERS
+
# Caching
per-file IpcDataCache* = file:/PERFORMANCE_OWNERS
diff --git a/core/tests/coretests/src/android/os/ParcelTest.java b/core/tests/coretests/src/android/os/ParcelTest.java
index da9d687ee2b0..3e6520106ab0 100644
--- a/core/tests/coretests/src/android/os/ParcelTest.java
+++ b/core/tests/coretests/src/android/os/ParcelTest.java
@@ -361,7 +361,11 @@ public class ParcelTest {
p.setClassCookie(ParcelTest.class, "to_be_discarded_cookie");
p.recycle();
- assertThat(p.getClassCookie(ParcelTest.class)).isNull();
+
+ // cannot access Parcel after it's recycled!
+ // this test is equivalent to checking hasClassCookie false
+ // after obtaining above
+ // assertThat(p.getClassCookie(ParcelTest.class)).isNull();
}
@Test
diff --git a/core/tests/coretests/src/com/android/internal/widget/NotificationProgressBarTest.java b/core/tests/coretests/src/com/android/internal/widget/NotificationProgressBarTest.java
index d26bb35e5481..5df2c1279eb8 100644
--- a/core/tests/coretests/src/com/android/internal/widget/NotificationProgressBarTest.java
+++ b/core/tests/coretests/src/com/android/internal/widget/NotificationProgressBarTest.java
@@ -20,12 +20,16 @@ import static com.google.common.truth.Truth.assertThat;
import android.app.Notification.ProgressStyle;
import android.graphics.Color;
+import android.util.Pair;
import androidx.test.ext.junit.runners.AndroidJUnit4;
-import com.android.internal.widget.NotificationProgressDrawable.Part;
-import com.android.internal.widget.NotificationProgressDrawable.Point;
-import com.android.internal.widget.NotificationProgressDrawable.Segment;
+import com.android.internal.widget.NotificationProgressBar.Part;
+import com.android.internal.widget.NotificationProgressBar.Point;
+import com.android.internal.widget.NotificationProgressBar.Segment;
+import com.android.internal.widget.NotificationProgressDrawable.DrawablePart;
+import com.android.internal.widget.NotificationProgressDrawable.DrawablePoint;
+import com.android.internal.widget.NotificationProgressDrawable.DrawableSegment;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -37,183 +41,287 @@ import java.util.List;
public class NotificationProgressBarTest {
@Test(expected = IllegalArgumentException.class)
- public void processAndConvertToDrawableParts_segmentsIsEmpty() {
+ public void processAndConvertToParts_segmentsIsEmpty() {
List<ProgressStyle.Segment> segments = new ArrayList<>();
List<ProgressStyle.Point> points = new ArrayList<>();
int progress = 50;
int progressMax = 100;
- boolean isStyledByProgress = true;
- NotificationProgressBar.processAndConvertToDrawableParts(segments, points, progress,
- progressMax,
- isStyledByProgress);
+ NotificationProgressBar.processAndConvertToViewParts(segments, points, progress,
+ progressMax);
}
@Test(expected = IllegalArgumentException.class)
- public void processAndConvertToDrawableParts_segmentsLengthNotMatchingProgressMax() {
+ public void processAndConvertToParts_segmentsLengthNotMatchingProgressMax() {
List<ProgressStyle.Segment> segments = new ArrayList<>();
segments.add(new ProgressStyle.Segment(50));
segments.add(new ProgressStyle.Segment(100));
List<ProgressStyle.Point> points = new ArrayList<>();
int progress = 50;
int progressMax = 100;
- boolean isStyledByProgress = true;
- NotificationProgressBar.processAndConvertToDrawableParts(segments, points, progress,
- progressMax,
- isStyledByProgress);
+ NotificationProgressBar.processAndConvertToViewParts(segments, points, progress,
+ progressMax);
}
@Test(expected = IllegalArgumentException.class)
- public void processAndConvertToDrawableParts_segmentLengthIsNegative() {
+ public void processAndConvertToParts_segmentLengthIsNegative() {
List<ProgressStyle.Segment> segments = new ArrayList<>();
segments.add(new ProgressStyle.Segment(-50));
segments.add(new ProgressStyle.Segment(150));
List<ProgressStyle.Point> points = new ArrayList<>();
int progress = 50;
int progressMax = 100;
- boolean isStyledByProgress = true;
- NotificationProgressBar.processAndConvertToDrawableParts(segments, points, progress,
- progressMax,
- isStyledByProgress);
+ NotificationProgressBar.processAndConvertToViewParts(segments, points, progress,
+ progressMax);
}
@Test(expected = IllegalArgumentException.class)
- public void processAndConvertToDrawableParts_segmentLengthIsZero() {
+ public void processAndConvertToParts_segmentLengthIsZero() {
List<ProgressStyle.Segment> segments = new ArrayList<>();
segments.add(new ProgressStyle.Segment(0));
segments.add(new ProgressStyle.Segment(100));
List<ProgressStyle.Point> points = new ArrayList<>();
int progress = 50;
int progressMax = 100;
- boolean isStyledByProgress = true;
- NotificationProgressBar.processAndConvertToDrawableParts(segments, points, progress,
- progressMax,
- isStyledByProgress);
+ NotificationProgressBar.processAndConvertToViewParts(segments, points, progress,
+ progressMax);
}
@Test(expected = IllegalArgumentException.class)
- public void processAndConvertToDrawableParts_progressIsNegative() {
+ public void processAndConvertToParts_progressIsNegative() {
List<ProgressStyle.Segment> segments = new ArrayList<>();
segments.add(new ProgressStyle.Segment(100));
List<ProgressStyle.Point> points = new ArrayList<>();
int progress = -50;
int progressMax = 100;
- boolean isStyledByProgress = true;
- NotificationProgressBar.processAndConvertToDrawableParts(segments, points, progress,
- progressMax,
- isStyledByProgress);
+ NotificationProgressBar.processAndConvertToViewParts(segments, points, progress,
+ progressMax);
}
@Test
- public void processAndConvertToDrawableParts_progressIsZero() {
+ public void processAndConvertToParts_progressIsZero() {
List<ProgressStyle.Segment> segments = new ArrayList<>();
segments.add(new ProgressStyle.Segment(100).setColor(Color.RED));
List<ProgressStyle.Point> points = new ArrayList<>();
int progress = 0;
int progressMax = 100;
+
+ List<Part> parts = NotificationProgressBar.processAndConvertToViewParts(segments, points,
+ progress, progressMax);
+
+ List<Part> expectedParts = new ArrayList<>(List.of(new Segment(1f, Color.RED)));
+
+ assertThat(parts).isEqualTo(expectedParts);
+
+ float drawableWidth = 300;
+ float segSegGap = 4;
+ float segPointGap = 4;
+ float pointRadius = 6;
+ boolean hasTrackerIcon = true;
+
+ List<DrawablePart> drawableParts = NotificationProgressBar.processAndConvertToDrawableParts(
+ parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon);
+
+ List<DrawablePart> expectedDrawableParts = new ArrayList<>(
+ List.of(new DrawableSegment(0, 300, Color.RED)));
+
+ assertThat(drawableParts).isEqualTo(expectedDrawableParts);
+
+ float segmentMinWidth = 16;
boolean isStyledByProgress = true;
- List<Part> parts = NotificationProgressBar.processAndConvertToDrawableParts(
- segments, points, progress, progressMax, isStyledByProgress);
+ Pair<List<DrawablePart>, Float> p = NotificationProgressBar.maybeStretchAndRescaleSegments(
+ parts, drawableParts, segmentMinWidth, pointRadius, (float) progress / progressMax,
+ 300, isStyledByProgress, hasTrackerIcon ? 0 : segSegGap);
// Colors with 40% opacity
int fadedRed = 0x66FF0000;
+ expectedDrawableParts = new ArrayList<>(
+ List.of(new DrawableSegment(0, 300, fadedRed, true)));
- List<Part> expected = new ArrayList<>(List.of(new Segment(1f, fadedRed, true)));
-
- assertThat(parts).isEqualTo(expected);
+ assertThat(p.second).isEqualTo(0);
+ assertThat(p.first).isEqualTo(expectedDrawableParts);
}
@Test
- public void processAndConvertToDrawableParts_progressAtMax() {
+ public void processAndConvertToParts_progressAtMax() {
List<ProgressStyle.Segment> segments = new ArrayList<>();
segments.add(new ProgressStyle.Segment(100).setColor(Color.RED));
List<ProgressStyle.Point> points = new ArrayList<>();
int progress = 100;
int progressMax = 100;
- boolean isStyledByProgress = true;
- List<Part> parts = NotificationProgressBar.processAndConvertToDrawableParts(
- segments, points, progress, progressMax, isStyledByProgress);
+ List<Part> parts = NotificationProgressBar.processAndConvertToViewParts(segments, points,
+ progress, progressMax);
+
+ List<Part> expectedParts = new ArrayList<>(List.of(new Segment(1f, Color.RED)));
+
+ assertThat(parts).isEqualTo(expectedParts);
+
+ float drawableWidth = 300;
+ float segSegGap = 4;
+ float segPointGap = 4;
+ float pointRadius = 6;
+ boolean hasTrackerIcon = true;
+
+ List<DrawablePart> drawableParts = NotificationProgressBar.processAndConvertToDrawableParts(
+ parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon);
+
+ List<DrawablePart> expectedDrawableParts = new ArrayList<>(
+ List.of(new DrawableSegment(0, 300, Color.RED)));
- List<Part> expected = new ArrayList<>(List.of(new Segment(1f, Color.RED)));
+ assertThat(drawableParts).isEqualTo(expectedDrawableParts);
- assertThat(parts).isEqualTo(expected);
+ float segmentMinWidth = 16;
+ boolean isStyledByProgress = true;
+
+ Pair<List<DrawablePart>, Float> p = NotificationProgressBar.maybeStretchAndRescaleSegments(
+ parts, drawableParts, segmentMinWidth, pointRadius, (float) progress / progressMax,
+ 300, isStyledByProgress, hasTrackerIcon ? 0 : segSegGap);
+
+ assertThat(p.second).isEqualTo(300);
+ assertThat(p.first).isEqualTo(expectedDrawableParts);
}
@Test(expected = IllegalArgumentException.class)
- public void processAndConvertToDrawableParts_progressAboveMax() {
+ public void processAndConvertToParts_progressAboveMax() {
List<ProgressStyle.Segment> segments = new ArrayList<>();
segments.add(new ProgressStyle.Segment(100));
List<ProgressStyle.Point> points = new ArrayList<>();
int progress = 150;
int progressMax = 100;
- boolean isStyledByProgress = true;
- NotificationProgressBar.processAndConvertToDrawableParts(segments, points, progress,
- progressMax, isStyledByProgress);
+ NotificationProgressBar.processAndConvertToViewParts(segments, points, progress,
+ progressMax);
}
@Test(expected = IllegalArgumentException.class)
- public void processAndConvertToDrawableParts_pointPositionIsNegative() {
+ public void processAndConvertToParts_pointPositionIsNegative() {
List<ProgressStyle.Segment> segments = new ArrayList<>();
segments.add(new ProgressStyle.Segment(100));
List<ProgressStyle.Point> points = new ArrayList<>();
points.add(new ProgressStyle.Point(-50).setColor(Color.RED));
int progress = 50;
int progressMax = 100;
- boolean isStyledByProgress = true;
- NotificationProgressBar.processAndConvertToDrawableParts(segments, points, progress,
- progressMax,
- isStyledByProgress);
+ NotificationProgressBar.processAndConvertToViewParts(segments, points, progress,
+ progressMax);
}
@Test(expected = IllegalArgumentException.class)
- public void processAndConvertToDrawableParts_pointPositionAboveMax() {
+ public void processAndConvertToParts_pointPositionAboveMax() {
List<ProgressStyle.Segment> segments = new ArrayList<>();
segments.add(new ProgressStyle.Segment(100));
List<ProgressStyle.Point> points = new ArrayList<>();
points.add(new ProgressStyle.Point(150).setColor(Color.RED));
int progress = 50;
int progressMax = 100;
- boolean isStyledByProgress = true;
- NotificationProgressBar.processAndConvertToDrawableParts(segments, points, progress,
- progressMax,
- isStyledByProgress);
+ NotificationProgressBar.processAndConvertToViewParts(segments, points, progress,
+ progressMax);
}
@Test
- public void processAndConvertToDrawableParts_multipleSegmentsWithoutPoints() {
+ public void processAndConvertToParts_multipleSegmentsWithoutPoints() {
List<ProgressStyle.Segment> segments = new ArrayList<>();
segments.add(new ProgressStyle.Segment(50).setColor(Color.RED));
segments.add(new ProgressStyle.Segment(50).setColor(Color.GREEN));
List<ProgressStyle.Point> points = new ArrayList<>();
int progress = 60;
int progressMax = 100;
- boolean isStyledByProgress = true;
- List<Part> parts = NotificationProgressBar.processAndConvertToDrawableParts(
- segments, points, progress, progressMax, isStyledByProgress);
+ List<Part> parts = NotificationProgressBar.processAndConvertToViewParts(segments, points,
+ progress, progressMax);
+
+ List<Part> expectedParts = new ArrayList<>(
+ List.of(new Segment(0.50f, Color.RED), new Segment(0.50f, Color.GREEN)));
+
+ assertThat(parts).isEqualTo(expectedParts);
+
+ float drawableWidth = 300;
+ float segSegGap = 4;
+ float segPointGap = 4;
+ float pointRadius = 6;
+ boolean hasTrackerIcon = true;
+
+ List<DrawablePart> drawableParts = NotificationProgressBar.processAndConvertToDrawableParts(
+ parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon);
+
+ List<DrawablePart> expectedDrawableParts = new ArrayList<>(
+ List.of(new DrawableSegment(0, 146, Color.RED),
+ new DrawableSegment(150, 300, Color.GREEN)));
+
+ assertThat(drawableParts).isEqualTo(expectedDrawableParts);
+
+ float segmentMinWidth = 16;
+ boolean isStyledByProgress = true;
+ Pair<List<DrawablePart>, Float> p = NotificationProgressBar.maybeStretchAndRescaleSegments(
+ parts, drawableParts, segmentMinWidth, pointRadius, (float) progress / progressMax,
+ 300, isStyledByProgress, hasTrackerIcon ? 0 : segSegGap);
// Colors with 40% opacity
int fadedGreen = 0x6600FF00;
+ expectedDrawableParts = new ArrayList<>(List.of(new DrawableSegment(0, 146, Color.RED),
+ new DrawableSegment(150, 180, Color.GREEN),
+ new DrawableSegment(180, 300, fadedGreen, true)));
+
+ assertThat(p.second).isEqualTo(180);
+ assertThat(p.first).isEqualTo(expectedDrawableParts);
+ }
+
+ @Test
+ public void processAndConvertToParts_multipleSegmentsWithoutPoints_noTracker() {
+ List<ProgressStyle.Segment> segments = new ArrayList<>();
+ segments.add(new ProgressStyle.Segment(50).setColor(Color.RED));
+ segments.add(new ProgressStyle.Segment(50).setColor(Color.GREEN));
+ List<ProgressStyle.Point> points = new ArrayList<>();
+ int progress = 60;
+ int progressMax = 100;
+ List<Part> parts = NotificationProgressBar.processAndConvertToViewParts(segments, points,
+ progress, progressMax);
+
+ List<Part> expectedParts = new ArrayList<>(
+ List.of(new Segment(0.50f, Color.RED), new Segment(0.50f, Color.GREEN)));
- List<Part> expected = new ArrayList<>(List.of(
- new Segment(0.50f, Color.RED),
- new Segment(0.10f, Color.GREEN),
- new Segment(0.40f, fadedGreen, true)));
+ assertThat(parts).isEqualTo(expectedParts);
+
+ float drawableWidth = 300;
+ float segSegGap = 4;
+ float segPointGap = 4;
+ float pointRadius = 6;
+ boolean hasTrackerIcon = false;
+
+ List<DrawablePart> drawableParts = NotificationProgressBar.processAndConvertToDrawableParts(
+ parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon);
+
+ List<DrawablePart> expectedDrawableParts = new ArrayList<>(
+ List.of(new DrawableSegment(0, 146, Color.RED),
+ new DrawableSegment(150, 300, Color.GREEN)));
+
+ assertThat(drawableParts).isEqualTo(expectedDrawableParts);
+
+ float segmentMinWidth = 16;
+ boolean isStyledByProgress = true;
+ Pair<List<DrawablePart>, Float> p = NotificationProgressBar.maybeStretchAndRescaleSegments(
+ parts, drawableParts, segmentMinWidth, pointRadius, (float) progress / progressMax,
+ 300, isStyledByProgress, hasTrackerIcon ? 0 : segSegGap);
- assertThat(parts).isEqualTo(expected);
+ // Colors with 40% opacity
+ int fadedGreen = 0x6600FF00;
+ expectedDrawableParts = new ArrayList<>(List.of(new DrawableSegment(0, 146, Color.RED),
+ new DrawableSegment(150, 176, Color.GREEN),
+ new DrawableSegment(180, 300, fadedGreen, true)));
+
+ assertThat(p.second).isEqualTo(180);
+ assertThat(p.first).isEqualTo(expectedDrawableParts);
}
@Test
- public void processAndConvertToDrawableParts_singleSegmentWithPoints() {
+ public void processAndConvertToParts_singleSegmentWithPoints() {
List<ProgressStyle.Segment> segments = new ArrayList<>();
segments.add(new ProgressStyle.Segment(100).setColor(Color.BLUE));
List<ProgressStyle.Point> points = new ArrayList<>();
@@ -223,31 +331,68 @@ public class NotificationProgressBarTest {
points.add(new ProgressStyle.Point(75).setColor(Color.YELLOW));
int progress = 60;
int progressMax = 100;
+
+ List<Part> parts = NotificationProgressBar.processAndConvertToViewParts(segments, points,
+ progress, progressMax);
+
+ List<Part> expectedParts = new ArrayList<>(
+ List.of(new Segment(0.15f, Color.BLUE), new Point(Color.RED),
+ new Segment(0.10f, Color.BLUE), new Point(Color.BLUE),
+ new Segment(0.35f, Color.BLUE), new Point(Color.BLUE),
+ new Segment(0.15f, Color.BLUE), new Point(Color.YELLOW),
+ new Segment(0.25f, Color.BLUE)));
+
+ assertThat(parts).isEqualTo(expectedParts);
+
+ float drawableWidth = 300;
+ float segSegGap = 4;
+ float segPointGap = 4;
+ float pointRadius = 6;
+ boolean hasTrackerIcon = true;
+
+ List<DrawablePart> drawableParts = NotificationProgressBar.processAndConvertToDrawableParts(
+ parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon);
+
+ List<DrawablePart> expectedDrawableParts = new ArrayList<>(
+ List.of(new DrawableSegment(0, 35, Color.BLUE),
+ new DrawablePoint(39, 51, Color.RED),
+ new DrawableSegment(55, 65, Color.BLUE),
+ new DrawablePoint(69, 81, Color.BLUE),
+ new DrawableSegment(85, 170, Color.BLUE),
+ new DrawablePoint(174, 186, Color.BLUE),
+ new DrawableSegment(190, 215, Color.BLUE),
+ new DrawablePoint(219, 231, Color.YELLOW),
+ new DrawableSegment(235, 300, Color.BLUE)));
+
+ assertThat(drawableParts).isEqualTo(expectedDrawableParts);
+
+ float segmentMinWidth = 16;
boolean isStyledByProgress = true;
+ Pair<List<DrawablePart>, Float> p = NotificationProgressBar.maybeStretchAndRescaleSegments(
+ parts, drawableParts, segmentMinWidth, pointRadius, (float) progress / progressMax,
+ 300, isStyledByProgress, hasTrackerIcon ? 0 : segSegGap);
+
// Colors with 40% opacity
int fadedBlue = 0x660000FF;
int fadedYellow = 0x66FFFF00;
-
- List<Part> expected = new ArrayList<>(List.of(
- new Segment(0.15f, Color.BLUE),
- new Point(null, Color.RED),
- new Segment(0.10f, Color.BLUE),
- new Point(null, Color.BLUE),
- new Segment(0.35f, Color.BLUE),
- new Point(null, Color.BLUE),
- new Segment(0.15f, fadedBlue, true),
- new Point(null, fadedYellow, true),
- new Segment(0.25f, fadedBlue, true)));
-
- List<Part> parts = NotificationProgressBar.processAndConvertToDrawableParts(
- segments, points, progress, progressMax, isStyledByProgress);
-
- assertThat(parts).isEqualTo(expected);
+ expectedDrawableParts = new ArrayList<>(
+ List.of(new DrawableSegment(0, 34.219177F, Color.BLUE),
+ new DrawablePoint(38.219177F, 50.219177F, Color.RED),
+ new DrawableSegment(54.219177F, 70.21918F, Color.BLUE),
+ new DrawablePoint(74.21918F, 86.21918F, Color.BLUE),
+ new DrawableSegment(90.21918F, 172.38356F, Color.BLUE),
+ new DrawablePoint(176.38356F, 188.38356F, Color.BLUE),
+ new DrawableSegment(192.38356F, 217.0137F, fadedBlue, true),
+ new DrawablePoint(221.0137F, 233.0137F, fadedYellow),
+ new DrawableSegment(237.0137F, 300F, fadedBlue, true)));
+
+ assertThat(p.second).isEqualTo(182.38356F);
+ assertThat(p.first).isEqualTo(expectedDrawableParts);
}
@Test
- public void processAndConvertToDrawableParts_multipleSegmentsWithPoints() {
+ public void processAndConvertToParts_multipleSegmentsWithPoints() {
List<ProgressStyle.Segment> segments = new ArrayList<>();
segments.add(new ProgressStyle.Segment(50).setColor(Color.RED));
segments.add(new ProgressStyle.Segment(50).setColor(Color.GREEN));
@@ -258,32 +403,68 @@ public class NotificationProgressBarTest {
points.add(new ProgressStyle.Point(75).setColor(Color.YELLOW));
int progress = 60;
int progressMax = 100;
+
+ List<Part> parts = NotificationProgressBar.processAndConvertToViewParts(segments, points,
+ progress, progressMax);
+
+ List<Part> expectedParts = new ArrayList<>(
+ List.of(new Segment(0.15f, Color.RED), new Point(Color.RED),
+ new Segment(0.10f, Color.RED), new Point(Color.BLUE),
+ new Segment(0.25f, Color.RED), new Segment(0.10f, Color.GREEN),
+ new Point(Color.BLUE), new Segment(0.15f, Color.GREEN),
+ new Point(Color.YELLOW), new Segment(0.25f, Color.GREEN)));
+
+ assertThat(parts).isEqualTo(expectedParts);
+
+ float drawableWidth = 300;
+ float segSegGap = 4;
+ float segPointGap = 4;
+ float pointRadius = 6;
+ boolean hasTrackerIcon = true;
+ List<DrawablePart> drawableParts = NotificationProgressBar.processAndConvertToDrawableParts(
+ parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon);
+
+ List<DrawablePart> expectedDrawableParts = new ArrayList<>(
+ List.of(new DrawableSegment(0, 35, Color.RED), new DrawablePoint(39, 51, Color.RED),
+ new DrawableSegment(55, 65, Color.RED),
+ new DrawablePoint(69, 81, Color.BLUE),
+ new DrawableSegment(85, 146, Color.RED),
+ new DrawableSegment(150, 170, Color.GREEN),
+ new DrawablePoint(174, 186, Color.BLUE),
+ new DrawableSegment(190, 215, Color.GREEN),
+ new DrawablePoint(219, 231, Color.YELLOW),
+ new DrawableSegment(235, 300, Color.GREEN)));
+
+ assertThat(drawableParts).isEqualTo(expectedDrawableParts);
+
+ float segmentMinWidth = 16;
boolean isStyledByProgress = true;
- List<Part> parts = NotificationProgressBar.processAndConvertToDrawableParts(
- segments, points, progress, progressMax, isStyledByProgress);
+ Pair<List<DrawablePart>, Float> p = NotificationProgressBar.maybeStretchAndRescaleSegments(
+ parts, drawableParts, segmentMinWidth, pointRadius, (float) progress / progressMax,
+ 300, isStyledByProgress, hasTrackerIcon ? 0 : segSegGap);
// Colors with 40% opacity
int fadedGreen = 0x6600FF00;
int fadedYellow = 0x66FFFF00;
-
- List<Part> expected = new ArrayList<>(List.of(
- new Segment(0.15f, Color.RED),
- new Point(null, Color.RED),
- new Segment(0.10f, Color.RED),
- new Point(null, Color.BLUE),
- new Segment(0.25f, Color.RED),
- new Segment(0.10f, Color.GREEN),
- new Point(null, Color.BLUE),
- new Segment(0.15f, fadedGreen, true),
- new Point(null, fadedYellow, true),
- new Segment(0.25f, fadedGreen, true)));
-
- assertThat(parts).isEqualTo(expected);
+ expectedDrawableParts = new ArrayList<>(
+ List.of(new DrawableSegment(0, 34.095238F, Color.RED),
+ new DrawablePoint(38.095238F, 50.095238F, Color.RED),
+ new DrawableSegment(54.095238F, 70.09524F, Color.RED),
+ new DrawablePoint(74.09524F, 86.09524F, Color.BLUE),
+ new DrawableSegment(90.09524F, 148.9524F, Color.RED),
+ new DrawableSegment(152.95238F, 172.7619F, Color.GREEN),
+ new DrawablePoint(176.7619F, 188.7619F, Color.BLUE),
+ new DrawableSegment(192.7619F, 217.33333F, fadedGreen, true),
+ new DrawablePoint(221.33333F, 233.33333F, fadedYellow),
+ new DrawableSegment(237.33333F, 299.99997F, fadedGreen, true)));
+
+ assertThat(p.second).isEqualTo(182.7619F);
+ assertThat(p.first).isEqualTo(expectedDrawableParts);
}
@Test
- public void processAndConvertToDrawableParts_multipleSegmentsWithPoints_notStyledByProgress() {
+ public void processAndConvertToParts_multipleSegmentsWithPoints_notStyledByProgress() {
List<ProgressStyle.Segment> segments = new ArrayList<>();
segments.add(new ProgressStyle.Segment(50).setColor(Color.RED));
segments.add(new ProgressStyle.Segment(50).setColor(Color.GREEN));
@@ -293,21 +474,223 @@ public class NotificationProgressBarTest {
points.add(new ProgressStyle.Point(75).setColor(Color.YELLOW));
int progress = 60;
int progressMax = 100;
+
+ List<Part> parts = NotificationProgressBar.processAndConvertToViewParts(segments, points,
+ progress, progressMax);
+
+ List<Part> expectedParts = new ArrayList<>(
+ List.of(new Segment(0.15f, Color.RED), new Point(Color.RED),
+ new Segment(0.10f, Color.RED), new Point(Color.BLUE),
+ new Segment(0.25f, Color.RED), new Segment(0.25f, Color.GREEN),
+ new Point(Color.YELLOW), new Segment(0.25f, Color.GREEN)));
+
+ assertThat(parts).isEqualTo(expectedParts);
+
+ float drawableWidth = 300;
+ float segSegGap = 4;
+ float segPointGap = 4;
+ float pointRadius = 6;
+ boolean hasTrackerIcon = true;
+
+ List<DrawablePart> drawableParts = NotificationProgressBar.processAndConvertToDrawableParts(
+ parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon);
+
+ List<DrawablePart> expectedDrawableParts = new ArrayList<>(
+ List.of(new DrawableSegment(0, 35, Color.RED), new DrawablePoint(39, 51, Color.RED),
+ new DrawableSegment(55, 65, Color.RED),
+ new DrawablePoint(69, 81, Color.BLUE),
+ new DrawableSegment(85, 146, Color.RED),
+ new DrawableSegment(150, 215, Color.GREEN),
+ new DrawablePoint(219, 231, Color.YELLOW),
+ new DrawableSegment(235, 300, Color.GREEN)));
+
+ assertThat(drawableParts).isEqualTo(expectedDrawableParts);
+
+ float segmentMinWidth = 16;
boolean isStyledByProgress = false;
- List<Part> parts = NotificationProgressBar.processAndConvertToDrawableParts(
- segments, points, progress, progressMax, isStyledByProgress);
+ Pair<List<DrawablePart>, Float> p = NotificationProgressBar.maybeStretchAndRescaleSegments(
+ parts, drawableParts, segmentMinWidth, pointRadius, (float) progress / progressMax,
+ 300, isStyledByProgress, hasTrackerIcon ? 0 : segSegGap);
+
+ expectedDrawableParts = new ArrayList<>(
+ List.of(new DrawableSegment(0, 34.296295F, Color.RED),
+ new DrawablePoint(38.296295F, 50.296295F, Color.RED),
+ new DrawableSegment(54.296295F, 70.296295F, Color.RED),
+ new DrawablePoint(74.296295F, 86.296295F, Color.BLUE),
+ new DrawableSegment(90.296295F, 149.62962F, Color.RED),
+ new DrawableSegment(153.62962F, 216.8148F, Color.GREEN),
+ new DrawablePoint(220.81482F, 232.81482F, Color.YELLOW),
+ new DrawableSegment(236.81482F, 300, Color.GREEN)));
+
+ assertThat(p.second).isEqualTo(182.9037F);
+ assertThat(p.first).isEqualTo(expectedDrawableParts);
+ }
+
+ // The only difference from the `zeroWidthDrawableSegment` test below is the longer
+ // segmentMinWidth (= 16dp).
+ @Test
+ public void maybeStretchAndRescaleSegments_negativeWidthDrawableSegment() {
+ List<ProgressStyle.Segment> segments = new ArrayList<>();
+ segments.add(new ProgressStyle.Segment(100).setColor(Color.BLUE));
+ segments.add(new ProgressStyle.Segment(200).setColor(Color.BLUE));
+ segments.add(new ProgressStyle.Segment(300).setColor(Color.BLUE));
+ segments.add(new ProgressStyle.Segment(400).setColor(Color.BLUE));
+ List<ProgressStyle.Point> points = new ArrayList<>();
+ points.add(new ProgressStyle.Point(0).setColor(Color.BLUE));
+ int progress = 1000;
+ int progressMax = 1000;
+
+ List<Part> parts = NotificationProgressBar.processAndConvertToViewParts(segments, points,
+ progress, progressMax);
+
+ List<Part> expectedParts = new ArrayList<>(
+ List.of(new Point(Color.BLUE), new Segment(0.1f, Color.BLUE),
+ new Segment(0.2f, Color.BLUE), new Segment(0.3f, Color.BLUE),
+ new Segment(0.4f, Color.BLUE)));
+
+ assertThat(parts).isEqualTo(expectedParts);
+
+ float drawableWidth = 200;
+ float segSegGap = 4;
+ float segPointGap = 4;
+ float pointRadius = 6;
+ boolean hasTrackerIcon = true;
+ List<DrawablePart> drawableParts = NotificationProgressBar.processAndConvertToDrawableParts(
+ parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon);
+
+ List<DrawablePart> expectedDrawableParts = new ArrayList<>(
+ List.of(new DrawablePoint(0, 12, Color.BLUE),
+ new DrawableSegment(16, 16, Color.BLUE),
+ new DrawableSegment(20, 56, Color.BLUE),
+ new DrawableSegment(60, 116, Color.BLUE),
+ new DrawableSegment(120, 200, Color.BLUE)));
+
+ assertThat(drawableParts).isEqualTo(expectedDrawableParts);
+
+ float segmentMinWidth = 16;
+ boolean isStyledByProgress = true;
+
+ Pair<List<DrawablePart>, Float> p = NotificationProgressBar.maybeStretchAndRescaleSegments(
+ parts, drawableParts, segmentMinWidth, pointRadius, (float) progress / progressMax,
+ 200, isStyledByProgress, hasTrackerIcon ? 0 : segSegGap);
+
+ expectedDrawableParts = new ArrayList<>(List.of(new DrawablePoint(0, 12, Color.BLUE),
+ new DrawableSegment(16, 32, Color.BLUE),
+ new DrawableSegment(36, 69.41936F, Color.BLUE),
+ new DrawableSegment(73.41936F, 124.25807F, Color.BLUE),
+ new DrawableSegment(128.25807F, 200, Color.BLUE)));
+
+ assertThat(p.second).isEqualTo(200);
+ assertThat(p.first).isEqualTo(expectedDrawableParts);
+ }
+
+ // The only difference from the `negativeWidthDrawableSegment` test above is the shorter
+ // segmentMinWidth (= 10dp).
+ @Test
+ public void maybeStretchAndRescaleSegments_zeroWidthDrawableSegment() {
+ List<ProgressStyle.Segment> segments = new ArrayList<>();
+ segments.add(new ProgressStyle.Segment(100).setColor(Color.BLUE));
+ segments.add(new ProgressStyle.Segment(200).setColor(Color.BLUE));
+ segments.add(new ProgressStyle.Segment(300).setColor(Color.BLUE));
+ segments.add(new ProgressStyle.Segment(400).setColor(Color.BLUE));
+ List<ProgressStyle.Point> points = new ArrayList<>();
+ points.add(new ProgressStyle.Point(0).setColor(Color.BLUE));
+ int progress = 1000;
+ int progressMax = 1000;
+
+ List<Part> parts = NotificationProgressBar.processAndConvertToViewParts(segments, points,
+ progress, progressMax);
+
+ List<Part> expectedParts = new ArrayList<>(
+ List.of(new Point(Color.BLUE), new Segment(0.1f, Color.BLUE),
+ new Segment(0.2f, Color.BLUE), new Segment(0.3f, Color.BLUE),
+ new Segment(0.4f, Color.BLUE)));
+
+ assertThat(parts).isEqualTo(expectedParts);
+
+ float drawableWidth = 200;
+ float segSegGap = 4;
+ float segPointGap = 4;
+ float pointRadius = 6;
+ boolean hasTrackerIcon = true;
+ List<DrawablePart> drawableParts = NotificationProgressBar.processAndConvertToDrawableParts(
+ parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon);
+
+ List<DrawablePart> expectedDrawableParts = new ArrayList<>(
+ List.of(new DrawablePoint(0, 12, Color.BLUE),
+ new DrawableSegment(16, 16, Color.BLUE),
+ new DrawableSegment(20, 56, Color.BLUE),
+ new DrawableSegment(60, 116, Color.BLUE),
+ new DrawableSegment(120, 200, Color.BLUE)));
+
+ assertThat(drawableParts).isEqualTo(expectedDrawableParts);
+
+ float segmentMinWidth = 10;
+ boolean isStyledByProgress = true;
+
+ Pair<List<DrawablePart>, Float> p = NotificationProgressBar.maybeStretchAndRescaleSegments(
+ parts, drawableParts, segmentMinWidth, pointRadius, (float) progress / progressMax,
+ 200, isStyledByProgress, hasTrackerIcon ? 0 : segSegGap);
+
+ expectedDrawableParts = new ArrayList<>(List.of(new DrawablePoint(0, 12, Color.BLUE),
+ new DrawableSegment(16, 26, Color.BLUE),
+ new DrawableSegment(30, 64.169014F, Color.BLUE),
+ new DrawableSegment(68.169014F, 120.92958F, Color.BLUE),
+ new DrawableSegment(124.92958F, 200, Color.BLUE)));
+
+ assertThat(p.second).isEqualTo(200);
+ assertThat(p.first).isEqualTo(expectedDrawableParts);
+ }
+
+ @Test
+ public void maybeStretchAndRescaleSegments_noStretchingNecessary() {
+ List<ProgressStyle.Segment> segments = new ArrayList<>();
+ segments.add(new ProgressStyle.Segment(200).setColor(Color.BLUE));
+ segments.add(new ProgressStyle.Segment(100).setColor(Color.BLUE));
+ segments.add(new ProgressStyle.Segment(300).setColor(Color.BLUE));
+ segments.add(new ProgressStyle.Segment(400).setColor(Color.BLUE));
+ List<ProgressStyle.Point> points = new ArrayList<>();
+ points.add(new ProgressStyle.Point(0).setColor(Color.BLUE));
+ int progress = 1000;
+ int progressMax = 1000;
+
+ List<Part> parts = NotificationProgressBar.processAndConvertToViewParts(segments, points,
+ progress, progressMax);
+
+ List<Part> expectedParts = new ArrayList<>(
+ List.of(new Point(Color.BLUE), new Segment(0.2f, Color.BLUE),
+ new Segment(0.1f, Color.BLUE), new Segment(0.3f, Color.BLUE),
+ new Segment(0.4f, Color.BLUE)));
+
+ assertThat(parts).isEqualTo(expectedParts);
+
+ float drawableWidth = 200;
+ float segSegGap = 4;
+ float segPointGap = 4;
+ float pointRadius = 6;
+ boolean hasTrackerIcon = true;
+
+ List<DrawablePart> drawableParts = NotificationProgressBar.processAndConvertToDrawableParts(
+ parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon);
+
+ List<DrawablePart> expectedDrawableParts = new ArrayList<>(
+ List.of(new DrawablePoint(0, 12, Color.BLUE),
+ new DrawableSegment(16, 36, Color.BLUE),
+ new DrawableSegment(40, 56, Color.BLUE),
+ new DrawableSegment(60, 116, Color.BLUE),
+ new DrawableSegment(120, 200, Color.BLUE)));
+
+ assertThat(drawableParts).isEqualTo(expectedDrawableParts);
+
+ float segmentMinWidth = 10;
+ boolean isStyledByProgress = true;
- List<Part> expected = new ArrayList<>(List.of(
- new Segment(0.15f, Color.RED),
- new Point(null, Color.RED),
- new Segment(0.10f, Color.RED),
- new Point(null, Color.BLUE),
- new Segment(0.25f, Color.RED),
- new Segment(0.25f, Color.GREEN),
- new Point(null, Color.YELLOW),
- new Segment(0.25f, Color.GREEN)));
+ Pair<List<DrawablePart>, Float> p = NotificationProgressBar.maybeStretchAndRescaleSegments(
+ parts, drawableParts, segmentMinWidth, pointRadius, (float) progress / progressMax,
+ 200, isStyledByProgress, hasTrackerIcon ? 0 : segSegGap);
- assertThat(parts).isEqualTo(expected);
+ assertThat(p.second).isEqualTo(200);
+ assertThat(p.first).isEqualTo(expectedDrawableParts);
}
}
diff --git a/graphics/java/android/graphics/Paint.java b/graphics/java/android/graphics/Paint.java
index 50c95a9fa882..b332cf0d751f 100644
--- a/graphics/java/android/graphics/Paint.java
+++ b/graphics/java/android/graphics/Paint.java
@@ -16,9 +16,10 @@
package android.graphics;
+import static com.android.text.flags.Flags.FLAG_DEPRECATE_ELEGANT_TEXT_HEIGHT_API;
import static com.android.text.flags.Flags.FLAG_FIX_LINE_HEIGHT_FOR_LOCALE;
import static com.android.text.flags.Flags.FLAG_LETTER_SPACING_JUSTIFICATION;
-import static com.android.text.flags.Flags.FLAG_DEPRECATE_ELEGANT_TEXT_HEIGHT_API;
+import static com.android.text.flags.Flags.FLAG_TYPEFACE_REDESIGN_READONLY;
import static com.android.text.flags.Flags.FLAG_VERTICAL_TEXT_LAYOUT;
import android.annotation.ColorInt;
@@ -34,7 +35,6 @@ import android.app.compat.CompatChanges;
import android.compat.annotation.ChangeId;
import android.compat.annotation.EnabledSince;
import android.compat.annotation.UnsupportedAppUsage;
-import android.graphics.fonts.FontStyle;
import android.graphics.fonts.FontVariationAxis;
import android.graphics.text.TextRunShaper;
import android.os.Build;
@@ -58,6 +58,7 @@ import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
+import java.util.List;
import java.util.Locale;
import java.util.Objects;
@@ -97,6 +98,7 @@ public class Paint {
private LocaleList mLocales;
private String mFontFeatureSettings;
private String mFontVariationSettings;
+ private String mFontVariationOverride;
private float mShadowLayerRadius;
private float mShadowLayerDx;
@@ -2100,14 +2102,6 @@ public class Paint {
}
/**
- * A change ID for new font variation settings management.
- * @hide
- */
- @ChangeId
- @EnabledSince(targetSdkVersion = 36)
- public static final long NEW_FONT_VARIATION_MANAGEMENT = 361260253L;
-
- /**
* Sets TrueType or OpenType font variation settings. The settings string is constructed from
* multiple pairs of axis tag and style values. The axis tag must contain four ASCII characters
* and must be wrapped with single quotes (U+0027) or double quotes (U+0022). Axis strings that
@@ -2136,16 +2130,12 @@ public class Paint {
* </li>
* </ul>
*
- * <p>Note: If the application that targets API 35 or before, this function mutates the
- * underlying typeface instance.
- *
* @param fontVariationSettings font variation settings. You can pass null or empty string as
* no variation settings.
*
- * @return If the application that targets API 36 or later and is running on devices API 36 or
- * later, this function always returns true. Otherwise, this function returns true if
- * the given settings is effective to at least one font file underlying this typeface.
- * This function also returns true for empty settings string. Otherwise returns false.
+ * @return true if the given settings is effective to at least one font file underlying this
+ * typeface. This function also returns true for empty settings string. Otherwise
+ * returns false
*
* @throws IllegalArgumentException If given string is not a valid font variation settings
* format
@@ -2153,40 +2143,13 @@ public class Paint {
* @see #getFontVariationSettings()
* @see FontVariationAxis
*/
+ // Add following API description once the setFontVariationOverride becomes public.
+ // This method generates new variation instance of the {@link Typeface} instance and set it to
+ // this object. Therefore, subsequent {@link #setTypeface(Typeface)} call will clear the font
+ // variation settings. Also, creating variation instance of the Typeface requires non trivial
+ // amount of time and memories, therefore consider using
+ // {@link #setFontVariationOverride(String, int)} for better performance.
public boolean setFontVariationSettings(String fontVariationSettings) {
- return setFontVariationSettings(fontVariationSettings, 0 /* wght adjust */);
- }
-
- /**
- * Set font variation settings with weight adjustment
- * @hide
- */
- public boolean setFontVariationSettings(String fontVariationSettings, int wghtAdjust) {
- final boolean useFontVariationStore = Flags.typefaceRedesignReadonly()
- && CompatChanges.isChangeEnabled(NEW_FONT_VARIATION_MANAGEMENT);
- if (useFontVariationStore) {
- FontVariationAxis[] axes =
- FontVariationAxis.fromFontVariationSettings(fontVariationSettings);
- if (axes == null) {
- nSetFontVariationOverride(mNativePaint, 0);
- mFontVariationSettings = null;
- return true;
- }
-
- long builderPtr = nCreateFontVariationBuilder(axes.length);
- for (int i = 0; i < axes.length; ++i) {
- int tag = axes[i].getOpenTypeTagValue();
- float value = axes[i].getStyleValue();
- if (tag == 0x77676874 /* wght */) {
- value = Math.clamp(value + wghtAdjust,
- FontStyle.FONT_WEIGHT_MIN, FontStyle.FONT_WEIGHT_MAX);
- }
- nAddFontVariationToBuilder(builderPtr, tag, value);
- }
- nSetFontVariationOverride(mNativePaint, builderPtr);
- mFontVariationSettings = fontVariationSettings;
- return true;
- }
final String settings = TextUtils.nullIfEmpty(fontVariationSettings);
if (settings == mFontVariationSettings
|| (settings != null && settings.equals(mFontVariationSettings))) {
@@ -2220,6 +2183,68 @@ public class Paint {
}
/**
+ * Sets TrueType or OpenType font variation settings for overriding.
+ *
+ * The settings string is constructed from multiple pairs of axis tag and style values. The axis
+ * tag must contain four ASCII characters and must be wrapped with single quotes (U+0027) or
+ * double quotes (U+0022). Axis strings that are longer or shorter than four characters, or
+ * contain characters outside of U+0020..U+007E are invalid.
+ *
+ * If invalid font variation settings is provided, this method does nothing and returning false
+ * with printing error message to the logcat.
+ *
+ * Different from {@link #setFontVariationSettings(String)}, this overrides the font variation
+ * settings which is already assigned to the font instance. For example, if the underlying font
+ * is configured as {@code 'wght' 500, 'ital' 1}, and if the override is specified as
+ * {@code 'wght' 700, `wdth` 150}, then the effective font variation setting is
+ * {@code `wght' 700, 'ital' 1, 'wdth' 150}. The `wght` value is updated by override, 'ital'
+ * value is preserved because no overrides, and `wdth` value is added by override.
+ *
+ * @param fontVariationOverride font variation settings. You can pass null or empty string as
+ * no variation settings.
+ *
+ * @return true if the provided font variation settings is valid. Otherwise returns false.
+ *
+ * @see #getFontVariationSettings()
+ * @see #setFontVariationSettings(String)
+ * @see #getFontVariationOverride()
+ * @see FontVariationAxis
+ */
+ @FlaggedApi(FLAG_TYPEFACE_REDESIGN_READONLY)
+ public boolean setFontVariationOverride(@Nullable String fontVariationOverride) {
+ if (Objects.equals(fontVariationOverride, mFontVariationOverride)) {
+ return true;
+ }
+
+ List<FontVariationAxis> axes;
+ try {
+ axes = FontVariationAxis.fromFontVariationSettingsForList(fontVariationOverride);
+ } catch (IllegalArgumentException e) {
+ Log.i(TAG, "failed to parse font variation settings.", e);
+ return false;
+ }
+ long builderPtr = nCreateFontVariationBuilder(axes.size());
+ for (int i = 0; i < axes.size(); ++i) {
+ FontVariationAxis axis = axes.get(i);
+ nAddFontVariationToBuilder(
+ builderPtr, axis.getOpenTypeTagValue(), axis.getStyleValue());
+ }
+ nSetFontVariationOverride(mNativePaint, builderPtr);
+ mFontVariationOverride = fontVariationOverride;
+ return true;
+ }
+
+ /**
+ * Gets the current font variation override value.
+ *
+ * @return a current font variation override value.
+ */
+ @FlaggedApi(FLAG_TYPEFACE_REDESIGN_READONLY)
+ public @Nullable String getFontVariationOverride() {
+ return mFontVariationOverride;
+ }
+
+ /**
* Get the current value of start hyphen edit.
*
* The default value is 0 which is equivalent to {@link #START_HYPHEN_EDIT_NO_EDIT}.
diff --git a/graphics/java/android/graphics/fonts/FontVariationAxis.java b/graphics/java/android/graphics/fonts/FontVariationAxis.java
index d1fe2cdbcd77..30a248bb3e0e 100644
--- a/graphics/java/android/graphics/fonts/FontVariationAxis.java
+++ b/graphics/java/android/graphics/fonts/FontVariationAxis.java
@@ -23,6 +23,7 @@ import android.os.Build;
import android.text.TextUtils;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.regex.Pattern;
@@ -139,9 +140,19 @@ public final class FontVariationAxis {
*/
public static @Nullable FontVariationAxis[] fromFontVariationSettings(
@Nullable String settings) {
- if (settings == null || settings.isEmpty()) {
+ List<FontVariationAxis> result = fromFontVariationSettingsForList(settings);
+ if (result.isEmpty()) {
return null;
}
+ return result.toArray(new FontVariationAxis[0]);
+ }
+
+ /** @hide */
+ public static @NonNull List<FontVariationAxis> fromFontVariationSettingsForList(
+ @Nullable String settings) {
+ if (settings == null || settings.isEmpty()) {
+ return Collections.emptyList();
+ }
final ArrayList<FontVariationAxis> axisList = new ArrayList<>();
final int length = settings.length();
for (int i = 0; i < length; i++) {
@@ -172,9 +183,9 @@ public final class FontVariationAxis {
i = endOfValueString;
}
if (axisList.isEmpty()) {
- return null;
+ return Collections.emptyList();
}
- return axisList.toArray(new FontVariationAxis[0]);
+ return axisList;
}
/**
diff --git a/keystore/java/android/security/keystore/KeyStoreManager.java b/keystore/java/android/security/keystore/KeyStoreManager.java
index 740ccb53a691..13f1a72469c2 100644
--- a/keystore/java/android/security/keystore/KeyStoreManager.java
+++ b/keystore/java/android/security/keystore/KeyStoreManager.java
@@ -312,9 +312,11 @@ public final class KeyStoreManager {
* When passed into getSupplementaryAttestationInfo, getSupplementaryAttestationInfo returns the
* DER-encoded structure corresponding to the `Modules` schema described in the KeyMint HAL's
* KeyCreationResult.aidl. The SHA-256 hash of this encoded structure is what's included with
- * the tag in attestations.
+ * the tag in attestations. To ensure the returned encoded structure is the one attested to,
+ * clients should verify its SHA-256 hash matches the one in the attestation. Note that the
+ * returned structure can vary between boots.
*/
- // TODO(b/369375199): Replace with Tag.MODULE_HASH when flagging is removed.
+ // TODO(b/380020528): Replace with Tag.MODULE_HASH when KeyMint V4 is frozen.
public static final int MODULE_HASH = TagType.BYTES | 724;
/**
diff --git a/libs/WindowManager/Shell/AndroidManifest.xml b/libs/WindowManager/Shell/AndroidManifest.xml
index b2ac640a468d..4f1cd9780f8b 100644
--- a/libs/WindowManager/Shell/AndroidManifest.xml
+++ b/libs/WindowManager/Shell/AndroidManifest.xml
@@ -26,6 +26,7 @@
<uses-permission android:name="android.permission.SUBSCRIBE_TO_KEYGUARD_LOCKED_STATE" />
<uses-permission android:name="android.permission.UPDATE_DOMAIN_VERIFICATION_USER_SELECTION" />
<uses-permission android:name="android.permission.MANAGE_KEY_GESTURES" />
+ <uses-permission android:name="android.permission.MANAGE_DISPLAYS" />
<application>
<activity
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 755f472ee22e..2fed1380b635 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
@@ -233,6 +233,16 @@ public class DesktopModeStatus {
}
/**
+ * Returns whether the multiple desktops feature is enabled for this device (both backend and
+ * frontend implementations).
+ */
+ public static boolean enableMultipleDesktops(@NonNull Context context) {
+ return Flags.enableMultipleDesktopsBackend()
+ && Flags.enableMultipleDesktopsFrontend()
+ && canEnterDesktopMode(context);
+ }
+
+ /**
* @return {@code true} if this device is requesting to show the app handle despite non
* necessarily enabling desktop mode
*/
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeTransitionSource.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeTransitionSource.kt
index d15fbed409b8..23498de72481 100644
--- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeTransitionSource.kt
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeTransitionSource.kt
@@ -32,9 +32,7 @@ enum class DesktopModeTransitionSource : Parcelable {
/** Transitions with source unknown. */
UNKNOWN;
- override fun describeContents(): Int {
- return 0
- }
+ override fun describeContents(): Int = 0
override fun writeToParcel(dest: Parcel, flags: Int) {
dest.writeString(name)
@@ -44,9 +42,8 @@ enum class DesktopModeTransitionSource : Parcelable {
@JvmField
val CREATOR =
object : Parcelable.Creator<DesktopModeTransitionSource> {
- override fun createFromParcel(parcel: Parcel): DesktopModeTransitionSource {
- return parcel.readString()?.let { valueOf(it) } ?: UNKNOWN
- }
+ override fun createFromParcel(parcel: Parcel): DesktopModeTransitionSource =
+ parcel.readString()?.let { valueOf(it) } ?: UNKNOWN
override fun newArray(size: Int) = arrayOfNulls<DesktopModeTransitionSource>(size)
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/animation/SizeChangeAnimation.java b/libs/WindowManager/Shell/src/com/android/wm/shell/animation/SizeChangeAnimation.java
new file mode 100644
index 000000000000..5018fdb615da
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/animation/SizeChangeAnimation.java
@@ -0,0 +1,288 @@
+/*
+ * 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.wm.shell.animation;
+
+import static com.android.wm.shell.transition.DefaultSurfaceAnimator.setupValueAnimator;
+
+import android.animation.Animator;
+import android.animation.ValueAnimator;
+import android.annotation.Nullable;
+import android.graphics.Matrix;
+import android.graphics.Rect;
+import android.view.Choreographer;
+import android.view.SurfaceControl;
+import android.view.View;
+import android.view.animation.AlphaAnimation;
+import android.view.animation.Animation;
+import android.view.animation.AnimationSet;
+import android.view.animation.ClipRectAnimation;
+import android.view.animation.ScaleAnimation;
+import android.view.animation.Transformation;
+import android.view.animation.TranslateAnimation;
+
+import java.util.function.Consumer;
+
+/**
+ * Animation implementation for size-changing window container animations. Ported from
+ * {@link com.android.server.wm.WindowChangeAnimationSpec}.
+ * <p>
+ * This animation behaves slightly differently depending on whether the window is growing
+ * or shrinking:
+ * <ul>
+ * <li>If growing, it will do a clip-reveal after quicker fade-out/scale of the smaller (old)
+ * snapshot.
+ * <li>If shrinking, it will do an opposite clip-reveal on the old snapshot followed by a quicker
+ * fade-out of the bigger (old) snapshot while simultaneously shrinking the new window into
+ * place.
+ * </ul>
+ */
+public class SizeChangeAnimation {
+ private final Rect mTmpRect = new Rect();
+ final Transformation mTmpTransform = new Transformation();
+ final Matrix mTmpMatrix = new Matrix();
+ final float[] mTmpFloats = new float[9];
+ final float[] mTmpVecs = new float[4];
+
+ private final Animation mAnimation;
+ private final Animation mSnapshotAnim;
+
+ private final ValueAnimator mAnimator = ValueAnimator.ofFloat(0f, 1f);
+
+ /**
+ * The maximum of stretching applied to any surface during interpolation (since the animation
+ * is a combination of stretching/cropping/fading).
+ */
+ private static final float SCALE_FACTOR = 0.7f;
+
+ /**
+ * Since this animation is made of several sub-animations, we want to pre-arrange the
+ * sub-animations on a "virtual timeline" and then drive the overall progress in lock-step.
+ *
+ * To do this, we have a single value-animator which animates progress from 0-1 with an
+ * arbitrary duration and interpolator. Then we convert the progress to a frame in our virtual
+ * timeline to get the interpolated transforms.
+ *
+ * The APIs for arranging the sub-animations use integral frame numbers, so we need to pick
+ * an integral "duration" for our virtual timeline. That's what this constant specifies. It
+ * is effectively an animation "resolution" since it divides-up the 0-1 interpolation-space.
+ */
+ private static final int ANIMATION_RESOLUTION = 1000;
+
+ public SizeChangeAnimation(Rect startBounds, Rect endBounds) {
+ mAnimation = buildContainerAnimation(startBounds, endBounds);
+ mSnapshotAnim = buildSnapshotAnimation(startBounds, endBounds);
+ }
+
+ /**
+ * Initialize a size-change animation for a container leash.
+ */
+ public void initialize(SurfaceControl leash, SurfaceControl snapshot,
+ SurfaceControl.Transaction startT) {
+ startT.reparent(snapshot, leash);
+ startT.setPosition(snapshot, 0, 0);
+ startT.show(snapshot);
+ startT.show(leash);
+ apply(startT, leash, snapshot, 0.f);
+ }
+
+ /**
+ * Initialize a size-change animation for a view containing the leash surface(s).
+ *
+ * Note that this **will** apply {@param startToApply}!
+ */
+ public void initialize(View view, SurfaceControl leash, SurfaceControl snapshot,
+ SurfaceControl.Transaction startToApply) {
+ startToApply.reparent(snapshot, leash);
+ startToApply.setPosition(snapshot, 0, 0);
+ startToApply.show(snapshot);
+ startToApply.show(leash);
+ apply(view, startToApply, leash, snapshot, 0.f);
+ }
+
+ private ValueAnimator buildAnimatorInner(ValueAnimator.AnimatorUpdateListener updater,
+ SurfaceControl leash, SurfaceControl snapshot, Consumer<Animator> onFinish,
+ SurfaceControl.Transaction transaction, @Nullable View view) {
+ return setupValueAnimator(mAnimator, updater, (anim) -> {
+ transaction.reparent(snapshot, null);
+ if (view != null) {
+ view.setClipBounds(null);
+ view.setAnimationMatrix(null);
+ transaction.setCrop(leash, null);
+ }
+ transaction.apply();
+ transaction.close();
+ onFinish.accept(anim);
+ });
+ }
+
+ /**
+ * Build an animator which works on a pair of surface controls (where the snapshot is assumed
+ * to be a child of the main leash).
+ *
+ * @param onFinish Called when animation finishes. This is called on the anim thread!
+ */
+ public ValueAnimator buildAnimator(SurfaceControl leash, SurfaceControl snapshot,
+ Consumer<Animator> onFinish) {
+ final SurfaceControl.Transaction transaction = new SurfaceControl.Transaction();
+ Choreographer choreographer = Choreographer.getInstance();
+ return buildAnimatorInner(animator -> {
+ // The finish callback in buildSurfaceAnimation will ensure that the animation ends
+ // with fraction 1.
+ final float progress = Math.clamp(animator.getAnimatedFraction(), 0.f, 1.f);
+ apply(transaction, leash, snapshot, progress);
+ transaction.setFrameTimelineVsync(choreographer.getVsyncId());
+ transaction.apply();
+ }, leash, snapshot, onFinish, transaction, null /* view */);
+ }
+
+ /**
+ * Build an animator which works on a view that contains a pair of surface controls (where
+ * the snapshot is assumed to be a child of the main leash).
+ *
+ * @param onFinish Called when animation finishes. This is called on the anim thread!
+ */
+ public ValueAnimator buildViewAnimator(View view, SurfaceControl leash,
+ SurfaceControl snapshot, Consumer<Animator> onFinish) {
+ final SurfaceControl.Transaction transaction = new SurfaceControl.Transaction();
+ return buildAnimatorInner(animator -> {
+ // The finish callback in buildSurfaceAnimation will ensure that the animation ends
+ // with fraction 1.
+ final float progress = Math.clamp(animator.getAnimatedFraction(), 0.f, 1.f);
+ apply(view, transaction, leash, snapshot, progress);
+ }, leash, snapshot, onFinish, transaction, view);
+ }
+
+ /** Animation for the whole container (snapshot is inside this container). */
+ private static AnimationSet buildContainerAnimation(Rect startBounds, Rect endBounds) {
+ final long duration = ANIMATION_RESOLUTION;
+ boolean growing = endBounds.width() - startBounds.width()
+ + endBounds.height() - startBounds.height() >= 0;
+ long scalePeriod = (long) (duration * SCALE_FACTOR);
+ float startScaleX = SCALE_FACTOR * ((float) startBounds.width()) / endBounds.width()
+ + (1.f - SCALE_FACTOR);
+ float startScaleY = SCALE_FACTOR * ((float) startBounds.height()) / endBounds.height()
+ + (1.f - SCALE_FACTOR);
+ final AnimationSet animSet = new AnimationSet(true);
+
+ final Animation scaleAnim = new ScaleAnimation(startScaleX, 1, startScaleY, 1);
+ scaleAnim.setDuration(scalePeriod);
+ if (!growing) {
+ scaleAnim.setStartOffset(duration - scalePeriod);
+ }
+ animSet.addAnimation(scaleAnim);
+ final Animation translateAnim = new TranslateAnimation(startBounds.left,
+ endBounds.left, startBounds.top, endBounds.top);
+ translateAnim.setDuration(duration);
+ animSet.addAnimation(translateAnim);
+ Rect startClip = new Rect(startBounds);
+ Rect endClip = new Rect(endBounds);
+ startClip.offsetTo(0, 0);
+ endClip.offsetTo(0, 0);
+ final Animation clipAnim = new ClipRectAnimation(startClip, endClip);
+ clipAnim.setDuration(duration);
+ animSet.addAnimation(clipAnim);
+ animSet.initialize(startBounds.width(), startBounds.height(),
+ endBounds.width(), endBounds.height());
+ return animSet;
+ }
+
+ /** The snapshot surface is assumed to be a child of the container surface. */
+ private static AnimationSet buildSnapshotAnimation(Rect startBounds, Rect endBounds) {
+ final long duration = ANIMATION_RESOLUTION;
+ boolean growing = endBounds.width() - startBounds.width()
+ + endBounds.height() - startBounds.height() >= 0;
+ long scalePeriod = (long) (duration * SCALE_FACTOR);
+ float endScaleX = 1.f / (SCALE_FACTOR * ((float) startBounds.width()) / endBounds.width()
+ + (1.f - SCALE_FACTOR));
+ float endScaleY = 1.f / (SCALE_FACTOR * ((float) startBounds.height()) / endBounds.height()
+ + (1.f - SCALE_FACTOR));
+
+ AnimationSet snapAnimSet = new AnimationSet(true);
+ // Animation for the "old-state" snapshot that is atop the task.
+ final Animation snapAlphaAnim = new AlphaAnimation(1.f, 0.f);
+ snapAlphaAnim.setDuration(scalePeriod);
+ if (!growing) {
+ snapAlphaAnim.setStartOffset(duration - scalePeriod);
+ }
+ snapAnimSet.addAnimation(snapAlphaAnim);
+ final Animation snapScaleAnim =
+ new ScaleAnimation(endScaleX, endScaleX, endScaleY, endScaleY);
+ snapScaleAnim.setDuration(duration);
+ snapAnimSet.addAnimation(snapScaleAnim);
+ snapAnimSet.initialize(startBounds.width(), startBounds.height(),
+ endBounds.width(), endBounds.height());
+ return snapAnimSet;
+ }
+
+ private void calcCurrentClipBounds(Rect outClip, Transformation fromTransform) {
+ // The following applies an inverse scale to the clip-rect so that it crops "after" the
+ // scale instead of before.
+ mTmpVecs[1] = mTmpVecs[2] = 0;
+ mTmpVecs[0] = mTmpVecs[3] = 1;
+ fromTransform.getMatrix().mapVectors(mTmpVecs);
+
+ mTmpVecs[0] = 1.f / mTmpVecs[0];
+ mTmpVecs[3] = 1.f / mTmpVecs[3];
+ final Rect clipRect = fromTransform.getClipRect();
+ outClip.left = (int) (clipRect.left * mTmpVecs[0] + 0.5f);
+ outClip.right = (int) (clipRect.right * mTmpVecs[0] + 0.5f);
+ outClip.top = (int) (clipRect.top * mTmpVecs[3] + 0.5f);
+ outClip.bottom = (int) (clipRect.bottom * mTmpVecs[3] + 0.5f);
+ }
+
+ private void apply(SurfaceControl.Transaction t, SurfaceControl leash, SurfaceControl snapshot,
+ float progress) {
+ long currentPlayTime = (long) (((float) ANIMATION_RESOLUTION) * progress);
+ // update thumbnail surface
+ mSnapshotAnim.getTransformation(currentPlayTime, mTmpTransform);
+ t.setMatrix(snapshot, mTmpTransform.getMatrix(), mTmpFloats);
+ t.setAlpha(snapshot, mTmpTransform.getAlpha());
+
+ // update container surface
+ mAnimation.getTransformation(currentPlayTime, mTmpTransform);
+ final Matrix matrix = mTmpTransform.getMatrix();
+ t.setMatrix(leash, matrix, mTmpFloats);
+
+ calcCurrentClipBounds(mTmpRect, mTmpTransform);
+ t.setCrop(leash, mTmpRect);
+ }
+
+ private void apply(View view, SurfaceControl.Transaction tmpT, SurfaceControl leash,
+ SurfaceControl snapshot, float progress) {
+ long currentPlayTime = (long) (((float) ANIMATION_RESOLUTION) * progress);
+ // update thumbnail surface
+ mSnapshotAnim.getTransformation(currentPlayTime, mTmpTransform);
+ tmpT.setMatrix(snapshot, mTmpTransform.getMatrix(), mTmpFloats);
+ tmpT.setAlpha(snapshot, mTmpTransform.getAlpha());
+
+ // update container surface
+ mAnimation.getTransformation(currentPlayTime, mTmpTransform);
+ final Matrix matrix = mTmpTransform.getMatrix();
+ mTmpMatrix.set(matrix);
+ // animationMatrix is applied after getTranslation, so "move" the translate to the end.
+ mTmpMatrix.preTranslate(-view.getTranslationX(), -view.getTranslationY());
+ mTmpMatrix.postTranslate(view.getTranslationX(), view.getTranslationY());
+ view.setAnimationMatrix(mTmpMatrix);
+
+ calcCurrentClipBounds(mTmpRect, mTmpTransform);
+ tmpT.setCrop(leash, mTmpRect);
+ view.setClipBounds(mTmpRect);
+
+ // this takes stuff out of mTmpT so mTmpT can be re-used immediately
+ view.getViewRootImpl().applyTransactionOnDraw(tmpT);
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/appzoomout/AppZoomOut.java b/libs/WindowManager/Shell/src/com/android/wm/shell/appzoomout/AppZoomOut.java
new file mode 100644
index 000000000000..9451374befe0
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/appzoomout/AppZoomOut.java
@@ -0,0 +1,33 @@
+/*
+ * 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.appzoomout;
+
+import com.android.wm.shell.shared.annotations.ExternalThread;
+
+/**
+ * Interface to engage with the app zoom out feature.
+ */
+@ExternalThread
+public interface AppZoomOut {
+
+ /**
+ * Called when the zoom out progress is updated, which is used to scale down the current app
+ * surface from fullscreen to the max pushback level we want to apply. {@param progress} ranges
+ * between [0,1], 0 when fullscreen, 1 when it's at the max pushback level.
+ */
+ void setProgress(float progress);
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/appzoomout/AppZoomOutController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/appzoomout/AppZoomOutController.java
new file mode 100644
index 000000000000..82ef00e46e8c
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/appzoomout/AppZoomOutController.java
@@ -0,0 +1,162 @@
+/*
+ * 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.appzoomout;
+
+import static android.app.WindowConfiguration.ROTATION_UNDEFINED;
+import static android.view.Display.DEFAULT_DISPLAY;
+
+import android.app.ActivityManager;
+import android.app.WindowConfiguration;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.util.Slog;
+import android.window.DisplayAreaInfo;
+import android.window.WindowContainerTransaction;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+
+import com.android.wm.shell.ShellTaskOrganizer;
+import com.android.wm.shell.common.DisplayChangeController;
+import com.android.wm.shell.common.DisplayController;
+import com.android.wm.shell.common.DisplayLayout;
+import com.android.wm.shell.common.RemoteCallable;
+import com.android.wm.shell.common.ShellExecutor;
+import com.android.wm.shell.shared.annotations.ExternalThread;
+import com.android.wm.shell.shared.annotations.ShellMainThread;
+import com.android.wm.shell.sysui.ShellInit;
+
+/** Class that manages the app zoom out UI and states. */
+public class AppZoomOutController implements RemoteCallable<AppZoomOutController>,
+ ShellTaskOrganizer.FocusListener, DisplayChangeController.OnDisplayChangingListener {
+
+ private static final String TAG = "AppZoomOutController";
+
+ private final Context mContext;
+ private final ShellTaskOrganizer mTaskOrganizer;
+ private final DisplayController mDisplayController;
+ private final AppZoomOutDisplayAreaOrganizer mDisplayAreaOrganizer;
+ private final ShellExecutor mMainExecutor;
+ private final AppZoomOutImpl mImpl = new AppZoomOutImpl();
+
+ private final DisplayController.OnDisplaysChangedListener mDisplaysChangedListener =
+ new DisplayController.OnDisplaysChangedListener() {
+ @Override
+ public void onDisplayConfigurationChanged(int displayId, Configuration newConfig) {
+ if (displayId != DEFAULT_DISPLAY) {
+ return;
+ }
+ updateDisplayLayout(displayId);
+ }
+
+ @Override
+ public void onDisplayAdded(int displayId) {
+ if (displayId != DEFAULT_DISPLAY) {
+ return;
+ }
+ updateDisplayLayout(displayId);
+ }
+ };
+
+
+ public static AppZoomOutController create(Context context, ShellInit shellInit,
+ ShellTaskOrganizer shellTaskOrganizer, DisplayController displayController,
+ DisplayLayout displayLayout, @ShellMainThread ShellExecutor mainExecutor) {
+ AppZoomOutDisplayAreaOrganizer displayAreaOrganizer = new AppZoomOutDisplayAreaOrganizer(
+ context, displayLayout, mainExecutor);
+ return new AppZoomOutController(context, shellInit, shellTaskOrganizer, displayController,
+ displayAreaOrganizer, mainExecutor);
+ }
+
+ @VisibleForTesting
+ AppZoomOutController(Context context, ShellInit shellInit,
+ ShellTaskOrganizer shellTaskOrganizer, DisplayController displayController,
+ AppZoomOutDisplayAreaOrganizer displayAreaOrganizer,
+ @ShellMainThread ShellExecutor mainExecutor) {
+ mContext = context;
+ mTaskOrganizer = shellTaskOrganizer;
+ mDisplayController = displayController;
+ mDisplayAreaOrganizer = displayAreaOrganizer;
+ mMainExecutor = mainExecutor;
+
+ shellInit.addInitCallback(this::onInit, this);
+ }
+
+ private void onInit() {
+ mTaskOrganizer.addFocusListener(this);
+
+ mDisplayController.addDisplayWindowListener(mDisplaysChangedListener);
+ mDisplayController.addDisplayChangingController(this);
+ updateDisplayLayout(mContext.getDisplayId());
+
+ mDisplayAreaOrganizer.registerOrganizer();
+ }
+
+ public AppZoomOut asAppZoomOut() {
+ return mImpl;
+ }
+
+ public void setProgress(float progress) {
+ mDisplayAreaOrganizer.setProgress(progress);
+ }
+
+ void updateDisplayLayout(int displayId) {
+ final DisplayLayout newDisplayLayout = mDisplayController.getDisplayLayout(displayId);
+ if (newDisplayLayout == null) {
+ Slog.w(TAG, "Failed to get new DisplayLayout.");
+ return;
+ }
+ mDisplayAreaOrganizer.setDisplayLayout(newDisplayLayout);
+ }
+
+ @Override
+ public void onFocusTaskChanged(ActivityManager.RunningTaskInfo taskInfo) {
+ if (taskInfo == null) {
+ return;
+ }
+ if (taskInfo.getActivityType() == WindowConfiguration.ACTIVITY_TYPE_HOME) {
+ mDisplayAreaOrganizer.setIsHomeTaskFocused(taskInfo.isFocused);
+ }
+ }
+
+ @Override
+ public void onDisplayChange(int displayId, int fromRotation, int toRotation,
+ @Nullable DisplayAreaInfo newDisplayAreaInfo, WindowContainerTransaction wct) {
+ // TODO: verify if there is synchronization issues.
+ if (toRotation != ROTATION_UNDEFINED) {
+ mDisplayAreaOrganizer.onRotateDisplay(mContext, toRotation);
+ }
+ }
+
+ @Override
+ public Context getContext() {
+ return mContext;
+ }
+
+ @Override
+ public ShellExecutor getRemoteCallExecutor() {
+ return mMainExecutor;
+ }
+
+ @ExternalThread
+ private class AppZoomOutImpl implements AppZoomOut {
+ @Override
+ public void setProgress(float progress) {
+ mMainExecutor.execute(() -> AppZoomOutController.this.setProgress(progress));
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/appzoomout/AppZoomOutDisplayAreaOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/appzoomout/AppZoomOutDisplayAreaOrganizer.java
new file mode 100644
index 000000000000..1c37461b2d2b
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/appzoomout/AppZoomOutDisplayAreaOrganizer.java
@@ -0,0 +1,157 @@
+/*
+ * 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.appzoomout;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.util.ArrayMap;
+import android.view.SurfaceControl;
+import android.window.DisplayAreaAppearedInfo;
+import android.window.DisplayAreaInfo;
+import android.window.DisplayAreaOrganizer;
+import android.window.WindowContainerToken;
+
+import com.android.internal.policy.ScreenDecorationsUtils;
+import com.android.wm.shell.common.DisplayLayout;
+
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Executor;
+
+/** Display area organizer that manages the app zoom out UI and states. */
+public class AppZoomOutDisplayAreaOrganizer extends DisplayAreaOrganizer {
+
+ private static final float PUSHBACK_SCALE_FOR_LAUNCHER = 0.05f;
+ private static final float PUSHBACK_SCALE_FOR_APP = 0.025f;
+ private static final float INVALID_PROGRESS = -1;
+
+ private final DisplayLayout mDisplayLayout = new DisplayLayout();
+ private final Context mContext;
+ private final float mCornerRadius;
+ private final Map<WindowContainerToken, SurfaceControl> mDisplayAreaTokenMap =
+ new ArrayMap<>();
+
+ private float mProgress = INVALID_PROGRESS;
+ // Denote whether the home task is focused, null when it's not yet initialized.
+ @Nullable private Boolean mIsHomeTaskFocused;
+
+ public AppZoomOutDisplayAreaOrganizer(Context context,
+ DisplayLayout displayLayout, Executor mainExecutor) {
+ super(mainExecutor);
+ mContext = context;
+ mCornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(mContext);
+ setDisplayLayout(displayLayout);
+ }
+
+ @Override
+ public void onDisplayAreaAppeared(DisplayAreaInfo displayAreaInfo, SurfaceControl leash) {
+ leash.setUnreleasedWarningCallSite(
+ "AppZoomOutDisplayAreaOrganizer.onDisplayAreaAppeared");
+ mDisplayAreaTokenMap.put(displayAreaInfo.token, leash);
+ }
+
+ @Override
+ public void onDisplayAreaVanished(DisplayAreaInfo displayAreaInfo) {
+ final SurfaceControl leash = mDisplayAreaTokenMap.get(displayAreaInfo.token);
+ if (leash != null) {
+ leash.release();
+ }
+ mDisplayAreaTokenMap.remove(displayAreaInfo.token);
+ }
+
+ public void registerOrganizer() {
+ final List<DisplayAreaAppearedInfo> displayAreaInfos = registerOrganizer(
+ AppZoomOutDisplayAreaOrganizer.FEATURE_APP_ZOOM_OUT);
+ for (int i = 0; i < displayAreaInfos.size(); i++) {
+ final DisplayAreaAppearedInfo info = displayAreaInfos.get(i);
+ onDisplayAreaAppeared(info.getDisplayAreaInfo(), info.getLeash());
+ }
+ }
+
+ @Override
+ public void unregisterOrganizer() {
+ super.unregisterOrganizer();
+ reset();
+ }
+
+ void setProgress(float progress) {
+ if (mProgress == progress) {
+ return;
+ }
+
+ mProgress = progress;
+ apply();
+ }
+
+ void setIsHomeTaskFocused(boolean isHomeTaskFocused) {
+ if (mIsHomeTaskFocused != null && mIsHomeTaskFocused == isHomeTaskFocused) {
+ return;
+ }
+
+ mIsHomeTaskFocused = isHomeTaskFocused;
+ apply();
+ }
+
+ private void apply() {
+ if (mIsHomeTaskFocused == null || mProgress == INVALID_PROGRESS) {
+ return;
+ }
+
+ SurfaceControl.Transaction tx = new SurfaceControl.Transaction();
+ float scale = mProgress * (mIsHomeTaskFocused
+ ? PUSHBACK_SCALE_FOR_LAUNCHER : PUSHBACK_SCALE_FOR_APP);
+ mDisplayAreaTokenMap.forEach((token, leash) -> updateSurface(tx, leash, scale));
+ tx.apply();
+ }
+
+ void setDisplayLayout(DisplayLayout displayLayout) {
+ mDisplayLayout.set(displayLayout);
+ }
+
+ private void reset() {
+ setProgress(0);
+ mProgress = INVALID_PROGRESS;
+ mIsHomeTaskFocused = null;
+ }
+
+ private void updateSurface(SurfaceControl.Transaction tx, SurfaceControl leash, float scale) {
+ if (scale == 0) {
+ // Reset when scale is set back to 0.
+ tx
+ .setCrop(leash, null)
+ .setScale(leash, 1, 1)
+ .setPosition(leash, 0, 0)
+ .setCornerRadius(leash, 0);
+ return;
+ }
+
+ tx
+ // Rounded corner can only be applied if a crop is set.
+ .setCrop(leash, 0, 0, mDisplayLayout.width(), mDisplayLayout.height())
+ .setScale(leash, 1 - scale, 1 - scale)
+ .setPosition(leash, scale * mDisplayLayout.width() * 0.5f,
+ scale * mDisplayLayout.height() * 0.5f)
+ .setCornerRadius(leash, mCornerRadius * (1 - scale));
+ }
+
+ void onRotateDisplay(Context context, int toRotation) {
+ if (mDisplayLayout.rotation() == toRotation) {
+ return;
+ }
+ mDisplayLayout.rotateTo(context.getResources(), toRotation);
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayController.java
index f532be6b8277..72be066fc7a7 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayController.java
@@ -21,6 +21,7 @@ import android.content.Context;
import android.content.res.Configuration;
import android.graphics.Rect;
import android.hardware.display.DisplayManager;
+import android.hardware.display.DisplayTopology;
import android.os.RemoteException;
import android.util.ArraySet;
import android.util.Size;
@@ -34,6 +35,7 @@ import android.window.WindowContainerTransaction;
import androidx.annotation.BinderThread;
+import com.android.window.flags.Flags;
import com.android.wm.shell.common.DisplayChangeController.OnDisplayChangingListener;
import com.android.wm.shell.shared.annotations.ShellMainThread;
import com.android.wm.shell.sysui.ShellInit;
@@ -54,6 +56,7 @@ public class DisplayController {
private final ShellExecutor mMainExecutor;
private final Context mContext;
private final IWindowManager mWmService;
+ private final DisplayManager mDisplayManager;
private final DisplayChangeController mChangeController;
private final IDisplayWindowListener mDisplayContainerListener;
@@ -61,10 +64,11 @@ public class DisplayController {
private final ArrayList<OnDisplaysChangedListener> mDisplayChangedListeners = new ArrayList<>();
public DisplayController(Context context, IWindowManager wmService, ShellInit shellInit,
- ShellExecutor mainExecutor) {
+ ShellExecutor mainExecutor, DisplayManager displayManager) {
mMainExecutor = mainExecutor;
mContext = context;
mWmService = wmService;
+ mDisplayManager = displayManager;
// TODO: Inject this instead
mChangeController = new DisplayChangeController(mWmService, shellInit, mainExecutor);
mDisplayContainerListener = new DisplayWindowListenerImpl();
@@ -74,7 +78,7 @@ public class DisplayController {
}
/**
- * Initializes the window listener.
+ * Initializes the window listener and the topology listener.
*/
public void onInit() {
try {
@@ -82,6 +86,12 @@ public class DisplayController {
for (int i = 0; i < displayIds.length; i++) {
onDisplayAdded(displayIds[i]);
}
+
+ if (Flags.enableConnectedDisplaysWindowDrag()) {
+ mDisplayManager.registerTopologyListener(mMainExecutor,
+ this::onDisplayTopologyChanged);
+ onDisplayTopologyChanged(mDisplayManager.getDisplayTopology());
+ }
} catch (RemoteException e) {
throw new RuntimeException("Unable to register display controller");
}
@@ -91,8 +101,7 @@ public class DisplayController {
* Gets a display by id from DisplayManager.
*/
public Display getDisplay(int displayId) {
- final DisplayManager displayManager = mContext.getSystemService(DisplayManager.class);
- return displayManager.getDisplay(displayId);
+ return mDisplayManager.getDisplay(displayId);
}
/**
@@ -221,6 +230,14 @@ public class DisplayController {
}
}
+ private void onDisplayTopologyChanged(DisplayTopology topology) {
+ // TODO(b/381472611): Call DisplayTopology#getCoordinates and update values in
+ // DisplayLayout when DM code is ready.
+ for (int i = 0; i < mDisplayChangedListeners.size(); ++i) {
+ mDisplayChangedListeners.get(i).onTopologyChanged();
+ }
+ }
+
private void onDisplayConfigurationChanged(int displayId, Configuration newConfig) {
synchronized (mDisplays) {
final DisplayRecord dr = mDisplays.get(displayId);
@@ -408,5 +425,10 @@ public class DisplayController {
*/
default void onKeepClearAreasChanged(int displayId, Set<Rect> restricted,
Set<Rect> unrestricted) {}
+
+ /**
+ * Called when the display topology has changed.
+ */
+ default void onTopologyChanged() {}
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayLayout.java
index b6a1686bd087..4973a6f16409 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayLayout.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayLayout.java
@@ -31,7 +31,9 @@ import android.content.ContentResolver;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Insets;
+import android.graphics.PointF;
import android.graphics.Rect;
+import android.graphics.RectF;
import android.os.SystemProperties;
import android.provider.Settings;
import android.util.DisplayMetrics;
@@ -71,9 +73,12 @@ public class DisplayLayout {
public static final int NAV_BAR_RIGHT = 1 << 1;
public static final int NAV_BAR_BOTTOM = 1 << 2;
+ private static final String TAG = "DisplayLayout";
+
private int mUiMode;
private int mWidth;
private int mHeight;
+ private RectF mGlobalBoundsDp;
private DisplayCutout mCutout;
private int mRotation;
private int mDensityDpi;
@@ -109,6 +114,7 @@ public class DisplayLayout {
return mUiMode == other.mUiMode
&& mWidth == other.mWidth
&& mHeight == other.mHeight
+ && Objects.equals(mGlobalBoundsDp, other.mGlobalBoundsDp)
&& Objects.equals(mCutout, other.mCutout)
&& mRotation == other.mRotation
&& mDensityDpi == other.mDensityDpi
@@ -127,8 +133,8 @@ public class DisplayLayout {
@Override
public int hashCode() {
- return Objects.hash(mUiMode, mWidth, mHeight, mCutout, mRotation, mDensityDpi,
- mNonDecorInsets, mStableInsets, mHasNavigationBar, mHasStatusBar,
+ return Objects.hash(mUiMode, mWidth, mHeight, mGlobalBoundsDp, mCutout, mRotation,
+ mDensityDpi, mNonDecorInsets, mStableInsets, mHasNavigationBar, mHasStatusBar,
mNavBarFrameHeight, mTaskbarFrameHeight, mAllowSeamlessRotationDespiteNavBarMoving,
mNavigationBarCanMove, mReverseDefaultRotation, mInsetsState);
}
@@ -170,6 +176,7 @@ public class DisplayLayout {
mUiMode = dl.mUiMode;
mWidth = dl.mWidth;
mHeight = dl.mHeight;
+ mGlobalBoundsDp = dl.mGlobalBoundsDp;
mCutout = dl.mCutout;
mRotation = dl.mRotation;
mDensityDpi = dl.mDensityDpi;
@@ -193,6 +200,7 @@ public class DisplayLayout {
mRotation = info.rotation;
mCutout = info.displayCutout;
mDensityDpi = info.logicalDensityDpi;
+ mGlobalBoundsDp = new RectF(0, 0, pxToDp(mWidth), pxToDp(mHeight));
mHasNavigationBar = hasNavigationBar;
mHasStatusBar = hasStatusBar;
mAllowSeamlessRotationDespiteNavBarMoving = res.getBoolean(
@@ -255,6 +263,11 @@ public class DisplayLayout {
recalcInsets(res);
}
+ /** Update the global bounds of this layout, in DP. */
+ public void setGlobalBoundsDp(RectF bounds) {
+ mGlobalBoundsDp = bounds;
+ }
+
/** Get this layout's non-decor insets. */
public Rect nonDecorInsets() {
return mNonDecorInsets;
@@ -265,16 +278,21 @@ public class DisplayLayout {
return mStableInsets;
}
- /** Get this layout's width. */
+ /** Get this layout's width in pixels. */
public int width() {
return mWidth;
}
- /** Get this layout's height. */
+ /** Get this layout's height in pixels. */
public int height() {
return mHeight;
}
+ /** Get this layout's global bounds in the multi-display coordinate system in DP. */
+ public RectF globalBoundsDp() {
+ return mGlobalBoundsDp;
+ }
+
/** Get this layout's display rotation. */
public int rotation() {
return mRotation;
@@ -486,4 +504,48 @@ public class DisplayLayout {
? R.dimen.navigation_bar_frame_height_landscape
: R.dimen.navigation_bar_frame_height);
}
+
+ /**
+ * Converts a pixel value to a density-independent pixel (dp) value.
+ *
+ * @param px The pixel value to convert.
+ * @return The equivalent value in DP units.
+ */
+ public float pxToDp(Number px) {
+ return px.floatValue() * DisplayMetrics.DENSITY_DEFAULT / mDensityDpi;
+ }
+
+ /**
+ * Converts a density-independent pixel (dp) value to a pixel value.
+ *
+ * @param dp The DP value to convert.
+ * @return The equivalent value in pixel units.
+ */
+ public float dpToPx(Number dp) {
+ return dp.floatValue() * mDensityDpi / DisplayMetrics.DENSITY_DEFAULT;
+ }
+
+ /**
+ * Converts local pixel coordinates on this layout to global DP coordinates.
+ *
+ * @param xPx The x-coordinate in pixels, relative to the layout's origin.
+ * @param yPx The y-coordinate in pixels, relative to the layout's origin.
+ * @return A PointF object representing the coordinates in global DP units.
+ */
+ public PointF localPxToGlobalDp(Number xPx, Number yPx) {
+ return new PointF(mGlobalBoundsDp.left + pxToDp(xPx),
+ mGlobalBoundsDp.top + pxToDp(yPx));
+ }
+
+ /**
+ * Converts global DP coordinates to local pixel coordinates on this layout.
+ *
+ * @param xDp The x-coordinate in global DP units.
+ * @param yDp The y-coordinate in global DP units.
+ * @return A PointF object representing the coordinates in local pixel units on this layout.
+ */
+ public PointF globalDpToLocalPx(Number xDp, Number yDp) {
+ return new PointF(dpToPx(xDp.floatValue() - mGlobalBoundsDp.left),
+ dpToPx(yDp.floatValue() - mGlobalBoundsDp.top));
+ }
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMComponent.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMComponent.java
index c493aadd57b0..151dc438702d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMComponent.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMComponent.java
@@ -20,6 +20,7 @@ import android.os.HandlerThread;
import androidx.annotation.Nullable;
+import com.android.wm.shell.appzoomout.AppZoomOut;
import com.android.wm.shell.back.BackAnimation;
import com.android.wm.shell.bubbles.Bubbles;
import com.android.wm.shell.desktopmode.DesktopMode;
@@ -112,4 +113,7 @@ public interface WMComponent {
*/
@WMSingleton
Optional<DesktopMode> getDesktopMode();
+
+ @WMSingleton
+ Optional<AppZoomOut> getAppZoomOut();
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
index 23a0f4adb6d2..cbbe8a2b5613 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
@@ -25,6 +25,7 @@ import android.annotation.NonNull;
import android.app.ActivityTaskManager;
import android.content.Context;
import android.content.pm.PackageManager;
+import android.hardware.display.DisplayManager;
import android.os.Handler;
import android.os.SystemProperties;
import android.provider.Settings;
@@ -91,6 +92,7 @@ import com.android.wm.shell.compatui.impl.DefaultComponentIdGenerator;
import com.android.wm.shell.desktopmode.DesktopMode;
import com.android.wm.shell.desktopmode.DesktopTasksController;
import com.android.wm.shell.desktopmode.DesktopUserRepositories;
+import com.android.wm.shell.desktopmode.desktopwallpaperactivity.DesktopWallpaperActivityTokenProvider;
import com.android.wm.shell.displayareahelper.DisplayAreaHelper;
import com.android.wm.shell.displayareahelper.DisplayAreaHelperController;
import com.android.wm.shell.freeform.FreeformComponents;
@@ -111,6 +113,8 @@ import com.android.wm.shell.shared.annotations.ShellAnimationThread;
import com.android.wm.shell.shared.annotations.ShellMainThread;
import com.android.wm.shell.shared.annotations.ShellSplashscreenThread;
import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
+import com.android.wm.shell.appzoomout.AppZoomOut;
+import com.android.wm.shell.appzoomout.AppZoomOutController;
import com.android.wm.shell.splitscreen.SplitScreen;
import com.android.wm.shell.splitscreen.SplitScreenController;
import com.android.wm.shell.startingsurface.StartingSurface;
@@ -172,8 +176,9 @@ public abstract class WMShellBaseModule {
static DisplayController provideDisplayController(Context context,
IWindowManager wmService,
ShellInit shellInit,
- @ShellMainThread ShellExecutor mainExecutor) {
- return new DisplayController(context, wmService, shellInit, mainExecutor);
+ @ShellMainThread ShellExecutor mainExecutor,
+ DisplayManager displayManager) {
+ return new DisplayController(context, wmService, shellInit, mainExecutor, displayManager);
}
@WMSingleton
@@ -1031,6 +1036,38 @@ public abstract class WMShellBaseModule {
});
}
+ @WMSingleton
+ @Provides
+ static DesktopWallpaperActivityTokenProvider provideDesktopWallpaperActivityTokenProvider() {
+ return new DesktopWallpaperActivityTokenProvider();
+ }
+
+ @WMSingleton
+ @Provides
+ static Optional<DesktopWallpaperActivityTokenProvider>
+ provideOptionalDesktopWallpaperActivityTokenProvider(
+ Context context,
+ DesktopWallpaperActivityTokenProvider desktopWallpaperActivityTokenProvider) {
+ if (DesktopModeStatus.canEnterDesktopMode(context)) {
+ return Optional.of(desktopWallpaperActivityTokenProvider);
+ }
+ return Optional.empty();
+ }
+
+ //
+ // App zoom out (optional feature)
+ //
+
+ @WMSingleton
+ @Provides
+ static Optional<AppZoomOut> provideAppZoomOut(
+ Optional<AppZoomOutController> appZoomOutController) {
+ return appZoomOutController.map((controller) -> controller.asAppZoomOut());
+ }
+
+ @BindsOptionalOf
+ abstract AppZoomOutController optionalAppZoomOutController();
+
//
// Task Stack
//
@@ -1075,6 +1112,7 @@ public abstract class WMShellBaseModule {
Optional<RecentTasksController> recentTasksOptional,
Optional<RecentsTransitionHandler> recentsTransitionHandlerOptional,
Optional<OneHandedController> oneHandedControllerOptional,
+ Optional<AppZoomOutController> appZoomOutControllerOptional,
Optional<HideDisplayCutoutController> hideDisplayCutoutControllerOptional,
Optional<ActivityEmbeddingController> activityEmbeddingOptional,
Optional<MixedTransitionHandler> mixedTransitionHandler,
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 1916215dea74..e8add56619c4 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
@@ -50,6 +50,7 @@ import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.activityembedding.ActivityEmbeddingController;
import com.android.wm.shell.apptoweb.AppToWebGenericLinksParser;
import com.android.wm.shell.apptoweb.AssistContentRequester;
+import com.android.wm.shell.appzoomout.AppZoomOutController;
import com.android.wm.shell.back.BackAnimationController;
import com.android.wm.shell.bubbles.BubbleController;
import com.android.wm.shell.bubbles.BubbleData;
@@ -945,7 +946,8 @@ public abstract class WMShellModule {
FocusTransitionObserver focusTransitionObserver,
DesktopModeEventLogger desktopModeEventLogger,
DesktopModeUiEventLogger desktopModeUiEventLogger,
- WindowDecorTaskResourceLoader taskResourceLoader
+ WindowDecorTaskResourceLoader taskResourceLoader,
+ RecentsTransitionHandler recentsTransitionHandler
) {
if (!DesktopModeStatus.canEnterDesktopModeOrShowAppHandle(context)) {
return Optional.empty();
@@ -961,7 +963,7 @@ public abstract class WMShellModule {
desktopTasksLimiter, appHandleEducationController, appToWebEducationController,
windowDecorCaptionHandleRepository, activityOrientationChangeHandler,
focusTransitionObserver, desktopModeEventLogger, desktopModeUiEventLogger,
- taskResourceLoader));
+ taskResourceLoader, recentsTransitionHandler));
}
@WMSingleton
@@ -1312,10 +1314,21 @@ public abstract class WMShellModule {
return new DesktopModeUiEventLogger(uiEventLogger, packageManager);
}
+ //
+ // App zoom out
+ //
+
@WMSingleton
@Provides
- static DesktopWallpaperActivityTokenProvider provideDesktopWallpaperActivityTokenProvider() {
- return new DesktopWallpaperActivityTokenProvider();
+ static AppZoomOutController provideAppZoomOutController(
+ Context context,
+ ShellInit shellInit,
+ ShellTaskOrganizer shellTaskOrganizer,
+ DisplayController displayController,
+ DisplayLayout displayLayout,
+ @ShellMainThread ShellExecutor mainExecutor) {
+ return AppZoomOutController.create(context, shellInit, shellTaskOrganizer,
+ displayController, displayLayout, mainExecutor);
}
//
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java
index 9e2b9b20be16..c8d0dab39837 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java
@@ -41,6 +41,7 @@ import com.android.wm.shell.common.pip.SizeSpecSource;
import com.android.wm.shell.dagger.WMShellBaseModule;
import com.android.wm.shell.dagger.WMSingleton;
import com.android.wm.shell.desktopmode.DesktopUserRepositories;
+import com.android.wm.shell.desktopmode.desktopwallpaperactivity.DesktopWallpaperActivityTokenProvider;
import com.android.wm.shell.pip2.phone.PhonePipMenuController;
import com.android.wm.shell.pip2.phone.PipController;
import com.android.wm.shell.pip2.phone.PipMotionHelper;
@@ -82,11 +83,14 @@ public abstract class Pip2Module {
@NonNull PipTransitionState pipStackListenerController,
@NonNull PipDisplayLayoutState pipDisplayLayoutState,
@NonNull PipUiStateChangeController pipUiStateChangeController,
- Optional<DesktopUserRepositories> desktopUserRepositoriesOptional) {
+ Optional<DesktopUserRepositories> desktopUserRepositoriesOptional,
+ Optional<DesktopWallpaperActivityTokenProvider>
+ desktopWallpaperActivityTokenProviderOptional) {
return new PipTransition(context, shellInit, shellTaskOrganizer, transitions,
pipBoundsState, null, pipBoundsAlgorithm, pipTaskListener,
pipScheduler, pipStackListenerController, pipDisplayLayoutState,
- pipUiStateChangeController, desktopUserRepositoriesOptional);
+ pipUiStateChangeController, desktopUserRepositoriesOptional,
+ desktopWallpaperActivityTokenProviderOptional);
}
@WMSingleton
@@ -138,9 +142,12 @@ public abstract class Pip2Module {
@ShellMainThread ShellExecutor mainExecutor,
PipTransitionState pipTransitionState,
Optional<DesktopUserRepositories> desktopUserRepositoriesOptional,
+ Optional<DesktopWallpaperActivityTokenProvider>
+ desktopWallpaperActivityTokenProviderOptional,
RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer) {
return new PipScheduler(context, pipBoundsState, mainExecutor, pipTransitionState,
- desktopUserRepositoriesOptional, rootTaskDisplayAreaOrganizer);
+ desktopUserRepositoriesOptional, desktopWallpaperActivityTokenProviderOptional,
+ rootTaskDisplayAreaOrganizer);
}
@WMSingleton
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeDragAndDropTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeDragAndDropTransitionHandler.kt
index ca02c72c174e..f6fd9679922a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeDragAndDropTransitionHandler.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeDragAndDropTransitionHandler.kt
@@ -93,9 +93,8 @@ class DesktopModeDragAndDropTransitionHandler(private val transitions: Transitio
return matchingChanges.first()
}
- private fun isValidTaskChange(change: TransitionInfo.Change): Boolean {
- return change.taskInfo != null && change.taskInfo?.taskId != -1
- }
+ private fun isValidTaskChange(change: TransitionInfo.Change): Boolean =
+ change.taskInfo != null && change.taskInfo?.taskId != -1
override fun handleRequest(
transition: IBinder,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt
index e975b586c1ee..c09504ee3725 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt
@@ -434,18 +434,14 @@ class DesktopModeLoggerTransitionObserver(
visibleFreeformTaskInfos.set(taskInfo.taskId, taskInfo)
}
- private fun TransitionInfo.Change.requireTaskInfo(): RunningTaskInfo {
- return this.taskInfo ?: throw IllegalStateException("Expected TaskInfo in the Change")
- }
+ private fun TransitionInfo.Change.requireTaskInfo(): RunningTaskInfo =
+ this.taskInfo ?: throw IllegalStateException("Expected TaskInfo in the Change")
- private fun TaskInfo.isFreeformWindow(): Boolean {
- return this.windowingMode == WINDOWING_MODE_FREEFORM
- }
+ private fun TaskInfo.isFreeformWindow(): Boolean = this.windowingMode == WINDOWING_MODE_FREEFORM
- private fun TransitionInfo.isExitToRecentsTransition(): Boolean {
- return this.type == WindowManager.TRANSIT_TO_FRONT &&
+ private fun TransitionInfo.isExitToRecentsTransition(): Boolean =
+ this.type == WindowManager.TRANSIT_TO_FRONT &&
this.flags == WindowManager.TRANSIT_FLAG_IS_RECENTS
- }
companion object {
@VisibleForTesting const val VISIBLE_TASKS_COUNTER_NAME = "desktop_mode_visible_tasks"
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeShellCommandHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeShellCommandHandler.kt
index dba8c9367654..cdfa14bbc4e2 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeShellCommandHandler.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeShellCommandHandler.kt
@@ -24,8 +24,8 @@ import java.io.PrintWriter
class DesktopModeShellCommandHandler(private val controller: DesktopTasksController) :
ShellCommandHandler.ShellCommandActionHandler {
- override fun onShellCommand(args: Array<String>, pw: PrintWriter): Boolean {
- return when (args[0]) {
+ override fun onShellCommand(args: Array<String>, pw: PrintWriter): Boolean =
+ when (args[0]) {
"moveToDesktop" -> {
if (!runMoveToDesktop(args, pw)) {
pw.println("Task not found. Please enter a valid taskId.")
@@ -47,7 +47,6 @@ class DesktopModeShellCommandHandler(private val controller: DesktopTasksControl
false
}
}
- }
private fun runMoveToDesktop(args: Array<String>, pw: PrintWriter): Boolean {
if (args.size < 2) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt
index d3066645f32e..1a58363dab81 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt
@@ -27,6 +27,7 @@ import androidx.core.util.forEach
import androidx.core.util.keyIterator
import androidx.core.util.valueIterator
import com.android.internal.protolog.ProtoLog
+import com.android.window.flags.Flags
import com.android.wm.shell.desktopmode.persistence.DesktopPersistentRepository
import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE
import com.android.wm.shell.shared.annotations.ShellMainThread
@@ -56,6 +57,10 @@ class DesktopRepository(
* @property topTransparentFullscreenTaskId the task id of any current top transparent
* fullscreen task launched on top of Desktop Mode. Cleared when the transparent task is
* closed or sent to back. (top is at index 0).
+ * @property pipTaskId the task id of PiP task entered while in Desktop Mode.
+ * @property pipShouldKeepDesktopActive whether an active PiP window should keep the Desktop
+ * Mode session active. Only false when we are explicitly exiting Desktop Mode (via user
+ * action) while there is an active PiP window.
*/
private data class DesktopTaskData(
val activeTasks: ArraySet<Int> = ArraySet(),
@@ -66,6 +71,8 @@ class DesktopRepository(
val freeformTasksInZOrder: ArrayList<Int> = ArrayList(),
var fullImmersiveTaskId: Int? = null,
var topTransparentFullscreenTaskId: Int? = null,
+ var pipTaskId: Int? = null,
+ var pipShouldKeepDesktopActive: Boolean = true,
) {
fun deepCopy(): DesktopTaskData =
DesktopTaskData(
@@ -76,6 +83,8 @@ class DesktopRepository(
freeformTasksInZOrder = ArrayList(freeformTasksInZOrder),
fullImmersiveTaskId = fullImmersiveTaskId,
topTransparentFullscreenTaskId = topTransparentFullscreenTaskId,
+ pipTaskId = pipTaskId,
+ pipShouldKeepDesktopActive = pipShouldKeepDesktopActive,
)
fun clear() {
@@ -86,6 +95,8 @@ class DesktopRepository(
freeformTasksInZOrder.clear()
fullImmersiveTaskId = null
topTransparentFullscreenTaskId = null
+ pipTaskId = null
+ pipShouldKeepDesktopActive = true
}
}
@@ -104,6 +115,9 @@ class DesktopRepository(
/* Tracks last bounds of task before toggled to immersive state. */
private val boundsBeforeFullImmersiveByTaskId = SparseArray<Rect>()
+ /* Callback for when a pending PiP transition has been aborted. */
+ private var onPipAbortedCallback: ((Int, Int) -> Unit)? = null
+
private var desktopGestureExclusionListener: Consumer<Region>? = null
private var desktopGestureExclusionExecutor: Executor? = null
@@ -302,6 +316,54 @@ class DesktopRepository(
}
}
+ /** Set whether the given task is the Desktop-entered PiP task in this display. */
+ fun setTaskInPip(displayId: Int, taskId: Int, enterPip: Boolean) {
+ val desktopData = desktopTaskDataByDisplayId.getOrCreate(displayId)
+ if (enterPip) {
+ desktopData.pipTaskId = taskId
+ desktopData.pipShouldKeepDesktopActive = true
+ } else {
+ desktopData.pipTaskId =
+ if (desktopData.pipTaskId == taskId) null
+ else {
+ logW(
+ "setTaskInPip: taskId=$taskId did not match saved taskId=${desktopData.pipTaskId}"
+ )
+ desktopData.pipTaskId
+ }
+ }
+ notifyVisibleTaskListeners(displayId, getVisibleTaskCount(displayId))
+ }
+
+ /** Returns whether there is a PiP that was entered/minimized from Desktop in this display. */
+ fun isMinimizedPipPresentInDisplay(displayId: Int): Boolean =
+ desktopTaskDataByDisplayId.getOrCreate(displayId).pipTaskId != null
+
+ /** Returns whether the given task is the Desktop-entered PiP task in this display. */
+ fun isTaskMinimizedPipInDisplay(displayId: Int, taskId: Int): Boolean =
+ desktopTaskDataByDisplayId.getOrCreate(displayId).pipTaskId == taskId
+
+ /** Returns whether Desktop session should be active in this display due to active PiP. */
+ fun shouldDesktopBeActiveForPip(displayId: Int): Boolean =
+ Flags.enableDesktopWindowingPip() &&
+ isMinimizedPipPresentInDisplay(displayId) &&
+ desktopTaskDataByDisplayId.getOrCreate(displayId).pipShouldKeepDesktopActive
+
+ /** Saves whether a PiP window should keep Desktop session active in this display. */
+ fun setPipShouldKeepDesktopActive(displayId: Int, keepActive: Boolean) {
+ desktopTaskDataByDisplayId.getOrCreate(displayId).pipShouldKeepDesktopActive = keepActive
+ }
+
+ /** Saves callback to handle a pending PiP transition being aborted. */
+ fun setOnPipAbortedCallback(callbackIfPipAborted: ((Int, Int) -> Unit)?) {
+ onPipAbortedCallback = callbackIfPipAborted
+ }
+
+ /** Invokes callback to handle a pending PiP transition with the given task id being aborted. */
+ fun onPipAborted(displayId: Int, pipTaskId: Int) {
+ onPipAbortedCallback?.invoke(displayId, pipTaskId)
+ }
+
/** Set whether the given task is the full-immersive task in this display. */
fun setTaskInFullImmersiveState(displayId: Int, taskId: Int, immersive: Boolean) {
val desktopData = desktopTaskDataByDisplayId.getOrCreate(displayId)
@@ -338,8 +400,12 @@ class DesktopRepository(
}
private fun notifyVisibleTaskListeners(displayId: Int, visibleTasksCount: Int) {
+ val visibleAndPipTasksCount =
+ if (shouldDesktopBeActiveForPip(displayId)) visibleTasksCount + 1 else visibleTasksCount
visibleTasksListeners.forEach { (listener, executor) ->
- executor.execute { listener.onTasksVisibilityChanged(displayId, visibleTasksCount) }
+ executor.execute {
+ listener.onTasksVisibilityChanged(displayId, visibleAndPipTasksCount)
+ }
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTaskPosition.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTaskPosition.kt
index 848d80ff4f0b..f29301d92292 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTaskPosition.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTaskPosition.kt
@@ -41,49 +41,35 @@ sealed class DesktopTaskPosition {
return Point(x, y.toInt())
}
- override fun next(): DesktopTaskPosition {
- return BottomRight
- }
+ override fun next(): DesktopTaskPosition = BottomRight
}
data object BottomRight : DesktopTaskPosition() {
- override fun getTopLeftCoordinates(frame: Rect, window: Rect): Point {
- return Point(frame.right - window.width(), frame.bottom - window.height())
- }
+ override fun getTopLeftCoordinates(frame: Rect, window: Rect): Point =
+ Point(frame.right - window.width(), frame.bottom - window.height())
- override fun next(): DesktopTaskPosition {
- return TopLeft
- }
+ override fun next(): DesktopTaskPosition = TopLeft
}
data object TopLeft : DesktopTaskPosition() {
- override fun getTopLeftCoordinates(frame: Rect, window: Rect): Point {
- return Point(frame.left, frame.top)
- }
+ override fun getTopLeftCoordinates(frame: Rect, window: Rect): Point =
+ Point(frame.left, frame.top)
- override fun next(): DesktopTaskPosition {
- return BottomLeft
- }
+ override fun next(): DesktopTaskPosition = BottomLeft
}
data object BottomLeft : DesktopTaskPosition() {
- override fun getTopLeftCoordinates(frame: Rect, window: Rect): Point {
- return Point(frame.left, frame.bottom - window.height())
- }
+ override fun getTopLeftCoordinates(frame: Rect, window: Rect): Point =
+ Point(frame.left, frame.bottom - window.height())
- override fun next(): DesktopTaskPosition {
- return TopRight
- }
+ override fun next(): DesktopTaskPosition = TopRight
}
data object TopRight : DesktopTaskPosition() {
- override fun getTopLeftCoordinates(frame: Rect, window: Rect): Point {
- return Point(frame.right - window.width(), frame.top)
- }
+ override fun getTopLeftCoordinates(frame: Rect, window: Rect): Point =
+ Point(frame.right - window.width(), frame.top)
- override fun next(): DesktopTaskPosition {
- return Center
- }
+ override fun next(): DesktopTaskPosition = Center
}
/**
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 050dfb6f562c..73d15270c811 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
@@ -225,7 +225,6 @@ class DesktopTasksController(
// Launch cookie used to identify a drag and drop transition to fullscreen after it has begun.
// Used to prevent handleRequest from moving the new fullscreen task to freeform.
private var dragAndDropFullscreenCookie: Binder? = null
- private var pendingPipTransitionAndTask: Pair<IBinder, Int>? = null
init {
desktopMode = DesktopModeImpl()
@@ -321,18 +320,29 @@ class DesktopTasksController(
fun visibleTaskCount(displayId: Int): Int = taskRepository.getVisibleTaskCount(displayId)
/**
- * Returns true if any freeform tasks are visible or if a transparent fullscreen task exists on
- * top in Desktop Mode.
+ * Returns true if any of the following is true:
+ * - Any freeform tasks are visible
+ * - A transparent fullscreen task exists on top in Desktop Mode
+ * - PiP on Desktop Windowing is enabled, there is an active PiP window and the desktop
+ * wallpaper is visible.
*/
fun isDesktopModeShowing(displayId: Int): Boolean {
+ val hasVisibleTasks = visibleTaskCount(displayId) > 0
+ val hasTopTransparentFullscreenTask =
+ taskRepository.getTopTransparentFullscreenTaskId(displayId) != null
+ val hasMinimizedPip =
+ Flags.enableDesktopWindowingPip() &&
+ taskRepository.isMinimizedPipPresentInDisplay(displayId) &&
+ desktopWallpaperActivityTokenProvider.isWallpaperActivityVisible(displayId)
if (
DesktopModeFlags.INCLUDE_TOP_TRANSPARENT_FULLSCREEN_TASK_IN_DESKTOP_HEURISTIC
.isTrue() && DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_MODALS_POLICY.isTrue()
) {
- return visibleTaskCount(displayId) > 0 ||
- taskRepository.getTopTransparentFullscreenTaskId(displayId) != null
+ return hasVisibleTasks || hasTopTransparentFullscreenTask || hasMinimizedPip
+ } else if (Flags.enableDesktopWindowingPip()) {
+ return hasVisibleTasks || hasMinimizedPip
}
- return visibleTaskCount(displayId) > 0
+ return hasVisibleTasks
}
/** Moves focused task to desktop mode for given [displayId]. */
@@ -592,7 +602,7 @@ class DesktopTasksController(
): ((IBinder) -> Unit)? {
val taskId = taskInfo.taskId
desktopTilingDecorViewModel.removeTaskIfTiled(displayId, taskId)
- performDesktopExitCleanupIfNeeded(taskId, displayId, wct)
+ performDesktopExitCleanupIfNeeded(taskId, displayId, wct, forceToFullscreen = false)
taskRepository.addClosingTask(displayId, taskId)
taskbarDesktopTaskListener?.onTaskbarCornerRoundingUpdate(
doesAnyTaskRequireTaskbarRounding(displayId, taskId)
@@ -624,8 +634,12 @@ class DesktopTasksController(
)
val requestRes = transitions.dispatchRequest(Binder(), requestInfo, /* skip= */ null)
wct.merge(requestRes.second, true)
- pendingPipTransitionAndTask =
- freeformTaskTransitionStarter.startPipTransition(wct) to taskInfo.taskId
+ freeformTaskTransitionStarter.startPipTransition(wct)
+ taskRepository.setTaskInPip(taskInfo.displayId, taskInfo.taskId, enterPip = true)
+ taskRepository.setOnPipAbortedCallback { displayId, taskId ->
+ minimizeTaskInner(shellTaskOrganizer.getRunningTaskInfo(taskId)!!)
+ taskRepository.setTaskInPip(displayId, taskId, enterPip = false)
+ }
return
}
@@ -636,7 +650,7 @@ class DesktopTasksController(
val taskId = taskInfo.taskId
val displayId = taskInfo.displayId
val wct = WindowContainerTransaction()
- performDesktopExitCleanupIfNeeded(taskId, displayId, wct)
+ performDesktopExitCleanupIfNeeded(taskId, displayId, wct, forceToFullscreen = false)
// Notify immersive handler as it might need to exit immersive state.
val exitResult =
desktopImmersiveController.exitImmersiveIfApplicable(
@@ -898,7 +912,12 @@ class DesktopTasksController(
}
if (Flags.enablePerDisplayDesktopWallpaperActivity()) {
- performDesktopExitCleanupIfNeeded(task.taskId, task.displayId, wct)
+ performDesktopExitCleanupIfNeeded(
+ task.taskId,
+ task.displayId,
+ wct,
+ forceToFullscreen = false,
+ )
}
transitions.startTransition(TRANSIT_CHANGE, wct, /* handler= */ null)
@@ -1414,7 +1433,9 @@ class DesktopTasksController(
taskId: Int,
displayId: Int,
wct: WindowContainerTransaction,
+ forceToFullscreen: Boolean,
) {
+ taskRepository.setPipShouldKeepDesktopActive(displayId, !forceToFullscreen)
if (Flags.enablePerDisplayDesktopWallpaperActivity()) {
if (!taskRepository.isOnlyVisibleNonClosingTask(taskId, displayId)) {
return
@@ -1422,6 +1443,12 @@ class DesktopTasksController(
if (displayId != DEFAULT_DISPLAY) {
return
}
+ } else if (
+ Flags.enableDesktopWindowingPip() &&
+ taskRepository.isMinimizedPipPresentInDisplay(displayId) &&
+ !forceToFullscreen
+ ) {
+ return
} else {
if (!taskRepository.isOnlyVisibleNonClosingTask(taskId)) {
return
@@ -1443,13 +1470,9 @@ class DesktopTasksController(
}
}
- override fun getContext(): Context {
- return context
- }
+ override fun getContext(): Context = context
- override fun getRemoteCallExecutor(): ShellExecutor {
- return mainExecutor
- }
+ override fun getRemoteCallExecutor(): ShellExecutor = mainExecutor
override fun startAnimation(
transition: IBinder,
@@ -1462,21 +1485,6 @@ class DesktopTasksController(
return false
}
- override fun onTransitionConsumed(
- transition: IBinder,
- aborted: Boolean,
- finishT: Transaction?,
- ) {
- pendingPipTransitionAndTask?.let { (pipTransition, taskId) ->
- if (transition == pipTransition) {
- if (aborted) {
- shellTaskOrganizer.getRunningTaskInfo(taskId)?.let { minimizeTaskInner(it) }
- }
- pendingPipTransitionAndTask = null
- }
- }
- }
-
override fun handleRequest(
transition: IBinder,
request: TransitionRequestInfo,
@@ -1650,11 +1658,10 @@ class DesktopTasksController(
DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_MODALS_POLICY.isTrue() &&
isTopActivityExemptFromDesktopWindowing(context, task)
- private fun shouldHandleTaskClosing(request: TransitionRequestInfo): Boolean {
- return ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY.isTrue() &&
+ private fun shouldHandleTaskClosing(request: TransitionRequestInfo): Boolean =
+ ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY.isTrue() &&
TransitionUtil.isClosingType(request.type) &&
request.triggerTask != null
- }
/** Open an existing instance of an app. */
fun openInstance(callingTask: RunningTaskInfo, requestedTaskId: Int) {
@@ -1926,7 +1933,12 @@ class DesktopTasksController(
if (!isDesktopModeShowing(task.displayId)) return null
val wct = WindowContainerTransaction()
- performDesktopExitCleanupIfNeeded(task.taskId, task.displayId, wct)
+ performDesktopExitCleanupIfNeeded(
+ task.taskId,
+ task.displayId,
+ wct,
+ forceToFullscreen = false,
+ )
if (!DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION.isTrue()) {
taskRepository.addClosingTask(task.displayId, task.taskId)
@@ -2053,7 +2065,12 @@ class DesktopTasksController(
wct.setDensityDpi(taskInfo.token, getDefaultDensityDpi())
}
- performDesktopExitCleanupIfNeeded(taskInfo.taskId, taskInfo.displayId, wct)
+ performDesktopExitCleanupIfNeeded(
+ taskInfo.taskId,
+ taskInfo.displayId,
+ wct,
+ forceToFullscreen = true,
+ )
}
private fun cascadeWindow(bounds: Rect, displayLayout: DisplayLayout, displayId: Int) {
@@ -2087,7 +2104,12 @@ class DesktopTasksController(
// want it overridden in multi-window.
wct.setDensityDpi(taskInfo.token, getDefaultDensityDpi())
- performDesktopExitCleanupIfNeeded(taskInfo.taskId, taskInfo.displayId, wct)
+ performDesktopExitCleanupIfNeeded(
+ taskInfo.taskId,
+ taskInfo.displayId,
+ wct,
+ forceToFullscreen = false,
+ )
}
/** Returns the ID of the Task that will be minimized, or null if no task will be minimized. */
@@ -2158,11 +2180,10 @@ class DesktopTasksController(
getFocusedFreeformTask(displayId)?.let { requestSplit(it, leftOrTop) }
}
- private fun getFocusedFreeformTask(displayId: Int): RunningTaskInfo? {
- return shellTaskOrganizer.getRunningTasks(displayId).find { taskInfo ->
+ private fun getFocusedFreeformTask(displayId: Int): RunningTaskInfo? =
+ shellTaskOrganizer.getRunningTasks(displayId).find { taskInfo ->
taskInfo.isFocused && taskInfo.windowingMode == WINDOWING_MODE_FREEFORM
}
- }
/**
* Requests a task be transitioned from desktop to split select. Applies needed windowing
@@ -2210,14 +2231,10 @@ class DesktopTasksController(
}
}
- private fun getDefaultDensityDpi(): Int {
- return context.resources.displayMetrics.densityDpi
- }
+ private fun getDefaultDensityDpi(): Int = context.resources.displayMetrics.densityDpi
/** Creates a new instance of the external interface to pass to another process. */
- private fun createExternalInterface(): ExternalInterfaceBinder {
- return IDesktopModeImpl(this)
- }
+ private fun createExternalInterface(): ExternalInterfaceBinder = IDesktopModeImpl(this)
/** Get connection interface between sysui and shell */
fun asDesktopMode(): DesktopMode {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt
index c2dd4d28305b..e4a28e9efe60 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt
@@ -138,11 +138,10 @@ class DesktopTasksLimiter(
)
}
- private fun getMinimizeChange(info: TransitionInfo, taskId: Int): TransitionInfo.Change? {
- return info.changes.find { change ->
+ private fun getMinimizeChange(info: TransitionInfo, taskId: Int): TransitionInfo.Change? =
+ info.changes.find { change ->
change.taskInfo?.taskId == taskId && change.mode == TRANSIT_TO_BACK
}
- }
override fun onTransitionMerged(merged: IBinder, playing: IBinder) {
if (activeTransitionTokensAndTasks.remove(merged) != null) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt
index 9bf5555fc194..d61ffdaf5cf8 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt
@@ -21,9 +21,11 @@ import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
import android.content.Context
import android.os.IBinder
import android.view.SurfaceControl
-import android.view.WindowManager
import android.view.WindowManager.TRANSIT_CLOSE
+import android.view.WindowManager.TRANSIT_OPEN
+import android.view.WindowManager.TRANSIT_PIP
import android.view.WindowManager.TRANSIT_TO_BACK
+import android.view.WindowManager.TRANSIT_TO_FRONT
import android.window.DesktopModeFlags
import android.window.DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY
import android.window.TransitionInfo
@@ -39,6 +41,8 @@ import com.android.wm.shell.shared.TransitionUtil
import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
import com.android.wm.shell.sysui.ShellInit
import com.android.wm.shell.transition.Transitions
+import com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP
+import com.android.wm.shell.transition.Transitions.TRANSIT_REMOVE_PIP
/**
* A [Transitions.TransitionObserver] that observes shell transitions and updates the
@@ -57,6 +61,8 @@ class DesktopTasksTransitionObserver(
) : Transitions.TransitionObserver {
private var transitionToCloseWallpaper: IBinder? = null
+ /* Pending PiP transition and its associated display id and task id. */
+ private var pendingPipTransitionAndPipTask: Triple<IBinder, Int, Int>? = null
private var currentProfileId: Int
init {
@@ -90,6 +96,33 @@ class DesktopTasksTransitionObserver(
removeTaskIfNeeded(info)
}
removeWallpaperOnLastTaskClosingIfNeeded(transition, info)
+
+ val desktopRepository = desktopUserRepositories.getProfile(currentProfileId)
+ info.changes.forEach { change ->
+ change.taskInfo?.let { taskInfo ->
+ if (
+ Flags.enableDesktopWindowingPip() &&
+ desktopRepository.isTaskMinimizedPipInDisplay(
+ taskInfo.displayId,
+ taskInfo.taskId,
+ )
+ ) {
+ when (info.type) {
+ TRANSIT_PIP ->
+ pendingPipTransitionAndPipTask =
+ Triple(transition, taskInfo.displayId, taskInfo.taskId)
+
+ TRANSIT_EXIT_PIP,
+ TRANSIT_REMOVE_PIP ->
+ desktopRepository.setTaskInPip(
+ taskInfo.displayId,
+ taskInfo.taskId,
+ enterPip = false,
+ )
+ }
+ }
+ }
+ }
}
private fun removeTaskIfNeeded(info: TransitionInfo) {
@@ -252,6 +285,18 @@ class DesktopTasksTransitionObserver(
}
}
transitionToCloseWallpaper = null
+ } else if (pendingPipTransitionAndPipTask?.first == transition) {
+ val desktopRepository = desktopUserRepositories.getProfile(currentProfileId)
+ if (aborted) {
+ pendingPipTransitionAndPipTask?.let {
+ desktopRepository.onPipAborted(
+ /*displayId=*/ it.second,
+ /* taskId=*/ it.third,
+ )
+ }
+ }
+ desktopRepository.setOnPipAbortedCallback(null)
+ pendingPipTransitionAndPipTask = null
}
}
@@ -263,11 +308,15 @@ class DesktopTasksTransitionObserver(
change.taskInfo?.let { taskInfo ->
if (DesktopWallpaperActivity.isWallpaperTask(taskInfo)) {
when (change.mode) {
- WindowManager.TRANSIT_OPEN -> {
+ TRANSIT_OPEN -> {
desktopWallpaperActivityTokenProvider.setToken(
taskInfo.token,
taskInfo.displayId,
)
+ desktopWallpaperActivityTokenProvider.setWallpaperActivityIsVisible(
+ isVisible = true,
+ taskInfo.displayId,
+ )
// After the task for the wallpaper is created, set it non-trimmable.
// This is important to prevent recents from trimming and removing the
// task.
@@ -278,6 +327,16 @@ class DesktopTasksTransitionObserver(
}
TRANSIT_CLOSE ->
desktopWallpaperActivityTokenProvider.removeToken(taskInfo.displayId)
+ TRANSIT_TO_FRONT ->
+ desktopWallpaperActivityTokenProvider.setWallpaperActivityIsVisible(
+ isVisible = true,
+ taskInfo.displayId,
+ )
+ TRANSIT_TO_BACK ->
+ desktopWallpaperActivityTokenProvider.setWallpaperActivityIsVisible(
+ isVisible = false,
+ taskInfo.displayId,
+ )
else -> {}
}
}
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 1380a9ca164f..91f10dc4faf5 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
@@ -765,9 +765,8 @@ sealed class DragToDesktopTransitionHandler(
transitionState = null
}
- private fun isSplitTask(taskId: Int): Boolean {
- return splitScreenController.isTaskInSplitScreen(taskId)
- }
+ private fun isSplitTask(taskId: Int): Boolean =
+ splitScreenController.isTaskInSplitScreen(taskId)
private fun getOtherSplitTask(taskId: Int): Int? {
val splitPos = splitScreenController.getSplitPosition(taskId)
@@ -781,9 +780,8 @@ sealed class DragToDesktopTransitionHandler(
return splitScreenController.getTaskInfo(otherTaskPos)?.taskId
}
- protected fun requireTransitionState(): TransitionState {
- return transitionState ?: error("Expected non-null transition state")
- }
+ protected fun requireTransitionState(): TransitionState =
+ transitionState ?: error("Expected non-null transition state")
/**
* Represents the layering (Z order) that will be given to any window based on its type during
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ToggleResizeDesktopTaskTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ToggleResizeDesktopTaskTransitionHandler.kt
index a47e9370b58d..5e84019b14f5 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ToggleResizeDesktopTaskTransitionHandler.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ToggleResizeDesktopTaskTransitionHandler.kt
@@ -156,13 +156,11 @@ class ToggleResizeDesktopTaskTransitionHandler(
return matchingChanges.first()
}
- private fun isWallpaper(change: TransitionInfo.Change): Boolean {
- return (change.flags and TransitionInfo.FLAG_IS_WALLPAPER) != 0
- }
+ private fun isWallpaper(change: TransitionInfo.Change): Boolean =
+ (change.flags and TransitionInfo.FLAG_IS_WALLPAPER) != 0
- private fun isValidTaskChange(change: TransitionInfo.Change): Boolean {
- return change.taskInfo != null && change.taskInfo?.taskId != -1
- }
+ private fun isValidTaskChange(change: TransitionInfo.Change): Boolean =
+ change.taskInfo != null && change.taskInfo?.taskId != -1
companion object {
private const val RESIZE_DURATION_MS = 300L
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/desktopwallpaperactivity/DesktopWallpaperActivityTokenProvider.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/desktopwallpaperactivity/DesktopWallpaperActivityTokenProvider.kt
index a87004c07d43..2bd7a9873a5e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/desktopwallpaperactivity/DesktopWallpaperActivityTokenProvider.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/desktopwallpaperactivity/DesktopWallpaperActivityTokenProvider.kt
@@ -17,6 +17,7 @@
package com.android.wm.shell.desktopmode.desktopwallpaperactivity
import android.util.SparseArray
+import android.util.SparseBooleanArray
import android.view.Display.DEFAULT_DISPLAY
import android.window.WindowContainerToken
@@ -24,6 +25,7 @@ import android.window.WindowContainerToken
class DesktopWallpaperActivityTokenProvider {
private val wallpaperActivityTokenByDisplayId = SparseArray<WindowContainerToken>()
+ private val wallpaperActivityVisByDisplayId = SparseBooleanArray()
fun setToken(token: WindowContainerToken, displayId: Int = DEFAULT_DISPLAY) {
wallpaperActivityTokenByDisplayId[displayId] = token
@@ -36,4 +38,16 @@ class DesktopWallpaperActivityTokenProvider {
fun removeToken(displayId: Int = DEFAULT_DISPLAY) {
wallpaperActivityTokenByDisplayId.delete(displayId)
}
+
+ fun setWallpaperActivityIsVisible(
+ isVisible: Boolean = false,
+ displayId: Int = DEFAULT_DISPLAY,
+ ) {
+ wallpaperActivityVisByDisplayId.put(displayId, isVisible)
+ }
+
+ fun isWallpaperActivityVisible(displayId: Int = DEFAULT_DISPLAY): Boolean {
+ return wallpaperActivityTokenByDisplayId[displayId] != null &&
+ wallpaperActivityVisByDisplayId.get(displayId, false)
+ }
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragUtils.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragUtils.java
index a62dd1c83520..4df9ae4b2ee3 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragUtils.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragUtils.java
@@ -107,8 +107,11 @@ public class DragUtils {
/**
* Returns a list of the mime types provided in the clip description.
*/
- public static String getMimeTypesConcatenated(ClipDescription description) {
+ public static String getMimeTypesConcatenated(@Nullable ClipDescription description) {
String mimeTypes = "";
+ if (description == null) {
+ return mimeTypes;
+ }
for (int i = 0; i < description.getMimeTypeCount(); i++) {
if (i > 0) {
mimeTypes += ", ";
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java
index 7f673d2efc68..ea8dac982703 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java
@@ -40,6 +40,7 @@ import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.pip.PipBoundsState;
import com.android.wm.shell.desktopmode.DesktopUserRepositories;
+import com.android.wm.shell.desktopmode.desktopwallpaperactivity.DesktopWallpaperActivityTokenProvider;
import com.android.wm.shell.pip.PipTransitionController;
import com.android.wm.shell.pip2.PipSurfaceTransactionHelper;
import com.android.wm.shell.pip2.animation.PipAlphaAnimator;
@@ -59,6 +60,8 @@ public class PipScheduler {
private final ShellExecutor mMainExecutor;
private final PipTransitionState mPipTransitionState;
private final Optional<DesktopUserRepositories> mDesktopUserRepositoriesOptional;
+ private final Optional<DesktopWallpaperActivityTokenProvider>
+ mDesktopWallpaperActivityTokenProviderOptional;
private final RootTaskDisplayAreaOrganizer mRootTaskDisplayAreaOrganizer;
private PipTransitionController mPipTransitionController;
private PipSurfaceTransactionHelper.SurfaceControlTransactionFactory
@@ -73,12 +76,16 @@ public class PipScheduler {
ShellExecutor mainExecutor,
PipTransitionState pipTransitionState,
Optional<DesktopUserRepositories> desktopUserRepositoriesOptional,
+ Optional<DesktopWallpaperActivityTokenProvider>
+ desktopWallpaperActivityTokenProviderOptional,
RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer) {
mContext = context;
mPipBoundsState = pipBoundsState;
mMainExecutor = mainExecutor;
mPipTransitionState = pipTransitionState;
mDesktopUserRepositoriesOptional = desktopUserRepositoriesOptional;
+ mDesktopWallpaperActivityTokenProviderOptional =
+ desktopWallpaperActivityTokenProviderOptional;
mRootTaskDisplayAreaOrganizer = rootTaskDisplayAreaOrganizer;
mSurfaceControlTransactionFactory =
@@ -260,10 +267,18 @@ public class PipScheduler {
/** Returns whether PiP is exiting while we're in desktop mode. */
private boolean isPipExitingToDesktopMode() {
- return Flags.enableDesktopWindowingPip() && mDesktopUserRepositoriesOptional.isPresent()
- && (mDesktopUserRepositoriesOptional.get().getCurrent().getVisibleTaskCount(
- Objects.requireNonNull(mPipTransitionState.getPipTaskInfo()).displayId) > 0
- || isDisplayInFreeform());
+ // Early return if PiP in Desktop Windowing is not supported.
+ if (!Flags.enableDesktopWindowingPip() || mDesktopUserRepositoriesOptional.isEmpty()
+ || mDesktopWallpaperActivityTokenProviderOptional.isEmpty()) {
+ return false;
+ }
+ final int displayId = Objects.requireNonNull(
+ mPipTransitionState.getPipTaskInfo()).displayId;
+ return mDesktopUserRepositoriesOptional.get().getCurrent().getVisibleTaskCount(displayId)
+ > 0
+ || mDesktopWallpaperActivityTokenProviderOptional.get().isWallpaperActivityVisible(
+ displayId)
+ || isDisplayInFreeform();
}
/**
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java
index 8061ee9090b6..38015ca6d45f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java
@@ -63,7 +63,9 @@ import com.android.wm.shell.common.pip.PipDisplayLayoutState;
import com.android.wm.shell.common.pip.PipMenuController;
import com.android.wm.shell.common.pip.PipUtils;
import com.android.wm.shell.common.split.SplitScreenUtils;
+import com.android.wm.shell.desktopmode.DesktopRepository;
import com.android.wm.shell.desktopmode.DesktopUserRepositories;
+import com.android.wm.shell.desktopmode.desktopwallpaperactivity.DesktopWallpaperActivityTokenProvider;
import com.android.wm.shell.pip.PipTransitionController;
import com.android.wm.shell.pip2.animation.PipAlphaAnimator;
import com.android.wm.shell.pip2.animation.PipEnterAnimator;
@@ -110,6 +112,8 @@ public class PipTransition extends PipTransitionController implements
private final PipTransitionState mPipTransitionState;
private final PipDisplayLayoutState mPipDisplayLayoutState;
private final Optional<DesktopUserRepositories> mDesktopUserRepositoriesOptional;
+ private final Optional<DesktopWallpaperActivityTokenProvider>
+ mDesktopWallpaperActivityTokenProviderOptional;
//
// Transition caches
@@ -145,7 +149,9 @@ public class PipTransition extends PipTransitionController implements
PipTransitionState pipTransitionState,
PipDisplayLayoutState pipDisplayLayoutState,
PipUiStateChangeController pipUiStateChangeController,
- Optional<DesktopUserRepositories> desktopUserRepositoriesOptional) {
+ Optional<DesktopUserRepositories> desktopUserRepositoriesOptional,
+ Optional<DesktopWallpaperActivityTokenProvider>
+ desktopWallpaperActivityTokenProviderOptional) {
super(shellInit, shellTaskOrganizer, transitions, pipBoundsState, pipMenuController,
pipBoundsAlgorithm);
@@ -157,6 +163,8 @@ public class PipTransition extends PipTransitionController implements
mPipTransitionState.addPipTransitionStateChangedListener(this);
mPipDisplayLayoutState = pipDisplayLayoutState;
mDesktopUserRepositoriesOptional = desktopUserRepositoriesOptional;
+ mDesktopWallpaperActivityTokenProviderOptional =
+ desktopWallpaperActivityTokenProviderOptional;
}
@Override
@@ -826,13 +834,14 @@ public class PipTransition extends PipTransitionController implements
return false;
}
-
// Since opening a new task while in Desktop Mode always first open in Fullscreen
// until DesktopMode Shell code resolves it to Freeform, PipTransition will get a
// possibility to handle it also. In this case return false to not have it enter PiP.
final boolean isInDesktopSession = !mDesktopUserRepositoriesOptional.isEmpty()
- && mDesktopUserRepositoriesOptional.get().getCurrent().getVisibleTaskCount(
- pipTask.displayId) > 0;
+ && (mDesktopUserRepositoriesOptional.get().getCurrent().getVisibleTaskCount(
+ pipTask.displayId) > 0
+ || mDesktopUserRepositoriesOptional.get().getCurrent()
+ .isMinimizedPipPresentInDisplay(pipTask.displayId));
if (isInDesktopSession) {
return false;
}
@@ -968,6 +977,27 @@ public class PipTransition extends PipTransitionController implements
"Unexpected bundle for " + mPipTransitionState);
break;
case PipTransitionState.EXITED_PIP:
+ final TaskInfo pipTask = mPipTransitionState.getPipTaskInfo();
+ final boolean desktopPipEnabled = Flags.enableDesktopWindowingPip()
+ && mDesktopUserRepositoriesOptional.isPresent()
+ && mDesktopWallpaperActivityTokenProviderOptional.isPresent();
+ if (desktopPipEnabled && pipTask != null) {
+ final DesktopRepository desktopRepository =
+ mDesktopUserRepositoriesOptional.get().getCurrent();
+ final boolean wallpaperIsVisible =
+ mDesktopWallpaperActivityTokenProviderOptional.get()
+ .isWallpaperActivityVisible(pipTask.displayId);
+ if (desktopRepository.getVisibleTaskCount(pipTask.displayId) == 0
+ && wallpaperIsVisible) {
+ mTransitions.startTransition(
+ TRANSIT_TO_BACK,
+ new WindowContainerTransaction().reorder(
+ mDesktopWallpaperActivityTokenProviderOptional.get()
+ .getToken(pipTask.displayId), /* onTop= */ false),
+ null
+ );
+ }
+ }
mPipTransitionState.setPinnedTaskLeash(null);
mPipTransitionState.setPipTaskInfo(null);
break;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentsAnimationRunner.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentsAnimationRunner.aidl
index 32c79a2d02de..8cdb8c4512a9 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentsAnimationRunner.aidl
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentsAnimationRunner.aidl
@@ -17,9 +17,10 @@
package com.android.wm.shell.recents;
import android.graphics.Rect;
+import android.os.Bundle;
import android.view.RemoteAnimationTarget;
import android.window.TaskSnapshot;
-import android.os.Bundle;
+import android.window.TransitionInfo;
import com.android.wm.shell.recents.IRecentsAnimationController;
@@ -57,7 +58,8 @@ oneway interface IRecentsAnimationRunner {
*/
void onAnimationStart(in IRecentsAnimationController controller,
in RemoteAnimationTarget[] apps, in RemoteAnimationTarget[] wallpapers,
- in Rect homeContentInsets, in Rect minimizedHomeBounds, in Bundle extras) = 2;
+ in Rect homeContentInsets, in Rect minimizedHomeBounds, in Bundle extras,
+ in TransitionInfo info) = 2;
/**
* Called when the task of an activity that has been started while the recents animation
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 76496b06a4dd..aeccd86e122c 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
@@ -411,10 +411,12 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler,
mInstanceId = System.identityHashCode(this);
mListener = listener;
mDeathHandler = () -> {
- ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
- "[%d] RecentsController.DeathRecipient: binder died", mInstanceId);
- finishInner(mWillFinishToHome, false /* leaveHint */, null /* finishCb */,
- "deathRecipient");
+ mExecutor.execute(() -> {
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
+ "[%d] RecentsController.DeathRecipient: binder died", mInstanceId);
+ finishInner(mWillFinishToHome, false /* leaveHint */, null /* finishCb */,
+ "deathRecipient");
+ });
};
try {
mListener.asBinder().linkToDeath(mDeathHandler, 0 /* flags */);
@@ -585,7 +587,8 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler,
mListener.onAnimationStart(this,
apps.toArray(new RemoteAnimationTarget[apps.size()]),
new RemoteAnimationTarget[0],
- new Rect(0, 0, 0, 0), new Rect(), new Bundle());
+ new Rect(0, 0, 0, 0), new Rect(), new Bundle(),
+ null);
for (int i = 0; i < mStateListeners.size(); i++) {
mStateListeners.get(i).onTransitionStateChanged(TRANSITION_STATE_ANIMATING);
}
@@ -816,7 +819,7 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler,
mListener.onAnimationStart(this,
apps.toArray(new RemoteAnimationTarget[apps.size()]),
wallpapers.toArray(new RemoteAnimationTarget[wallpapers.size()]),
- new Rect(0, 0, 0, 0), new Rect(), b);
+ new Rect(0, 0, 0, 0), new Rect(), b, info);
for (int i = 0; i < mStateListeners.size(); i++) {
mStateListeners.get(i).onTransitionStateChanged(TRANSITION_STATE_ANIMATING);
}
@@ -1273,6 +1276,11 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler,
"requested"));
}
+ /**
+ * @param runnerFinishCb The remote finish callback to run after finish is complete, this is
+ * not the same as mFinishCb which reports the transition is finished
+ * to WM.
+ */
private void finishInner(boolean toHome, boolean sendUserLeaveHint,
IResultReceiver runnerFinishCb, String reason) {
if (finishSyntheticTransition(runnerFinishCb, reason)) {
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 5aa329108596..b6bd879c75eb 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
@@ -2974,9 +2974,11 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
final int transitType = info.getType();
TransitionInfo.Change pipChange = null;
int closingSplitTaskId = -1;
- // This array tracks if we are sending stages TO_BACK in this transition.
- // TODO (b/349828130): Update for n apps
- boolean[] stagesSentToBack = new boolean[2];
+ // This array tracks where we are sending stages (TO_BACK/TO_FRONT) in this transition.
+ // TODO (b/349828130): Update for n apps (needs to handle different indices than 0/1).
+ // Also make sure having multiple changes per stage (2+ tasks in one stage) is being
+ // handled properly.
+ int[] stageChanges = new int[2];
for (int iC = 0; iC < info.getChanges().size(); ++iC) {
final TransitionInfo.Change change = info.getChanges().get(iC);
@@ -3040,18 +3042,25 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
+ " with " + taskId + " before startAnimation().");
}
}
- if (isClosingType(change.getMode()) &&
- getStageOfTask(taskId) != STAGE_TYPE_UNDEFINED) {
-
- // Record which stages are getting sent to back
- if (change.getMode() == TRANSIT_TO_BACK) {
- stagesSentToBack[getStageOfTask(taskId)] = true;
- }
+ final int stageOfTaskId = getStageOfTask(taskId);
+ if (stageOfTaskId == STAGE_TYPE_UNDEFINED) {
+ continue;
+ }
+ if (isClosingType(change.getMode())) {
// (For PiP transitions) If either one of the 2 stages is closing we're assuming
// we'll break split
closingSplitTaskId = taskId;
}
+ if (transitType == WindowManager.TRANSIT_WAKE) {
+ // Record which stages are receiving which changes
+ if ((change.getMode() == TRANSIT_TO_BACK
+ || change.getMode() == TRANSIT_TO_FRONT)
+ && (stageOfTaskId == STAGE_TYPE_MAIN
+ || stageOfTaskId == STAGE_TYPE_SIDE)) {
+ stageChanges[stageOfTaskId] = change.getMode();
+ }
+ }
}
if (pipChange != null) {
@@ -3076,19 +3085,11 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
return true;
}
- // If keyguard is active, check to see if we have our TO_BACK transitions in order.
- // This array should either be all false (no split stages sent to back) or all true
- // (all stages sent to back). In any other case (which can happen with SHOW_ABOVE_LOCKED
- // apps) we should break split.
- if (mKeyguardActive) {
- boolean isFirstStageSentToBack = stagesSentToBack[0];
- for (boolean b : stagesSentToBack) {
- // Compare each boolean to the first one. If any are different, break split.
- if (b != isFirstStageSentToBack) {
- dismissSplitKeepingLastActiveStage(EXIT_REASON_SCREEN_LOCKED_SHOW_ON_TOP);
- break;
- }
- }
+ // If keyguard is active, check to see if we have all our stages showing. If one stage
+ // was moved but not the other (which can happen with SHOW_ABOVE_LOCKED apps), we should
+ // break split.
+ if (mKeyguardActive && stageChanges[STAGE_TYPE_MAIN] != stageChanges[STAGE_TYPE_SIDE]) {
+ dismissSplitKeepingLastActiveStage(EXIT_REASON_SCREEN_LOCKED_SHOW_ON_TOP);
}
final ArraySet<StageTaskListener> dismissStages = record.getShouldDismissedStage();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultSurfaceAnimator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultSurfaceAnimator.java
index d8884f6d8d38..f5aaaad93229 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultSurfaceAnimator.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultSurfaceAnimator.java
@@ -33,6 +33,7 @@ import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.shared.TransactionPool;
import java.util.ArrayList;
+import java.util.function.Consumer;
public class DefaultSurfaceAnimator {
@@ -58,42 +59,12 @@ public class DefaultSurfaceAnimator {
// Animation length is already expected to be scaled.
va.overrideDurationScale(1.0f);
va.setDuration(anim.computeDurationHint());
- va.addUpdateListener(updateListener);
- va.addListener(new AnimatorListenerAdapter() {
- // It is possible for the end/cancel to be called more than once, which may cause
- // issues if the animating surface has already been released. Track the finished
- // state here to skip duplicate callbacks. See b/252872225.
- private boolean mFinished;
-
- @Override
- public void onAnimationEnd(Animator animation) {
- onFinish();
- }
-
- @Override
- public void onAnimationCancel(Animator animation) {
- onFinish();
- }
-
- private void onFinish() {
- if (mFinished) return;
- mFinished = true;
- // Apply transformation of end state in case the animation is canceled.
- if (va.getAnimatedFraction() < 1f) {
- va.setCurrentFraction(1f);
- }
-
- pool.release(transaction);
- mainExecutor.execute(() -> {
- animations.remove(va);
- finishCallback.run();
- });
- // The update listener can continue to be called after the animation has ended if
- // end() is called manually again before the finisher removes the animation.
- // Remove it manually here to prevent animating a released surface.
- // See b/252872225.
- va.removeUpdateListener(updateListener);
- }
+ setupValueAnimator(va, updateListener, (vanim) -> {
+ pool.release(transaction);
+ mainExecutor.execute(() -> {
+ animations.remove(vanim);
+ finishCallback.run();
+ });
});
animations.add(va);
}
@@ -188,4 +159,50 @@ public class DefaultSurfaceAnimator {
}
}
}
+
+ /**
+ * Setup some callback logic on a value-animator. This helper ensures that a value animator
+ * finishes at its final fraction (1f) and that relevant callbacks are only called once.
+ */
+ public static ValueAnimator setupValueAnimator(ValueAnimator animator,
+ ValueAnimator.AnimatorUpdateListener updateListener,
+ Consumer<ValueAnimator> afterFinish) {
+ animator.addUpdateListener(updateListener);
+ animator.addListener(new AnimatorListenerAdapter() {
+ // It is possible for the end/cancel to be called more than once, which may cause
+ // issues if the animating surface has already been released. Track the finished
+ // state here to skip duplicate callbacks. See b/252872225.
+ private boolean mFinished;
+
+ @Override
+ public void onAnimationStart(Animator animation) {
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ onFinish();
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ onFinish();
+ }
+
+ private void onFinish() {
+ if (mFinished) return;
+ mFinished = true;
+ // Apply transformation of end state in case the animation is canceled.
+ if (animator.getAnimatedFraction() < 1f) {
+ animator.setCurrentFraction(1f);
+ }
+ afterFinish.accept(animator);
+ // The update listener can continue to be called after the animation has ended if
+ // end() is called manually again before the finisher removes the animation.
+ // Remove it manually here to prevent animating a released surface.
+ // See b/252872225.
+ animator.removeUpdateListener(updateListener);
+ }
+ });
+ return animator;
+ }
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
index 1689bb5778ae..36c3e9711f5c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
@@ -55,6 +55,7 @@ import static android.window.TransitionInfo.FLAG_SHOW_WALLPAPER;
import static android.window.TransitionInfo.FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT;
import static android.window.TransitionInfo.FLAG_TRANSLUCENT;
+import static com.android.internal.policy.TransitionAnimation.DEFAULT_APP_TRANSITION_DURATION;
import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_CHANGE;
import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_CLOSE;
import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_INTRA_CLOSE;
@@ -69,6 +70,7 @@ import static com.android.wm.shell.transition.TransitionAnimationHelper.isCovere
import static com.android.wm.shell.transition.TransitionAnimationHelper.loadAttributeAnimation;
import android.animation.Animator;
+import android.animation.ValueAnimator;
import android.annotation.ColorInt;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -104,6 +106,7 @@ import com.android.internal.policy.TransitionAnimation;
import com.android.internal.protolog.ProtoLog;
import com.android.window.flags.Flags;
import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
+import com.android.wm.shell.animation.SizeChangeAnimation;
import com.android.wm.shell.common.DisplayController;
import com.android.wm.shell.common.DisplayLayout;
import com.android.wm.shell.common.ShellExecutor;
@@ -422,6 +425,14 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler {
ROTATION_ANIMATION_ROTATE, 0 /* flags */, animations, onAnimFinish);
continue;
}
+
+ if (Flags.portWindowSizeAnimation() && isTask
+ && TransitionInfo.isIndependent(change, info)
+ && change.getSnapshot() != null) {
+ startBoundsChangeAnimation(startTransaction, animations, change, onAnimFinish,
+ mMainExecutor);
+ continue;
+ }
}
// Hide the invisible surface directly without animating it if there is a display
@@ -734,6 +745,21 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler {
}
}
+ private void startBoundsChangeAnimation(@NonNull SurfaceControl.Transaction startT,
+ @NonNull ArrayList<Animator> animations, @NonNull TransitionInfo.Change change,
+ @NonNull Runnable finishCb, @NonNull ShellExecutor mainExecutor) {
+ final SizeChangeAnimation sca =
+ new SizeChangeAnimation(change.getStartAbsBounds(), change.getEndAbsBounds());
+ sca.initialize(change.getLeash(), change.getSnapshot(), startT);
+ final ValueAnimator va = sca.buildAnimator(change.getLeash(), change.getSnapshot(),
+ (animator) -> mainExecutor.execute(() -> {
+ animations.remove(animator);
+ finishCb.run();
+ }));
+ va.setDuration(DEFAULT_APP_TRANSITION_DURATION);
+ animations.add(va);
+ }
+
@Nullable
@Override
public WindowContainerTransaction handleRequest(@NonNull IBinder transition,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
index 9fbda46bd2b7..429e0564dd2c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
@@ -126,6 +126,8 @@ import com.android.wm.shell.desktopmode.common.ToggleTaskSizeUtilsKt;
import com.android.wm.shell.desktopmode.education.AppHandleEducationController;
import com.android.wm.shell.desktopmode.education.AppToWebEducationController;
import com.android.wm.shell.freeform.FreeformTaskTransitionStarter;
+import com.android.wm.shell.recents.RecentsTransitionHandler;
+import com.android.wm.shell.recents.RecentsTransitionStateListener;
import com.android.wm.shell.shared.FocusTransitionListener;
import com.android.wm.shell.shared.annotations.ShellBackgroundThread;
import com.android.wm.shell.shared.annotations.ShellMainThread;
@@ -157,8 +159,10 @@ import kotlinx.coroutines.MainCoroutineDispatcher;
import java.io.PrintWriter;
import java.util.ArrayList;
+import java.util.HashSet;
import java.util.List;
import java.util.Optional;
+import java.util.Set;
import java.util.function.Supplier;
/**
@@ -247,6 +251,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel,
private final DesktopModeEventLogger mDesktopModeEventLogger;
private final DesktopModeUiEventLogger mDesktopModeUiEventLogger;
private final WindowDecorTaskResourceLoader mTaskResourceLoader;
+ private final RecentsTransitionHandler mRecentsTransitionHandler;
public DesktopModeWindowDecorViewModel(
Context context,
@@ -282,7 +287,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel,
FocusTransitionObserver focusTransitionObserver,
DesktopModeEventLogger desktopModeEventLogger,
DesktopModeUiEventLogger desktopModeUiEventLogger,
- WindowDecorTaskResourceLoader taskResourceLoader) {
+ WindowDecorTaskResourceLoader taskResourceLoader,
+ RecentsTransitionHandler recentsTransitionHandler) {
this(
context,
shellExecutor,
@@ -323,7 +329,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel,
focusTransitionObserver,
desktopModeEventLogger,
desktopModeUiEventLogger,
- taskResourceLoader);
+ taskResourceLoader,
+ recentsTransitionHandler);
}
@VisibleForTesting
@@ -367,7 +374,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel,
FocusTransitionObserver focusTransitionObserver,
DesktopModeEventLogger desktopModeEventLogger,
DesktopModeUiEventLogger desktopModeUiEventLogger,
- WindowDecorTaskResourceLoader taskResourceLoader) {
+ WindowDecorTaskResourceLoader taskResourceLoader,
+ RecentsTransitionHandler recentsTransitionHandler) {
mContext = context;
mMainExecutor = shellExecutor;
mMainHandler = mainHandler;
@@ -436,6 +444,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel,
mDesktopModeEventLogger = desktopModeEventLogger;
mDesktopModeUiEventLogger = desktopModeUiEventLogger;
mTaskResourceLoader = taskResourceLoader;
+ mRecentsTransitionHandler = recentsTransitionHandler;
shellInit.addInitCallback(this::onInit, this);
}
@@ -450,6 +459,10 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel,
new DesktopModeOnTaskResizeAnimationListener());
mDesktopTasksController.setOnTaskRepositionAnimationListener(
new DesktopModeOnTaskRepositionAnimationListener());
+ if (Flags.enableDesktopRecentsTransitionsCornersBugfix()) {
+ mRecentsTransitionHandler.addTransitionStateListener(
+ new DesktopModeRecentsTransitionStateListener());
+ }
mDisplayController.addDisplayChangingController(mOnDisplayChangingListener);
try {
mWindowManager.registerSystemGestureExclusionListener(mGestureExclusionListener,
@@ -1859,6 +1872,38 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel,
}
}
+ private class DesktopModeRecentsTransitionStateListener
+ implements RecentsTransitionStateListener {
+ final Set<Integer> mAnimatingTaskIds = new HashSet<>();
+
+ @Override
+ public void onTransitionStateChanged(int state) {
+ switch (state) {
+ case RecentsTransitionStateListener.TRANSITION_STATE_REQUESTED:
+ for (int n = 0; n < mWindowDecorByTaskId.size(); n++) {
+ int taskId = mWindowDecorByTaskId.keyAt(n);
+ mAnimatingTaskIds.add(taskId);
+ setIsRecentsTransitionRunningForTask(taskId, true);
+ }
+ return;
+ case RecentsTransitionStateListener.TRANSITION_STATE_NOT_RUNNING:
+ // No Recents transition running - clean up window decorations
+ for (int taskId : mAnimatingTaskIds) {
+ setIsRecentsTransitionRunningForTask(taskId, false);
+ }
+ mAnimatingTaskIds.clear();
+ return;
+ default:
+ }
+ }
+
+ private void setIsRecentsTransitionRunningForTask(int taskId, boolean isRecentsRunning) {
+ final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(taskId);
+ if (decoration == null) return;
+ decoration.setIsRecentsTransitionRunning(isRecentsRunning);
+ }
+ }
+
private class DragEventListenerImpl
implements DragPositioningCallbackUtility.DragEventListener {
@Override
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
index 4ac89546c9c7..39a989ce7c7f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
@@ -204,6 +204,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
private final MultiInstanceHelper mMultiInstanceHelper;
private final WindowDecorCaptionHandleRepository mWindowDecorCaptionHandleRepository;
private final DesktopUserRepositories mDesktopUserRepositories;
+ private boolean mIsRecentsTransitionRunning = false;
private Runnable mLoadAppInfoRunnable;
private Runnable mSetAppInfoRunnable;
@@ -498,7 +499,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
applyStartTransactionOnDraw, shouldSetTaskVisibilityPositionAndCrop,
mIsStatusBarVisible, mIsKeyguardVisibleAndOccluded, inFullImmersive,
mDisplayController.getInsetsState(taskInfo.displayId), hasGlobalFocus,
- displayExclusionRegion);
+ displayExclusionRegion, mIsRecentsTransitionRunning);
final WindowDecorLinearLayout oldRootView = mResult.mRootView;
final SurfaceControl oldDecorationSurface = mDecorationContainerSurface;
@@ -869,7 +870,8 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
boolean inFullImmersiveMode,
@NonNull InsetsState displayInsetsState,
boolean hasGlobalFocus,
- @NonNull Region displayExclusionRegion) {
+ @NonNull Region displayExclusionRegion,
+ boolean shouldIgnoreCornerRadius) {
final int captionLayoutId = getDesktopModeWindowDecorLayoutId(taskInfo.getWindowingMode());
final boolean isAppHeader =
captionLayoutId == R.layout.desktop_mode_app_header;
@@ -1006,13 +1008,19 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
relayoutParams.mWindowDecorConfig = windowDecorConfig;
if (DesktopModeStatus.useRoundedCorners()) {
- relayoutParams.mCornerRadius = taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM
- ? loadDimensionPixelSize(context.getResources(),
- R.dimen.desktop_windowing_freeform_rounded_corner_radius)
- : INVALID_CORNER_RADIUS;
+ relayoutParams.mCornerRadius = shouldIgnoreCornerRadius ? INVALID_CORNER_RADIUS :
+ getCornerRadius(context, relayoutParams.mLayoutResId);
}
}
+ private static int getCornerRadius(@NonNull Context context, int layoutResId) {
+ if (layoutResId == R.layout.desktop_mode_app_header) {
+ return loadDimensionPixelSize(context.getResources(),
+ R.dimen.desktop_windowing_freeform_rounded_corner_radius);
+ }
+ return INVALID_CORNER_RADIUS;
+ }
+
/**
* If task has focused window decor, return the caption id of the fullscreen caption size
* resource. Otherwise, return ID_NULL and caption width be set to task width.
@@ -1740,6 +1748,17 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
}
/**
+ * Declares whether a Recents transition is currently active.
+ *
+ * <p> When a Recents transition is active we allow that transition to take ownership of the
+ * corner radius of its task surfaces, so each window decoration should stop updating the corner
+ * radius of its task surface during that time.
+ */
+ void setIsRecentsTransitionRunning(boolean isRecentsTransitionRunning) {
+ mIsRecentsTransitionRunning = isRecentsTransitionRunning;
+ }
+
+ /**
* Called when there is a {@link MotionEvent#ACTION_HOVER_EXIT} on the maximize window button.
*/
void onMaximizeButtonHoverExit() {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
index 5d1bedb85b5e..fa7183ad0fd8 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
@@ -967,4 +967,4 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer>
return Objects.hash(mToken, mOwner, mFrame, Arrays.hashCode(mBoundingRects), mFlags);
}
}
-} \ No newline at end of file
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientation.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientation.kt
index 636549fa0662..a6f8150ffc55 100644
--- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientation.kt
+++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientation.kt
@@ -176,7 +176,6 @@ open class EnterPipToOtherOrientation(flicker: LegacyFlickerTest) : PipTransitio
* transition
*/
@Ignore("TODO(b/356277166): enable the tablet test")
- @Postsubmit
@Test
open fun pipAppLayerPlusLetterboxCoversFullScreenOnStartTablet() {
assumeTrue(tapl.isTablet)
diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/nonmatchparent/BottomHalfEnterPipToOtherOrientation.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/nonmatchparent/BottomHalfEnterPipToOtherOrientation.kt
index 4987ab7b9344..d65f158e00d6 100644
--- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/nonmatchparent/BottomHalfEnterPipToOtherOrientation.kt
+++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/nonmatchparent/BottomHalfEnterPipToOtherOrientation.kt
@@ -78,7 +78,6 @@ class BottomHalfEnterPipToOtherOrientation(flicker: LegacyFlickerTest) :
}
@Ignore("TODO(b/356277166): enable the tablet test")
- @Presubmit
@Test
override fun pipAppLayerPlusLetterboxCoversFullScreenOnStartTablet() {
// Test app and pip app should covers the entire screen on start.
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/appzoomout/AppZoomOutControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/appzoomout/AppZoomOutControllerTest.java
new file mode 100644
index 000000000000..e91a1238a390
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/appzoomout/AppZoomOutControllerTest.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.wm.shell.appzoomout;
+
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
+
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.ActivityManager;
+import android.testing.AndroidTestingRunner;
+import android.view.Display;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.wm.shell.ShellTaskOrganizer;
+import com.android.wm.shell.ShellTestCase;
+import com.android.wm.shell.common.DisplayController;
+import com.android.wm.shell.common.DisplayLayout;
+import com.android.wm.shell.common.ShellExecutor;
+import com.android.wm.shell.sysui.ShellInit;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+public class AppZoomOutControllerTest extends ShellTestCase {
+
+ @Mock private ShellTaskOrganizer mTaskOrganizer;
+ @Mock private DisplayController mDisplayController;
+ @Mock private AppZoomOutDisplayAreaOrganizer mDisplayAreaOrganizer;
+ @Mock private ShellExecutor mExecutor;
+ @Mock private ActivityManager.RunningTaskInfo mRunningTaskInfo;
+
+ private AppZoomOutController mController;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+
+ Display display = mContext.getDisplay();
+ DisplayLayout displayLayout = new DisplayLayout(mContext, display);
+ when(mDisplayController.getDisplayLayout(anyInt())).thenReturn(displayLayout);
+
+ ShellInit shellInit = spy(new ShellInit(mExecutor));
+ mController = spy(new AppZoomOutController(mContext, shellInit, mTaskOrganizer,
+ mDisplayController, mDisplayAreaOrganizer, mExecutor));
+ }
+
+ @Test
+ public void isHomeTaskFocused_zoomOutForHome() {
+ mRunningTaskInfo.isFocused = true;
+ when(mRunningTaskInfo.getActivityType()).thenReturn(ACTIVITY_TYPE_HOME);
+ mController.onFocusTaskChanged(mRunningTaskInfo);
+
+ verify(mDisplayAreaOrganizer).setIsHomeTaskFocused(true);
+ }
+
+ @Test
+ public void isHomeTaskNotFocused_zoomOutForApp() {
+ mRunningTaskInfo.isFocused = false;
+ when(mRunningTaskInfo.getActivityType()).thenReturn(ACTIVITY_TYPE_HOME);
+ mController.onFocusTaskChanged(mRunningTaskInfo);
+
+ verify(mDisplayAreaOrganizer).setIsHomeTaskFocused(false);
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayControllerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayControllerTests.java
index 1e5e153fdfe1..d3de0f7c09b4 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayControllerTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayControllerTests.java
@@ -22,6 +22,7 @@ import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import android.content.Context;
+import android.hardware.display.DisplayManager;
import android.view.IWindowManager;
import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -50,12 +51,14 @@ public class DisplayControllerTests extends ShellTestCase {
private @Mock IWindowManager mWM;
private @Mock ShellInit mShellInit;
private @Mock ShellExecutor mMainExecutor;
+ private @Mock DisplayManager mDisplayManager;
private DisplayController mController;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
- mController = new DisplayController(mContext, mWM, mShellInit, mMainExecutor);
+ mController = new DisplayController(
+ mContext, mWM, mShellInit, mMainExecutor, mDisplayManager);
}
@Test
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayLayoutTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayLayoutTest.java
index d467b399ebbb..b0a455d1bcf8 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayLayoutTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayLayoutTest.java
@@ -33,7 +33,9 @@ import static org.mockito.Mockito.when;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Insets;
+import android.graphics.PointF;
import android.graphics.Rect;
+import android.graphics.RectF;
import android.view.DisplayCutout;
import android.view.DisplayInfo;
@@ -58,6 +60,7 @@ import org.mockito.quality.Strictness;
@SmallTest
public class DisplayLayoutTest extends ShellTestCase {
private MockitoSession mMockitoSession;
+ private static final float DELTA = 0.1f; // Constant for assertion delta
@Before
public void setup() {
@@ -130,6 +133,39 @@ public class DisplayLayoutTest extends ShellTestCase {
assertEquals(new Rect(40, 0, 60, 0), dl.nonDecorInsets());
}
+ @Test
+ public void testDpPxConversion() {
+ int px = 100;
+ float dp = 53.33f;
+ int xPx = 100;
+ int yPx = 200;
+ float xDp = 164.33f;
+ float yDp = 328.66f;
+
+ Resources res = createResources(40, 50, false);
+ DisplayInfo info = createDisplayInfo(1000, 1500, 0, ROTATION_0);
+ DisplayLayout dl = new DisplayLayout(info, res, false, false);
+ dl.setGlobalBoundsDp(new RectF(111f, 222f, 300f, 400f));
+
+ // Test pxToDp
+ float resultDp = dl.pxToDp(px);
+ assertEquals(dp, resultDp, DELTA);
+
+ // Test dpToPx
+ float resultPx = dl.dpToPx(dp);
+ assertEquals(px, resultPx, DELTA);
+
+ // Test localPxToGlobalDp
+ PointF resultGlobalDp = dl.localPxToGlobalDp(xPx, yPx);
+ assertEquals(xDp, resultGlobalDp.x, DELTA);
+ assertEquals(yDp, resultGlobalDp.y, DELTA);
+
+ // Test globalDpToLocalPx
+ PointF resultLocalPx = dl.globalDpToLocalPx(xDp, yDp);
+ assertEquals(xPx, resultLocalPx.x, DELTA);
+ assertEquals(yPx, resultLocalPx.y, DELTA);
+ }
+
private Resources createResources(int navLand, int navPort, boolean navMoves) {
Configuration cfg = new Configuration();
cfg.uiMode = UI_MODE_TYPE_NORMAL;
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeEventLoggerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeEventLoggerTest.kt
index c0ff2f0652b3..eb6f1d75e6ca 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeEventLoggerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeEventLoggerTest.kt
@@ -52,6 +52,7 @@ import org.junit.Test
import org.mockito.ArgumentMatchers.anyInt
import org.mockito.kotlin.eq
import org.mockito.kotlin.mock
+import org.mockito.kotlin.never
import org.mockito.kotlin.whenever
/** Tests for [DesktopModeEventLogger]. */
@@ -90,20 +91,12 @@ class DesktopModeEventLoggerTest : ShellTestCase() {
val sessionId = desktopModeEventLogger.currentSessionId.get()
assertThat(sessionId).isNotEqualTo(NO_SESSION_ID)
- verify {
- FrameworkStatsLog.write(
- eq(FrameworkStatsLog.DESKTOP_MODE_UI_CHANGED),
- /* event */
- eq(FrameworkStatsLog.DESKTOP_MODE_UICHANGED__EVENT__ENTER),
- /* enter_reason */
- eq(FrameworkStatsLog.DESKTOP_MODE_UICHANGED__ENTER_REASON__KEYBOARD_SHORTCUT_ENTER),
- /* exit_reason */
- eq(0),
- /* sessionId */
- eq(sessionId),
- )
- }
- verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java))
+ verifyOnlyOneUiChangedLogging(
+ FrameworkStatsLog.DESKTOP_MODE_UICHANGED__EVENT__ENTER,
+ FrameworkStatsLog.DESKTOP_MODE_UICHANGED__ENTER_REASON__KEYBOARD_SHORTCUT_ENTER,
+ 0,
+ sessionId,
+ )
verify {
EventLogTags.writeWmShellEnterDesktopMode(
eq(EnterReason.KEYBOARD_SHORTCUT_ENTER.reason),
@@ -122,20 +115,13 @@ class DesktopModeEventLoggerTest : ShellTestCase() {
val sessionId = desktopModeEventLogger.currentSessionId.get()
assertThat(sessionId).isNotEqualTo(NO_SESSION_ID)
assertThat(sessionId).isNotEqualTo(previousSessionId)
- verify {
- FrameworkStatsLog.write(
- eq(FrameworkStatsLog.DESKTOP_MODE_UI_CHANGED),
- /* event */
- eq(FrameworkStatsLog.DESKTOP_MODE_UICHANGED__EVENT__ENTER),
- /* enter_reason */
- eq(FrameworkStatsLog.DESKTOP_MODE_UICHANGED__ENTER_REASON__KEYBOARD_SHORTCUT_ENTER),
- /* exit_reason */
- eq(0),
- /* sessionId */
- eq(sessionId),
- )
- }
- verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java))
+ verifyOnlyOneUiChangedLogging(
+ FrameworkStatsLog.DESKTOP_MODE_UICHANGED__EVENT__ENTER,
+ FrameworkStatsLog.DESKTOP_MODE_UICHANGED__ENTER_REASON__KEYBOARD_SHORTCUT_ENTER,
+ /* exit_reason */
+ 0,
+ sessionId,
+ )
verify {
EventLogTags.writeWmShellEnterDesktopMode(
eq(EnterReason.KEYBOARD_SHORTCUT_ENTER.reason),
@@ -149,7 +135,7 @@ class DesktopModeEventLoggerTest : ShellTestCase() {
fun logSessionExit_noOngoingSession_doesNotLog() {
desktopModeEventLogger.logSessionExit(ExitReason.DRAG_TO_EXIT)
- verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java))
+ verifyNoLogging()
verifyZeroInteractions(staticMockMarker(EventLogTags::class.java))
}
@@ -159,20 +145,13 @@ class DesktopModeEventLoggerTest : ShellTestCase() {
desktopModeEventLogger.logSessionExit(ExitReason.DRAG_TO_EXIT)
- verify {
- FrameworkStatsLog.write(
- eq(FrameworkStatsLog.DESKTOP_MODE_UI_CHANGED),
- /* event */
- eq(FrameworkStatsLog.DESKTOP_MODE_UICHANGED__EVENT__EXIT),
- /* enter_reason */
- eq(0),
- /* exit_reason */
- eq(FrameworkStatsLog.DESKTOP_MODE_UICHANGED__EXIT_REASON__DRAG_TO_EXIT),
- /* sessionId */
- eq(sessionId),
- )
- }
- verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java))
+ verifyOnlyOneUiChangedLogging(
+ FrameworkStatsLog.DESKTOP_MODE_UICHANGED__EVENT__EXIT,
+ /* enter_reason */
+ 0,
+ FrameworkStatsLog.DESKTOP_MODE_UICHANGED__EXIT_REASON__DRAG_TO_EXIT,
+ sessionId,
+ )
verify {
EventLogTags.writeWmShellExitDesktopMode(
eq(ExitReason.DRAG_TO_EXIT.reason),
@@ -187,7 +166,7 @@ class DesktopModeEventLoggerTest : ShellTestCase() {
fun logTaskAdded_noOngoingSession_doesNotLog() {
desktopModeEventLogger.logTaskAdded(TASK_UPDATE)
- verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java))
+ verifyNoLogging()
verifyZeroInteractions(staticMockMarker(EventLogTags::class.java))
}
@@ -197,32 +176,19 @@ class DesktopModeEventLoggerTest : ShellTestCase() {
desktopModeEventLogger.logTaskAdded(TASK_UPDATE)
- verify {
- FrameworkStatsLog.write(
- eq(FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE),
- /* task_event */
- eq(FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_ADDED),
- /* instance_id */
- eq(TASK_UPDATE.instanceId),
- /* uid */
- eq(TASK_UPDATE.uid),
- /* task_height */
- eq(TASK_UPDATE.taskHeight),
- /* task_width */
- eq(TASK_UPDATE.taskWidth),
- /* task_x */
- eq(TASK_UPDATE.taskX),
- /* task_y */
- eq(TASK_UPDATE.taskY),
- /* session_id */
- eq(sessionId),
- eq(UNSET_MINIMIZE_REASON),
- eq(UNSET_UNMINIMIZE_REASON),
- /* visible_task_count */
- eq(TASK_COUNT),
- )
- }
- verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java))
+ verifyOnlyOneTaskUpdateLogging(
+ FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_ADDED,
+ TASK_UPDATE.instanceId,
+ TASK_UPDATE.uid,
+ TASK_UPDATE.taskHeight,
+ TASK_UPDATE.taskWidth,
+ TASK_UPDATE.taskX,
+ TASK_UPDATE.taskY,
+ sessionId,
+ UNSET_MINIMIZE_REASON,
+ UNSET_UNMINIMIZE_REASON,
+ TASK_COUNT,
+ )
verify {
EventLogTags.writeWmShellDesktopModeTaskUpdate(
eq(FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_ADDED),
@@ -245,7 +211,7 @@ class DesktopModeEventLoggerTest : ShellTestCase() {
fun logTaskRemoved_noOngoingSession_doesNotLog() {
desktopModeEventLogger.logTaskRemoved(TASK_UPDATE)
- verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java))
+ verifyNoLogging()
verifyZeroInteractions(staticMockMarker(EventLogTags::class.java))
}
@@ -255,32 +221,19 @@ class DesktopModeEventLoggerTest : ShellTestCase() {
desktopModeEventLogger.logTaskRemoved(TASK_UPDATE)
- verify {
- FrameworkStatsLog.write(
- eq(FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE),
- /* task_event */
- eq(FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_REMOVED),
- /* instance_id */
- eq(TASK_UPDATE.instanceId),
- /* uid */
- eq(TASK_UPDATE.uid),
- /* task_height */
- eq(TASK_UPDATE.taskHeight),
- /* task_width */
- eq(TASK_UPDATE.taskWidth),
- /* task_x */
- eq(TASK_UPDATE.taskX),
- /* task_y */
- eq(TASK_UPDATE.taskY),
- /* session_id */
- eq(sessionId),
- eq(UNSET_MINIMIZE_REASON),
- eq(UNSET_UNMINIMIZE_REASON),
- /* visible_task_count */
- eq(TASK_COUNT),
- )
- }
- verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java))
+ verifyOnlyOneTaskUpdateLogging(
+ FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_REMOVED,
+ TASK_UPDATE.instanceId,
+ TASK_UPDATE.uid,
+ TASK_UPDATE.taskHeight,
+ TASK_UPDATE.taskWidth,
+ TASK_UPDATE.taskX,
+ TASK_UPDATE.taskY,
+ sessionId,
+ UNSET_MINIMIZE_REASON,
+ UNSET_UNMINIMIZE_REASON,
+ TASK_COUNT,
+ )
verify {
EventLogTags.writeWmShellDesktopModeTaskUpdate(
eq(FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_REMOVED),
@@ -303,7 +256,7 @@ class DesktopModeEventLoggerTest : ShellTestCase() {
fun logTaskInfoChanged_noOngoingSession_doesNotLog() {
desktopModeEventLogger.logTaskInfoChanged(TASK_UPDATE)
- verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java))
+ verifyNoLogging()
verifyZeroInteractions(staticMockMarker(EventLogTags::class.java))
}
@@ -313,35 +266,19 @@ class DesktopModeEventLoggerTest : ShellTestCase() {
desktopModeEventLogger.logTaskInfoChanged(TASK_UPDATE)
- verify {
- FrameworkStatsLog.write(
- eq(FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE),
- /* task_event */
- eq(
- FrameworkStatsLog
- .DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_INFO_CHANGED
- ),
- /* instance_id */
- eq(TASK_UPDATE.instanceId),
- /* uid */
- eq(TASK_UPDATE.uid),
- /* task_height */
- eq(TASK_UPDATE.taskHeight),
- /* task_width */
- eq(TASK_UPDATE.taskWidth),
- /* task_x */
- eq(TASK_UPDATE.taskX),
- /* task_y */
- eq(TASK_UPDATE.taskY),
- /* session_id */
- eq(sessionId),
- eq(UNSET_MINIMIZE_REASON),
- eq(UNSET_UNMINIMIZE_REASON),
- /* visible_task_count */
- eq(TASK_COUNT),
- )
- }
- verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java))
+ verifyOnlyOneTaskUpdateLogging(
+ FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_INFO_CHANGED,
+ TASK_UPDATE.instanceId,
+ TASK_UPDATE.uid,
+ TASK_UPDATE.taskHeight,
+ TASK_UPDATE.taskWidth,
+ TASK_UPDATE.taskX,
+ TASK_UPDATE.taskY,
+ sessionId,
+ UNSET_MINIMIZE_REASON,
+ UNSET_UNMINIMIZE_REASON,
+ TASK_COUNT,
+ )
verify {
EventLogTags.writeWmShellDesktopModeTaskUpdate(
eq(
@@ -371,37 +308,19 @@ class DesktopModeEventLoggerTest : ShellTestCase() {
createTaskUpdate(minimizeReason = MinimizeReason.TASK_LIMIT)
)
- verify {
- FrameworkStatsLog.write(
- eq(FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE),
- /* task_event */
- eq(
- FrameworkStatsLog
- .DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_INFO_CHANGED
- ),
- /* instance_id */
- eq(TASK_UPDATE.instanceId),
- /* uid */
- eq(TASK_UPDATE.uid),
- /* task_height */
- eq(TASK_UPDATE.taskHeight),
- /* task_width */
- eq(TASK_UPDATE.taskWidth),
- /* task_x */
- eq(TASK_UPDATE.taskX),
- /* task_y */
- eq(TASK_UPDATE.taskY),
- /* session_id */
- eq(sessionId),
- /* minimize_reason */
- eq(MinimizeReason.TASK_LIMIT.reason),
- /* unminimize_reason */
- eq(UNSET_UNMINIMIZE_REASON),
- /* visible_task_count */
- eq(TASK_COUNT),
- )
- }
- verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java))
+ verifyOnlyOneTaskUpdateLogging(
+ FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_INFO_CHANGED,
+ TASK_UPDATE.instanceId,
+ TASK_UPDATE.uid,
+ TASK_UPDATE.taskHeight,
+ TASK_UPDATE.taskWidth,
+ TASK_UPDATE.taskX,
+ TASK_UPDATE.taskY,
+ sessionId,
+ MinimizeReason.TASK_LIMIT.reason,
+ UNSET_UNMINIMIZE_REASON,
+ TASK_COUNT,
+ )
verify {
EventLogTags.writeWmShellDesktopModeTaskUpdate(
eq(
@@ -431,37 +350,19 @@ class DesktopModeEventLoggerTest : ShellTestCase() {
createTaskUpdate(unminimizeReason = UnminimizeReason.TASKBAR_TAP)
)
- verify {
- FrameworkStatsLog.write(
- eq(FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE),
- /* task_event */
- eq(
- FrameworkStatsLog
- .DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_INFO_CHANGED
- ),
- /* instance_id */
- eq(TASK_UPDATE.instanceId),
- /* uid */
- eq(TASK_UPDATE.uid),
- /* task_height */
- eq(TASK_UPDATE.taskHeight),
- /* task_width */
- eq(TASK_UPDATE.taskWidth),
- /* task_x */
- eq(TASK_UPDATE.taskX),
- /* task_y */
- eq(TASK_UPDATE.taskY),
- /* session_id */
- eq(sessionId),
- /* minimize_reason */
- eq(UNSET_MINIMIZE_REASON),
- /* unminimize_reason */
- eq(UnminimizeReason.TASKBAR_TAP.reason),
- /* visible_task_count */
- eq(TASK_COUNT),
- )
- }
- verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java))
+ verifyOnlyOneTaskUpdateLogging(
+ FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_INFO_CHANGED,
+ TASK_UPDATE.instanceId,
+ TASK_UPDATE.uid,
+ TASK_UPDATE.taskHeight,
+ TASK_UPDATE.taskWidth,
+ TASK_UPDATE.taskX,
+ TASK_UPDATE.taskY,
+ sessionId,
+ UNSET_MINIMIZE_REASON,
+ UnminimizeReason.TASKBAR_TAP.reason,
+ TASK_COUNT,
+ )
verify {
EventLogTags.writeWmShellDesktopModeTaskUpdate(
eq(
@@ -491,7 +392,7 @@ class DesktopModeEventLoggerTest : ShellTestCase() {
createTaskInfo(),
)
- verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java))
+ verifyNoLogging()
verifyZeroInteractions(staticMockMarker(EventLogTags::class.java))
}
@@ -509,39 +410,17 @@ class DesktopModeEventLoggerTest : ShellTestCase() {
displayController,
)
- verify {
- FrameworkStatsLog.write(
- eq(FrameworkStatsLog.DESKTOP_MODE_TASK_SIZE_UPDATED),
- /* resize_trigger */
- eq(
- FrameworkStatsLog
- .DESKTOP_MODE_TASK_SIZE_UPDATED__RESIZE_TRIGGER__CORNER_RESIZE_TRIGGER
- ),
- /* resizing_stage */
- eq(
- FrameworkStatsLog
- .DESKTOP_MODE_TASK_SIZE_UPDATED__RESIZING_STAGE__START_RESIZING_STAGE
- ),
- /* input_method */
- eq(
- FrameworkStatsLog
- .DESKTOP_MODE_TASK_SIZE_UPDATED__INPUT_METHOD__UNKNOWN_INPUT_METHOD
- ),
- /* desktop_mode_session_id */
- eq(sessionId),
- /* instance_id */
- eq(TASK_SIZE_UPDATE.instanceId),
- /* uid */
- eq(TASK_SIZE_UPDATE.uid),
- /* task_width */
- eq(TASK_SIZE_UPDATE.taskWidth),
- /* task_height */
- eq(TASK_SIZE_UPDATE.taskHeight),
- /* display_area */
- eq(DISPLAY_AREA),
- )
- }
- verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java))
+ verifyOnlyOneTaskSizeUpdatedLogging(
+ FrameworkStatsLog.DESKTOP_MODE_TASK_SIZE_UPDATED__RESIZE_TRIGGER__CORNER_RESIZE_TRIGGER,
+ FrameworkStatsLog.DESKTOP_MODE_TASK_SIZE_UPDATED__RESIZING_STAGE__START_RESIZING_STAGE,
+ FrameworkStatsLog.DESKTOP_MODE_TASK_SIZE_UPDATED__INPUT_METHOD__UNKNOWN_INPUT_METHOD,
+ sessionId,
+ TASK_SIZE_UPDATE.instanceId,
+ TASK_SIZE_UPDATE.uid,
+ TASK_SIZE_UPDATE.taskWidth,
+ TASK_SIZE_UPDATE.taskHeight,
+ DISPLAY_AREA,
+ )
}
@Test
@@ -552,7 +431,7 @@ class DesktopModeEventLoggerTest : ShellTestCase() {
createTaskInfo(),
)
- verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java))
+ verifyNoLogging()
verifyZeroInteractions(staticMockMarker(EventLogTags::class.java))
}
@@ -568,39 +447,17 @@ class DesktopModeEventLoggerTest : ShellTestCase() {
displayController = displayController,
)
- verify {
- FrameworkStatsLog.write(
- eq(FrameworkStatsLog.DESKTOP_MODE_TASK_SIZE_UPDATED),
- /* resize_trigger */
- eq(
- FrameworkStatsLog
- .DESKTOP_MODE_TASK_SIZE_UPDATED__RESIZE_TRIGGER__CORNER_RESIZE_TRIGGER
- ),
- /* resizing_stage */
- eq(
- FrameworkStatsLog
- .DESKTOP_MODE_TASK_SIZE_UPDATED__RESIZING_STAGE__END_RESIZING_STAGE
- ),
- /* input_method */
- eq(
- FrameworkStatsLog
- .DESKTOP_MODE_TASK_SIZE_UPDATED__INPUT_METHOD__UNKNOWN_INPUT_METHOD
- ),
- /* desktop_mode_session_id */
- eq(sessionId),
- /* instance_id */
- eq(TASK_SIZE_UPDATE.instanceId),
- /* uid */
- eq(TASK_SIZE_UPDATE.uid),
- /* task_width */
- eq(TASK_SIZE_UPDATE.taskWidth),
- /* task_height */
- eq(TASK_SIZE_UPDATE.taskHeight),
- /* display_area */
- eq(DISPLAY_AREA),
- )
- }
- verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java))
+ verifyOnlyOneTaskSizeUpdatedLogging(
+ FrameworkStatsLog.DESKTOP_MODE_TASK_SIZE_UPDATED__RESIZE_TRIGGER__CORNER_RESIZE_TRIGGER,
+ FrameworkStatsLog.DESKTOP_MODE_TASK_SIZE_UPDATED__RESIZING_STAGE__END_RESIZING_STAGE,
+ FrameworkStatsLog.DESKTOP_MODE_TASK_SIZE_UPDATED__INPUT_METHOD__UNKNOWN_INPUT_METHOD,
+ sessionId,
+ TASK_SIZE_UPDATE.instanceId,
+ TASK_SIZE_UPDATE.uid,
+ TASK_SIZE_UPDATE.taskWidth,
+ TASK_SIZE_UPDATE.taskHeight,
+ DISPLAY_AREA,
+ )
}
private fun startDesktopModeSession(): Int {
@@ -644,12 +501,176 @@ class DesktopModeEventLoggerTest : ShellTestCase() {
}
}
- private fun createTaskInfo(): RunningTaskInfo {
- return TestRunningTaskInfoBuilder()
+ private fun createTaskInfo(): RunningTaskInfo =
+ TestRunningTaskInfoBuilder()
.setTaskId(TASK_ID)
.setUid(TASK_UID)
.setBounds(Rect(TASK_X, TASK_Y, TASK_WIDTH, TASK_HEIGHT))
.build()
+
+ private fun verifyNoLogging() {
+ verify(
+ {
+ FrameworkStatsLog.write(
+ eq(FrameworkStatsLog.DESKTOP_MODE_UI_CHANGED),
+ anyInt(),
+ anyInt(),
+ anyInt(),
+ anyInt(),
+ )
+ },
+ never(),
+ )
+ verify(
+ {
+ FrameworkStatsLog.write(
+ eq(FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE),
+ anyInt(),
+ anyInt(),
+ anyInt(),
+ anyInt(),
+ anyInt(),
+ anyInt(),
+ anyInt(),
+ anyInt(),
+ anyInt(),
+ anyInt(),
+ anyInt(),
+ )
+ },
+ never(),
+ )
+ verify(
+ {
+ FrameworkStatsLog.write(
+ eq(FrameworkStatsLog.DESKTOP_MODE_TASK_SIZE_UPDATED),
+ anyInt(),
+ anyInt(),
+ anyInt(),
+ anyInt(),
+ anyInt(),
+ anyInt(),
+ anyInt(),
+ anyInt(),
+ anyInt(),
+ )
+ },
+ never(),
+ )
+ }
+
+ private fun verifyOnlyOneUiChangedLogging(
+ event: Int,
+ enterReason: Int,
+ exitReason: Int,
+ sessionId: Int,
+ ) {
+ verify({
+ FrameworkStatsLog.write(
+ eq(FrameworkStatsLog.DESKTOP_MODE_UI_CHANGED),
+ eq(event),
+ eq(enterReason),
+ eq(exitReason),
+ eq(sessionId),
+ )
+ })
+ verify({
+ FrameworkStatsLog.write(
+ eq(FrameworkStatsLog.DESKTOP_MODE_UI_CHANGED),
+ anyInt(),
+ anyInt(),
+ anyInt(),
+ anyInt(),
+ )
+ })
+ }
+
+ private fun verifyOnlyOneTaskUpdateLogging(
+ taskEvent: Int,
+ instanceId: Int,
+ uid: Int,
+ taskHeight: Int,
+ taskWidth: Int,
+ taskX: Int,
+ taskY: Int,
+ sessionId: Int,
+ minimizeReason: Int,
+ unminimizeReason: Int,
+ visibleTaskCount: Int,
+ ) {
+ verify({
+ FrameworkStatsLog.write(
+ eq(FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE),
+ eq(taskEvent),
+ eq(instanceId),
+ eq(uid),
+ eq(taskHeight),
+ eq(taskWidth),
+ eq(taskX),
+ eq(taskY),
+ eq(sessionId),
+ eq(minimizeReason),
+ eq(unminimizeReason),
+ eq(visibleTaskCount),
+ )
+ })
+ verify({
+ FrameworkStatsLog.write(
+ eq(FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE),
+ anyInt(),
+ anyInt(),
+ anyInt(),
+ anyInt(),
+ anyInt(),
+ anyInt(),
+ anyInt(),
+ anyInt(),
+ anyInt(),
+ anyInt(),
+ anyInt(),
+ )
+ })
+ }
+
+ private fun verifyOnlyOneTaskSizeUpdatedLogging(
+ resizeTrigger: Int,
+ resizingStage: Int,
+ inputMethod: Int,
+ sessionId: Int,
+ instanceId: Int,
+ uid: Int,
+ taskWidth: Int,
+ taskHeight: Int,
+ displayArea: Int,
+ ) {
+ verify({
+ FrameworkStatsLog.write(
+ eq(FrameworkStatsLog.DESKTOP_MODE_TASK_SIZE_UPDATED),
+ eq(resizeTrigger),
+ eq(resizingStage),
+ eq(inputMethod),
+ eq(sessionId),
+ eq(instanceId),
+ eq(uid),
+ eq(taskWidth),
+ eq(taskHeight),
+ eq(displayArea),
+ )
+ })
+ verify({
+ FrameworkStatsLog.write(
+ eq(FrameworkStatsLog.DESKTOP_MODE_TASK_SIZE_UPDATED),
+ anyInt(),
+ anyInt(),
+ anyInt(),
+ anyInt(),
+ anyInt(),
+ anyInt(),
+ anyInt(),
+ anyInt(),
+ anyInt(),
+ )
+ })
}
private companion object {
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt
index 5629127b8c54..daecccef9344 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt
@@ -25,6 +25,7 @@ import android.view.Display.DEFAULT_DISPLAY
import android.view.Display.INVALID_DISPLAY
import androidx.test.filters.SmallTest
import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_PERSISTENCE
+import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_PIP
import com.android.wm.shell.ShellTestCase
import com.android.wm.shell.TestShellExecutor
import com.android.wm.shell.common.ShellExecutor
@@ -1067,6 +1068,67 @@ class DesktopRepositoryTest : ShellTestCase() {
assertThat(repo.getTaskInFullImmersiveState(DEFAULT_DESKTOP_ID + 1)).isEqualTo(2)
}
+ @Test
+ fun setTaskInPip_savedAsMinimizedPipInDisplay() {
+ assertThat(repo.isTaskMinimizedPipInDisplay(DEFAULT_DESKTOP_ID, taskId = 1)).isFalse()
+
+ repo.setTaskInPip(DEFAULT_DESKTOP_ID, taskId = 1, enterPip = true)
+
+ assertThat(repo.isTaskMinimizedPipInDisplay(DEFAULT_DESKTOP_ID, taskId = 1)).isTrue()
+ }
+
+ @Test
+ fun removeTaskInPip_removedAsMinimizedPipInDisplay() {
+ repo.setTaskInPip(DEFAULT_DESKTOP_ID, taskId = 1, enterPip = true)
+ assertThat(repo.isTaskMinimizedPipInDisplay(DEFAULT_DESKTOP_ID, taskId = 1)).isTrue()
+
+ repo.setTaskInPip(DEFAULT_DESKTOP_ID, taskId = 1, enterPip = false)
+
+ assertThat(repo.isTaskMinimizedPipInDisplay(DEFAULT_DESKTOP_ID, taskId = 1)).isFalse()
+ }
+
+ @Test
+ fun setTaskInPip_multipleDisplays_bothAreInPip() {
+ repo.setTaskInPip(DEFAULT_DESKTOP_ID, taskId = 1, enterPip = true)
+ repo.setTaskInPip(DEFAULT_DESKTOP_ID + 1, taskId = 2, enterPip = true)
+
+ assertThat(repo.isTaskMinimizedPipInDisplay(DEFAULT_DESKTOP_ID, taskId = 1)).isTrue()
+ assertThat(repo.isTaskMinimizedPipInDisplay(DEFAULT_DESKTOP_ID + 1, taskId = 2)).isTrue()
+ }
+
+ @Test
+ @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PIP)
+ fun setPipShouldKeepDesktopActive_shouldKeepDesktopActive() {
+ assertThat(repo.shouldDesktopBeActiveForPip(DEFAULT_DESKTOP_ID)).isFalse()
+
+ repo.setTaskInPip(DEFAULT_DESKTOP_ID, taskId = 1, enterPip = true)
+ repo.setPipShouldKeepDesktopActive(DEFAULT_DESKTOP_ID, keepActive = true)
+
+ assertThat(repo.shouldDesktopBeActiveForPip(DEFAULT_DESKTOP_ID)).isTrue()
+ }
+
+ @Test
+ @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PIP)
+ fun setPipShouldNotKeepDesktopActive_shouldNotKeepDesktopActive() {
+ repo.setTaskInPip(DEFAULT_DESKTOP_ID, taskId = 1, enterPip = true)
+ assertThat(repo.shouldDesktopBeActiveForPip(DEFAULT_DESKTOP_ID)).isTrue()
+
+ repo.setPipShouldKeepDesktopActive(DEFAULT_DESKTOP_ID, keepActive = false)
+
+ assertThat(repo.shouldDesktopBeActiveForPip(DEFAULT_DESKTOP_ID)).isFalse()
+ }
+
+ @Test
+ @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PIP)
+ fun removeTaskInPip_shouldNotKeepDesktopActive() {
+ repo.setTaskInPip(DEFAULT_DESKTOP_ID, taskId = 1, enterPip = true)
+ assertThat(repo.shouldDesktopBeActiveForPip(DEFAULT_DESKTOP_ID)).isTrue()
+
+ repo.setTaskInPip(DEFAULT_DESKTOP_ID, taskId = 1, enterPip = false)
+
+ assertThat(repo.shouldDesktopBeActiveForPip(DEFAULT_DESKTOP_ID)).isFalse()
+ }
+
class TestListener : DesktopRepository.ActiveTasksListener {
var activeChangesOnDefaultDisplay = 0
var activeChangesOnSecondaryDisplay = 0
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 692b50303038..95ed8b4d4511 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
@@ -82,6 +82,7 @@ import com.android.dx.mockito.inline.extended.StaticMockitoSession
import com.android.internal.jank.InteractionJankMonitor
import com.android.window.flags.Flags
import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE
+import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_PIP
import com.android.window.flags.Flags.FLAG_ENABLE_DISPLAY_FOCUS_IN_SHELL_TRANSITIONS
import com.android.window.flags.Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP
import com.android.window.flags.Flags.FLAG_ENABLE_MOVE_TO_NEXT_DISPLAY_SHORTCUT
@@ -352,8 +353,8 @@ class DesktopTasksControllerTest : ShellTestCase() {
taskRepository = userRepositories.current
}
- private fun createController(): DesktopTasksController {
- return DesktopTasksController(
+ private fun createController() =
+ DesktopTasksController(
context,
shellInit,
shellCommandHandler,
@@ -387,7 +388,6 @@ class DesktopTasksControllerTest : ShellTestCase() {
desktopWallpaperActivityTokenProvider,
Optional.of(bubbleController),
)
- }
@After
fun tearDown() {
@@ -569,6 +569,38 @@ class DesktopTasksControllerTest : ShellTestCase() {
}
@Test
+ @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PIP)
+ fun isDesktopModeShowing_minimizedPipTask_wallpaperVisible_returnsTrue() {
+ val pipTask = setUpPipTask(autoEnterEnabled = true)
+ whenever(desktopWallpaperActivityTokenProvider.isWallpaperActivityVisible())
+ .thenReturn(true)
+
+ taskRepository.setTaskInPip(DEFAULT_DISPLAY, pipTask.taskId, enterPip = true)
+
+ assertThat(controller.isDesktopModeShowing(displayId = DEFAULT_DISPLAY)).isTrue()
+ }
+
+ @Test
+ @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PIP)
+ fun isDesktopModeShowing_minimizedPipTask_wallpaperNotVisible_returnsFalse() {
+ val pipTask = setUpPipTask(autoEnterEnabled = true)
+ whenever(desktopWallpaperActivityTokenProvider.isWallpaperActivityVisible())
+ .thenReturn(false)
+
+ taskRepository.setTaskInPip(DEFAULT_DISPLAY, pipTask.taskId, enterPip = true)
+
+ assertThat(controller.isDesktopModeShowing(displayId = DEFAULT_DISPLAY)).isFalse()
+ }
+
+ @Test
+ @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PIP)
+ fun isDesktopModeShowing_pipTaskNotMinimizedNorVisible_returnsFalse() {
+ setUpPipTask(autoEnterEnabled = true)
+
+ assertThat(controller.isDesktopModeShowing(displayId = DEFAULT_DISPLAY)).isFalse()
+ }
+
+ @Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
fun showDesktopApps_onSecondaryDisplay_desktopWallpaperEnabled_shouldNotShowWallpaper() {
val homeTask = setUpHomeTask(SECOND_DISPLAY)
@@ -2039,6 +2071,41 @@ class DesktopTasksControllerTest : ShellTestCase() {
}
@Test
+ @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PIP)
+ fun onDesktopWindowClose_minimizedPipPresent_doesNotExitDesktop() {
+ val freeformTask = setUpFreeformTask().apply { isFocused = true }
+ val pipTask = setUpPipTask(autoEnterEnabled = true)
+
+ taskRepository.setTaskInPip(DEFAULT_DISPLAY, pipTask.taskId, enterPip = true)
+ val wct = WindowContainerTransaction()
+ controller.onDesktopWindowClose(wct, displayId = DEFAULT_DISPLAY, freeformTask)
+
+ verifyExitDesktopWCTNotExecuted()
+ }
+
+ @Test
+ @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PIP)
+ fun onDesktopWindowClose_minimizedPipNotPresent_exitDesktop() {
+ val freeformTask = setUpFreeformTask()
+ val pipTask = setUpPipTask(autoEnterEnabled = true)
+ val handler = mock(TransitionHandler::class.java)
+ whenever(transitions.dispatchRequest(any(), any(), anyOrNull()))
+ .thenReturn(android.util.Pair(handler, WindowContainerTransaction()))
+
+ controller.minimizeTask(pipTask)
+ verifyExitDesktopWCTNotExecuted()
+
+ taskRepository.setTaskInPip(DEFAULT_DISPLAY, pipTask.taskId, enterPip = false)
+ val wct = WindowContainerTransaction()
+ controller.onDesktopWindowClose(wct, displayId = DEFAULT_DISPLAY, freeformTask)
+
+ // Remove wallpaper operation
+ wct.hierarchyOps.any { hop ->
+ hop.type == HIERARCHY_OP_TYPE_REMOVE_TASK && hop.container == wallpaperToken.asBinder()
+ }
+ }
+
+ @Test
fun onDesktopWindowMinimize_noActiveTask_doesntRemoveWallpaper() {
val task = setUpFreeformTask(active = false)
val transition = Binder()
@@ -2055,10 +2122,9 @@ class DesktopTasksControllerTest : ShellTestCase() {
}
@Test
- fun onDesktopWindowMinimize_pipTask_autoEnterEnabled_startPipTransition() {
+ fun onPipTaskMinimize_autoEnterEnabled_startPipTransition() {
val task = setUpPipTask(autoEnterEnabled = true)
val handler = mock(TransitionHandler::class.java)
- whenever(freeformTaskTransitionStarter.startPipTransition(any())).thenReturn(Binder())
whenever(transitions.dispatchRequest(any(), any(), anyOrNull()))
.thenReturn(android.util.Pair(handler, WindowContainerTransaction()))
@@ -2069,7 +2135,7 @@ class DesktopTasksControllerTest : ShellTestCase() {
}
@Test
- fun onDesktopWindowMinimize_pipTask_autoEnterDisabled_startMinimizeTransition() {
+ fun onPipTaskMinimize_autoEnterDisabled_startMinimizeTransition() {
val task = setUpPipTask(autoEnterEnabled = false)
whenever(freeformTaskTransitionStarter.startMinimizedModeTransition(any()))
.thenReturn(Binder())
@@ -2081,6 +2147,22 @@ class DesktopTasksControllerTest : ShellTestCase() {
}
@Test
+ fun onPipTaskMinimize_doesntRemoveWallpaper() {
+ val task = setUpPipTask(autoEnterEnabled = true)
+ val handler = mock(TransitionHandler::class.java)
+ whenever(transitions.dispatchRequest(any(), any(), anyOrNull()))
+ .thenReturn(android.util.Pair(handler, WindowContainerTransaction()))
+
+ controller.minimizeTask(task)
+
+ val captor = ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
+ verify(freeformTaskTransitionStarter).startPipTransition(captor.capture())
+ captor.value.hierarchyOps.none { hop ->
+ hop.type == HIERARCHY_OP_TYPE_REMOVE_TASK && hop.container == wallpaperToken.asBinder()
+ }
+ }
+
+ @Test
fun onDesktopWindowMinimize_singleActiveTask_noWallpaperActivityToken_doesntRemoveWallpaper() {
val task = setUpFreeformTask(active = true)
val transition = Binder()
@@ -3125,6 +3207,31 @@ class DesktopTasksControllerTest : ShellTestCase() {
}
@Test
+ @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PIP)
+ fun moveFocusedTaskToFullscreen_minimizedPipPresent_removeWallpaperActivity() {
+ val freeformTask = setUpFreeformTask()
+ val pipTask = setUpPipTask(autoEnterEnabled = true)
+ val handler = mock(TransitionHandler::class.java)
+ whenever(transitions.dispatchRequest(any(), any(), anyOrNull()))
+ .thenReturn(android.util.Pair(handler, WindowContainerTransaction()))
+
+ controller.minimizeTask(pipTask)
+ verifyExitDesktopWCTNotExecuted()
+
+ freeformTask.isFocused = true
+ controller.enterFullscreen(DEFAULT_DISPLAY, transitionSource = UNKNOWN)
+
+ val wct = getLatestExitDesktopWct()
+ val taskChange = assertNotNull(wct.changes[freeformTask.token.asBinder()])
+ assertThat(taskChange.windowingMode)
+ .isEqualTo(WINDOWING_MODE_UNDEFINED) // inherited FULLSCREEN
+ // Remove wallpaper operation
+ wct.hierarchyOps.any { hop ->
+ hop.type == HIERARCHY_OP_TYPE_REMOVE_TASK && hop.container == wallpaperToken.asBinder()
+ }
+ }
+
+ @Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION)
fun removeDesktop_multipleTasks_removesAll() {
val task1 = setUpFreeformTask()
@@ -4850,12 +4957,12 @@ class DesktopTasksControllerTest : ShellTestCase() {
return task
}
- private fun setUpPipTask(autoEnterEnabled: Boolean): RunningTaskInfo {
- return setUpFreeformTask().apply {
+ private fun setUpPipTask(autoEnterEnabled: Boolean): RunningTaskInfo =
+ // active = false marks the task as non-visible; PiP window doesn't count as visible tasks
+ setUpFreeformTask(active = false).apply {
pictureInPictureParams =
PictureInPictureParams.Builder().setAutoEnterEnabled(autoEnterEnabled).build()
}
- }
private fun setUpHomeTask(displayId: Int = DEFAULT_DISPLAY): RunningTaskInfo {
val task = createHomeTask(displayId)
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt
index 89ab65a42bbf..622cb4cad363 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt
@@ -22,6 +22,7 @@ import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN
import android.content.ComponentName
import android.content.Context
import android.content.Intent
+import android.os.Binder
import android.os.IBinder
import android.platform.test.annotations.EnableFlags
import android.platform.test.flag.junit.SetFlagsRule
@@ -29,7 +30,9 @@ import android.view.Display.DEFAULT_DISPLAY
import android.view.WindowManager
import android.view.WindowManager.TRANSIT_CLOSE
import android.view.WindowManager.TRANSIT_OPEN
+import android.view.WindowManager.TRANSIT_PIP
import android.view.WindowManager.TRANSIT_TO_BACK
+import android.view.WindowManager.TRANSIT_TO_FRONT
import android.window.IWindowContainerToken
import android.window.TransitionInfo
import android.window.TransitionInfo.Change
@@ -38,6 +41,7 @@ import android.window.WindowContainerTransaction
import android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REORDER
import com.android.modules.utils.testing.ExtendedMockitoRule
import com.android.window.flags.Flags
+import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_PIP
import com.android.wm.shell.MockToken
import com.android.wm.shell.ShellTaskOrganizer
import com.android.wm.shell.back.BackAnimationController
@@ -47,6 +51,8 @@ import com.android.wm.shell.desktopmode.desktopwallpaperactivity.DesktopWallpape
import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
import com.android.wm.shell.sysui.ShellInit
import com.android.wm.shell.transition.Transitions
+import com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP
+import com.android.wm.shell.transition.Transitions.TRANSIT_REMOVE_PIP
import com.google.common.truth.Truth.assertThat
import com.google.common.truth.Truth.assertWithMessage
import org.junit.Before
@@ -300,6 +306,115 @@ class DesktopTasksTransitionObserverTest {
verify(taskRepository).clearTopTransparentFullscreenTaskId(topTransparentTask.displayId)
}
+ @Test
+ fun transitOpenWallpaper_wallpaperActivityVisibilitySaved() {
+ val wallpaperTask = createWallpaperTaskInfo()
+
+ transitionObserver.onTransitionReady(
+ transition = mock(),
+ info = createOpenChangeTransition(wallpaperTask),
+ startTransaction = mock(),
+ finishTransaction = mock(),
+ )
+
+ verify(desktopWallpaperActivityTokenProvider)
+ .setWallpaperActivityIsVisible(isVisible = true, wallpaperTask.displayId)
+ }
+
+ @Test
+ fun transitToFrontWallpaper_wallpaperActivityVisibilitySaved() {
+ val wallpaperTask = createWallpaperTaskInfo()
+
+ transitionObserver.onTransitionReady(
+ transition = mock(),
+ info = createToFrontTransition(wallpaperTask),
+ startTransaction = mock(),
+ finishTransaction = mock(),
+ )
+
+ verify(desktopWallpaperActivityTokenProvider)
+ .setWallpaperActivityIsVisible(isVisible = true, wallpaperTask.displayId)
+ }
+
+ @Test
+ fun transitToBackWallpaper_wallpaperActivityVisibilitySaved() {
+ val wallpaperTask = createWallpaperTaskInfo()
+
+ transitionObserver.onTransitionReady(
+ transition = mock(),
+ info = createToBackTransition(wallpaperTask),
+ startTransaction = mock(),
+ finishTransaction = mock(),
+ )
+
+ verify(desktopWallpaperActivityTokenProvider)
+ .setWallpaperActivityIsVisible(isVisible = false, wallpaperTask.displayId)
+ }
+
+ @Test
+ fun transitCloseWallpaper_wallpaperActivityVisibilitySaved() {
+ val wallpaperTask = createWallpaperTaskInfo()
+
+ transitionObserver.onTransitionReady(
+ transition = mock(),
+ info = createCloseTransition(wallpaperTask),
+ startTransaction = mock(),
+ finishTransaction = mock(),
+ )
+
+ verify(desktopWallpaperActivityTokenProvider).removeToken(wallpaperTask.displayId)
+ }
+
+ @Test
+ @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PIP)
+ fun pendingPipTransitionAborted_taskRepositoryOnPipAbortedInvoked() {
+ val task = createTaskInfo(1, WINDOWING_MODE_FREEFORM)
+ val pipTransition = Binder()
+ whenever(taskRepository.isTaskMinimizedPipInDisplay(any(), any())).thenReturn(true)
+
+ transitionObserver.onTransitionReady(
+ transition = pipTransition,
+ info = createOpenChangeTransition(task, TRANSIT_PIP),
+ startTransaction = mock(),
+ finishTransaction = mock(),
+ )
+ transitionObserver.onTransitionFinished(transition = pipTransition, aborted = true)
+
+ verify(taskRepository).onPipAborted(task.displayId, task.taskId)
+ }
+
+ @Test
+ @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PIP)
+ fun exitPipTransition_taskRepositoryClearTaskInPip() {
+ val task = createTaskInfo(1, WINDOWING_MODE_FREEFORM)
+ whenever(taskRepository.isTaskMinimizedPipInDisplay(any(), any())).thenReturn(true)
+
+ transitionObserver.onTransitionReady(
+ transition = mock(),
+ info = createOpenChangeTransition(task, type = TRANSIT_EXIT_PIP),
+ startTransaction = mock(),
+ finishTransaction = mock(),
+ )
+
+ verify(taskRepository).setTaskInPip(task.displayId, task.taskId, enterPip = false)
+ }
+
+ @Test
+ @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PIP)
+ fun removePipTransition_taskRepositoryClearTaskInPip() {
+ val task = createTaskInfo(1, WINDOWING_MODE_FREEFORM)
+ whenever(taskRepository.isTaskMinimizedPipInDisplay(any(), any())).thenReturn(true)
+
+ transitionObserver.onTransitionReady(
+ transition = mock(),
+ info = createOpenChangeTransition(task, type = TRANSIT_REMOVE_PIP),
+ startTransaction = mock(),
+ finishTransaction = mock(),
+ )
+
+ verify(taskRepository).setTaskInPip(task.displayId, task.taskId, enterPip = false)
+ }
+
private fun createBackNavigationTransition(
task: RunningTaskInfo?,
type: Int = TRANSIT_TO_BACK,
@@ -331,7 +446,7 @@ class DesktopTasksTransitionObserverTest {
task: RunningTaskInfo?,
type: Int = TRANSIT_OPEN,
): TransitionInfo {
- return TransitionInfo(TRANSIT_OPEN, /* flags= */ 0).apply {
+ return TransitionInfo(type, /* flags= */ 0).apply {
addChange(
Change(mock(), mock()).apply {
mode = TRANSIT_OPEN
@@ -343,8 +458,8 @@ class DesktopTasksTransitionObserverTest {
}
}
- private fun createCloseTransition(task: RunningTaskInfo?): TransitionInfo {
- return TransitionInfo(TRANSIT_CLOSE, /* flags= */ 0).apply {
+ private fun createCloseTransition(task: RunningTaskInfo?) =
+ TransitionInfo(TRANSIT_CLOSE, /* flags= */ 0).apply {
addChange(
Change(mock(), mock()).apply {
mode = TRANSIT_CLOSE
@@ -354,10 +469,9 @@ class DesktopTasksTransitionObserverTest {
}
)
}
- }
- private fun createToBackTransition(task: RunningTaskInfo?): TransitionInfo {
- return TransitionInfo(TRANSIT_TO_BACK, /* flags= */ 0).apply {
+ private fun createToBackTransition(task: RunningTaskInfo?) =
+ TransitionInfo(TRANSIT_TO_BACK, /* flags= */ 0).apply {
addChange(
Change(mock(), mock()).apply {
mode = TRANSIT_TO_BACK
@@ -367,6 +481,18 @@ class DesktopTasksTransitionObserverTest {
}
)
}
+
+ private fun createToFrontTransition(task: RunningTaskInfo?): TransitionInfo {
+ return TransitionInfo(TRANSIT_TO_FRONT, 0 /* flags */).apply {
+ addChange(
+ Change(mock(), mock()).apply {
+ mode = TRANSIT_TO_FRONT
+ parent = null
+ taskInfo = task
+ flags = flags
+ }
+ )
+ }
}
private fun getLatestWct(
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 341df0299a97..bf9cf00050dc 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
@@ -676,8 +676,8 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() {
}
}
- private fun createTransitionInfo(type: Int, draggedTask: RunningTaskInfo): TransitionInfo {
- return TransitionInfo(type, /* flags= */ 0).apply {
+ private fun createTransitionInfo(type: Int, draggedTask: RunningTaskInfo) =
+ TransitionInfo(type, /* flags= */ 0).apply {
addChange( // Home.
TransitionInfo.Change(mock(), homeTaskLeash).apply {
parent = null
@@ -700,7 +700,6 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() {
}
)
}
- }
private fun systemPropertiesKey(name: String) =
"${SpringDragToDesktopTransitionHandler.SYSTEM_PROPERTIES_GROUP}.$name"
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropControllerTest.java
index 32bb8bbdbbe3..1b1a5a909220 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropControllerTest.java
@@ -152,17 +152,16 @@ public class DragAndDropControllerTest extends ShellTestCase {
}
@Test
- public void testOnDragStarted_withNoClipData() {
+ public void testOnDragStarted_withNoClipDataOrDescription() {
final View dragLayout = mock(View.class);
final Display display = mock(Display.class);
doReturn(display).when(dragLayout).getDisplay();
doReturn(DEFAULT_DISPLAY).when(display).getDisplayId();
- final ClipData clipData = createAppClipData(MIMETYPE_APPLICATION_SHORTCUT);
final DragEvent event = mock(DragEvent.class);
doReturn(ACTION_DRAG_STARTED).when(event).getAction();
doReturn(null).when(event).getClipData();
- doReturn(clipData.getDescription()).when(event).getClipDescription();
+ doReturn(null).when(event).getClipDescription();
// Ensure there's a target so that onDrag will execute
mController.addDisplayDropTarget(0, mContext, mock(WindowManager.class),
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/GlobalDragListenerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/GlobalDragListenerTest.kt
index d410151b4602..5389c94bc15d 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/GlobalDragListenerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/GlobalDragListenerTest.kt
@@ -43,7 +43,7 @@ import org.mockito.kotlin.verify
*/
@SmallTest
@RunWith(AndroidJUnit4::class)
-class UnhandledDragControllerTest : ShellTestCase() {
+class GlobalDragListenerTest : ShellTestCase() {
private val mIWindowManager = mock<IWindowManager>()
private val mMainExecutor = mock<ShellExecutor>()
@@ -74,7 +74,7 @@ class UnhandledDragControllerTest : ShellTestCase() {
@Test
fun onUnhandledDrop_noListener_expectNotifyUnhandled() {
// Simulate an unhandled drop
- val dropEvent = DragEvent.obtain(ACTION_DROP, 0f, 0f, 0f, 0f, 0, null, null, null,
+ val dropEvent = DragEvent.obtain(ACTION_DROP, 0f, 0f, 0f, 0f, 0, 0, null, null, null,
null, null, false)
val wmCallback = mock<IUnhandledDragCallback>()
mController.onUnhandledDrop(dropEvent, wmCallback)
@@ -98,7 +98,7 @@ class UnhandledDragControllerTest : ShellTestCase() {
// Simulate an unhandled drop
val dragSurface = mock<SurfaceControl>()
- val dropEvent = DragEvent.obtain(ACTION_DROP, 0f, 0f, 0f, 0f, 0, null, null, null,
+ val dropEvent = DragEvent.obtain(ACTION_DROP, 0f, 0f, 0f, 0f, 0, 0, null, null, null,
dragSurface, null, false)
val wmCallback = mock<IUnhandledDragCallback>()
mController.onUnhandledDrop(dropEvent, wmCallback)
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipSchedulerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipSchedulerTest.java
index a8aa25700c7e..c42f6c35bcb0 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipSchedulerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipSchedulerTest.java
@@ -30,6 +30,7 @@ import static org.mockito.kotlin.MatchersKt.eq;
import static org.mockito.kotlin.VerificationKt.times;
import static org.mockito.kotlin.VerificationKt.verify;
+import android.app.TaskInfo;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Matrix;
@@ -45,7 +46,9 @@ import androidx.test.filters.SmallTest;
import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.pip.PipBoundsState;
+import com.android.wm.shell.desktopmode.DesktopRepository;
import com.android.wm.shell.desktopmode.DesktopUserRepositories;
+import com.android.wm.shell.desktopmode.desktopwallpaperactivity.DesktopWallpaperActivityTokenProvider;
import com.android.wm.shell.pip.PipTransitionController;
import com.android.wm.shell.pip2.PipSurfaceTransactionHelper;
import com.android.wm.shell.pip2.animation.PipAlphaAnimator;
@@ -56,6 +59,7 @@ import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
+import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import java.util.Optional;
@@ -83,7 +87,8 @@ public class PipSchedulerTest {
@Mock private PipSurfaceTransactionHelper.SurfaceControlTransactionFactory mMockFactory;
@Mock private SurfaceControl.Transaction mMockTransaction;
@Mock private PipAlphaAnimator mMockAlphaAnimator;
- @Mock private Optional<DesktopUserRepositories> mMockOptionalDesktopUserRepositories;
+ @Mock private DesktopUserRepositories mMockDesktopUserRepositories;
+ @Mock private DesktopWallpaperActivityTokenProvider mMockDesktopWallpaperActivityTokenProvider;
@Mock private RootTaskDisplayAreaOrganizer mRootTaskDisplayAreaOrganizer;
@Captor private ArgumentCaptor<Runnable> mRunnableArgumentCaptor;
@@ -100,9 +105,13 @@ public class PipSchedulerTest {
when(mMockFactory.getTransaction()).thenReturn(mMockTransaction);
when(mMockTransaction.setMatrix(any(SurfaceControl.class), any(Matrix.class), any()))
.thenReturn(mMockTransaction);
+ when(mMockDesktopUserRepositories.getCurrent())
+ .thenReturn(Mockito.mock(DesktopRepository.class));
+ when(mMockPipTransitionState.getPipTaskInfo()).thenReturn(Mockito.mock(TaskInfo.class));
mPipScheduler = new PipScheduler(mMockContext, mMockPipBoundsState, mMockMainExecutor,
- mMockPipTransitionState, mMockOptionalDesktopUserRepositories,
+ mMockPipTransitionState, Optional.of(mMockDesktopUserRepositories),
+ Optional.of(mMockDesktopWallpaperActivityTokenProvider),
mRootTaskDisplayAreaOrganizer);
mPipScheduler.setPipTransitionController(mMockPipTransitionController);
mPipScheduler.setSurfaceControlTransactionFactory(mMockFactory);
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 894d238b7e15..ab43119b14c0 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
@@ -169,7 +169,7 @@ public class RecentsTransitionHandlerTest extends ShellTestCase {
final IResultReceiver finishCallback = mock(IResultReceiver.class);
final IBinder transition = startRecentsTransition(/* synthetic= */ true, runner);
- verify(runner).onAnimationStart(any(), any(), any(), any(), any(), any());
+ verify(runner).onAnimationStart(any(), any(), any(), any(), any(), any(), any());
// Finish and verify no transition remains and that the provided finish callback is called
mRecentsTransitionHandler.findController(transition).finish(true /* toHome */,
@@ -184,7 +184,7 @@ public class RecentsTransitionHandlerTest extends ShellTestCase {
final IRecentsAnimationRunner runner = mock(IRecentsAnimationRunner.class);
final IBinder transition = startRecentsTransition(/* synthetic= */ true, runner);
- verify(runner).onAnimationStart(any(), any(), any(), any(), any(), any());
+ verify(runner).onAnimationStart(any(), any(), any(), any(), any(), any(), any());
mRecentsTransitionHandler.findController(transition).cancel("test");
mMainExecutor.flushAll();
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 ffe8e7135513..79e9b9c8cd77 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
@@ -59,11 +59,12 @@ import com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn
import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession
import com.android.window.flags.Flags
import com.android.wm.shell.R
-import com.android.wm.shell.desktopmode.common.ToggleTaskSizeInteraction
import com.android.wm.shell.desktopmode.DesktopImmersiveController
import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.InputMethod
import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.ResizeTrigger
import com.android.wm.shell.desktopmode.DesktopTasksController.SnapPosition
+import com.android.wm.shell.desktopmode.common.ToggleTaskSizeInteraction
+import com.android.wm.shell.recents.RecentsTransitionStateListener
import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource
import com.android.wm.shell.splitscreen.SplitScreenController
@@ -539,7 +540,8 @@ class DesktopModeWindowDecorViewModelTests : DesktopModeWindowDecorViewModelTest
onLeftSnapClickListenerCaptor.value.invoke()
verify(mockDesktopTasksController, never())
- .snapToHalfScreen(eq(decor.mTaskInfo), any(), eq(currentBounds), eq(SnapPosition.LEFT),
+ .snapToHalfScreen(
+ eq(decor.mTaskInfo), any(), eq(currentBounds), eq(SnapPosition.LEFT),
eq(ResizeTrigger.MAXIMIZE_BUTTON),
eq(InputMethod.UNKNOWN_INPUT_METHOD),
eq(decor),
@@ -616,11 +618,12 @@ class DesktopModeWindowDecorViewModelTests : DesktopModeWindowDecorViewModelTest
onRightSnapClickListenerCaptor.value.invoke()
verify(mockDesktopTasksController, never())
- .snapToHalfScreen(eq(decor.mTaskInfo), any(), eq(currentBounds), eq(SnapPosition.RIGHT),
+ .snapToHalfScreen(
+ eq(decor.mTaskInfo), any(), eq(currentBounds), eq(SnapPosition.RIGHT),
eq(ResizeTrigger.MAXIMIZE_BUTTON),
eq(InputMethod.UNKNOWN_INPUT_METHOD),
eq(decor),
- )
+ )
}
@Test
@@ -1223,6 +1226,49 @@ class DesktopModeWindowDecorViewModelTests : DesktopModeWindowDecorViewModelTest
verify(task2, never()).onExclusionRegionChanged(newRegion)
}
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_RECENTS_TRANSITIONS_CORNERS_BUGFIX)
+ fun testRecentsTransitionStateListener_requestedState_setsTransitionRunning() {
+ val task = createTask(windowingMode = WINDOWING_MODE_FREEFORM)
+ val decoration = setUpMockDecorationForTask(task)
+ onTaskOpening(task, SurfaceControl())
+
+ desktopModeRecentsTransitionStateListener.onTransitionStateChanged(
+ RecentsTransitionStateListener.TRANSITION_STATE_REQUESTED)
+
+ verify(decoration).setIsRecentsTransitionRunning(true)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_RECENTS_TRANSITIONS_CORNERS_BUGFIX)
+ fun testRecentsTransitionStateListener_nonRunningState_setsTransitionNotRunning() {
+ val task = createTask(windowingMode = WINDOWING_MODE_FREEFORM)
+ val decoration = setUpMockDecorationForTask(task)
+ onTaskOpening(task, SurfaceControl())
+ desktopModeRecentsTransitionStateListener.onTransitionStateChanged(
+ RecentsTransitionStateListener.TRANSITION_STATE_REQUESTED)
+
+ desktopModeRecentsTransitionStateListener.onTransitionStateChanged(
+ RecentsTransitionStateListener.TRANSITION_STATE_NOT_RUNNING)
+
+ verify(decoration).setIsRecentsTransitionRunning(false)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_RECENTS_TRANSITIONS_CORNERS_BUGFIX)
+ fun testRecentsTransitionStateListener_requestedAndAnimating_setsTransitionRunningOnce() {
+ val task = createTask(windowingMode = WINDOWING_MODE_FREEFORM)
+ val decoration = setUpMockDecorationForTask(task)
+ onTaskOpening(task, SurfaceControl())
+
+ desktopModeRecentsTransitionStateListener.onTransitionStateChanged(
+ RecentsTransitionStateListener.TRANSITION_STATE_REQUESTED)
+ desktopModeRecentsTransitionStateListener.onTransitionStateChanged(
+ RecentsTransitionStateListener.TRANSITION_STATE_ANIMATING)
+
+ verify(decoration, times(1)).setIsRecentsTransitionRunning(true)
+ }
+
private fun createOpenTaskDecoration(
@WindowingMode windowingMode: Int,
taskSurface: SurfaceControl = SurfaceControl(),
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTestsBase.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTestsBase.kt
index b5e8cebc1277..8af8285d031c 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTestsBase.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTestsBase.kt
@@ -40,6 +40,7 @@ import android.view.SurfaceControl
import android.view.WindowInsets.Type.statusBars
import com.android.dx.mockito.inline.extended.StaticMockitoSession
import com.android.internal.jank.InteractionJankMonitor
+import com.android.window.flags.Flags
import com.android.wm.shell.RootTaskDisplayAreaOrganizer
import com.android.wm.shell.ShellTaskOrganizer
import com.android.wm.shell.ShellTestCase
@@ -65,6 +66,8 @@ import com.android.wm.shell.desktopmode.WindowDecorCaptionHandleRepository
import com.android.wm.shell.desktopmode.education.AppHandleEducationController
import com.android.wm.shell.desktopmode.education.AppToWebEducationController
import com.android.wm.shell.freeform.FreeformTaskTransitionStarter
+import com.android.wm.shell.recents.RecentsTransitionHandler
+import com.android.wm.shell.recents.RecentsTransitionStateListener
import com.android.wm.shell.splitscreen.SplitScreenController
import com.android.wm.shell.sysui.ShellCommandHandler
import com.android.wm.shell.sysui.ShellController
@@ -151,6 +154,7 @@ open class DesktopModeWindowDecorViewModelTestsBase : ShellTestCase() {
protected val mockFocusTransitionObserver = mock<FocusTransitionObserver>()
protected val mockCaptionHandleRepository = mock<WindowDecorCaptionHandleRepository>()
protected val mockDesktopRepository: DesktopRepository = mock<DesktopRepository>()
+ protected val mockRecentsTransitionHandler = mock<RecentsTransitionHandler>()
protected val motionEvent = mock<MotionEvent>()
val displayLayout = mock<DisplayLayout>()
protected lateinit var spyContext: TestableContext
@@ -164,6 +168,7 @@ open class DesktopModeWindowDecorViewModelTestsBase : ShellTestCase() {
protected lateinit var mockitoSession: StaticMockitoSession
protected lateinit var shellInit: ShellInit
internal lateinit var desktopModeOnInsetsChangedListener: DesktopModeOnInsetsChangedListener
+ protected lateinit var desktopModeRecentsTransitionStateListener: RecentsTransitionStateListener
protected lateinit var displayChangingListener:
DisplayChangeController.OnDisplayChangingListener
internal lateinit var desktopModeOnKeyguardChangedListener: DesktopModeKeyguardChangeListener
@@ -219,7 +224,8 @@ open class DesktopModeWindowDecorViewModelTestsBase : ShellTestCase() {
mockFocusTransitionObserver,
desktopModeEventLogger,
mock<DesktopModeUiEventLogger>(),
- mock<WindowDecorTaskResourceLoader>()
+ mock<WindowDecorTaskResourceLoader>(),
+ mockRecentsTransitionHandler,
)
desktopModeWindowDecorViewModel.setSplitScreenController(mockSplitScreenController)
whenever(mockDisplayController.getDisplayLayout(any())).thenReturn(mockDisplayLayout)
@@ -256,6 +262,13 @@ open class DesktopModeWindowDecorViewModelTestsBase : ShellTestCase() {
verify(displayInsetsController)
.addGlobalInsetsChangedListener(insetsChangedCaptor.capture())
desktopModeOnInsetsChangedListener = insetsChangedCaptor.firstValue
+ val recentsTransitionStateListenerCaptor = argumentCaptor<RecentsTransitionStateListener>()
+ if (Flags.enableDesktopRecentsTransitionsCornersBugfix()) {
+ verify(mockRecentsTransitionHandler)
+ .addTransitionStateListener(recentsTransitionStateListenerCaptor.capture())
+ desktopModeRecentsTransitionStateListener =
+ recentsTransitionStateListenerCaptor.firstValue
+ }
val keyguardChangedCaptor =
argumentCaptor<DesktopModeKeyguardChangeListener>()
verify(mockShellController).addKeyguardChangeListener(keyguardChangedCaptor.capture())
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java
index 6b02aeffd42a..9ea5fd6e1abe 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java
@@ -169,6 +169,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {
private static final boolean DEFAULT_IS_KEYGUARD_VISIBLE_AND_OCCLUDED = false;
private static final boolean DEFAULT_IS_IN_FULL_IMMERSIVE_MODE = false;
private static final boolean DEFAULT_HAS_GLOBAL_FOCUS = true;
+ private static final boolean DEFAULT_SHOULD_IGNORE_CORNER_RADIUS = false;
@Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(DEVICE_DEFAULT);
@@ -396,6 +397,31 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {
}
@Test
+ public void updateRelayoutParams_shouldIgnoreCornerRadius_roundedCornersNotSet() {
+ final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true);
+ taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FREEFORM);
+ fillRoundedCornersResources(/* fillValue= */ 30);
+ RelayoutParams relayoutParams = new RelayoutParams();
+
+ DesktopModeWindowDecoration.updateRelayoutParams(
+ relayoutParams,
+ mTestableContext,
+ taskInfo,
+ mMockSplitScreenController,
+ DEFAULT_APPLY_START_TRANSACTION_ON_DRAW,
+ DEFAULT_SHOULD_SET_TASK_POSITIONING_AND_CROP,
+ DEFAULT_IS_STATUSBAR_VISIBLE,
+ DEFAULT_IS_KEYGUARD_VISIBLE_AND_OCCLUDED,
+ DEFAULT_IS_IN_FULL_IMMERSIVE_MODE,
+ new InsetsState(),
+ DEFAULT_HAS_GLOBAL_FOCUS,
+ mExclusionRegion,
+ /* shouldIgnoreCornerRadius= */ true);
+
+ assertThat(relayoutParams.mCornerRadius).isEqualTo(INVALID_CORNER_RADIUS);
+ }
+
+ @Test
@EnableFlags(Flags.FLAG_ENABLE_APP_HEADER_WITH_TASK_DENSITY)
public void updateRelayoutParams_appHeader_usesTaskDensity() {
final int systemDensity = mTestableContext.getOrCreateTestableResources().getResources()
@@ -634,7 +660,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {
/* inFullImmersiveMode */ true,
insetsState,
DEFAULT_HAS_GLOBAL_FOCUS,
- mExclusionRegion);
+ mExclusionRegion,
+ DEFAULT_SHOULD_IGNORE_CORNER_RADIUS);
// Takes status bar inset as padding, ignores caption bar inset.
assertThat(relayoutParams.mCaptionTopPadding).isEqualTo(50);
@@ -659,7 +686,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {
/* inFullImmersiveMode */ true,
new InsetsState(),
DEFAULT_HAS_GLOBAL_FOCUS,
- mExclusionRegion);
+ mExclusionRegion,
+ DEFAULT_SHOULD_IGNORE_CORNER_RADIUS);
assertThat(relayoutParams.mIsInsetSource).isFalse();
}
@@ -683,7 +711,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {
DEFAULT_IS_IN_FULL_IMMERSIVE_MODE,
new InsetsState(),
DEFAULT_HAS_GLOBAL_FOCUS,
- mExclusionRegion);
+ mExclusionRegion,
+ DEFAULT_SHOULD_IGNORE_CORNER_RADIUS);
// Header is always shown because it's assumed the status bar is always visible.
assertThat(relayoutParams.mIsCaptionVisible).isTrue();
@@ -707,7 +736,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {
DEFAULT_IS_IN_FULL_IMMERSIVE_MODE,
new InsetsState(),
DEFAULT_HAS_GLOBAL_FOCUS,
- mExclusionRegion);
+ mExclusionRegion,
+ DEFAULT_SHOULD_IGNORE_CORNER_RADIUS);
assertThat(relayoutParams.mIsCaptionVisible).isTrue();
}
@@ -730,7 +760,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {
DEFAULT_IS_IN_FULL_IMMERSIVE_MODE,
new InsetsState(),
DEFAULT_HAS_GLOBAL_FOCUS,
- mExclusionRegion);
+ mExclusionRegion,
+ DEFAULT_SHOULD_IGNORE_CORNER_RADIUS);
assertThat(relayoutParams.mIsCaptionVisible).isFalse();
}
@@ -753,7 +784,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {
DEFAULT_IS_IN_FULL_IMMERSIVE_MODE,
new InsetsState(),
DEFAULT_HAS_GLOBAL_FOCUS,
- mExclusionRegion);
+ mExclusionRegion,
+ DEFAULT_SHOULD_IGNORE_CORNER_RADIUS);
assertThat(relayoutParams.mIsCaptionVisible).isFalse();
}
@@ -777,7 +809,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {
/* inFullImmersiveMode */ true,
new InsetsState(),
DEFAULT_HAS_GLOBAL_FOCUS,
- mExclusionRegion);
+ mExclusionRegion,
+ DEFAULT_SHOULD_IGNORE_CORNER_RADIUS);
assertThat(relayoutParams.mIsCaptionVisible).isTrue();
@@ -793,7 +826,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {
/* inFullImmersiveMode */ true,
new InsetsState(),
DEFAULT_HAS_GLOBAL_FOCUS,
- mExclusionRegion);
+ mExclusionRegion,
+ DEFAULT_SHOULD_IGNORE_CORNER_RADIUS);
assertThat(relayoutParams.mIsCaptionVisible).isFalse();
}
@@ -817,7 +851,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {
/* inFullImmersiveMode */ true,
new InsetsState(),
DEFAULT_HAS_GLOBAL_FOCUS,
- mExclusionRegion);
+ mExclusionRegion,
+ DEFAULT_SHOULD_IGNORE_CORNER_RADIUS);
assertThat(relayoutParams.mIsCaptionVisible).isFalse();
}
@@ -1480,7 +1515,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {
DEFAULT_IS_IN_FULL_IMMERSIVE_MODE,
new InsetsState(),
DEFAULT_HAS_GLOBAL_FOCUS,
- mExclusionRegion);
+ mExclusionRegion,
+ DEFAULT_SHOULD_IGNORE_CORNER_RADIUS);
}
private DesktopModeWindowDecoration createWindowDecoration(
diff --git a/libs/androidfw/ApkAssets.cpp b/libs/androidfw/ApkAssets.cpp
index e693fcfd3918..dbb891455ddd 100644
--- a/libs/androidfw/ApkAssets.cpp
+++ b/libs/androidfw/ApkAssets.cpp
@@ -162,13 +162,10 @@ const std::string& ApkAssets::GetDebugName() const {
return assets_provider_->GetDebugName();
}
-UpToDate ApkAssets::IsUpToDate() const {
+bool ApkAssets::IsUpToDate() const {
// Loaders are invalidated by the app, not the system, so assume they are up to date.
- if (IsLoader()) {
- return UpToDate::Always;
- }
- const auto idmap_res = loaded_idmap_ ? loaded_idmap_->IsUpToDate() : UpToDate::Always;
- return combine(idmap_res, [this] { return assets_provider_->IsUpToDate(); });
+ return IsLoader() || ((!loaded_idmap_ || loaded_idmap_->IsUpToDate())
+ && assets_provider_->IsUpToDate());
}
} // namespace android
diff --git a/libs/androidfw/AssetsProvider.cpp b/libs/androidfw/AssetsProvider.cpp
index 11b12eb030a6..2d3c06506a1f 100644
--- a/libs/androidfw/AssetsProvider.cpp
+++ b/libs/androidfw/AssetsProvider.cpp
@@ -24,8 +24,9 @@
#include <ziparchive/zip_archive.h>
namespace android {
-
-static constexpr std::string_view kEmptyDebugString = "<empty>";
+namespace {
+constexpr const char* kEmptyDebugString = "<empty>";
+} // namespace
std::unique_ptr<Asset> AssetsProvider::Open(const std::string& path, Asset::AccessMode mode,
bool* file_exists) const {
@@ -85,9 +86,11 @@ void ZipAssetsProvider::ZipCloser::operator()(ZipArchive* a) const {
}
ZipAssetsProvider::ZipAssetsProvider(ZipArchiveHandle handle, PathOrDebugName&& path,
- package_property_t flags, ModDate last_mod_time)
- : zip_handle_(handle), name_(std::move(path)), flags_(flags), last_mod_time_(last_mod_time) {
-}
+ package_property_t flags, time_t last_mod_time)
+ : zip_handle_(handle),
+ name_(std::move(path)),
+ flags_(flags),
+ last_mod_time_(last_mod_time) {}
std::unique_ptr<ZipAssetsProvider> ZipAssetsProvider::Create(std::string path,
package_property_t flags,
@@ -101,10 +104,10 @@ std::unique_ptr<ZipAssetsProvider> ZipAssetsProvider::Create(std::string path,
return {};
}
- ModDate mod_date = kInvalidModDate;
+ struct stat sb{.st_mtime = -1};
// Skip all up-to-date checks if the file won't ever change.
- if (isKnownWritablePath(path.c_str()) || !isReadonlyFilesystem(GetFileDescriptor(handle))) {
- if (mod_date = getFileModDate(GetFileDescriptor(handle)); mod_date == kInvalidModDate) {
+ if (!isReadonlyFilesystem(path.c_str())) {
+ if ((released_fd < 0 ? stat(path.c_str(), &sb) : fstat(released_fd, &sb)) < 0) {
// Stat requires execute permissions on all directories path to the file. If the process does
// not have execute permissions on this file, allow the zip to be opened but IsUpToDate() will
// always have to return true.
@@ -113,7 +116,7 @@ std::unique_ptr<ZipAssetsProvider> ZipAssetsProvider::Create(std::string path,
}
return std::unique_ptr<ZipAssetsProvider>(
- new ZipAssetsProvider(handle, PathOrDebugName::Path(std::move(path)), flags, mod_date));
+ new ZipAssetsProvider(handle, PathOrDebugName::Path(std::move(path)), flags, sb.st_mtime));
}
std::unique_ptr<ZipAssetsProvider> ZipAssetsProvider::Create(base::unique_fd fd,
@@ -134,10 +137,10 @@ std::unique_ptr<ZipAssetsProvider> ZipAssetsProvider::Create(base::unique_fd fd,
return {};
}
- ModDate mod_date = kInvalidModDate;
+ struct stat sb{.st_mtime = -1};
// Skip all up-to-date checks if the file won't ever change.
if (!isReadonlyFilesystem(released_fd)) {
- if (mod_date = getFileModDate(released_fd); mod_date == kInvalidModDate) {
+ if (fstat(released_fd, &sb) < 0) {
// Stat requires execute permissions on all directories path to the file. If the process does
// not have execute permissions on this file, allow the zip to be opened but IsUpToDate() will
// always have to return true.
@@ -147,7 +150,7 @@ std::unique_ptr<ZipAssetsProvider> ZipAssetsProvider::Create(base::unique_fd fd,
}
return std::unique_ptr<ZipAssetsProvider>(new ZipAssetsProvider(
- handle, PathOrDebugName::DebugName(std::move(friendly_name)), flags, mod_date));
+ handle, PathOrDebugName::DebugName(std::move(friendly_name)), flags, sb.st_mtime));
}
std::unique_ptr<Asset> ZipAssetsProvider::OpenInternal(const std::string& path,
@@ -279,16 +282,21 @@ const std::string& ZipAssetsProvider::GetDebugName() const {
return name_.GetDebugName();
}
-UpToDate ZipAssetsProvider::IsUpToDate() const {
- if (last_mod_time_ == kInvalidModDate) {
- return UpToDate::Always;
+bool ZipAssetsProvider::IsUpToDate() const {
+ if (last_mod_time_ == -1) {
+ return true;
+ }
+ struct stat sb{};
+ if (fstat(GetFileDescriptor(zip_handle_.get()), &sb) < 0) {
+ // If fstat fails on the zip archive, return true so the zip archive the resource system does
+ // attempt to refresh the ApkAsset.
+ return true;
}
- return fromBool(last_mod_time_ == getFileModDate(GetFileDescriptor(zip_handle_.get())));
+ return last_mod_time_ == sb.st_mtime;
}
-DirectoryAssetsProvider::DirectoryAssetsProvider(std::string&& path, ModDate last_mod_time)
- : dir_(std::move(path)), last_mod_time_(last_mod_time) {
-}
+DirectoryAssetsProvider::DirectoryAssetsProvider(std::string&& path, time_t last_mod_time)
+ : dir_(std::move(path)), last_mod_time_(last_mod_time) {}
std::unique_ptr<DirectoryAssetsProvider> DirectoryAssetsProvider::Create(std::string path) {
struct stat sb;
@@ -309,7 +317,7 @@ std::unique_ptr<DirectoryAssetsProvider> DirectoryAssetsProvider::Create(std::st
const bool isReadonly = isReadonlyFilesystem(path.c_str());
return std::unique_ptr<DirectoryAssetsProvider>(
- new DirectoryAssetsProvider(std::move(path), isReadonly ? kInvalidModDate : getModDate(sb)));
+ new DirectoryAssetsProvider(std::move(path), isReadonly ? -1 : sb.st_mtime));
}
std::unique_ptr<Asset> DirectoryAssetsProvider::OpenInternal(const std::string& path,
@@ -338,11 +346,17 @@ const std::string& DirectoryAssetsProvider::GetDebugName() const {
return dir_;
}
-UpToDate DirectoryAssetsProvider::IsUpToDate() const {
- if (last_mod_time_ == kInvalidModDate) {
- return UpToDate::Always;
+bool DirectoryAssetsProvider::IsUpToDate() const {
+ if (last_mod_time_ == -1) {
+ return true;
+ }
+ struct stat sb;
+ if (stat(dir_.c_str(), &sb) < 0) {
+ // If stat fails on the zip archive, return true so the zip archive the resource system does
+ // attempt to refresh the ApkAsset.
+ return true;
}
- return fromBool(last_mod_time_ == getFileModDate(dir_.c_str()));
+ return last_mod_time_ == sb.st_mtime;
}
MultiAssetsProvider::MultiAssetsProvider(std::unique_ptr<AssetsProvider>&& primary,
@@ -355,14 +369,8 @@ MultiAssetsProvider::MultiAssetsProvider(std::unique_ptr<AssetsProvider>&& prima
std::unique_ptr<AssetsProvider> MultiAssetsProvider::Create(
std::unique_ptr<AssetsProvider>&& primary, std::unique_ptr<AssetsProvider>&& secondary) {
- if (primary == nullptr && secondary == nullptr) {
- return EmptyAssetsProvider::Create();
- }
- if (!primary) {
- return secondary;
- }
- if (!secondary) {
- return primary;
+ if (primary == nullptr || secondary == nullptr) {
+ return nullptr;
}
return std::unique_ptr<MultiAssetsProvider>(new MultiAssetsProvider(std::move(primary),
std::move(secondary)));
@@ -389,8 +397,8 @@ const std::string& MultiAssetsProvider::GetDebugName() const {
return debug_name_;
}
-UpToDate MultiAssetsProvider::IsUpToDate() const {
- return combine(primary_->IsUpToDate(), [this] { return secondary_->IsUpToDate(); });
+bool MultiAssetsProvider::IsUpToDate() const {
+ return primary_->IsUpToDate() && secondary_->IsUpToDate();
}
EmptyAssetsProvider::EmptyAssetsProvider(std::optional<std::string>&& path) :
@@ -430,12 +438,12 @@ const std::string& EmptyAssetsProvider::GetDebugName() const {
if (path_.has_value()) {
return *path_;
}
- constexpr static std::string kEmpty{kEmptyDebugString};
+ const static std::string kEmpty = kEmptyDebugString;
return kEmpty;
}
-UpToDate EmptyAssetsProvider::IsUpToDate() const {
- return UpToDate::Always;
+bool EmptyAssetsProvider::IsUpToDate() const {
+ return true;
}
} // namespace android
diff --git a/libs/androidfw/Idmap.cpp b/libs/androidfw/Idmap.cpp
index 262e7df185b7..3ecd82b074a1 100644
--- a/libs/androidfw/Idmap.cpp
+++ b/libs/androidfw/Idmap.cpp
@@ -22,10 +22,9 @@
#include "android-base/logging.h"
#include "android-base/stringprintf.h"
#include "android-base/utf8.h"
-#include "androidfw/AssetManager.h"
+#include "androidfw/misc.h"
#include "androidfw/ResourceTypes.h"
#include "androidfw/Util.h"
-#include "androidfw/misc.h"
#include "utils/ByteOrder.h"
#include "utils/Trace.h"
@@ -269,16 +268,11 @@ LoadedIdmap::LoadedIdmap(const std::string& idmap_path, const Idmap_header* head
configurations_(configs),
overlay_entries_(overlay_entries),
string_pool_(std::move(string_pool)),
+ idmap_fd_(
+ android::base::utf8::open(idmap_path.c_str(), O_RDONLY | O_CLOEXEC | O_BINARY | O_PATH)),
overlay_apk_path_(overlay_apk_path),
target_apk_path_(target_apk_path),
- idmap_last_mod_time_(kInvalidModDate) {
- if (!isReadonlyFilesystem(std::string(overlay_apk_path_).c_str()) ||
- !(target_apk_path_ == AssetManager::TARGET_APK_PATH ||
- isReadonlyFilesystem(std::string(target_apk_path_).c_str()))) {
- idmap_fd_.reset(
- android::base::utf8::open(idmap_path.c_str(), O_RDONLY | O_CLOEXEC | O_BINARY | O_PATH));
- idmap_last_mod_time_ = getFileModDate(idmap_fd_);
- }
+ idmap_last_mod_time_(getFileModDate(idmap_fd_.get())) {
}
std::unique_ptr<LoadedIdmap> LoadedIdmap::Load(StringPiece idmap_path, StringPiece idmap_data) {
@@ -387,11 +381,8 @@ std::unique_ptr<LoadedIdmap> LoadedIdmap::Load(StringPiece idmap_path, StringPie
overlay_entries, std::move(idmap_string_pool), *overlay_path, *target_path));
}
-UpToDate LoadedIdmap::IsUpToDate() const {
- if (idmap_last_mod_time_ == kInvalidModDate) {
- return UpToDate::Always;
- }
- return fromBool(idmap_last_mod_time_ == getFileModDate(idmap_fd_.get()));
+bool LoadedIdmap::IsUpToDate() const {
+ return idmap_last_mod_time_ == getFileModDate(idmap_fd_.get());
}
} // namespace android
diff --git a/libs/androidfw/ResourceTypes.cpp b/libs/androidfw/ResourceTypes.cpp
index a8eb062a2ece..de9991a8be5e 100644
--- a/libs/androidfw/ResourceTypes.cpp
+++ b/libs/androidfw/ResourceTypes.cpp
@@ -152,11 +152,12 @@ static void fill9patchOffsets(Res_png_9patch* patch) {
patch->colorsOffset = patch->yDivsOffset + (patch->numYDivs * sizeof(int32_t));
}
-void Res_value::copyFrom_dtoh_slow(const Res_value& src) {
- size = dtohs(src.size);
- res0 = src.res0;
- dataType = src.dataType;
- data = dtohl(src.data);
+void Res_value::copyFrom_dtoh(const Res_value& src)
+{
+ size = dtohs(src.size);
+ res0 = src.res0;
+ dataType = src.dataType;
+ data = dtohl(src.data);
}
void Res_png_9patch::deviceToFile()
@@ -2030,6 +2031,16 @@ status_t ResXMLTree::validateNode(const ResXMLTree_node* node) const
// --------------------------------------------------------------------
// --------------------------------------------------------------------
+void ResTable_config::copyFromDeviceNoSwap(const ResTable_config& o) {
+ const size_t size = dtohl(o.size);
+ if (size >= sizeof(ResTable_config)) {
+ *this = o;
+ } else {
+ memcpy(this, &o, size);
+ memset(((uint8_t*)this)+size, 0, sizeof(ResTable_config)-size);
+ }
+}
+
/* static */ size_t unpackLanguageOrRegion(const char in[2], const char base,
char out[4]) {
if (in[0] & 0x80) {
@@ -2094,33 +2105,34 @@ size_t ResTable_config::unpackRegion(char region[4]) const {
return unpackLanguageOrRegion(this->country, '0', region);
}
-void ResTable_config::copyFromDtoH_slow(const ResTable_config& o) {
- copyFromDeviceNoSwap(o);
- size = sizeof(ResTable_config);
- mcc = dtohs(mcc);
- mnc = dtohs(mnc);
- density = dtohs(density);
- screenWidth = dtohs(screenWidth);
- screenHeight = dtohs(screenHeight);
- sdkVersion = dtohs(sdkVersion);
- minorVersion = dtohs(minorVersion);
- smallestScreenWidthDp = dtohs(smallestScreenWidthDp);
- screenWidthDp = dtohs(screenWidthDp);
- screenHeightDp = dtohs(screenHeightDp);
-}
-
-void ResTable_config::swapHtoD_slow() {
- size = htodl(size);
- mcc = htods(mcc);
- mnc = htods(mnc);
- density = htods(density);
- screenWidth = htods(screenWidth);
- screenHeight = htods(screenHeight);
- sdkVersion = htods(sdkVersion);
- minorVersion = htods(minorVersion);
- smallestScreenWidthDp = htods(smallestScreenWidthDp);
- screenWidthDp = htods(screenWidthDp);
- screenHeightDp = htods(screenHeightDp);
+
+void ResTable_config::copyFromDtoH(const ResTable_config& o) {
+ copyFromDeviceNoSwap(o);
+ size = sizeof(ResTable_config);
+ mcc = dtohs(mcc);
+ mnc = dtohs(mnc);
+ density = dtohs(density);
+ screenWidth = dtohs(screenWidth);
+ screenHeight = dtohs(screenHeight);
+ sdkVersion = dtohs(sdkVersion);
+ minorVersion = dtohs(minorVersion);
+ smallestScreenWidthDp = dtohs(smallestScreenWidthDp);
+ screenWidthDp = dtohs(screenWidthDp);
+ screenHeightDp = dtohs(screenHeightDp);
+}
+
+void ResTable_config::swapHtoD() {
+ size = htodl(size);
+ mcc = htods(mcc);
+ mnc = htods(mnc);
+ density = htods(density);
+ screenWidth = htods(screenWidth);
+ screenHeight = htods(screenHeight);
+ sdkVersion = htods(sdkVersion);
+ minorVersion = htods(minorVersion);
+ smallestScreenWidthDp = htods(smallestScreenWidthDp);
+ screenWidthDp = htods(screenWidthDp);
+ screenHeightDp = htods(screenHeightDp);
}
/* static */ inline int compareLocales(const ResTable_config &l, const ResTable_config &r) {
@@ -2133,7 +2145,7 @@ void ResTable_config::swapHtoD_slow() {
// systems should happen very infrequently (if at all.)
// The comparison code relies on memcmp low-level optimizations that make it
// more efficient than strncmp.
- static constexpr char emptyScript[sizeof(l.localeScript)] = {'\0', '\0', '\0', '\0'};
+ const char emptyScript[sizeof(l.localeScript)] = {'\0', '\0', '\0', '\0'};
const char *lScript = l.localeScriptWasComputed ? emptyScript : l.localeScript;
const char *rScript = r.localeScriptWasComputed ? emptyScript : r.localeScript;
diff --git a/libs/androidfw/Util.cpp b/libs/androidfw/Util.cpp
index 86c459fb4647..be55fe8b4bb6 100644
--- a/libs/androidfw/Util.cpp
+++ b/libs/androidfw/Util.cpp
@@ -32,18 +32,13 @@ namespace android {
namespace util {
void ReadUtf16StringFromDevice(const uint16_t* src, size_t len, std::string* out) {
- static constexpr bool kDeviceEndiannessSame = dtohs(0x1001) == 0x1001;
- if constexpr (kDeviceEndiannessSame) {
- *out = Utf16ToUtf8({(const char16_t*)src, strnlen16((const char16_t*)src, len)});
- } else {
- char buf[5];
- while (*src && len != 0) {
- char16_t c = static_cast<char16_t>(dtohs(*src));
- utf16_to_utf8(&c, 1, buf, sizeof(buf));
- out->append(buf, strlen(buf));
- ++src;
- --len;
- }
+ char buf[5];
+ while (*src && len != 0) {
+ char16_t c = static_cast<char16_t>(dtohs(*src));
+ utf16_to_utf8(&c, 1, buf, sizeof(buf));
+ out->append(buf, strlen(buf));
+ ++src;
+ --len;
}
}
@@ -68,10 +63,8 @@ std::string Utf16ToUtf8(StringPiece16 utf16) {
}
std::string utf8;
- utf8.resize_and_overwrite(utf8_length, [&utf16](char* data, size_t size) {
- utf16_to_utf8(utf16.data(), utf16.length(), data, size + 1);
- return size;
- });
+ utf8.resize(utf8_length);
+ utf16_to_utf8(utf16.data(), utf16.length(), &*utf8.begin(), utf8_length + 1);
return utf8;
}
diff --git a/libs/androidfw/ZipUtils.cpp b/libs/androidfw/ZipUtils.cpp
index a1385f2cf7b1..f7f62c51a25b 100644
--- a/libs/androidfw/ZipUtils.cpp
+++ b/libs/androidfw/ZipUtils.cpp
@@ -87,19 +87,29 @@ class BufferReader final : public zip_archive::Reader {
}
bool ReadAtOffset(uint8_t* buf, size_t len, off64_t offset) const override {
- if (mInputSize < len || offset > mInputSize - len) {
- return false;
- }
-
- const incfs::map_ptr<uint8_t> pos = mInput.offset(offset);
- if (!pos.verify(len)) {
+ auto in = AccessAtOffset(buf, len, offset);
+ if (!in) {
return false;
}
-
- memcpy(buf, pos.unsafe_ptr(), len);
+ memcpy(buf, in, len);
return true;
}
+ const uint8_t* AccessAtOffset(uint8_t*, size_t len, off64_t offset) const override {
+ if (offset > mInputSize - len) {
+ return nullptr;
+ }
+ const incfs::map_ptr<uint8_t> pos = mInput.offset(offset);
+ if (!pos.verify(len)) {
+ return nullptr;
+ }
+ return pos.unsafe_ptr();
+ }
+
+ bool IsZeroCopy() const override {
+ return true;
+ }
+
private:
const incfs::map_ptr<uint8_t> mInput;
const size_t mInputSize;
@@ -107,7 +117,7 @@ class BufferReader final : public zip_archive::Reader {
class BufferWriter final : public zip_archive::Writer {
public:
- BufferWriter(void* output, size_t outputSize) : Writer(),
+ BufferWriter(void* output, size_t outputSize) :
mOutput(reinterpret_cast<uint8_t*>(output)), mOutputSize(outputSize), mBytesWritten(0) {
}
@@ -121,6 +131,12 @@ class BufferWriter final : public zip_archive::Writer {
return true;
}
+ Buffer GetBuffer(size_t length) override {
+ const auto remaining_size = mOutputSize - mBytesWritten;
+ return remaining_size >= length
+ ? Buffer(mOutput + mBytesWritten, remaining_size) : Buffer();
+ }
+
private:
uint8_t* const mOutput;
const size_t mOutputSize;
diff --git a/libs/androidfw/include/androidfw/ApkAssets.h b/libs/androidfw/include/androidfw/ApkAssets.h
index 3f6f4661f2f7..231808beb718 100644
--- a/libs/androidfw/include/androidfw/ApkAssets.h
+++ b/libs/androidfw/include/androidfw/ApkAssets.h
@@ -116,7 +116,7 @@ class ApkAssets : public RefBase {
return resources_asset_ != nullptr && resources_asset_->isAllocated();
}
- UpToDate IsUpToDate() const;
+ bool IsUpToDate() const;
// DANGER!
// This is a destructive method that rips the assets provider out of ApkAssets object.
diff --git a/libs/androidfw/include/androidfw/AssetsProvider.h b/libs/androidfw/include/androidfw/AssetsProvider.h
index e3b3ae41f7f4..d33c325ff369 100644
--- a/libs/androidfw/include/androidfw/AssetsProvider.h
+++ b/libs/androidfw/include/androidfw/AssetsProvider.h
@@ -14,7 +14,8 @@
* limitations under the License.
*/
-#pragma once
+#ifndef ANDROIDFW_ASSETSPROVIDER_H
+#define ANDROIDFW_ASSETSPROVIDER_H
#include <memory>
#include <string>
@@ -57,7 +58,7 @@ struct AssetsProvider {
WARN_UNUSED virtual const std::string& GetDebugName() const = 0;
// Returns whether the interface provides the most recent version of its files.
- WARN_UNUSED virtual UpToDate IsUpToDate() const = 0;
+ WARN_UNUSED virtual bool IsUpToDate() const = 0;
// Creates an Asset from a file on disk.
static std::unique_ptr<Asset> CreateAssetFromFile(const std::string& path);
@@ -94,7 +95,7 @@ struct ZipAssetsProvider : public AssetsProvider {
WARN_UNUSED std::optional<std::string_view> GetPath() const override;
WARN_UNUSED const std::string& GetDebugName() const override;
- WARN_UNUSED UpToDate IsUpToDate() const override;
+ WARN_UNUSED bool IsUpToDate() const override;
WARN_UNUSED std::optional<uint32_t> GetCrc(std::string_view path) const;
~ZipAssetsProvider() override = default;
@@ -105,7 +106,7 @@ struct ZipAssetsProvider : public AssetsProvider {
private:
struct PathOrDebugName;
ZipAssetsProvider(ZipArchive* handle, PathOrDebugName&& path, package_property_t flags,
- ModDate last_mod_time);
+ time_t last_mod_time);
struct PathOrDebugName {
static PathOrDebugName Path(std::string value) {
@@ -134,7 +135,7 @@ struct ZipAssetsProvider : public AssetsProvider {
std::unique_ptr<ZipArchive, ZipCloser> zip_handle_;
PathOrDebugName name_;
package_property_t flags_;
- ModDate last_mod_time_;
+ time_t last_mod_time_;
};
// Supplies assets from a root directory.
@@ -146,7 +147,7 @@ struct DirectoryAssetsProvider : public AssetsProvider {
WARN_UNUSED std::optional<std::string_view> GetPath() const override;
WARN_UNUSED const std::string& GetDebugName() const override;
- WARN_UNUSED UpToDate IsUpToDate() const override;
+ WARN_UNUSED bool IsUpToDate() const override;
~DirectoryAssetsProvider() override = default;
protected:
@@ -155,23 +156,23 @@ struct DirectoryAssetsProvider : public AssetsProvider {
bool* file_exists) const override;
private:
- explicit DirectoryAssetsProvider(std::string&& path, ModDate last_mod_time);
+ explicit DirectoryAssetsProvider(std::string&& path, time_t last_mod_time);
std::string dir_;
- ModDate last_mod_time_;
+ time_t last_mod_time_;
};
// Supplies assets from a `primary` asset provider and falls back to supplying assets from the
// `secondary` asset provider if the asset cannot be found in the `primary`.
struct MultiAssetsProvider : public AssetsProvider {
static std::unique_ptr<AssetsProvider> Create(std::unique_ptr<AssetsProvider>&& primary,
- std::unique_ptr<AssetsProvider>&& secondary = {});
+ std::unique_ptr<AssetsProvider>&& secondary);
bool ForEachFile(const std::string& root_path,
base::function_ref<void(StringPiece, FileType)> f) const override;
WARN_UNUSED std::optional<std::string_view> GetPath() const override;
WARN_UNUSED const std::string& GetDebugName() const override;
- WARN_UNUSED UpToDate IsUpToDate() const override;
+ WARN_UNUSED bool IsUpToDate() const override;
~MultiAssetsProvider() override = default;
protected:
@@ -198,7 +199,7 @@ struct EmptyAssetsProvider : public AssetsProvider {
WARN_UNUSED std::optional<std::string_view> GetPath() const override;
WARN_UNUSED const std::string& GetDebugName() const override;
- WARN_UNUSED UpToDate IsUpToDate() const override;
+ WARN_UNUSED bool IsUpToDate() const override;
~EmptyAssetsProvider() override = default;
protected:
@@ -211,3 +212,5 @@ struct EmptyAssetsProvider : public AssetsProvider {
};
} // namespace android
+
+#endif /* ANDROIDFW_ASSETSPROVIDER_H */
diff --git a/libs/androidfw/include/androidfw/Idmap.h b/libs/androidfw/include/androidfw/Idmap.h
index 87f3c9df9a91..ac75eb3bb98c 100644
--- a/libs/androidfw/include/androidfw/Idmap.h
+++ b/libs/androidfw/include/androidfw/Idmap.h
@@ -14,7 +14,8 @@
* limitations under the License.
*/
-#pragma once
+#ifndef IDMAP_H_
+#define IDMAP_H_
#include <memory>
#include <string>
@@ -31,31 +32,6 @@
namespace android {
-// An enum that tracks more states than just 'up to date' or 'not' for a resources container:
-// there are several cases where we know for sure that the object can't change and won't get
-// out of date. Reporting those states to the managed layer allows it to stop checking here
-// completely, speeding up the cache lookups by dozens of milliseconds.
-enum class UpToDate : int { False, True, Always };
-
-// Combines two UpToDate values, and only accesses the second one if it matters to the result.
-template <class Getter>
-UpToDate combine(UpToDate first, Getter secondGetter) {
- switch (first) {
- case UpToDate::False:
- return UpToDate::False;
- case UpToDate::True: {
- const auto second = secondGetter();
- return second == UpToDate::False ? UpToDate::False : UpToDate::True;
- }
- case UpToDate::Always:
- return secondGetter();
- }
-}
-
-inline UpToDate fromBool(bool value) {
- return value ? UpToDate::True : UpToDate::False;
-}
-
class LoadedIdmap;
class IdmapResMap;
struct Idmap_header;
@@ -220,7 +196,7 @@ class LoadedIdmap {
// Returns whether the idmap file on disk has not been modified since the construction of this
// LoadedIdmap.
- UpToDate IsUpToDate() const;
+ bool IsUpToDate() const;
protected:
// Exposed as protected so that tests can subclass and mock this class out.
@@ -255,3 +231,5 @@ class LoadedIdmap {
};
} // namespace android
+
+#endif // IDMAP_H_
diff --git a/libs/androidfw/include/androidfw/ResourceTypes.h b/libs/androidfw/include/androidfw/ResourceTypes.h
index 819fe4b38c87..e330410ed1a0 100644
--- a/libs/androidfw/include/androidfw/ResourceTypes.h
+++ b/libs/androidfw/include/androidfw/ResourceTypes.h
@@ -47,8 +47,6 @@
namespace android {
-constexpr const bool kDeviceEndiannessSame = dtohs(0x1001) == 0x1001;
-
constexpr const uint32_t kIdmapMagic = 0x504D4449u;
constexpr const uint32_t kIdmapCurrentVersion = 0x0000000Au;
@@ -410,16 +408,7 @@ struct Res_value
typedef uint32_t data_type;
data_type data;
- void copyFrom_dtoh(const Res_value& src) {
- if constexpr (kDeviceEndiannessSame) {
- *this = src;
- } else {
- copyFrom_dtoh_slow(src);
- }
- }
-
- private:
- void copyFrom_dtoh_slow(const Res_value& src);
+ void copyFrom_dtoh(const Res_value& src);
};
/**
@@ -1265,32 +1254,11 @@ struct ResTable_config
// Varies in length from 3 to 8 chars. Zero-filled value.
char localeNumberingSystem[8];
- void copyFromDeviceNoSwap(const ResTable_config& o) {
- const auto o_size = dtohl(o.size);
- if (o_size >= sizeof(ResTable_config)) [[likely]] {
- *this = o;
- } else {
- memcpy(this, &o, o_size);
- memset(((uint8_t*)this) + o_size, 0, sizeof(ResTable_config) - o_size);
- }
- this->size = sizeof(*this);
- }
-
- void copyFromDtoH(const ResTable_config& o) {
- if constexpr (kDeviceEndiannessSame) {
- copyFromDeviceNoSwap(o);
- } else {
- copyFromDtoH_slow(o);
- }
- }
-
- void swapHtoD() {
- if constexpr (kDeviceEndiannessSame) {
- ; // noop
- } else {
- swapHtoD_slow();
- }
- }
+ void copyFromDeviceNoSwap(const ResTable_config& o);
+
+ void copyFromDtoH(const ResTable_config& o);
+
+ void swapHtoD();
int compare(const ResTable_config& o) const;
int compareLogical(const ResTable_config& o) const;
@@ -1416,10 +1384,6 @@ struct ResTable_config
bool isBetterThanBeforeLocale(const ResTable_config& o, const ResTable_config* requested) const;
String8 toString() const;
-
- private:
- void copyFromDtoH_slow(const ResTable_config& o);
- void swapHtoD_slow();
};
/**
diff --git a/libs/androidfw/include/androidfw/misc.h b/libs/androidfw/include/androidfw/misc.h
index d8ca64a174a2..c9ba8a01a5e9 100644
--- a/libs/androidfw/include/androidfw/misc.h
+++ b/libs/androidfw/include/androidfw/misc.h
@@ -15,7 +15,6 @@
*/
#pragma once
-#include <sys/stat.h>
#include <time.h>
//
@@ -65,15 +64,10 @@ ModDate getFileModDate(const char* fileName);
/* same, but also returns -1 if the file has already been deleted */
ModDate getFileModDate(int fd);
-// Extract the modification date from the stat structure.
-ModDate getModDate(const struct ::stat& st);
-
// Check if |path| or |fd| resides on a readonly filesystem.
bool isReadonlyFilesystem(const char* path);
bool isReadonlyFilesystem(int fd);
-bool isKnownWritablePath(const char* path);
-
} // namespace android
// Whoever uses getFileModDate() will need this as well
diff --git a/libs/androidfw/misc.cpp b/libs/androidfw/misc.cpp
index 26eb320805c9..32f3624a3aee 100644
--- a/libs/androidfw/misc.cpp
+++ b/libs/androidfw/misc.cpp
@@ -16,10 +16,10 @@
#define LOG_TAG "misc"
-#include "androidfw/misc.h"
-
-#include <errno.h>
-#include <sys/stat.h>
+//
+// Miscellaneous utility functions.
+//
+#include <androidfw/misc.h>
#include "android-base/logging.h"
@@ -28,7 +28,9 @@
#include <sys/vfs.h>
#endif // __linux__
-#include <array>
+#include <errno.h>
+#include <sys/stat.h>
+
#include <cstdio>
#include <cstring>
#include <tuple>
@@ -38,26 +40,28 @@ namespace android {
/*
* Get a file's type.
*/
-FileType getFileType(const char* fileName) {
- struct stat sb;
- if (stat(fileName, &sb) < 0) {
- if (errno == ENOENT || errno == ENOTDIR)
- return kFileTypeNonexistent;
- else {
- PLOG(ERROR) << "getFileType(): stat(" << fileName << ") failed";
- return kFileTypeUnknown;
- }
- } else {
- if (S_ISREG(sb.st_mode))
- return kFileTypeRegular;
- else if (S_ISDIR(sb.st_mode))
- return kFileTypeDirectory;
- else if (S_ISCHR(sb.st_mode))
- return kFileTypeCharDev;
- else if (S_ISBLK(sb.st_mode))
- return kFileTypeBlockDev;
- else if (S_ISFIFO(sb.st_mode))
- return kFileTypeFifo;
+FileType getFileType(const char* fileName)
+{
+ struct stat sb;
+
+ if (stat(fileName, &sb) < 0) {
+ if (errno == ENOENT || errno == ENOTDIR)
+ return kFileTypeNonexistent;
+ else {
+ PLOG(ERROR) << "getFileType(): stat(" << fileName << ") failed";
+ return kFileTypeUnknown;
+ }
+ } else {
+ if (S_ISREG(sb.st_mode))
+ return kFileTypeRegular;
+ else if (S_ISDIR(sb.st_mode))
+ return kFileTypeDirectory;
+ else if (S_ISCHR(sb.st_mode))
+ return kFileTypeCharDev;
+ else if (S_ISBLK(sb.st_mode))
+ return kFileTypeBlockDev;
+ else if (S_ISFIFO(sb.st_mode))
+ return kFileTypeFifo;
#if defined(S_ISLNK)
else if (S_ISLNK(sb.st_mode))
return kFileTypeSymlink;
@@ -71,7 +75,7 @@ FileType getFileType(const char* fileName) {
}
}
-ModDate getModDate(const struct stat& st) {
+static ModDate getModDate(const struct stat& st) {
#ifdef _WIN32
return st.st_mtime;
#elif defined(__APPLE__)
@@ -109,14 +113,8 @@ bool isReadonlyFilesystem(const char*) {
bool isReadonlyFilesystem(int) {
return false;
}
-bool isKnownWritablePath(const char*) {
- return false;
-}
#else // __linux__
bool isReadonlyFilesystem(const char* path) {
- if (isKnownWritablePath(path)) {
- return false;
- }
struct statfs sfs;
if (::statfs(path, &sfs)) {
PLOG(ERROR) << "isReadonlyFilesystem(): statfs(" << path << ") failed";
@@ -133,13 +131,6 @@ bool isReadonlyFilesystem(int fd) {
}
return (sfs.f_flags & ST_RDONLY) != 0;
}
-
-bool isKnownWritablePath(const char* path) {
- // We know that all paths in /data/ are writable.
- static constexpr char kRwPrefix[] = "/data/";
- return strncmp(kRwPrefix, path, std::size(kRwPrefix) - 1) == 0;
-}
-
#endif // __linux__
} // namespace android
diff --git a/libs/androidfw/tests/Idmap_test.cpp b/libs/androidfw/tests/Idmap_test.cpp
index 22b9e69500d9..cb2e56f5f5e4 100644
--- a/libs/androidfw/tests/Idmap_test.cpp
+++ b/libs/androidfw/tests/Idmap_test.cpp
@@ -218,11 +218,10 @@ TEST_F(IdmapTest, OverlayAssetsIsUpToDate) {
auto apk_assets = ApkAssets::LoadOverlay(temp_file.path);
ASSERT_NE(nullptr, apk_assets);
- ASSERT_TRUE(apk_assets->IsOverlay());
- ASSERT_EQ(UpToDate::True, apk_assets->IsUpToDate());
+ ASSERT_TRUE(apk_assets->IsUpToDate());
unlink(temp_file.path);
- ASSERT_EQ(UpToDate::False, apk_assets->IsUpToDate());
+ ASSERT_FALSE(apk_assets->IsUpToDate());
const auto sleep_duration =
std::chrono::nanoseconds(std::max(kModDateResolutionNs, 1'000'000ull));
@@ -231,27 +230,7 @@ TEST_F(IdmapTest, OverlayAssetsIsUpToDate) {
base::WriteStringToFile("hello", temp_file.path);
std::this_thread::sleep_for(sleep_duration);
- ASSERT_EQ(UpToDate::False, apk_assets->IsUpToDate());
-}
-
-TEST(IdmapTestUpToDate, Combine) {
- ASSERT_EQ(UpToDate::False, combine(UpToDate::False, [] {
- ADD_FAILURE(); // Shouldn't get called at all.
- return UpToDate::False;
- }));
-
- ASSERT_EQ(UpToDate::False, combine(UpToDate::True, [] { return UpToDate::False; }));
-
- ASSERT_EQ(UpToDate::True, combine(UpToDate::True, [] { return UpToDate::True; }));
- ASSERT_EQ(UpToDate::True, combine(UpToDate::True, [] { return UpToDate::Always; }));
- ASSERT_EQ(UpToDate::True, combine(UpToDate::Always, [] { return UpToDate::True; }));
-
- ASSERT_EQ(UpToDate::Always, combine(UpToDate::Always, [] { return UpToDate::Always; }));
-}
-
-TEST(IdmapTestUpToDate, FromBool) {
- ASSERT_EQ(UpToDate::False, fromBool(false));
- ASSERT_EQ(UpToDate::True, fromBool(true));
+ ASSERT_FALSE(apk_assets->IsUpToDate());
}
} // namespace
diff --git a/libs/appfunctions/api/current.txt b/libs/appfunctions/api/current.txt
index f8fb6b7207a0..139ccfd22b0e 100644
--- a/libs/appfunctions/api/current.txt
+++ b/libs/appfunctions/api/current.txt
@@ -36,8 +36,7 @@ package com.android.extensions.appfunctions {
public abstract class AppFunctionService extends android.app.Service {
ctor public AppFunctionService();
method @NonNull public final android.os.IBinder onBind(@Nullable android.content.Intent);
- method @MainThread public void onExecuteFunction(@NonNull com.android.extensions.appfunctions.ExecuteAppFunctionRequest, @NonNull String, @NonNull android.content.pm.SigningInfo, @NonNull android.os.CancellationSignal, @NonNull android.os.OutcomeReceiver<com.android.extensions.appfunctions.ExecuteAppFunctionResponse,com.android.extensions.appfunctions.AppFunctionException>);
- method @Deprecated @MainThread public abstract void onExecuteFunction(@NonNull com.android.extensions.appfunctions.ExecuteAppFunctionRequest, @NonNull String, @NonNull android.os.CancellationSignal, @NonNull android.os.OutcomeReceiver<com.android.extensions.appfunctions.ExecuteAppFunctionResponse,com.android.extensions.appfunctions.AppFunctionException>);
+ method @MainThread public abstract void onExecuteFunction(@NonNull com.android.extensions.appfunctions.ExecuteAppFunctionRequest, @NonNull String, @NonNull android.os.CancellationSignal, @NonNull android.os.OutcomeReceiver<com.android.extensions.appfunctions.ExecuteAppFunctionResponse,com.android.extensions.appfunctions.AppFunctionException>);
field @NonNull public static final String BIND_APP_FUNCTION_SERVICE = "android.permission.BIND_APP_FUNCTION_SERVICE";
field @NonNull public static final String SERVICE_INTERFACE = "android.app.appfunctions.AppFunctionService";
}
diff --git a/libs/appfunctions/java/com/android/extensions/appfunctions/AppFunctionService.java b/libs/appfunctions/java/com/android/extensions/appfunctions/AppFunctionService.java
index a09451ede4fc..81d9d81c4f58 100644
--- a/libs/appfunctions/java/com/android/extensions/appfunctions/AppFunctionService.java
+++ b/libs/appfunctions/java/com/android/extensions/appfunctions/AppFunctionService.java
@@ -25,7 +25,6 @@ import android.annotation.Nullable;
import android.annotation.SdkConstant;
import android.app.Service;
import android.content.Intent;
-import android.content.pm.SigningInfo;
import android.os.Binder;
import android.os.CancellationSignal;
import android.os.IBinder;
@@ -82,7 +81,6 @@ public abstract class AppFunctionService extends Service {
SidecarConverter.getSidecarExecuteAppFunctionRequest(
platformRequest),
callingPackage,
- callingPackageSigningInfo,
cancellationSignal,
new OutcomeReceiver<>() {
@Override
@@ -129,52 +127,10 @@ public abstract class AppFunctionService extends Service {
*
* @param request The function execution request.
* @param callingPackage The package name of the app that is requesting the execution.
- * @param callingPackageSigningInfo The signing information of the app that is requesting the
- * execution.
* @param cancellationSignal A signal to cancel the execution.
* @param callback A callback to report back the result or error.
*/
@MainThread
- public void onExecuteFunction(
- @NonNull ExecuteAppFunctionRequest request,
- @NonNull String callingPackage,
- @NonNull SigningInfo callingPackageSigningInfo,
- @NonNull CancellationSignal cancellationSignal,
- @NonNull OutcomeReceiver<ExecuteAppFunctionResponse, AppFunctionException> callback) {
- onExecuteFunction(request, callingPackage, cancellationSignal, callback);
- }
-
- /**
- * Called by the system to execute a specific app function.
- *
- * <p>This method is the entry point for handling all app function requests in an app. When the
- * system needs your AppFunctionService to perform a function, it will invoke this method.
- *
- * <p>Each function you've registered is identified by a unique identifier. This identifier
- * doesn't need to be globally unique, but it must be unique within your app. For example, a
- * function to order food could be identified as "orderFood". In most cases, this identifier is
- * automatically generated by the AppFunctions SDK.
- *
- * <p>You can determine which function to execute by calling {@link
- * ExecuteAppFunctionRequest#getFunctionIdentifier()}. This allows your service to route the
- * incoming request to the appropriate logic for handling the specific function.
- *
- * <p>This method is always triggered in the main thread. You should run heavy tasks on a worker
- * thread and dispatch the result with the given callback. You should always report back the
- * result using the callback, no matter if the execution was successful or not.
- *
- * <p>This method also accepts a {@link CancellationSignal} that the app should listen to cancel
- * the execution of function if requested by the system.
- *
- * @param request The function execution request.
- * @param callingPackage The package name of the app that is requesting the execution.
- * @param cancellationSignal A signal to cancel the execution.
- * @param callback A callback to report back the result or error.
- * @deprecated Use {@link #onExecuteFunction(ExecuteAppFunctionRequest, String, SigningInfo,
- * CancellationSignal, OutcomeReceiver)} instead.
- */
- @MainThread
- @Deprecated
public abstract void onExecuteFunction(
@NonNull ExecuteAppFunctionRequest request,
@NonNull String callingPackage,
diff --git a/libs/hwui/aconfig/hwui_flags.aconfig b/libs/hwui/aconfig/hwui_flags.aconfig
index 76ad2acccf89..5e71d3360f39 100644
--- a/libs/hwui/aconfig/hwui_flags.aconfig
+++ b/libs/hwui/aconfig/hwui_flags.aconfig
@@ -34,13 +34,6 @@ flag {
}
flag {
- name: "high_contrast_text_luminance"
- namespace: "accessibility"
- description: "Use luminance to determine how to make text more high contrast, instead of RGB heuristic"
- bug: "186567103"
-}
-
-flag {
name: "high_contrast_text_small_text_rect"
namespace: "accessibility"
description: "Draw a solid rectangle background behind text instead of a stroke outline"
diff --git a/libs/hwui/hwui/DrawTextFunctor.h b/libs/hwui/hwui/DrawTextFunctor.h
index e13e136550ca..e05c3d695463 100644
--- a/libs/hwui/hwui/DrawTextFunctor.h
+++ b/libs/hwui/hwui/DrawTextFunctor.h
@@ -34,9 +34,6 @@
namespace flags = com::android::graphics::hwui::flags;
#else
namespace flags {
-constexpr bool high_contrast_text_luminance() {
- return false;
-}
constexpr bool high_contrast_text_small_text_rect() {
return false;
}
@@ -114,15 +111,10 @@ public:
if (CC_UNLIKELY(canvas->isHighContrastText() && paint.getAlpha() != 0)) {
// high contrast draw path
int color = paint.getColor();
- bool darken;
- // This equation should match the one in core/java/android/text/Layout.java
- if (flags::high_contrast_text_luminance()) {
- uirenderer::Lab lab = uirenderer::sRGBToLab(color);
- darken = lab.L <= 50;
- } else {
- int channelSum = SkColorGetR(color) + SkColorGetG(color) + SkColorGetB(color);
- darken = channelSum < (128 * 3);
- }
+ // LINT.IfChange(hct_darken)
+ uirenderer::Lab lab = uirenderer::sRGBToLab(color);
+ bool darken = lab.L <= 50;
+ // LINT.ThenChange(/core/java/android/text/Layout.java:hct_darken)
// outline
gDrawTextBlobMode = DrawTextBlobMode::HctOutline;
diff --git a/libs/hwui/hwui/MinikinUtils.cpp b/libs/hwui/hwui/MinikinUtils.cpp
index 7b45070af312..290df997a8ed 100644
--- a/libs/hwui/hwui/MinikinUtils.cpp
+++ b/libs/hwui/hwui/MinikinUtils.cpp
@@ -36,7 +36,7 @@ minikin::MinikinPaint MinikinUtils::prepareMinikinPaint(const Paint* paint,
const Typeface* resolvedFace = Typeface::resolveDefault(typeface);
const SkFont& font = paint->getSkFont();
- minikin::MinikinPaint minikinPaint(resolvedFace->fFontCollection);
+ minikin::MinikinPaint minikinPaint(resolvedFace->getFontCollection());
/* Prepare minikin Paint */
minikinPaint.size =
font.isLinearMetrics() ? font.getSize() : static_cast<int>(font.getSize());
@@ -46,9 +46,9 @@ minikin::MinikinPaint MinikinUtils::prepareMinikinPaint(const Paint* paint,
minikinPaint.wordSpacing = paint->getWordSpacing();
minikinPaint.fontFlags = MinikinFontSkia::packFontFlags(font);
minikinPaint.localeListId = paint->getMinikinLocaleListId();
- minikinPaint.fontStyle = resolvedFace->fStyle;
+ minikinPaint.fontStyle = resolvedFace->getFontStyle();
minikinPaint.fontFeatureSettings = paint->getFontFeatureSettings();
- if (!resolvedFace->fIsVariationInstance) {
+ if (!resolvedFace->isVariationInstance()) {
// This is an optimization for direct private API use typically done by System UI.
// In the public API surface, if Typeface is already configured for variation instance
// (Target SDK <= 35) the font variation settings of Paint is not set.
@@ -132,7 +132,7 @@ minikin::MinikinExtent MinikinUtils::getFontExtent(const Paint* paint, minikin::
bool MinikinUtils::hasVariationSelector(const Typeface* typeface, uint32_t codepoint, uint32_t vs) {
const Typeface* resolvedFace = Typeface::resolveDefault(typeface);
- return resolvedFace->fFontCollection->hasVariationSelector(codepoint, vs);
+ return resolvedFace->getFontCollection()->hasVariationSelector(codepoint, vs);
}
float MinikinUtils::xOffsetForTextAlign(Paint* paint, const minikin::Layout& layout) {
diff --git a/libs/hwui/hwui/Typeface.cpp b/libs/hwui/hwui/Typeface.cpp
index 4dfe05377a48..a73aac632752 100644
--- a/libs/hwui/hwui/Typeface.cpp
+++ b/libs/hwui/hwui/Typeface.cpp
@@ -70,74 +70,45 @@ const Typeface* Typeface::resolveDefault(const Typeface* src) {
Typeface* Typeface::createRelative(Typeface* src, Typeface::Style style) {
const Typeface* resolvedFace = Typeface::resolveDefault(src);
- Typeface* result = new Typeface;
- if (result != nullptr) {
- result->fFontCollection = resolvedFace->fFontCollection;
- result->fBaseWeight = resolvedFace->fBaseWeight;
- result->fAPIStyle = style;
- result->fStyle = computeRelativeStyle(result->fBaseWeight, style);
- result->fIsVariationInstance = resolvedFace->fIsVariationInstance;
- }
- return result;
+ return new Typeface(resolvedFace->getFontCollection(),
+ computeRelativeStyle(resolvedFace->getBaseWeight(), style), style,
+ resolvedFace->getBaseWeight(), resolvedFace->isVariationInstance());
}
Typeface* Typeface::createAbsolute(Typeface* base, int weight, bool italic) {
const Typeface* resolvedFace = Typeface::resolveDefault(base);
- Typeface* result = new Typeface();
- if (result != nullptr) {
- result->fFontCollection = resolvedFace->fFontCollection;
- result->fBaseWeight = resolvedFace->fBaseWeight;
- result->fAPIStyle = computeAPIStyle(weight, italic);
- result->fStyle = computeMinikinStyle(weight, italic);
- result->fIsVariationInstance = resolvedFace->fIsVariationInstance;
- }
- return result;
+ return new Typeface(resolvedFace->getFontCollection(), computeMinikinStyle(weight, italic),
+ computeAPIStyle(weight, italic), resolvedFace->getBaseWeight(),
+ resolvedFace->isVariationInstance());
}
Typeface* Typeface::createFromTypefaceWithVariation(Typeface* src,
const minikin::VariationSettings& variations) {
const Typeface* resolvedFace = Typeface::resolveDefault(src);
- Typeface* result = new Typeface();
- if (result != nullptr) {
- result->fFontCollection =
- resolvedFace->fFontCollection->createCollectionWithVariation(variations);
- if (result->fFontCollection == nullptr) {
+ const std::shared_ptr<minikin::FontCollection>& fc =
+ resolvedFace->getFontCollection()->createCollectionWithVariation(variations);
+ return new Typeface(
// None of passed axes are supported by this collection.
// So we will reuse the same collection with incrementing reference count.
- result->fFontCollection = resolvedFace->fFontCollection;
- }
- // Do not update styles.
- // TODO: We may want to update base weight if the 'wght' is specified.
- result->fBaseWeight = resolvedFace->fBaseWeight;
- result->fAPIStyle = resolvedFace->fAPIStyle;
- result->fStyle = resolvedFace->fStyle;
- result->fIsVariationInstance = true;
- }
- return result;
+ fc ? fc : resolvedFace->getFontCollection(),
+ // Do not update styles.
+ // TODO: We may want to update base weight if the 'wght' is specified.
+ resolvedFace->fStyle, resolvedFace->getAPIStyle(), resolvedFace->getBaseWeight(), true);
}
Typeface* Typeface::createWithDifferentBaseWeight(Typeface* src, int weight) {
const Typeface* resolvedFace = Typeface::resolveDefault(src);
- Typeface* result = new Typeface;
- if (result != nullptr) {
- result->fFontCollection = resolvedFace->fFontCollection;
- result->fBaseWeight = weight;
- result->fAPIStyle = resolvedFace->fAPIStyle;
- result->fStyle = computeRelativeStyle(weight, result->fAPIStyle);
- result->fIsVariationInstance = resolvedFace->fIsVariationInstance;
- }
- return result;
+ return new Typeface(resolvedFace->getFontCollection(),
+ computeRelativeStyle(weight, resolvedFace->getAPIStyle()),
+ resolvedFace->getAPIStyle(), weight, resolvedFace->isVariationInstance());
}
Typeface* Typeface::createFromFamilies(std::vector<std::shared_ptr<minikin::FontFamily>>&& families,
int weight, int italic, const Typeface* fallback) {
- Typeface* result = new Typeface;
- if (fallback == nullptr) {
- result->fFontCollection = minikin::FontCollection::create(std::move(families));
- } else {
- result->fFontCollection =
- fallback->fFontCollection->createCollectionWithFamilies(std::move(families));
- }
+ const std::shared_ptr<minikin::FontCollection>& fc =
+ fallback ? fallback->getFontCollection()->createCollectionWithFamilies(
+ std::move(families))
+ : minikin::FontCollection::create(std::move(families));
if (weight == RESOLVE_BY_FONT_TABLE || italic == RESOLVE_BY_FONT_TABLE) {
int weightFromFont;
@@ -171,11 +142,8 @@ Typeface* Typeface::createFromFamilies(std::vector<std::shared_ptr<minikin::Font
weight = SkFontStyle::kNormal_Weight;
}
- result->fBaseWeight = weight;
- result->fAPIStyle = computeAPIStyle(weight, italic);
- result->fStyle = computeMinikinStyle(weight, italic);
- result->fIsVariationInstance = false;
- return result;
+ return new Typeface(fc, computeMinikinStyle(weight, italic), computeAPIStyle(weight, italic),
+ weight, false);
}
void Typeface::setDefault(const Typeface* face) {
@@ -205,11 +173,8 @@ void Typeface::setRobotoTypefaceForTest() {
std::shared_ptr<minikin::FontCollection> collection =
minikin::FontCollection::create(minikin::FontFamily::create(std::move(fonts)));
- Typeface* hwTypeface = new Typeface();
- hwTypeface->fFontCollection = collection;
- hwTypeface->fAPIStyle = Typeface::kNormal;
- hwTypeface->fBaseWeight = SkFontStyle::kNormal_Weight;
- hwTypeface->fStyle = minikin::FontStyle();
+ Typeface* hwTypeface = new Typeface(collection, minikin::FontStyle(), Typeface::kNormal,
+ SkFontStyle::kNormal_Weight, false);
Typeface::setDefault(hwTypeface);
#endif
diff --git a/libs/hwui/hwui/Typeface.h b/libs/hwui/hwui/Typeface.h
index 97d1bf4ef011..e8233a6bc6d8 100644
--- a/libs/hwui/hwui/Typeface.h
+++ b/libs/hwui/hwui/Typeface.h
@@ -32,21 +32,39 @@ constexpr int RESOLVE_BY_FONT_TABLE = -1;
struct ANDROID_API Typeface {
public:
- std::shared_ptr<minikin::FontCollection> fFontCollection;
+ enum Style : uint8_t { kNormal = 0, kBold = 0x01, kItalic = 0x02, kBoldItalic = 0x03 };
+ Typeface(const std::shared_ptr<minikin::FontCollection> fc, minikin::FontStyle style,
+ Style apiStyle, int baseWeight, bool isVariationInstance)
+ : fFontCollection(fc)
+ , fStyle(style)
+ , fAPIStyle(apiStyle)
+ , fBaseWeight(baseWeight)
+ , fIsVariationInstance(isVariationInstance) {}
+
+ const std::shared_ptr<minikin::FontCollection>& getFontCollection() const {
+ return fFontCollection;
+ }
// resolved style actually used for rendering
- minikin::FontStyle fStyle;
+ minikin::FontStyle getFontStyle() const { return fStyle; }
// style used in the API
- enum Style : uint8_t { kNormal = 0, kBold = 0x01, kItalic = 0x02, kBoldItalic = 0x03 };
- Style fAPIStyle;
+ Style getAPIStyle() const { return fAPIStyle; }
// base weight in CSS-style units, 1..1000
- int fBaseWeight;
+ int getBaseWeight() const { return fBaseWeight; }
// True if the Typeface is already created for variation settings.
- bool fIsVariationInstance;
+ bool isVariationInstance() const { return fIsVariationInstance; }
+private:
+ std::shared_ptr<minikin::FontCollection> fFontCollection;
+ minikin::FontStyle fStyle;
+ Style fAPIStyle;
+ int fBaseWeight;
+ bool fIsVariationInstance = false;
+
+public:
static const Typeface* resolveDefault(const Typeface* src);
// The following three functions create new Typeface from an existing Typeface with a different
diff --git a/libs/hwui/jni/Paint.cpp b/libs/hwui/jni/Paint.cpp
index 8d3a5eb2b4af..f6fdec1c82bc 100644
--- a/libs/hwui/jni/Paint.cpp
+++ b/libs/hwui/jni/Paint.cpp
@@ -609,7 +609,8 @@ namespace PaintGlue {
SkFont* font = &paint->getSkFont();
const Typeface* typeface = paint->getAndroidTypeface();
typeface = Typeface::resolveDefault(typeface);
- minikin::FakedFont baseFont = typeface->fFontCollection->baseFontFaked(typeface->fStyle);
+ minikin::FakedFont baseFont =
+ typeface->getFontCollection()->baseFontFaked(typeface->getFontStyle());
float saveSkewX = font->getSkewX();
bool savefakeBold = font->isEmbolden();
MinikinFontSkia::populateSkFont(font, baseFont.typeface().get(), baseFont.fakery);
@@ -641,7 +642,7 @@ namespace PaintGlue {
if (useLocale) {
minikin::MinikinPaint minikinPaint = MinikinUtils::prepareMinikinPaint(paint, typeface);
minikin::MinikinExtent extent =
- typeface->fFontCollection->getReferenceExtentForLocale(minikinPaint);
+ typeface->getFontCollection()->getReferenceExtentForLocale(minikinPaint);
metrics->fAscent = std::min(extent.ascent, metrics->fAscent);
metrics->fDescent = std::max(extent.descent, metrics->fDescent);
metrics->fTop = std::min(metrics->fAscent, metrics->fTop);
diff --git a/libs/hwui/jni/Typeface.cpp b/libs/hwui/jni/Typeface.cpp
index c5095c1a0704..63906de80745 100644
--- a/libs/hwui/jni/Typeface.cpp
+++ b/libs/hwui/jni/Typeface.cpp
@@ -99,17 +99,17 @@ static jlong Typeface_getReleaseFunc(CRITICAL_JNI_PARAMS) {
// CriticalNative
static jint Typeface_getStyle(CRITICAL_JNI_PARAMS_COMMA jlong faceHandle) {
- return toTypeface(faceHandle)->fAPIStyle;
+ return toTypeface(faceHandle)->getAPIStyle();
}
// CriticalNative
static jint Typeface_getWeight(CRITICAL_JNI_PARAMS_COMMA jlong faceHandle) {
- return toTypeface(faceHandle)->fStyle.weight();
+ return toTypeface(faceHandle)->getFontStyle().weight();
}
// Critical Native
static jboolean Typeface_isVariationInstance(CRITICAL_JNI_PARAMS_COMMA jlong faceHandle) {
- return toTypeface(faceHandle)->fIsVariationInstance;
+ return toTypeface(faceHandle)->isVariationInstance();
}
static jlong Typeface_createFromArray(JNIEnv *env, jobject, jlongArray familyArray,
@@ -128,18 +128,18 @@ static jlong Typeface_createFromArray(JNIEnv *env, jobject, jlongArray familyArr
// CriticalNative
static void Typeface_setDefault(CRITICAL_JNI_PARAMS_COMMA jlong faceHandle) {
Typeface::setDefault(toTypeface(faceHandle));
- minikin::SystemFonts::registerDefault(toTypeface(faceHandle)->fFontCollection);
+ minikin::SystemFonts::registerDefault(toTypeface(faceHandle)->getFontCollection());
}
static jobject Typeface_getSupportedAxes(JNIEnv *env, jobject, jlong faceHandle) {
Typeface* face = toTypeface(faceHandle);
- const size_t length = face->fFontCollection->getSupportedAxesCount();
+ const size_t length = face->getFontCollection()->getSupportedAxesCount();
if (length == 0) {
return nullptr;
}
std::vector<jint> tagVec(length);
for (size_t i = 0; i < length; i++) {
- tagVec[i] = face->fFontCollection->getSupportedAxisAt(i);
+ tagVec[i] = face->getFontCollection()->getSupportedAxisAt(i);
}
std::sort(tagVec.begin(), tagVec.end());
const jintArray result = env->NewIntArray(length);
@@ -150,7 +150,7 @@ static jobject Typeface_getSupportedAxes(JNIEnv *env, jobject, jlong faceHandle)
static void Typeface_registerGenericFamily(JNIEnv *env, jobject, jstring familyName, jlong ptr) {
ScopedUtfChars familyNameChars(env, familyName);
minikin::SystemFonts::registerFallback(familyNameChars.c_str(),
- toTypeface(ptr)->fFontCollection);
+ toTypeface(ptr)->getFontCollection());
}
#ifdef __ANDROID__
@@ -315,18 +315,19 @@ static jint Typeface_writeTypefaces(JNIEnv* env, jobject, jobject buffer, jint p
std::vector<std::shared_ptr<minikin::FontCollection>> fontCollections;
std::unordered_map<std::shared_ptr<minikin::FontCollection>, size_t> fcToIndex;
for (Typeface* typeface : typefaces) {
- bool inserted = fcToIndex.emplace(typeface->fFontCollection, fontCollections.size()).second;
+ bool inserted =
+ fcToIndex.emplace(typeface->getFontCollection(), fontCollections.size()).second;
if (inserted) {
- fontCollections.push_back(typeface->fFontCollection);
+ fontCollections.push_back(typeface->getFontCollection());
}
}
minikin::FontCollection::writeVector(&writer, fontCollections);
writer.write<uint32_t>(typefaces.size());
for (Typeface* typeface : typefaces) {
- writer.write<uint32_t>(fcToIndex.find(typeface->fFontCollection)->second);
- typeface->fStyle.writeTo(&writer);
- writer.write<Typeface::Style>(typeface->fAPIStyle);
- writer.write<int>(typeface->fBaseWeight);
+ writer.write<uint32_t>(fcToIndex.find(typeface->getFontCollection())->second);
+ typeface->getFontStyle().writeTo(&writer);
+ writer.write<Typeface::Style>(typeface->getAPIStyle());
+ writer.write<int>(typeface->getBaseWeight());
}
return static_cast<jint>(writer.size());
}
@@ -349,12 +350,10 @@ static jlongArray Typeface_readTypefaces(JNIEnv* env, jobject, jobject buffer, j
std::vector<jlong> faceHandles;
faceHandles.reserve(typefaceCount);
for (uint32_t i = 0; i < typefaceCount; i++) {
- Typeface* typeface = new Typeface;
- typeface->fFontCollection = fontCollections[reader.read<uint32_t>()];
- typeface->fStyle = minikin::FontStyle(&reader);
- typeface->fAPIStyle = reader.read<Typeface::Style>();
- typeface->fBaseWeight = reader.read<int>();
- typeface->fIsVariationInstance = false;
+ Typeface* typeface =
+ new Typeface(fontCollections[reader.read<uint32_t>()], minikin::FontStyle(&reader),
+ reader.read<Typeface::Style>(), reader.read<int>(),
+ false /* isVariationInstance */);
faceHandles.push_back(toJLong(typeface));
}
const jlongArray result = env->NewLongArray(typefaceCount);
@@ -382,7 +381,8 @@ static void Typeface_warmUpCache(JNIEnv* env, jobject, jstring jFilePath) {
// Critical Native
static void Typeface_addFontCollection(CRITICAL_JNI_PARAMS_COMMA jlong faceHandle) {
- std::shared_ptr<minikin::FontCollection> collection = toTypeface(faceHandle)->fFontCollection;
+ std::shared_ptr<minikin::FontCollection> collection =
+ toTypeface(faceHandle)->getFontCollection();
minikin::SystemFonts::addFontMap(std::move(collection));
}
diff --git a/libs/hwui/jni/text/TextShaper.cpp b/libs/hwui/jni/text/TextShaper.cpp
index d1782b285b34..7a4ae8330de8 100644
--- a/libs/hwui/jni/text/TextShaper.cpp
+++ b/libs/hwui/jni/text/TextShaper.cpp
@@ -104,7 +104,7 @@ static jlong shapeTextRun(const uint16_t* text, int textSize, int start, int cou
} else {
fontId = fonts.size(); // This is new to us. Create new one.
std::shared_ptr<minikin::Font> font;
- if (resolvedFace->fIsVariationInstance) {
+ if (resolvedFace->isVariationInstance()) {
// The optimization for target SDK 35 or before because the variation instance
// is already created and no runtime variation resolution happens on such
// environment.
diff --git a/libs/hwui/renderthread/VulkanManager.cpp b/libs/hwui/renderthread/VulkanManager.cpp
index 6571d92aeafa..a67aea466c1c 100644
--- a/libs/hwui/renderthread/VulkanManager.cpp
+++ b/libs/hwui/renderthread/VulkanManager.cpp
@@ -729,7 +729,7 @@ VulkanManager::VkDrawResult VulkanManager::finishFrame(SkSurface* surface) {
VkSemaphore semaphore;
VkResult err = mCreateSemaphore(mDevice, &semaphoreInfo, nullptr, &semaphore);
ALOGE_IF(VK_SUCCESS != err,
- "VulkanManager::makeSwapSemaphore(): Failed to create semaphore");
+ "VulkanManager::finishFrame(): Failed to create semaphore");
if (err == VK_SUCCESS) {
sharedSemaphore = sp<SharedSemaphoreInfo>::make(mDestroySemaphore, mDevice, semaphore);
@@ -777,7 +777,7 @@ VulkanManager::VkDrawResult VulkanManager::finishFrame(SkSurface* surface) {
int fenceFd = -1;
VkResult err = mGetSemaphoreFdKHR(mDevice, &getFdInfo, &fenceFd);
- ALOGE_IF(VK_SUCCESS != err, "VulkanManager::swapBuffers(): Failed to get semaphore Fd");
+ ALOGE_IF(VK_SUCCESS != err, "VulkanManager::finishFrame(): Failed to get semaphore Fd");
drawResult.presentFence.reset(fenceFd);
} else {
ALOGE("VulkanManager::finishFrame(): Semaphore submission failed");
diff --git a/libs/hwui/tests/common/TestUtils.cpp b/libs/hwui/tests/common/TestUtils.cpp
index 93118aeafaaf..b51414fd3c02 100644
--- a/libs/hwui/tests/common/TestUtils.cpp
+++ b/libs/hwui/tests/common/TestUtils.cpp
@@ -183,8 +183,11 @@ SkRect TestUtils::getLocalClipBounds(const SkCanvas* canvas) {
}
SkFont TestUtils::defaultFont() {
- const std::shared_ptr<minikin::MinikinFont>& minikinFont =
- Typeface::resolveDefault(nullptr)->fFontCollection->getFamilyAt(0)->getFont(0)->baseTypeface();
+ const std::shared_ptr<minikin::MinikinFont>& minikinFont = Typeface::resolveDefault(nullptr)
+ ->getFontCollection()
+ ->getFamilyAt(0)
+ ->getFont(0)
+ ->baseTypeface();
SkTypeface* skTypeface = reinterpret_cast<const MinikinFontSkia*>(minikinFont.get())->GetSkTypeface();
LOG_ALWAYS_FATAL_IF(skTypeface == nullptr);
return SkFont(sk_ref_sp(skTypeface));
diff --git a/libs/hwui/tests/unit/TypefaceTests.cpp b/libs/hwui/tests/unit/TypefaceTests.cpp
index c71c4d243a8b..7bcd937397b0 100644
--- a/libs/hwui/tests/unit/TypefaceTests.cpp
+++ b/libs/hwui/tests/unit/TypefaceTests.cpp
@@ -90,40 +90,40 @@ TEST(TypefaceTest, resolveDefault_and_setDefaultTest) {
TEST(TypefaceTest, createWithDifferentBaseWeight) {
std::unique_ptr<Typeface> bold(Typeface::createWithDifferentBaseWeight(nullptr, 700));
- EXPECT_EQ(700, bold->fStyle.weight());
- EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->fStyle.slant());
- EXPECT_EQ(Typeface::kNormal, bold->fAPIStyle);
+ EXPECT_EQ(700, bold->getFontStyle().weight());
+ EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->getFontStyle().slant());
+ EXPECT_EQ(Typeface::kNormal, bold->getAPIStyle());
std::unique_ptr<Typeface> light(Typeface::createWithDifferentBaseWeight(nullptr, 300));
- EXPECT_EQ(300, light->fStyle.weight());
- EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, light->fStyle.slant());
- EXPECT_EQ(Typeface::kNormal, light->fAPIStyle);
+ EXPECT_EQ(300, light->getFontStyle().weight());
+ EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, light->getFontStyle().slant());
+ EXPECT_EQ(Typeface::kNormal, light->getAPIStyle());
}
TEST(TypefaceTest, createRelativeTest_fromRegular) {
// In Java, Typeface.create(Typeface.DEFAULT, Typeface.NORMAL);
std::unique_ptr<Typeface> normal(Typeface::createRelative(nullptr, Typeface::kNormal));
- EXPECT_EQ(400, normal->fStyle.weight());
- EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, normal->fStyle.slant());
- EXPECT_EQ(Typeface::kNormal, normal->fAPIStyle);
+ EXPECT_EQ(400, normal->getFontStyle().weight());
+ EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, normal->getFontStyle().slant());
+ EXPECT_EQ(Typeface::kNormal, normal->getAPIStyle());
// In Java, Typeface.create(Typeface.DEFAULT, Typeface.BOLD);
std::unique_ptr<Typeface> bold(Typeface::createRelative(nullptr, Typeface::kBold));
- EXPECT_EQ(700, bold->fStyle.weight());
- EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->fStyle.slant());
- EXPECT_EQ(Typeface::kBold, bold->fAPIStyle);
+ EXPECT_EQ(700, bold->getFontStyle().weight());
+ EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->getFontStyle().slant());
+ EXPECT_EQ(Typeface::kBold, bold->getAPIStyle());
// In Java, Typeface.create(Typeface.DEFAULT, Typeface.ITALIC);
std::unique_ptr<Typeface> italic(Typeface::createRelative(nullptr, Typeface::kItalic));
- EXPECT_EQ(400, italic->fStyle.weight());
- EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, italic->fStyle.slant());
- EXPECT_EQ(Typeface::kItalic, italic->fAPIStyle);
+ EXPECT_EQ(400, italic->getFontStyle().weight());
+ EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, italic->getFontStyle().slant());
+ EXPECT_EQ(Typeface::kItalic, italic->getAPIStyle());
// In Java, Typeface.create(Typeface.DEFAULT, Typeface.BOLD_ITALIC);
std::unique_ptr<Typeface> boldItalic(Typeface::createRelative(nullptr, Typeface::kBoldItalic));
- EXPECT_EQ(700, boldItalic->fStyle.weight());
- EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, boldItalic->fStyle.slant());
- EXPECT_EQ(Typeface::kBoldItalic, boldItalic->fAPIStyle);
+ EXPECT_EQ(700, boldItalic->getFontStyle().weight());
+ EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, boldItalic->getFontStyle().slant());
+ EXPECT_EQ(Typeface::kBoldItalic, boldItalic->getAPIStyle());
}
TEST(TypefaceTest, createRelativeTest_BoldBase) {
@@ -132,31 +132,31 @@ TEST(TypefaceTest, createRelativeTest_BoldBase) {
// In Java, Typeface.create(Typeface.create("sans-serif-bold"),
// Typeface.NORMAL);
std::unique_ptr<Typeface> normal(Typeface::createRelative(base.get(), Typeface::kNormal));
- EXPECT_EQ(700, normal->fStyle.weight());
- EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, normal->fStyle.slant());
- EXPECT_EQ(Typeface::kNormal, normal->fAPIStyle);
+ EXPECT_EQ(700, normal->getFontStyle().weight());
+ EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, normal->getFontStyle().slant());
+ EXPECT_EQ(Typeface::kNormal, normal->getAPIStyle());
// In Java, Typeface.create(Typeface.create("sans-serif-bold"),
// Typeface.BOLD);
std::unique_ptr<Typeface> bold(Typeface::createRelative(base.get(), Typeface::kBold));
- EXPECT_EQ(1000, bold->fStyle.weight());
- EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->fStyle.slant());
- EXPECT_EQ(Typeface::kBold, bold->fAPIStyle);
+ EXPECT_EQ(1000, bold->getFontStyle().weight());
+ EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->getFontStyle().slant());
+ EXPECT_EQ(Typeface::kBold, bold->getAPIStyle());
// In Java, Typeface.create(Typeface.create("sans-serif-bold"),
// Typeface.ITALIC);
std::unique_ptr<Typeface> italic(Typeface::createRelative(base.get(), Typeface::kItalic));
- EXPECT_EQ(700, italic->fStyle.weight());
- EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, italic->fStyle.slant());
- EXPECT_EQ(Typeface::kItalic, italic->fAPIStyle);
+ EXPECT_EQ(700, italic->getFontStyle().weight());
+ EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, italic->getFontStyle().slant());
+ EXPECT_EQ(Typeface::kItalic, italic->getAPIStyle());
// In Java, Typeface.create(Typeface.create("sans-serif-bold"),
// Typeface.BOLD_ITALIC);
std::unique_ptr<Typeface> boldItalic(
Typeface::createRelative(base.get(), Typeface::kBoldItalic));
- EXPECT_EQ(1000, boldItalic->fStyle.weight());
- EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, boldItalic->fStyle.slant());
- EXPECT_EQ(Typeface::kBoldItalic, boldItalic->fAPIStyle);
+ EXPECT_EQ(1000, boldItalic->getFontStyle().weight());
+ EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, boldItalic->getFontStyle().slant());
+ EXPECT_EQ(Typeface::kBoldItalic, boldItalic->getAPIStyle());
}
TEST(TypefaceTest, createRelativeTest_LightBase) {
@@ -165,31 +165,31 @@ TEST(TypefaceTest, createRelativeTest_LightBase) {
// In Java, Typeface.create(Typeface.create("sans-serif-light"),
// Typeface.NORMAL);
std::unique_ptr<Typeface> normal(Typeface::createRelative(base.get(), Typeface::kNormal));
- EXPECT_EQ(300, normal->fStyle.weight());
- EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, normal->fStyle.slant());
- EXPECT_EQ(Typeface::kNormal, normal->fAPIStyle);
+ EXPECT_EQ(300, normal->getFontStyle().weight());
+ EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, normal->getFontStyle().slant());
+ EXPECT_EQ(Typeface::kNormal, normal->getAPIStyle());
// In Java, Typeface.create(Typeface.create("sans-serif-light"),
// Typeface.BOLD);
std::unique_ptr<Typeface> bold(Typeface::createRelative(base.get(), Typeface::kBold));
- EXPECT_EQ(600, bold->fStyle.weight());
- EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->fStyle.slant());
- EXPECT_EQ(Typeface::kBold, bold->fAPIStyle);
+ EXPECT_EQ(600, bold->getFontStyle().weight());
+ EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->getFontStyle().slant());
+ EXPECT_EQ(Typeface::kBold, bold->getAPIStyle());
// In Java, Typeface.create(Typeface.create("sans-serif-light"),
// Typeface.ITLIC);
std::unique_ptr<Typeface> italic(Typeface::createRelative(base.get(), Typeface::kItalic));
- EXPECT_EQ(300, italic->fStyle.weight());
- EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, italic->fStyle.slant());
- EXPECT_EQ(Typeface::kItalic, italic->fAPIStyle);
+ EXPECT_EQ(300, italic->getFontStyle().weight());
+ EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, italic->getFontStyle().slant());
+ EXPECT_EQ(Typeface::kItalic, italic->getAPIStyle());
// In Java, Typeface.create(Typeface.create("sans-serif-light"),
// Typeface.BOLD_ITALIC);
std::unique_ptr<Typeface> boldItalic(
Typeface::createRelative(base.get(), Typeface::kBoldItalic));
- EXPECT_EQ(600, boldItalic->fStyle.weight());
- EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, boldItalic->fStyle.slant());
- EXPECT_EQ(Typeface::kBoldItalic, boldItalic->fAPIStyle);
+ EXPECT_EQ(600, boldItalic->getFontStyle().weight());
+ EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, boldItalic->getFontStyle().slant());
+ EXPECT_EQ(Typeface::kBoldItalic, boldItalic->getAPIStyle());
}
TEST(TypefaceTest, createRelativeTest_fromBoldStyled) {
@@ -198,32 +198,32 @@ TEST(TypefaceTest, createRelativeTest_fromBoldStyled) {
// In Java, Typeface.create(Typeface.create(Typeface.DEFAULT, Typeface.BOLD),
// Typeface.NORMAL);
std::unique_ptr<Typeface> normal(Typeface::createRelative(base.get(), Typeface::kNormal));
- EXPECT_EQ(400, normal->fStyle.weight());
- EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, normal->fStyle.slant());
- EXPECT_EQ(Typeface::kNormal, normal->fAPIStyle);
+ EXPECT_EQ(400, normal->getFontStyle().weight());
+ EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, normal->getFontStyle().slant());
+ EXPECT_EQ(Typeface::kNormal, normal->getAPIStyle());
// In Java Typeface.create(Typeface.create(Typeface.DEFAULT, Typeface.BOLD),
// Typeface.BOLD);
std::unique_ptr<Typeface> bold(Typeface::createRelative(base.get(), Typeface::kBold));
- EXPECT_EQ(700, bold->fStyle.weight());
- EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->fStyle.slant());
- EXPECT_EQ(Typeface::kBold, bold->fAPIStyle);
+ EXPECT_EQ(700, bold->getFontStyle().weight());
+ EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->getFontStyle().slant());
+ EXPECT_EQ(Typeface::kBold, bold->getAPIStyle());
// In Java, Typeface.create(Typeface.create(Typeface.DEFAULT, Typeface.BOLD),
// Typeface.ITALIC);
std::unique_ptr<Typeface> italic(Typeface::createRelative(base.get(), Typeface::kItalic));
- EXPECT_EQ(400, normal->fStyle.weight());
- EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, italic->fStyle.slant());
- EXPECT_EQ(Typeface::kItalic, italic->fAPIStyle);
+ EXPECT_EQ(400, normal->getFontStyle().weight());
+ EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, italic->getFontStyle().slant());
+ EXPECT_EQ(Typeface::kItalic, italic->getAPIStyle());
// In Java,
// Typeface.create(Typeface.create(Typeface.DEFAULT, Typeface.BOLD),
// Typeface.BOLD_ITALIC);
std::unique_ptr<Typeface> boldItalic(
Typeface::createRelative(base.get(), Typeface::kBoldItalic));
- EXPECT_EQ(700, boldItalic->fStyle.weight());
- EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, boldItalic->fStyle.slant());
- EXPECT_EQ(Typeface::kBoldItalic, boldItalic->fAPIStyle);
+ EXPECT_EQ(700, boldItalic->getFontStyle().weight());
+ EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, boldItalic->getFontStyle().slant());
+ EXPECT_EQ(Typeface::kBoldItalic, boldItalic->getAPIStyle());
}
TEST(TypefaceTest, createRelativeTest_fromItalicStyled) {
@@ -233,33 +233,33 @@ TEST(TypefaceTest, createRelativeTest_fromItalicStyled) {
// Typeface.create(Typeface.create(Typeface.DEFAULT, Typeface.ITALIC),
// Typeface.NORMAL);
std::unique_ptr<Typeface> normal(Typeface::createRelative(base.get(), Typeface::kNormal));
- EXPECT_EQ(400, normal->fStyle.weight());
- EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, normal->fStyle.slant());
- EXPECT_EQ(Typeface::kNormal, normal->fAPIStyle);
+ EXPECT_EQ(400, normal->getFontStyle().weight());
+ EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, normal->getFontStyle().slant());
+ EXPECT_EQ(Typeface::kNormal, normal->getAPIStyle());
// In Java, Typeface.create(Typeface.create(Typeface.DEFAULT,
// Typeface.ITALIC), Typeface.BOLD);
std::unique_ptr<Typeface> bold(Typeface::createRelative(base.get(), Typeface::kBold));
- EXPECT_EQ(700, bold->fStyle.weight());
- EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->fStyle.slant());
- EXPECT_EQ(Typeface::kBold, bold->fAPIStyle);
+ EXPECT_EQ(700, bold->getFontStyle().weight());
+ EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->getFontStyle().slant());
+ EXPECT_EQ(Typeface::kBold, bold->getAPIStyle());
// In Java,
// Typeface.create(Typeface.create(Typeface.DEFAULT, Typeface.ITALIC),
// Typeface.ITALIC);
std::unique_ptr<Typeface> italic(Typeface::createRelative(base.get(), Typeface::kItalic));
- EXPECT_EQ(400, italic->fStyle.weight());
- EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, italic->fStyle.slant());
- EXPECT_EQ(Typeface::kItalic, italic->fAPIStyle);
+ EXPECT_EQ(400, italic->getFontStyle().weight());
+ EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, italic->getFontStyle().slant());
+ EXPECT_EQ(Typeface::kItalic, italic->getAPIStyle());
// In Java,
// Typeface.create(Typeface.create(Typeface.DEFAULT, Typeface.ITALIC),
// Typeface.BOLD_ITALIC);
std::unique_ptr<Typeface> boldItalic(
Typeface::createRelative(base.get(), Typeface::kBoldItalic));
- EXPECT_EQ(700, boldItalic->fStyle.weight());
- EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, boldItalic->fStyle.slant());
- EXPECT_EQ(Typeface::kBoldItalic, boldItalic->fAPIStyle);
+ EXPECT_EQ(700, boldItalic->getFontStyle().weight());
+ EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, boldItalic->getFontStyle().slant());
+ EXPECT_EQ(Typeface::kBoldItalic, boldItalic->getAPIStyle());
}
TEST(TypefaceTest, createRelativeTest_fromSpecifiedStyled) {
@@ -270,27 +270,27 @@ TEST(TypefaceTest, createRelativeTest_fromSpecifiedStyled) {
// .setWeight(700).setItalic(false).build();
// Typeface.create(typeface, Typeface.NORMAL);
std::unique_ptr<Typeface> normal(Typeface::createRelative(base.get(), Typeface::kNormal));
- EXPECT_EQ(400, normal->fStyle.weight());
- EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, normal->fStyle.slant());
- EXPECT_EQ(Typeface::kNormal, normal->fAPIStyle);
+ EXPECT_EQ(400, normal->getFontStyle().weight());
+ EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, normal->getFontStyle().slant());
+ EXPECT_EQ(Typeface::kNormal, normal->getAPIStyle());
// In Java,
// Typeface typeface = new Typeface.Builder(invalid).setFallback("sans-serif")
// .setWeight(700).setItalic(false).build();
// Typeface.create(typeface, Typeface.BOLD);
std::unique_ptr<Typeface> bold(Typeface::createRelative(base.get(), Typeface::kBold));
- EXPECT_EQ(700, bold->fStyle.weight());
- EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->fStyle.slant());
- EXPECT_EQ(Typeface::kBold, bold->fAPIStyle);
+ EXPECT_EQ(700, bold->getFontStyle().weight());
+ EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->getFontStyle().slant());
+ EXPECT_EQ(Typeface::kBold, bold->getAPIStyle());
// In Java,
// Typeface typeface = new Typeface.Builder(invalid).setFallback("sans-serif")
// .setWeight(700).setItalic(false).build();
// Typeface.create(typeface, Typeface.ITALIC);
std::unique_ptr<Typeface> italic(Typeface::createRelative(base.get(), Typeface::kItalic));
- EXPECT_EQ(400, italic->fStyle.weight());
- EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, italic->fStyle.slant());
- EXPECT_EQ(Typeface::kItalic, italic->fAPIStyle);
+ EXPECT_EQ(400, italic->getFontStyle().weight());
+ EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, italic->getFontStyle().slant());
+ EXPECT_EQ(Typeface::kItalic, italic->getAPIStyle());
// In Java,
// Typeface typeface = new Typeface.Builder(invalid).setFallback("sans-serif")
@@ -298,9 +298,9 @@ TEST(TypefaceTest, createRelativeTest_fromSpecifiedStyled) {
// Typeface.create(typeface, Typeface.BOLD_ITALIC);
std::unique_ptr<Typeface> boldItalic(
Typeface::createRelative(base.get(), Typeface::kBoldItalic));
- EXPECT_EQ(700, boldItalic->fStyle.weight());
- EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, boldItalic->fStyle.slant());
- EXPECT_EQ(Typeface::kBoldItalic, boldItalic->fAPIStyle);
+ EXPECT_EQ(700, boldItalic->getFontStyle().weight());
+ EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, boldItalic->getFontStyle().slant());
+ EXPECT_EQ(Typeface::kBoldItalic, boldItalic->getAPIStyle());
}
TEST(TypefaceTest, createAbsolute) {
@@ -309,45 +309,45 @@ TEST(TypefaceTest, createAbsolute) {
// Typeface.Builder(invalid).setFallback("sans-serif").setWeight(400).setItalic(false)
// .build();
std::unique_ptr<Typeface> regular(Typeface::createAbsolute(nullptr, 400, false));
- EXPECT_EQ(400, regular->fStyle.weight());
- EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, regular->fStyle.slant());
- EXPECT_EQ(Typeface::kNormal, regular->fAPIStyle);
+ EXPECT_EQ(400, regular->getFontStyle().weight());
+ EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, regular->getFontStyle().slant());
+ EXPECT_EQ(Typeface::kNormal, regular->getAPIStyle());
// In Java,
// new
// Typeface.Builder(invalid).setFallback("sans-serif").setWeight(700).setItalic(false)
// .build();
std::unique_ptr<Typeface> bold(Typeface::createAbsolute(nullptr, 700, false));
- EXPECT_EQ(700, bold->fStyle.weight());
- EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->fStyle.slant());
- EXPECT_EQ(Typeface::kBold, bold->fAPIStyle);
+ EXPECT_EQ(700, bold->getFontStyle().weight());
+ EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->getFontStyle().slant());
+ EXPECT_EQ(Typeface::kBold, bold->getAPIStyle());
// In Java,
// new
// Typeface.Builder(invalid).setFallback("sans-serif").setWeight(400).setItalic(true)
// .build();
std::unique_ptr<Typeface> italic(Typeface::createAbsolute(nullptr, 400, true));
- EXPECT_EQ(400, italic->fStyle.weight());
- EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, italic->fStyle.slant());
- EXPECT_EQ(Typeface::kItalic, italic->fAPIStyle);
+ EXPECT_EQ(400, italic->getFontStyle().weight());
+ EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, italic->getFontStyle().slant());
+ EXPECT_EQ(Typeface::kItalic, italic->getAPIStyle());
// In Java,
// new
// Typeface.Builder(invalid).setFallback("sans-serif").setWeight(700).setItalic(true)
// .build();
std::unique_ptr<Typeface> boldItalic(Typeface::createAbsolute(nullptr, 700, true));
- EXPECT_EQ(700, boldItalic->fStyle.weight());
- EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, boldItalic->fStyle.slant());
- EXPECT_EQ(Typeface::kBoldItalic, boldItalic->fAPIStyle);
+ EXPECT_EQ(700, boldItalic->getFontStyle().weight());
+ EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, boldItalic->getFontStyle().slant());
+ EXPECT_EQ(Typeface::kBoldItalic, boldItalic->getAPIStyle());
// In Java,
// new
// Typeface.Builder(invalid).setFallback("sans-serif").setWeight(1100).setItalic(true)
// .build();
std::unique_ptr<Typeface> over1000(Typeface::createAbsolute(nullptr, 1100, false));
- EXPECT_EQ(1000, over1000->fStyle.weight());
- EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, over1000->fStyle.slant());
- EXPECT_EQ(Typeface::kBold, over1000->fAPIStyle);
+ EXPECT_EQ(1000, over1000->getFontStyle().weight());
+ EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, over1000->getFontStyle().slant());
+ EXPECT_EQ(Typeface::kBold, over1000->getAPIStyle());
}
TEST(TypefaceTest, createFromFamilies_Single) {
@@ -355,43 +355,43 @@ TEST(TypefaceTest, createFromFamilies_Single) {
// Typeface.Builder("Roboto-Regular.ttf").setWeight(400).setItalic(false).build();
std::unique_ptr<Typeface> regular(Typeface::createFromFamilies(
makeSingleFamlyVector(kRobotoVariable), 400, false, nullptr /* fallback */));
- EXPECT_EQ(400, regular->fStyle.weight());
- EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, regular->fStyle.slant());
- EXPECT_EQ(Typeface::kNormal, regular->fAPIStyle);
+ EXPECT_EQ(400, regular->getFontStyle().weight());
+ EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, regular->getFontStyle().slant());
+ EXPECT_EQ(Typeface::kNormal, regular->getAPIStyle());
// In Java, new
// Typeface.Builder("Roboto-Regular.ttf").setWeight(700).setItalic(false).build();
std::unique_ptr<Typeface> bold(Typeface::createFromFamilies(
makeSingleFamlyVector(kRobotoVariable), 700, false, nullptr /* fallback */));
- EXPECT_EQ(700, bold->fStyle.weight());
- EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->fStyle.slant());
- EXPECT_EQ(Typeface::kBold, bold->fAPIStyle);
+ EXPECT_EQ(700, bold->getFontStyle().weight());
+ EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->getFontStyle().slant());
+ EXPECT_EQ(Typeface::kBold, bold->getAPIStyle());
// In Java, new
// Typeface.Builder("Roboto-Regular.ttf").setWeight(400).setItalic(true).build();
std::unique_ptr<Typeface> italic(Typeface::createFromFamilies(
makeSingleFamlyVector(kRobotoVariable), 400, true, nullptr /* fallback */));
- EXPECT_EQ(400, italic->fStyle.weight());
- EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, italic->fStyle.slant());
- EXPECT_EQ(Typeface::kItalic, italic->fAPIStyle);
+ EXPECT_EQ(400, italic->getFontStyle().weight());
+ EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, italic->getFontStyle().slant());
+ EXPECT_EQ(Typeface::kItalic, italic->getAPIStyle());
// In Java,
// new
// Typeface.Builder("Roboto-Regular.ttf").setWeight(700).setItalic(true).build();
std::unique_ptr<Typeface> boldItalic(Typeface::createFromFamilies(
makeSingleFamlyVector(kRobotoVariable), 700, true, nullptr /* fallback */));
- EXPECT_EQ(700, boldItalic->fStyle.weight());
- EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, boldItalic->fStyle.slant());
- EXPECT_EQ(Typeface::kItalic, italic->fAPIStyle);
+ EXPECT_EQ(700, boldItalic->getFontStyle().weight());
+ EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, boldItalic->getFontStyle().slant());
+ EXPECT_EQ(Typeface::kItalic, italic->getAPIStyle());
// In Java,
// new
// Typeface.Builder("Roboto-Regular.ttf").setWeight(1100).setItalic(false).build();
std::unique_ptr<Typeface> over1000(Typeface::createFromFamilies(
makeSingleFamlyVector(kRobotoVariable), 1100, false, nullptr /* fallback */));
- EXPECT_EQ(1000, over1000->fStyle.weight());
- EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, over1000->fStyle.slant());
- EXPECT_EQ(Typeface::kBold, over1000->fAPIStyle);
+ EXPECT_EQ(1000, over1000->getFontStyle().weight());
+ EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, over1000->getFontStyle().slant());
+ EXPECT_EQ(Typeface::kBold, over1000->getAPIStyle());
}
TEST(TypefaceTest, createFromFamilies_Single_resolveByTable) {
@@ -399,33 +399,33 @@ TEST(TypefaceTest, createFromFamilies_Single_resolveByTable) {
std::unique_ptr<Typeface> regular(
Typeface::createFromFamilies(makeSingleFamlyVector(kRegularFont), RESOLVE_BY_FONT_TABLE,
RESOLVE_BY_FONT_TABLE, nullptr /* fallback */));
- EXPECT_EQ(400, regular->fStyle.weight());
- EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, regular->fStyle.slant());
- EXPECT_EQ(Typeface::kNormal, regular->fAPIStyle);
+ EXPECT_EQ(400, regular->getFontStyle().weight());
+ EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, regular->getFontStyle().slant());
+ EXPECT_EQ(Typeface::kNormal, regular->getAPIStyle());
// In Java, new Typeface.Builder("Family-Bold.ttf").build();
std::unique_ptr<Typeface> bold(
Typeface::createFromFamilies(makeSingleFamlyVector(kBoldFont), RESOLVE_BY_FONT_TABLE,
RESOLVE_BY_FONT_TABLE, nullptr /* fallback */));
- EXPECT_EQ(700, bold->fStyle.weight());
- EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->fStyle.slant());
- EXPECT_EQ(Typeface::kBold, bold->fAPIStyle);
+ EXPECT_EQ(700, bold->getFontStyle().weight());
+ EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->getFontStyle().slant());
+ EXPECT_EQ(Typeface::kBold, bold->getAPIStyle());
// In Java, new Typeface.Builder("Family-Italic.ttf").build();
std::unique_ptr<Typeface> italic(
Typeface::createFromFamilies(makeSingleFamlyVector(kItalicFont), RESOLVE_BY_FONT_TABLE,
RESOLVE_BY_FONT_TABLE, nullptr /* fallback */));
- EXPECT_EQ(400, italic->fStyle.weight());
- EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, italic->fStyle.slant());
- EXPECT_EQ(Typeface::kItalic, italic->fAPIStyle);
+ EXPECT_EQ(400, italic->getFontStyle().weight());
+ EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, italic->getFontStyle().slant());
+ EXPECT_EQ(Typeface::kItalic, italic->getAPIStyle());
// In Java, new Typeface.Builder("Family-BoldItalic.ttf").build();
std::unique_ptr<Typeface> boldItalic(Typeface::createFromFamilies(
makeSingleFamlyVector(kBoldItalicFont), RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE,
nullptr /* fallback */));
- EXPECT_EQ(700, boldItalic->fStyle.weight());
- EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, boldItalic->fStyle.slant());
- EXPECT_EQ(Typeface::kItalic, italic->fAPIStyle);
+ EXPECT_EQ(700, boldItalic->getFontStyle().weight());
+ EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, boldItalic->getFontStyle().slant());
+ EXPECT_EQ(Typeface::kItalic, italic->getAPIStyle());
}
TEST(TypefaceTest, createFromFamilies_Family) {
@@ -435,8 +435,8 @@ TEST(TypefaceTest, createFromFamilies_Family) {
std::unique_ptr<Typeface> typeface(
Typeface::createFromFamilies(std::move(families), RESOLVE_BY_FONT_TABLE,
RESOLVE_BY_FONT_TABLE, nullptr /* fallback */));
- EXPECT_EQ(400, typeface->fStyle.weight());
- EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, typeface->fStyle.slant());
+ EXPECT_EQ(400, typeface->getFontStyle().weight());
+ EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, typeface->getFontStyle().slant());
}
TEST(TypefaceTest, createFromFamilies_Family_withoutRegular) {
@@ -445,8 +445,8 @@ TEST(TypefaceTest, createFromFamilies_Family_withoutRegular) {
std::unique_ptr<Typeface> typeface(
Typeface::createFromFamilies(std::move(families), RESOLVE_BY_FONT_TABLE,
RESOLVE_BY_FONT_TABLE, nullptr /* fallback */));
- EXPECT_EQ(700, typeface->fStyle.weight());
- EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, typeface->fStyle.slant());
+ EXPECT_EQ(700, typeface->getFontStyle().weight());
+ EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, typeface->getFontStyle().slant());
}
TEST(TypefaceTest, createFromFamilies_Family_withFallback) {
@@ -458,8 +458,8 @@ TEST(TypefaceTest, createFromFamilies_Family_withFallback) {
std::unique_ptr<Typeface> regular(
Typeface::createFromFamilies(makeSingleFamlyVector(kRegularFont), RESOLVE_BY_FONT_TABLE,
RESOLVE_BY_FONT_TABLE, fallback.get()));
- EXPECT_EQ(400, regular->fStyle.weight());
- EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, regular->fStyle.slant());
+ EXPECT_EQ(400, regular->getFontStyle().weight());
+ EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, regular->getFontStyle().slant());
}
} // namespace
diff --git a/media/TEST_MAPPING b/media/TEST_MAPPING
index e52e0b16eca3..6a21496f1165 100644
--- a/media/TEST_MAPPING
+++ b/media/TEST_MAPPING
@@ -1,7 +1,10 @@
{
"presubmit": [
{
- "name": "CtsMediaBetterTogetherTestCases"
+ "name": "CtsMediaRouterTestCases"
+ },
+ {
+ "name": "CtsMediaSessionTestCases"
},
{
"name": "mediaroutertest"
diff --git a/media/java/android/media/IAudioService.aidl b/media/java/android/media/IAudioService.aidl
index 54a87ad7fd39..2a740f85aa72 100644
--- a/media/java/android/media/IAudioService.aidl
+++ b/media/java/android/media/IAudioService.aidl
@@ -32,6 +32,7 @@ import android.media.BluetoothProfileConnectionInfo;
import android.media.FadeManagerConfiguration;
import android.media.IAudioDeviceVolumeDispatcher;
import android.media.IAudioFocusDispatcher;
+import android.media.IAudioManagerNative;
import android.media.IAudioModeDispatcher;
import android.media.IAudioRoutesObserver;
import android.media.IAudioServerStateDispatcher;
@@ -83,6 +84,7 @@ interface IAudioService {
// When a method's argument list is changed, BpAudioManager's corresponding serialization code
// (if any) in frameworks/native/services/audiomanager/IAudioManager.cpp must be updated.
+ IAudioManagerNative getNativeInterface();
int trackPlayer(in PlayerBase.PlayerIdCard pic);
oneway void playerAttributes(in int piid, in AudioAttributes attr);
diff --git a/media/java/android/media/MediaRoute2Info.java b/media/java/android/media/MediaRoute2Info.java
index bbb03e77c8c9..88981eac9bb5 100644
--- a/media/java/android/media/MediaRoute2Info.java
+++ b/media/java/android/media/MediaRoute2Info.java
@@ -961,8 +961,7 @@ public final class MediaRoute2Info implements Parcelable {
*
* @hide
*/
- @FlaggedApi(FLAG_ENABLE_MEDIA_ROUTE_2_INFO_PROVIDER_PACKAGE_NAME)
- public boolean isVisibleTo(String packageName) {
+ public boolean isVisibleTo(@NonNull String packageName) {
return !mIsVisibilityRestricted
|| TextUtils.equals(getProviderPackageName(), packageName)
|| mAllowedPackages.contains(packageName);
diff --git a/media/java/android/media/MediaRouter2.java b/media/java/android/media/MediaRouter2.java
index 3738312b762f..e57148fe5a6a 100644
--- a/media/java/android/media/MediaRouter2.java
+++ b/media/java/android/media/MediaRouter2.java
@@ -19,7 +19,6 @@ package android.media;
import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
import static com.android.media.flags.Flags.FLAG_ENABLE_BUILT_IN_SPEAKER_ROUTE_SUITABILITY_STATUSES;
import static com.android.media.flags.Flags.FLAG_ENABLE_GET_TRANSFERABLE_ROUTES;
-import static com.android.media.flags.Flags.FLAG_ENABLE_MEDIA_ROUTE_2_INFO_PROVIDER_PACKAGE_NAME;
import static com.android.media.flags.Flags.FLAG_ENABLE_PRIVILEGED_ROUTING_FOR_MEDIA_ROUTING_CONTROL;
import static com.android.media.flags.Flags.FLAG_ENABLE_RLP_CALLBACKS_IN_MEDIA_ROUTER2;
import static com.android.media.flags.Flags.FLAG_ENABLE_SCREEN_OFF_SCANNING;
@@ -1406,7 +1405,6 @@ public final class MediaRouter2 {
requestCreateController(controller, route, managerRequestId);
}
- @FlaggedApi(FLAG_ENABLE_MEDIA_ROUTE_2_INFO_PROVIDER_PACKAGE_NAME)
private List<MediaRoute2Info> getSortedRoutes(
List<MediaRoute2Info> routes, List<String> packageOrder) {
if (packageOrder.isEmpty()) {
@@ -1427,7 +1425,6 @@ public final class MediaRouter2 {
}
@GuardedBy("mLock")
- @FlaggedApi(FLAG_ENABLE_MEDIA_ROUTE_2_INFO_PROVIDER_PACKAGE_NAME)
private List<MediaRoute2Info> filterRoutesWithCompositePreferenceLocked(
List<MediaRoute2Info> routes) {
@@ -3654,7 +3651,6 @@ public final class MediaRouter2 {
}
}
- @FlaggedApi(FLAG_ENABLE_MEDIA_ROUTE_2_INFO_PROVIDER_PACKAGE_NAME)
@Override
public List<MediaRoute2Info> filterRoutesWithIndividualPreference(
List<MediaRoute2Info> routes, RouteDiscoveryPreference discoveryPreference) {
diff --git a/media/java/android/media/MediaRouter2Manager.java b/media/java/android/media/MediaRouter2Manager.java
index 3854747f46e0..3f18eef2f9aa 100644
--- a/media/java/android/media/MediaRouter2Manager.java
+++ b/media/java/android/media/MediaRouter2Manager.java
@@ -20,11 +20,9 @@ import static android.media.MediaRouter2.SCANNING_STATE_NOT_SCANNING;
import static android.media.MediaRouter2.SCANNING_STATE_WHILE_INTERACTIVE;
import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
-import static com.android.media.flags.Flags.FLAG_ENABLE_MEDIA_ROUTE_2_INFO_PROVIDER_PACKAGE_NAME;
import android.Manifest;
import android.annotation.CallbackExecutor;
-import android.annotation.FlaggedApi;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
@@ -287,7 +285,6 @@ public final class MediaRouter2Manager {
(route) -> sessionInfo.isSystemSession() ^ route.isSystemRoute());
}
- @FlaggedApi(FLAG_ENABLE_MEDIA_ROUTE_2_INFO_PROVIDER_PACKAGE_NAME)
private List<MediaRoute2Info> getSortedRoutes(RouteDiscoveryPreference preference) {
if (!preference.shouldRemoveDuplicates()) {
synchronized (mRoutesLock) {
@@ -311,7 +308,6 @@ public final class MediaRouter2Manager {
return routes;
}
- @FlaggedApi(FLAG_ENABLE_MEDIA_ROUTE_2_INFO_PROVIDER_PACKAGE_NAME)
private List<MediaRoute2Info> getFilteredRoutes(
@NonNull RoutingSessionInfo sessionInfo,
boolean includeSelectedRoutes,
diff --git a/media/java/android/media/flags/media_better_together.aconfig b/media/java/android/media/flags/media_better_together.aconfig
index 4398b261377b..c48b5f4e4aea 100644
--- a/media/java/android/media/flags/media_better_together.aconfig
+++ b/media/java/android/media/flags/media_better_together.aconfig
@@ -11,6 +11,16 @@ flag {
}
flag {
+ name: "disable_set_bluetooth_ad2p_on_calls"
+ namespace: "media_better_together"
+ description: "Prevents calls to AudioService.setBluetoothA2dpOn(), known to cause incorrect audio routing to the built-in speakers."
+ bug: "294968421"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+flag {
name: "enable_audio_input_device_routing_and_volume_control"
namespace: "media_better_together"
description: "Allows audio input devices routing and volume control via system settings."
diff --git a/mime/Android.bp b/mime/Android.bp
index 20110f1dfb47..b609548fcbab 100644
--- a/mime/Android.bp
+++ b/mime/Android.bp
@@ -49,6 +49,17 @@ java_library {
],
}
+java_library {
+ name: "mimemap-testing-alt",
+ defaults: ["mimemap-defaults"],
+ static_libs: ["mimemap-testing-alt-res.jar"],
+ jarjar_rules: "jarjar-rules-alt.txt",
+ visibility: [
+ "//cts/tests/tests/mimemap:__subpackages__",
+ "//frameworks/base:__subpackages__",
+ ],
+}
+
// The mimemap-res.jar and mimemap-testing-res.jar genrules produce a .jar that
// has the resource file in a subdirectory res/ and testres/, respectively.
// They need to be in different paths because one of them ends up in a
@@ -86,6 +97,19 @@ java_genrule {
cmd: "mkdir $(genDir)/testres/ && cp $(in) $(genDir)/testres/ && $(location soong_zip) -C $(genDir) -o $(out) -D $(genDir)/testres/",
}
+// The same as mimemap-testing-res.jar except that the resources are placed in a different directory.
+// They get bundled with CTS so that CTS can compare a device's MimeMap implementation vs.
+// the stock Android one from when CTS was built.
+java_genrule {
+ name: "mimemap-testing-alt-res.jar",
+ tools: [
+ "soong_zip",
+ ],
+ srcs: [":mime.types.minimized-alt"],
+ out: ["mimemap-testing-alt-res.jar"],
+ cmd: "mkdir $(genDir)/testres-alt/ && cp $(in) $(genDir)/testres-alt/ && $(location soong_zip) -C $(genDir) -o $(out) -D $(genDir)/testres-alt/",
+}
+
// Combination of all *mime.types.minimized resources.
filegroup {
name: "mime.types.minimized",
@@ -99,6 +123,19 @@ filegroup {
],
}
+// Combination of all *mime.types.minimized resources.
+filegroup {
+ name: "mime.types.minimized-alt",
+ visibility: [
+ "//visibility:private",
+ ],
+ device_common_srcs: [
+ ":debian.mime.types.minimized-alt",
+ ":android.mime.types.minimized",
+ ":vendor.mime.types.minimized",
+ ],
+}
+
java_genrule {
name: "android.mime.types.minimized",
visibility: [
diff --git a/mime/jarjar-rules-alt.txt b/mime/jarjar-rules-alt.txt
new file mode 100644
index 000000000000..9a7644325336
--- /dev/null
+++ b/mime/jarjar-rules-alt.txt
@@ -0,0 +1 @@
+rule android.content.type.DefaultMimeMapFactory android.content.type.cts.StockAndroidAltMimeMapFactory
diff --git a/mime/jarjar-rules.txt b/mime/jarjar-rules.txt
index 145d1dbf3d11..e1ea8e10314c 100644
--- a/mime/jarjar-rules.txt
+++ b/mime/jarjar-rules.txt
@@ -1 +1 @@
-rule android.content.type.DefaultMimeMapFactory android.content.type.cts.StockAndroidMimeMapFactory \ No newline at end of file
+rule android.content.type.DefaultMimeMapFactory android.content.type.cts.StockAndroidMimeMapFactory
diff --git a/native/android/tests/system_health/OWNERS b/native/android/tests/system_health/OWNERS
new file mode 100644
index 000000000000..e3bbee92057d
--- /dev/null
+++ b/native/android/tests/system_health/OWNERS
@@ -0,0 +1 @@
+include /ADPF_OWNERS
diff --git a/packages/FusedLocation/src/com/android/location/fused/FusedLocationProvider.java b/packages/FusedLocation/src/com/android/location/fused/FusedLocationProvider.java
index 068074ae1b89..8e52a00fe545 100644
--- a/packages/FusedLocation/src/com/android/location/fused/FusedLocationProvider.java
+++ b/packages/FusedLocation/src/com/android/location/fused/FusedLocationProvider.java
@@ -38,6 +38,7 @@ import android.location.provider.LocationProviderBase;
import android.location.provider.ProviderProperties;
import android.location.provider.ProviderRequest;
import android.os.Bundle;
+import android.util.Log;
import android.util.SparseArray;
import com.android.internal.annotations.GuardedBy;
@@ -301,8 +302,13 @@ public class FusedLocationProvider extends LocationProviderBase {
.setWorkSource(mRequest.getWorkSource())
.setHiddenFromAppOps(true)
.build();
- mLocationManager.requestLocationUpdates(mProvider, request,
- mContext.getMainExecutor(), this);
+
+ try {
+ mLocationManager.requestLocationUpdates(
+ mProvider, request, mContext.getMainExecutor(), this);
+ } catch (IllegalArgumentException e) {
+ Log.e(TAG, "Failed to request location updates");
+ }
}
}
}
@@ -311,7 +317,11 @@ public class FusedLocationProvider extends LocationProviderBase {
synchronized (mLock) {
int requestCode = mNextFlushCode++;
mPendingFlushes.put(requestCode, callback);
- mLocationManager.requestFlush(mProvider, this, requestCode);
+ try {
+ mLocationManager.requestFlush(mProvider, this, requestCode);
+ } catch (IllegalArgumentException e) {
+ Log.e(TAG, "Failed to request flush");
+ }
}
}
diff --git a/packages/InputDevices/res/raw/keyboard_layout_romanian.kcm b/packages/InputDevices/res/raw/keyboard_layout_romanian.kcm
index b384a2418ff2..b0308b5dccd4 100644
--- a/packages/InputDevices/res/raw/keyboard_layout_romanian.kcm
+++ b/packages/InputDevices/res/raw/keyboard_layout_romanian.kcm
@@ -120,78 +120,90 @@ key EQUALS {
key Q {
label: 'Q'
- base, capslock+shift: 'q'
+ base: 'q'
shift, capslock: 'Q'
+ shift+capslock: 'q'
}
key W {
label: 'W'
- base, capslock+shift: 'w'
+ base: 'w'
shift, capslock: 'W'
+ shift+capslock: 'w'
}
key E {
label: 'E'
- base, capslock+shift: 'e'
+ base: 'e'
shift, capslock: 'E'
+ shift+capslock: 'e'
ralt: '\u20ac'
}
key R {
label: 'R'
- base, capslock+shift: 'r'
+ base: 'r'
shift, capslock: 'R'
+ shift+capslock: 'r'
}
key T {
label: 'T'
- base, capslock+shift: 't'
+ base: 't'
shift, capslock: 'T'
+ shift+capslock: 't'
}
key Y {
label: 'Y'
- base, capslock+shift: 'y'
+ base: 'y'
shift, capslock: 'Y'
+ shift+capslock: 'y'
}
key U {
label: 'U'
- base, capslock+shift: 'u'
+ base: 'u'
shift, capslock: 'U'
+ shift+capslock: 'u'
}
key I {
label: 'I'
- base, capslock+shift: 'i'
+ base: 'i'
shift, capslock: 'I'
+ shift+capslock: 'i'
}
key O {
label: 'O'
- base, capslock+shift: 'o'
+ base: 'o'
shift, capslock: 'O'
+ shift+capslock: 'o'
}
key P {
label: 'P'
- base, capslock+shift: 'p'
+ base: 'p'
shift, capslock: 'P'
+ shift+capslock: 'p'
ralt: '\u00a7'
}
key LEFT_BRACKET {
label: '\u0102'
- base, capslock+shift: '\u0103'
+ base: '\u0103'
shift, capslock: '\u0102'
+ shift+capslock: '\u0103'
ralt: '['
ralt+shift: '{'
}
key RIGHT_BRACKET {
label: '\u00ce'
- base, capslock+shift: '\u00ee'
+ base: '\u00ee'
shift, capslock: '\u00ce'
+ shift+capslock: '\u00ee'
ralt: ']'
ralt+shift: '}'
}
@@ -200,21 +212,24 @@ key RIGHT_BRACKET {
key A {
label: 'A'
- base, capslock+shift: 'a'
+ base: 'a'
shift, capslock: 'A'
+ shift+capslock: 'a'
}
key S {
label: 'S'
- base, capslock+shift: 's'
+ base: 's'
shift, capslock: 'S'
+ shift+capslock: 's'
ralt: '\u00df'
}
key D {
label: 'D'
- base, capslock+shift: 'd'
+ base: 'd'
shift, capslock: 'D'
+ shift+capslock: 'd'
ralt: '\u0111'
ralt+shift, ralt+capslock: '\u0110'
ralt+shift+capslock: '\u0111'
@@ -222,38 +237,44 @@ key D {
key F {
label: 'F'
- base, capslock+shift: 'f'
+ base: 'f'
shift, capslock: 'F'
+ shift+capslock: 'f'
}
key G {
label: 'G'
- base, capslock+shift: 'g'
+ base: 'g'
shift, capslock: 'G'
+ shift+capslock: 'g'
}
key H {
label: 'H'
- base, capslock+shift: 'h'
+ base: 'h'
shift, capslock: 'H'
+ shift+capslock: 'h'
}
key J {
label: 'J'
- base, capslock+shift: 'j'
+ base: 'j'
shift, capslock: 'J'
+ shift+capslock: 'j'
}
key K {
label: 'K'
- base, capslock+shift: 'k'
+ base: 'k'
shift, capslock: 'K'
+ shift+capslock: 'k'
}
key L {
label: 'L'
- base, capslock+shift: 'l'
+ base: 'l'
shift, capslock: 'L'
+ shift+capslock: 'l'
ralt: '\u0142'
ralt+shift, ralt+capslock: '\u0141'
ralt+shift+capslock: '\u0142'
@@ -261,24 +282,27 @@ key L {
key SEMICOLON {
label: '\u0218'
- base, capslock+shift: '\u0219'
+ base: '\u0219'
shift, capslock: '\u0218'
+ shift+capslock: '\u0219'
ralt: ';'
ralt+shift: ':'
}
key APOSTROPHE {
label: '\u021a'
- base, capslock+shift: '\u021b'
+ base: '\u021b'
shift, capslock: '\u021a'
+ shift+capslock: '\u021b'
ralt: '\''
ralt+shift: '\u0022'
}
key BACKSLASH {
label: '\u00c2'
- base, capslock+shift: '\u00e2'
+ base: '\u00e2'
shift, capslock: '\u00c2'
+ shift+capslock: '\u00e2'
ralt: '\\'
ralt+shift: '|'
}
@@ -293,45 +317,52 @@ key PLUS {
key Z {
label: 'Z'
- base, capslock+shift: 'z'
+ base: 'z'
shift, capslock: 'Z'
+ shift+capslock: 'z'
}
key X {
label: 'X'
- base, capslock+shift: 'x'
+ base: 'x'
shift, capslock: 'X'
+ shift+capslock: 'x'
}
key C {
label: 'C'
- base, capslock+shift: 'c'
+ base: 'c'
shift, capslock: 'C'
+ shift+capslock: 'c'
ralt: '\u00a9'
}
key V {
label: 'V'
- base, capslock+shift: 'v'
+ base: 'v'
shift, capslock: 'V'
+ shift+capslock: 'v'
}
key B {
label: 'B'
- base, capslock+shift: 'b'
+ base: 'b'
shift, capslock: 'B'
+ shift+capslock: 'b'
}
key N {
label: 'N'
- base, capslock+shift: 'n'
+ base: 'n'
shift, capslock: 'N'
+ shift+capslock: 'n'
}
key M {
label: 'M'
- base, capslock+shift: 'm'
+ base: 'm'
shift, capslock: 'M'
+ shift+capslock: 'm'
}
key COMMA {
diff --git a/packages/InputDevices/res/raw/keyboard_layout_serbian_and_montenegrin_cyrillic.kcm b/packages/InputDevices/res/raw/keyboard_layout_serbian_and_montenegrin_cyrillic.kcm
index 6fa54f9d052f..9df78c9af923 100644
--- a/packages/InputDevices/res/raw/keyboard_layout_serbian_and_montenegrin_cyrillic.kcm
+++ b/packages/InputDevices/res/raw/keyboard_layout_serbian_and_montenegrin_cyrillic.kcm
@@ -104,149 +104,173 @@ key EQUALS {
key Q {
label: '\u0409'
- base, capslock+shift: '\u0459'
+ base: '\u0459'
shift, capslock: '\u0409'
+ shift+capslock: '\u0459'
}
key W {
label: '\u040a'
- base, capslock+shift: '\u045a'
+ base: '\u045a'
shift, capslock: '\u040a'
+ shift+capslock: '\u045a'
}
key E {
label: '\u0415'
- base, capslock+shift: '\u0435'
+ base: '\u0435'
shift, capslock: '\u0415'
+ shift+capslock: '\u0435'
ralt: '\u20ac'
}
key R {
label: '\u0420'
- base, capslock+shift: '\u0440'
+ base: '\u0440'
shift, capslock: '\u0420'
+ shift+capslock: '\u0440'
}
key T {
label: '\u0422'
- base, capslock+shift: '\u0442'
+ base: '\u0442'
shift, capslock: '\u0422'
+ shift+capslock: '\u0442'
}
key Y {
label: '\u0417'
- base, capslock+shift: '\u0437'
+ base: '\u0437'
shift, capslock: '\u0417'
+ shift+capslock: '\u0437'
}
key U {
label: '\u0423'
- base, capslock+shift: '\u0443'
+ base: '\u0443'
shift, capslock: '\u0423'
+ shift+capslock: '\u0443'
}
key I {
label: '\u0418'
- base, capslock+shift: '\u0438'
+ base: '\u0438'
shift, capslock: '\u0418'
+ shift+capslock: '\u0438'
}
key O {
label: '\u041e'
- base, capslock+shift: '\u043e'
+ base: '\u043e'
shift, capslock: '\u041e'
+ shift+capslock: '\u043e'
}
key P {
label: '\u041f'
- base, capslock+shift: '\u043f'
+ base: '\u043f'
shift, capslock: '\u041f'
+ shift+capslock: '\u043f'
}
key LEFT_BRACKET {
label: '\u0428'
- base, capslock+shift: '\u0448'
+ base: '\u0448'
shift, capslock: '\u0428'
+ shift+capslock: '\u0448'
}
key RIGHT_BRACKET {
label: '\u0402'
- base, capslock+shift: '\u0452'
+ base: '\u0452'
shift, capslock: '\u0402'
+ shift+capslock: '\u0452'
}
### ROW 3
key A {
label: '\u0410'
- base, capslock+shift: '\u0430'
+ base: '\u0430'
shift, capslock: '\u0410'
+ shift+capslock: '\u0430'
}
key S {
label: '\u0421'
- base, capslock+shift: '\u0441'
+ base: '\u0441'
shift, capslock: '\u0421'
+ shift+capslock: '\u0441'
}
key D {
label: '\u0414'
- base, capslock+shift: '\u0434'
+ base: '\u0434'
shift, capslock: '\u0414'
+ shift+capslock: '\u0434'
}
key F {
label: '\u0424'
- base, capslock+shift: '\u0444'
+ base: '\u0444'
shift, capslock: '\u0424'
+ shift+capslock: '\u0444'
}
key G {
label: '\u0413'
- base, capslock+shift: '\u0433'
+ base: '\u0433'
shift, capslock: '\u0413'
+ shift+capslock: '\u0433'
}
key H {
label: '\u0425'
- base, capslock+shift: '\u0445'
+ base: '\u0445'
shift, capslock: '\u0425'
+ shift+capslock: '\u0445'
}
key J {
label: '\u0408'
- base, capslock+shift: '\u0458'
+ base: '\u0458'
shift, capslock: '\u0408'
+ shift+capslock: '\u0458'
}
key K {
label: '\u041a'
- base, capslock+shift: '\u043a'
+ base: '\u043a'
shift, capslock: '\u041a'
+ shift+capslock: '\u043a'
}
key L {
label: '\u041b'
- base, capslock+shift: '\u043b'
+ base: '\u043b'
shift, capslock: '\u041b'
+ shift+capslock: '\u043b'
}
key SEMICOLON {
label: '\u0427'
- base, capslock+shift: '\u0447'
+ base: '\u0447'
shift, capslock: '\u0427'
+ shift+capslock: '\u0447'
}
key APOSTROPHE {
label: '\u040b'
- base, capslock+shift: '\u045b'
+ base: '\u045b'
shift, capslock: '\u040b'
+ shift+capslock: '\u045b'
}
key BACKSLASH {
label: '\u0416'
- base, capslock+shift: '\u0436'
+ base: '\u0436'
shift, capslock: '\u0416'
+ shift+capslock: '\u0436'
}
### ROW 4
@@ -259,44 +283,51 @@ key PLUS {
key Z {
label: '\u0405'
- base, capslock+shift: '\u0455'
+ base: '\u0455'
shift, capslock: '\u0405'
+ shift+capslock: '\u0455'
}
key X {
label: '\u040f'
- base, capslock+shift: '\u045f'
+ base: '\u045f'
shift, capslock: '\u040f'
+ shift+capslock: '\u045f'
}
key C {
label: '\u0426'
- base, capslock+shift: '\u0446'
+ base: '\u0446'
shift, capslock: '\u0426'
+ shift+capslock: '\u0446'
}
key V {
label: '\u0412'
- base, capslock+shift: '\u0432'
+ base: '\u0432'
shift, capslock: '\u0412'
+ shift+capslock: '\u0432'
}
key B {
label: '\u0411'
- base, capslock+shift: '\u0431'
+ base: '\u0431'
shift, capslock: '\u0411'
+ shift+capslock: '\u0431'
}
key N {
label: '\u041d'
- base, capslock+shift: '\u043d'
+ base: '\u043d'
shift, capslock: '\u041d'
+ shift+capslock: '\u043d'
}
key M {
label: '\u041c'
- base, capslock+shift: '\u043c'
+ base: '\u043c'
shift, capslock: '\u041c'
+ shift+capslock: '\u043c'
}
key COMMA {
@@ -317,4 +348,4 @@ key SLASH {
label: '-'
base: '-'
shift: '_'
-} \ No newline at end of file
+}
diff --git a/packages/InputDevices/res/raw/keyboard_layout_serbian_and_montenegrin_latin.kcm b/packages/InputDevices/res/raw/keyboard_layout_serbian_and_montenegrin_latin.kcm
index 8e4d7b147faa..4c8997b16a26 100644
--- a/packages/InputDevices/res/raw/keyboard_layout_serbian_and_montenegrin_latin.kcm
+++ b/packages/InputDevices/res/raw/keyboard_layout_serbian_and_montenegrin_latin.kcm
@@ -120,78 +120,90 @@ key EQUALS {
key Q {
label: 'Q'
- base, capslock+shift: 'q'
+ base: 'q'
shift, capslock: 'Q'
+ shift+capslock: 'q'
ralt: '\\'
}
key W {
label: 'W'
- base, capslock+shift: 'w'
+ base: 'w'
shift, capslock: 'W'
+ shift+capslock: 'w'
ralt: '|'
}
key E {
label: 'E'
- base, capslock+shift: 'e'
+ base: 'e'
shift, capslock: 'E'
+ shift+capslock: 'e'
ralt: '\u20ac'
}
key R {
label: 'R'
- base, capslock+shift: 'r'
+ base: 'r'
shift, capslock: 'R'
+ shift+capslock: 'r'
}
key T {
label: 'T'
- base, capslock+shift: 't'
+ base: 't'
shift, capslock: 'T'
+ shift+capslock: 't'
}
key Z {
label: 'Z'
- base, capslock+shift: 'z'
+ base: 'z'
shift, capslock: 'Z'
+ shift+capslock: 'z'
}
key U {
label: 'U'
- base, capslock+shift: 'u'
+ base: 'u'
shift, capslock: 'U'
+ shift+capslock: 'u'
}
key I {
label: 'I'
- base, capslock+shift: 'i'
+ base: 'i'
shift, capslock: 'I'
+ shift+capslock: 'i'
}
key O {
label: 'O'
- base, capslock+shift: 'o'
+ base: 'o'
shift, capslock: 'O'
+ shift+capslock: 'o'
}
key P {
label: 'P'
- base, capslock+shift: 'p'
+ base: 'p'
shift, capslock: 'P'
+ shift+capslock: 'p'
}
key LEFT_BRACKET {
label: '\u0160'
- base, capslock+shift: '\u0161'
+ base: '\u0161'
shift, capslock: '\u0160'
+ shift+capslock: '\u0161'
ralt: '\u00f7'
}
key RIGHT_BRACKET {
label: '\u0110'
- base, capslock+shift: '\u0111'
+ base: '\u0111'
shift, capslock: '\u0110'
+ shift+capslock: '\u0111'
ralt: '\u00d7'
}
@@ -199,79 +211,91 @@ key RIGHT_BRACKET {
key A {
label: 'A'
- base, capslock+shift: 'a'
+ base: 'a'
shift, capslock: 'A'
+ shift+capslock: 'a'
}
key S {
label: 'S'
- base, capslock+shift: 's'
+ base: 's'
shift, capslock: 'S'
+ shift+capslock: 's'
}
key D {
label: 'D'
- base, capslock+shift: 'd'
+ base: 'd'
shift, capslock: 'D'
+ shift+capslock: 'd'
}
key F {
label: 'F'
- base, capslock+shift: 'f'
+ base: 'f'
shift, capslock: 'F'
+ shift+capslock: 'f'
ralt: '['
}
key G {
label: 'G'
- base, capslock+shift: 'g'
+ base: 'g'
shift, capslock: 'G'
+ shift+capslock: 'g'
ralt: ']'
}
key H {
label: 'H'
- base, capslock+shift: 'h'
+ base: 'h'
shift, capslock: 'H'
+ shift+capslock: 'h'
}
key J {
label: 'J'
- base, capslock+shift: 'j'
+ base: 'j'
shift, capslock: 'J'
+ shift+capslock: 'j'
}
key K {
label: 'K'
- base, capslock+shift: 'k'
+ base: 'k'
shift, capslock: 'K'
+ shift+capslock: 'k'
ralt: '\u0142'
}
key L {
label: 'L'
- base, capslock+shift: 'l'
+ base: 'l'
shift, capslock: 'L'
+ shift+capslock: 'l'
ralt: '\u0141'
}
key SEMICOLON {
label: '\u010c'
- base, capslock+shift: '\u010d'
+ base: '\u010d'
shift, capslock: '\u010c'
+ shift+capslock: '\u010d'
}
key APOSTROPHE {
label: '\u0106'
- base, capslock+shift: '\u0107'
+ base: '\u0107'
shift, capslock: '\u0106'
+ shift+capslock: '\u0107'
ralt: '\u00df'
}
key BACKSLASH {
label: '\u017d'
- base, capslock+shift: '\u017e'
+ base: '\u017e'
shift, capslock: '\u017d'
+ shift+capslock: '\u017e'
ralt: '\u00a4'
}
@@ -285,47 +309,54 @@ key PLUS {
key Y {
label: 'Y'
- base, capslock+shift: 'y'
+ base: 'y'
shift, capslock: 'Y'
+ shift+capslock: 'y'
}
key X {
label: 'X'
- base, capslock+shift: 'x'
+ base: 'x'
shift, capslock: 'X'
+ shift+capslock: 'x'
}
key C {
label: 'C'
- base, capslock+shift: 'c'
+ base: 'c'
shift, capslock: 'C'
+ shift+capslock: 'c'
}
key V {
label: 'V'
- base, capslock+shift: 'v'
+ base: 'v'
shift, capslock: 'V'
+ shift+capslock: 'v'
ralt: '@'
}
key B {
label: 'B'
- base, capslock+shift: 'b'
+ base: 'b'
shift, capslock: 'B'
+ shift+capslock: 'b'
ralt: '{'
}
key N {
label: 'N'
- base, capslock+shift: 'n'
+ base: 'n'
shift, capslock: 'N'
+ shift+capslock: 'n'
ralt: '}'
}
key M {
label: 'M'
- base, capslock+shift: 'm'
+ base: 'm'
shift, capslock: 'M'
+ shift+capslock: 'm'
ralt: '\u00a7'
}
@@ -347,4 +378,4 @@ key MINUS {
label: '-'
base: '-'
shift: '_'
-} \ No newline at end of file
+}
diff --git a/packages/PrintSpooler/Android.bp b/packages/PrintSpooler/Android.bp
index 6af3c6624f62..000e20fb4280 100644
--- a/packages/PrintSpooler/Android.bp
+++ b/packages/PrintSpooler/Android.bp
@@ -59,6 +59,21 @@ android_library {
"android-support-core-ui",
"android-support-fragment",
"android-support-annotations",
+ "printspooler_aconfig_flags_java_lib",
],
manifest: "AndroidManifest.xml",
}
+
+aconfig_declarations {
+ name: "printspooler_aconfig_declarations",
+ package: "com.android.printspooler.flags",
+ container: "system",
+ srcs: [
+ "flags/flags.aconfig",
+ ],
+}
+
+java_aconfig_library {
+ name: "printspooler_aconfig_flags_java_lib",
+ aconfig_declarations: "printspooler_aconfig_declarations",
+}
diff --git a/packages/PrintSpooler/flags/flags.aconfig b/packages/PrintSpooler/flags/flags.aconfig
new file mode 100644
index 000000000000..4a76dff405d0
--- /dev/null
+++ b/packages/PrintSpooler/flags/flags.aconfig
@@ -0,0 +1,9 @@
+package: "com.android.printspooler.flags"
+container: "system"
+
+flag {
+ name: "log_print_jobs"
+ namespace: "printing"
+ description: "Log print job creation and state transitions."
+ bug: "385340868"
+}
diff --git a/packages/PrintSpooler/src/com/android/printspooler/model/PrintSpoolerService.java b/packages/PrintSpooler/src/com/android/printspooler/model/PrintSpoolerService.java
index bba57d5fe0a2..1a9309c13bd7 100644
--- a/packages/PrintSpooler/src/com/android/printspooler/model/PrintSpoolerService.java
+++ b/packages/PrintSpooler/src/com/android/printspooler/model/PrintSpoolerService.java
@@ -68,6 +68,7 @@ import com.android.internal.util.Preconditions;
import com.android.internal.util.dump.DualDumpOutputStream;
import com.android.internal.util.function.pooled.PooledLambda;
import com.android.printspooler.R;
+import com.android.printspooler.flags.Flags;
import com.android.printspooler.util.ApprovedPrintServices;
import libcore.io.IoUtils;
@@ -493,7 +494,7 @@ public final class PrintSpoolerService extends Service {
keepAwakeLocked();
}
- if (DEBUG_PRINT_JOB_LIFECYCLE) {
+ if (Flags.logPrintJobs() || DEBUG_PRINT_JOB_LIFECYCLE) {
Slog.i(LOG_TAG, "[ADD] " + printJob);
}
}
@@ -506,7 +507,7 @@ public final class PrintSpoolerService extends Service {
PrintJobInfo printJob = mPrintJobs.get(i);
if (isObsoleteState(printJob.getState())) {
mPrintJobs.remove(i);
- if (DEBUG_PRINT_JOB_LIFECYCLE) {
+ if (Flags.logPrintJobs() || DEBUG_PRINT_JOB_LIFECYCLE) {
Slog.i(LOG_TAG, "[REMOVE] " + printJob.getId().flattenToString());
}
removePrintJobFileLocked(printJob.getId());
@@ -568,7 +569,7 @@ public final class PrintSpoolerService extends Service {
checkIfStillKeepAwakeLocked();
}
- if (DEBUG_PRINT_JOB_LIFECYCLE) {
+ if (Flags.logPrintJobs() || DEBUG_PRINT_JOB_LIFECYCLE) {
Slog.i(LOG_TAG, "[STATE CHANGED] " + printJob);
}
diff --git a/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/DataChangeReason.kt b/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/DataChangeReason.kt
index 145fabea52af..ac36b08512e8 100644
--- a/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/DataChangeReason.kt
+++ b/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/DataChangeReason.kt
@@ -39,5 +39,7 @@ annotation class DataChangeReason {
const val RESTORE = 3
/** Data is synced from another profile (e.g. personal profile to work profile). */
const val SYNC_ACROSS_PROFILES = 4
+
+ fun isDataChange(reason: Int): Boolean = reason in UNKNOWN..SYNC_ACROSS_PROFILES
}
}
diff --git a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGetterApi.kt b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGetterApi.kt
index 2fac54557bef..6fc6b5405eb2 100644
--- a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGetterApi.kt
+++ b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGetterApi.kt
@@ -22,6 +22,7 @@ import com.android.settingslib.graph.proto.PreferenceProto
import com.android.settingslib.ipc.ApiDescriptor
import com.android.settingslib.ipc.ApiHandler
import com.android.settingslib.ipc.ApiPermissionChecker
+import com.android.settingslib.metadata.PreferenceCoordinate
import com.android.settingslib.metadata.PreferenceHierarchyNode
import com.android.settingslib.metadata.PreferenceScreenRegistry
diff --git a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGetterCodecs.kt b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGetterCodecs.kt
index ff14eb5aae55..70ce62c8383c 100644
--- a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGetterCodecs.kt
+++ b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGetterCodecs.kt
@@ -20,6 +20,7 @@ import android.os.Bundle
import android.os.Parcel
import com.android.settingslib.graph.proto.PreferenceProto
import com.android.settingslib.ipc.MessageCodec
+import com.android.settingslib.metadata.PreferenceCoordinate
import java.util.Arrays
/** Message codec for [PreferenceGetterRequest]. */
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 3c870acf2291..ea795542a5f6 100644
--- a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceSetterApi.kt
+++ b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceSetterApi.kt
@@ -34,6 +34,8 @@ import com.android.settingslib.metadata.PreferenceRestrictionProvider
import com.android.settingslib.metadata.PreferenceScreenRegistry
import com.android.settingslib.metadata.RangeValue
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. */
data class PreferenceSetterRequest(
@@ -187,6 +189,8 @@ fun <T> PersistentPreference<T>.evalWritePermit(
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)
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 e5bf41f87999..83725aaec377 100644
--- a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PersistentPreference.kt
+++ b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PersistentPreference.kt
@@ -44,6 +44,24 @@ annotation class ReadWritePermit {
}
}
+/** The reason of preference change. */
+@IntDef(
+ PreferenceChangeReason.VALUE,
+ PreferenceChangeReason.STATE,
+ PreferenceChangeReason.DEPENDENT,
+)
+@Retention(AnnotationRetention.SOURCE)
+annotation class PreferenceChangeReason {
+ companion object {
+ /** Preference value is changed. */
+ const val VALUE = 1000
+ /** Preference state (title/summary, enable state, etc.) is changed. */
+ const val STATE = 1001
+ /** Dependent preference state is changed. */
+ const val DEPENDENT = 1002
+ }
+}
+
/** Indicates how sensitive of the data. */
@Retention(AnnotationRetention.SOURCE)
@Target(AnnotationTarget.TYPE)
diff --git a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceCoordinate.kt b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceCoordinate.kt
index 68aa2d258295..2dd736ae6083 100644
--- a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceCoordinate.kt
+++ b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceCoordinate.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2024 The Android Open Source Project
+ * Copyright (C) 2025 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.settingslib.graph
+package com.android.settingslib.metadata
import android.os.Parcel
import android.os.Parcelable
diff --git a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenBindingHelper.kt b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenBindingHelper.kt
index 91abd8b4c9e9..8358ab921fb6 100644
--- a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenBindingHelper.kt
+++ b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenBindingHelper.kt
@@ -25,12 +25,14 @@ import androidx.preference.Preference
import androidx.preference.PreferenceDataStore
import androidx.preference.PreferenceGroup
import androidx.preference.PreferenceScreen
+import com.android.settingslib.datastore.DataChangeReason
import com.android.settingslib.datastore.HandlerExecutor
import com.android.settingslib.datastore.KeyValueStore
import com.android.settingslib.datastore.KeyedDataObservable
import com.android.settingslib.datastore.KeyedObservable
import com.android.settingslib.datastore.KeyedObserver
import com.android.settingslib.metadata.PersistentPreference
+import com.android.settingslib.metadata.PreferenceChangeReason
import com.android.settingslib.metadata.PreferenceHierarchy
import com.android.settingslib.metadata.PreferenceHierarchyNode
import com.android.settingslib.metadata.PreferenceLifecycleContext
@@ -73,7 +75,7 @@ class PreferenceScreenBindingHelper(
?.keyValueStore
override fun notifyPreferenceChange(key: String) =
- notifyChange(key, CHANGE_REASON_STATE)
+ notifyChange(key, PreferenceChangeReason.STATE)
@Suppress("DEPRECATION")
override fun startActivityForResult(
@@ -91,7 +93,13 @@ class PreferenceScreenBindingHelper(
private val preferenceObserver: KeyedObserver<String?>
private val storageObserver =
- KeyedObserver<String> { key, _ -> notifyChange(key, CHANGE_REASON_VALUE) }
+ KeyedObserver<String> { key, reason ->
+ if (DataChangeReason.isDataChange(reason)) {
+ notifyChange(key, PreferenceChangeReason.VALUE)
+ } else {
+ notifyChange(key, PreferenceChangeReason.STATE)
+ }
+ }
init {
val preferencesBuilder = ImmutableMap.builder<String, PreferenceHierarchyNode>()
@@ -148,7 +156,7 @@ class PreferenceScreenBindingHelper(
}
// check reason to avoid potential infinite loop
- if (reason != CHANGE_REASON_DEPENDENT) {
+ if (reason != PreferenceChangeReason.DEPENDENT) {
notifyDependents(key, mutableSetOf())
}
}
@@ -157,7 +165,7 @@ class PreferenceScreenBindingHelper(
private fun notifyDependents(key: String, notifiedKeys: MutableSet<String>) {
if (!notifiedKeys.add(key)) return
for (dependency in dependencies[key]) {
- notifyChange(dependency, CHANGE_REASON_DEPENDENT)
+ notifyChange(dependency, PreferenceChangeReason.DEPENDENT)
notifyDependents(dependency, notifiedKeys)
}
}
@@ -210,13 +218,6 @@ class PreferenceScreenBindingHelper(
}
companion object {
- /** Preference value is changed. */
- const val CHANGE_REASON_VALUE = 0
- /** Preference state (title/summary, enable state, etc.) is changed. */
- const val CHANGE_REASON_STATE = 1
- /** Dependent preference state is changed. */
- const val CHANGE_REASON_DEPENDENT = 2
-
/** Updates preference screen that has incomplete hierarchy. */
@JvmStatic
fun bind(preferenceScreen: PreferenceScreen) {
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListRepository.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListRepository.kt
index e1e1ee5a8feb..78d6c31ac783 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListRepository.kt
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListRepository.kt
@@ -88,6 +88,7 @@ class AppListRepositoryImpl(
matchAnyUserForAdmin: Boolean,
): List<ApplicationInfo> = try {
coroutineScope {
+ // TODO(b/382016780): to be removed after flag cleanup.
val hiddenSystemModulesDeferred = async { packageManager.getHiddenSystemModules() }
val hideWhenDisabledPackagesDeferred = async {
context.resources.getStringArray(R.array.config_hideWhenDisabled_packageNames)
@@ -95,6 +96,7 @@ class AppListRepositoryImpl(
val installedApplicationsAsUser =
getInstalledApplications(userId, matchAnyUserForAdmin)
+ // TODO(b/382016780): to be removed after flag cleanup.
val hiddenSystemModules = hiddenSystemModulesDeferred.await()
val hideWhenDisabledPackages = hideWhenDisabledPackagesDeferred.await()
installedApplicationsAsUser.filter { app ->
@@ -206,6 +208,7 @@ class AppListRepositoryImpl(
private fun isSystemApp(app: ApplicationInfo, homeOrLauncherPackages: Set<String>): Boolean =
app.isSystemApp && !app.isUpdatedSystemApp && app.packageName !in homeOrLauncherPackages
+ // TODO(b/382016780): to be removed after flag cleanup.
private fun PackageManager.getHiddenSystemModules(): Set<String> {
val moduleInfos = getInstalledModules(0).filter { it.isHidden }
val hiddenApps = moduleInfos.mapNotNull { it.packageName }.toMutableSet()
@@ -218,13 +221,14 @@ class AppListRepositoryImpl(
companion object {
private const val TAG = "AppListRepository"
+ // TODO(b/382016780): to be removed after flag cleanup.
private fun ApplicationInfo.isInAppList(
showInstantApps: Boolean,
hiddenSystemModules: Set<String>,
hideWhenDisabledPackages: Array<String>,
) = when {
!showInstantApps && isInstantApp -> false
- packageName in hiddenSystemModules -> false
+ !Flags.removeHiddenModuleUsage() && (packageName in hiddenSystemModules) -> false
packageName in hideWhenDisabledPackages -> enabled && !isDisabledUntilUsed
enabled -> true
else -> enabledSetting == PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER
diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppListRepositoryTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppListRepositoryTest.kt
index b1baa8601f28..fd4b189c51ff 100644
--- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppListRepositoryTest.kt
+++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppListRepositoryTest.kt
@@ -281,6 +281,23 @@ class AppListRepositoryTest {
)
}
+ @EnableFlags(Flags.FLAG_REMOVE_HIDDEN_MODULE_USAGE)
+ @Test
+ fun loadApps_shouldIncludeAllSystemModuleApps() = runTest {
+ packageManager.stub {
+ on { getInstalledModules(any()) } doReturn listOf(HIDDEN_MODULE)
+ }
+ mockInstalledApplications(
+ listOf(NORMAL_APP, HIDDEN_APEX_APP, HIDDEN_MODULE_APP),
+ ADMIN_USER_ID
+ )
+
+ val appList = repository.loadApps(userId = ADMIN_USER_ID)
+
+ assertThat(appList).containsExactly(NORMAL_APP, HIDDEN_APEX_APP, HIDDEN_MODULE_APP)
+ }
+
+ @DisableFlags(Flags.FLAG_REMOVE_HIDDEN_MODULE_USAGE)
@EnableFlags(Flags.FLAG_PROVIDE_INFO_OF_APK_IN_APEX)
@Test
fun loadApps_hasApkInApexInfo_shouldNotIncludeAllHiddenApps() = runTest {
@@ -297,7 +314,7 @@ class AppListRepositoryTest {
assertThat(appList).containsExactly(NORMAL_APP)
}
- @DisableFlags(Flags.FLAG_PROVIDE_INFO_OF_APK_IN_APEX)
+ @DisableFlags(Flags.FLAG_PROVIDE_INFO_OF_APK_IN_APEX, Flags.FLAG_REMOVE_HIDDEN_MODULE_USAGE)
@Test
fun loadApps_noApkInApexInfo_shouldNotIncludeHiddenSystemModule() = runTest {
packageManager.stub {
@@ -456,6 +473,7 @@ class AppListRepositoryTest {
isArchived = true
}
+ // TODO(b/382016780): to be removed after flag cleanup.
val HIDDEN_APEX_APP = ApplicationInfo().apply {
packageName = "hidden.apex.package"
}
diff --git a/packages/SettingsLib/src/com/android/settingslib/applications/AppUtils.java b/packages/SettingsLib/src/com/android/settingslib/applications/AppUtils.java
index c4829951d61a..3390296ef6fc 100644
--- a/packages/SettingsLib/src/com/android/settingslib/applications/AppUtils.java
+++ b/packages/SettingsLib/src/com/android/settingslib/applications/AppUtils.java
@@ -137,6 +137,7 @@ public class AppUtils {
/**
* Returns a boolean indicating whether the given package is a hidden system module
+ * TODO(b/382016780): to be removed after flag cleanup.
*/
public static boolean isHiddenSystemModule(Context context, String packageName) {
return ApplicationsState.getInstance((Application) context.getApplicationContext())
diff --git a/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java b/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java
index fd9a008ee078..4110d536da61 100644
--- a/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java
+++ b/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java
@@ -157,6 +157,7 @@ public class ApplicationsState {
int mCurComputingSizeUserId;
boolean mSessionsChanged;
// Maps all installed modules on the system to whether they're hidden or not.
+ // TODO(b/382016780): to be removed after flag cleanup.
final HashMap<String, Boolean> mSystemModules = new HashMap<>();
// Temporary for dispatching session callbacks. Only touched by main thread.
@@ -226,12 +227,14 @@ public class ApplicationsState {
mRetrieveFlags = PackageManager.MATCH_DISABLED_COMPONENTS |
PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS;
- final List<ModuleInfo> moduleInfos = mPm.getInstalledModules(0 /* flags */);
- for (ModuleInfo info : moduleInfos) {
- mSystemModules.put(info.getPackageName(), info.isHidden());
- if (Flags.provideInfoOfApkInApex()) {
- for (String apkInApexPackageName : info.getApkInApexPackageNames()) {
- mSystemModules.put(apkInApexPackageName, info.isHidden());
+ if (!Flags.removeHiddenModuleUsage()) {
+ final List<ModuleInfo> moduleInfos = mPm.getInstalledModules(0 /* flags */);
+ for (ModuleInfo info : moduleInfos) {
+ mSystemModules.put(info.getPackageName(), info.isHidden());
+ if (Flags.provideInfoOfApkInApex()) {
+ for (String apkInApexPackageName : info.getApkInApexPackageNames()) {
+ mSystemModules.put(apkInApexPackageName, info.isHidden());
+ }
}
}
}
@@ -336,7 +339,7 @@ public class ApplicationsState {
}
mHaveDisabledApps = true;
}
- if (isHiddenModule(info.packageName)) {
+ if (!Flags.removeHiddenModuleUsage() && isHiddenModule(info.packageName)) {
mApplications.remove(i--);
continue;
}
@@ -453,6 +456,7 @@ public class ApplicationsState {
return mHaveInstantApps;
}
+ // TODO(b/382016780): to be removed after flag cleanup.
boolean isHiddenModule(String packageName) {
Boolean isHidden = mSystemModules.get(packageName);
if (isHidden == null) {
@@ -462,6 +466,7 @@ public class ApplicationsState {
return isHidden;
}
+ // TODO(b/382016780): to be removed after flag cleanup.
boolean isSystemModule(String packageName) {
return mSystemModules.containsKey(packageName);
}
@@ -755,7 +760,7 @@ public class ApplicationsState {
Log.i(TAG, "Looking up entry of pkg " + info.packageName + ": " + entry);
}
if (entry == null) {
- if (isHiddenModule(info.packageName)) {
+ if (!Flags.removeHiddenModuleUsage() && isHiddenModule(info.packageName)) {
if (DEBUG) {
Log.i(TAG, "No AppEntry for " + info.packageName + " (hidden module)");
}
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidDeviceManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidDeviceManager.java
index b2c279466ee4..e05f0a1bcde0 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidDeviceManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidDeviceManager.java
@@ -483,14 +483,18 @@ public class HearingAidDeviceManager {
void onActiveDeviceChanged(CachedBluetoothDevice device) {
if (FeatureFlagUtils.isEnabled(mContext, FeatureFlagUtils.SETTINGS_AUDIO_ROUTING)) {
- if (device.isConnectedHearingAidDevice()) {
+ if (device.isConnectedHearingAidDevice()
+ && (device.isActiveDevice(BluetoothProfile.HEARING_AID)
+ || device.isActiveDevice(BluetoothProfile.LE_AUDIO))) {
setAudioRoutingConfig(device);
} else {
clearAudioRoutingConfig();
}
}
if (com.android.settingslib.flags.Flags.hearingDevicesInputRoutingControl()) {
- if (device.isConnectedHearingAidDevice()) {
+ if (device.isConnectedHearingAidDevice()
+ && (device.isActiveDevice(BluetoothProfile.HEARING_AID)
+ || device.isActiveDevice(BluetoothProfile.LE_AUDIO))) {
setMicrophoneForCalls(device);
} else {
clearMicrophoneForCalls();
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LeAudioProfile.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LeAudioProfile.java
index 6be4336178eb..155c7e6530aa 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LeAudioProfile.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LeAudioProfile.java
@@ -21,6 +21,7 @@ import static android.bluetooth.BluetoothAdapter.ACTIVE_DEVICE_ALL;
import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_ALLOWED;
import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_FORBIDDEN;
+import android.annotation.CallbackExecutor;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothClass;
import android.bluetooth.BluetoothCsipSetCoordinator;
@@ -39,6 +40,7 @@ import com.android.settingslib.R;
import java.util.ArrayList;
import java.util.List;
+import java.util.concurrent.Executor;
public class LeAudioProfile implements LocalBluetoothProfile {
private static final String TAG = "LeAudioProfile";
@@ -317,6 +319,78 @@ public class LeAudioProfile implements LocalBluetoothProfile {
return mService.getAudioLocation(device);
}
+ /**
+ * Sets the fallback group id when broadcast switches to unicast.
+ *
+ * @param groupId the target fallback group id
+ */
+ public void setBroadcastToUnicastFallbackGroup(int groupId) {
+ if (mService == null) {
+ Log.w(TAG, "Proxy not attached to service. Cannot set fallback group: " + groupId);
+ return;
+ }
+
+ mService.setBroadcastToUnicastFallbackGroup(groupId);
+ }
+
+ /**
+ * Gets the fallback group id when broadcast switches to unicast.
+ *
+ * @return current fallback group id
+ */
+ public int getBroadcastToUnicastFallbackGroup() {
+ if (mService == null) {
+ Log.w(TAG, "Proxy not attached to service. Cannot get fallback group.");
+ return BluetoothCsipSetCoordinator.GROUP_ID_INVALID;
+ }
+ return mService.getBroadcastToUnicastFallbackGroup();
+ }
+
+ /**
+ * Registers a {@link BluetoothLeAudio.Callback} that will be invoked during the
+ * operation of this profile.
+ *
+ * Repeated registration of the same <var>callback</var> object after the first call to this
+ * method will result with IllegalArgumentException being thrown, even when the
+ * <var>executor</var> is different. API caller would have to call
+ * {@link #unregisterCallback(BluetoothLeAudio.Callback)} with the same callback object
+ * before registering it again.
+ *
+ * @param executor an {@link Executor} to execute given callback
+ * @param callback user implementation of the {@link BluetoothLeAudio.Callback}
+ * @throws NullPointerException if a null executor, or callback is given, or
+ * IllegalArgumentException if the same <var>callback</var> is
+ * already registered.
+ */
+ public void registerCallback(
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull BluetoothLeAudio.Callback callback) {
+ if (mService == null) {
+ Log.w(TAG, "Proxy not attached to service. Cannot register callback.");
+ return;
+ }
+ mService.registerCallback(executor, callback);
+ }
+
+ /**
+ * Unregisters the specified {@link BluetoothLeAudio.Callback}.
+ * <p>The same {@link BluetoothLeAudio.Callback} object used when calling
+ * {@link #registerCallback(Executor, BluetoothLeAudio.Callback)} must be used.
+ *
+ * <p>Callbacks are automatically unregistered when application process goes away
+ *
+ * @param callback user implementation of the {@link BluetoothLeAudio.Callback}
+ * @throws NullPointerException when callback is null or IllegalArgumentException when no
+ * callback is registered
+ */
+ public void unregisterCallback(@NonNull BluetoothLeAudio.Callback callback) {
+ if (mService == null) {
+ Log.w(TAG, "Proxy not attached to service. Cannot unregister callback.");
+ return;
+ }
+ mService.unregisterCallback(callback);
+ }
+
@RequiresApi(Build.VERSION_CODES.S)
protected void finalize() {
if (DEBUG) {
diff --git a/packages/SettingsLib/src/com/android/settingslib/dream/DreamBackend.java b/packages/SettingsLib/src/com/android/settingslib/dream/DreamBackend.java
index dc40304ba24a..51259e2f311d 100644
--- a/packages/SettingsLib/src/com/android/settingslib/dream/DreamBackend.java
+++ b/packages/SettingsLib/src/com/android/settingslib/dream/DreamBackend.java
@@ -16,6 +16,8 @@
package com.android.settingslib.dream;
+import static android.service.dreams.Flags.allowDreamWhenPostured;
+
import android.annotation.IntDef;
import android.content.ComponentName;
import android.content.Context;
@@ -78,14 +80,21 @@ public class DreamBackend {
}
@Retention(RetentionPolicy.SOURCE)
- @IntDef({WHILE_CHARGING, WHILE_DOCKED, EITHER, NEVER})
+ @IntDef({
+ WHILE_CHARGING,
+ WHILE_DOCKED,
+ WHILE_POSTURED,
+ WHILE_CHARGING_OR_DOCKED,
+ NEVER
+ })
public @interface WhenToDream {
}
public static final int WHILE_CHARGING = 0;
public static final int WHILE_DOCKED = 1;
- public static final int EITHER = 2;
- public static final int NEVER = 3;
+ public static final int WHILE_POSTURED = 2;
+ public static final int WHILE_CHARGING_OR_DOCKED = 3;
+ public static final int NEVER = 4;
/**
* The type of dream complications which can be provided by a
@@ -134,6 +143,8 @@ public class DreamBackend {
.DREAM_SETTING_CHANGED__WHEN_TO_DREAM__WHEN_TO_DREAM_WHILE_CHARGING_ONLY;
private static final int WHEN_TO_DREAM_DOCKED = FrameworkStatsLog
.DREAM_SETTING_CHANGED__WHEN_TO_DREAM__WHEN_TO_DREAM_WHILE_DOCKED_ONLY;
+ private static final int WHEN_TO_DREAM_POSTURED = FrameworkStatsLog
+ .DREAM_SETTING_CHANGED__WHEN_TO_DREAM__WHEN_TO_DREAM_WHILE_POSTURED_ONLY;
private static final int WHEN_TO_DREAM_CHARGING_OR_DOCKED = FrameworkStatsLog
.DREAM_SETTING_CHANGED__WHEN_TO_DREAM__WHEN_TO_DREAM_EITHER_CHARGING_OR_DOCKED;
@@ -143,6 +154,7 @@ public class DreamBackend {
private final boolean mDreamsEnabledByDefault;
private final boolean mDreamsActivatedOnSleepByDefault;
private final boolean mDreamsActivatedOnDockByDefault;
+ private final boolean mDreamsActivatedOnPosturedByDefault;
private final Set<ComponentName> mDisabledDreams;
private final List<String> mLoggableDreamPrefixes;
private Set<Integer> mSupportedComplications;
@@ -168,6 +180,8 @@ public class DreamBackend {
com.android.internal.R.bool.config_dreamsActivatedOnSleepByDefault);
mDreamsActivatedOnDockByDefault = resources.getBoolean(
com.android.internal.R.bool.config_dreamsActivatedOnDockByDefault);
+ mDreamsActivatedOnPosturedByDefault = resources.getBoolean(
+ com.android.internal.R.bool.config_dreamsActivatedOnPosturedByDefault);
mDisabledDreams = Arrays.stream(resources.getStringArray(
com.android.internal.R.array.config_disabledDreamComponents))
.map(ComponentName::unflattenFromString)
@@ -280,10 +294,11 @@ public class DreamBackend {
@WhenToDream
public int getWhenToDreamSetting() {
- return isActivatedOnDock() && isActivatedOnSleep() ? EITHER
+ return isActivatedOnDock() && isActivatedOnSleep() ? WHILE_CHARGING_OR_DOCKED
: isActivatedOnDock() ? WHILE_DOCKED
- : isActivatedOnSleep() ? WHILE_CHARGING
- : NEVER;
+ : isActivatedOnPostured() ? WHILE_POSTURED
+ : isActivatedOnSleep() ? WHILE_CHARGING
+ : NEVER;
}
public void setWhenToDream(@WhenToDream int whenToDream) {
@@ -293,16 +308,25 @@ public class DreamBackend {
case WHILE_CHARGING:
setActivatedOnDock(false);
setActivatedOnSleep(true);
+ setActivatedOnPostured(false);
break;
case WHILE_DOCKED:
setActivatedOnDock(true);
setActivatedOnSleep(false);
+ setActivatedOnPostured(false);
break;
- case EITHER:
+ case WHILE_CHARGING_OR_DOCKED:
setActivatedOnDock(true);
setActivatedOnSleep(true);
+ setActivatedOnPostured(false);
+ break;
+
+ case WHILE_POSTURED:
+ setActivatedOnPostured(true);
+ setActivatedOnSleep(false);
+ setActivatedOnDock(false);
break;
case NEVER:
@@ -407,6 +431,22 @@ public class DreamBackend {
setBoolean(Settings.Secure.SCREENSAVER_ACTIVATE_ON_SLEEP, value);
}
+ public boolean isActivatedOnPostured() {
+ return allowDreamWhenPostured()
+ && getBoolean(Settings.Secure.SCREENSAVER_ACTIVATE_ON_POSTURED,
+ mDreamsActivatedOnPosturedByDefault);
+ }
+
+ /**
+ * Sets whether dreams should be activated when the device is postured (stationary and upright)
+ */
+ public void setActivatedOnPostured(boolean value) {
+ if (allowDreamWhenPostured()) {
+ logd("setActivatedOnPostured(%s)", value);
+ setBoolean(Settings.Secure.SCREENSAVER_ACTIVATE_ON_POSTURED, value);
+ }
+ }
+
private boolean getBoolean(String key, boolean def) {
return Settings.Secure.getInt(mContext.getContentResolver(), key, def ? 1 : 0) == 1;
}
@@ -548,7 +588,9 @@ public class DreamBackend {
return WHEN_TO_DREAM_CHARGING;
case WHILE_DOCKED:
return WHEN_TO_DREAM_DOCKED;
- case EITHER:
+ case WHILE_POSTURED:
+ return WHEN_TO_DREAM_POSTURED;
+ case WHILE_CHARGING_OR_DOCKED:
return WHEN_TO_DREAM_CHARGING_OR_DOCKED;
case NEVER:
default:
diff --git a/packages/SettingsLib/src/com/android/settingslib/enterprise/ActionDisabledByAdminControllerFactory.java b/packages/SettingsLib/src/com/android/settingslib/enterprise/ActionDisabledByAdminControllerFactory.java
index 7516d2e6ab1b..e3d7902f34b2 100644
--- a/packages/SettingsLib/src/com/android/settingslib/enterprise/ActionDisabledByAdminControllerFactory.java
+++ b/packages/SettingsLib/src/com/android/settingslib/enterprise/ActionDisabledByAdminControllerFactory.java
@@ -22,6 +22,7 @@ import static com.android.settingslib.enterprise.ActionDisabledLearnMoreButtonLa
import static com.android.settingslib.enterprise.ManagedDeviceActionDisabledByAdminController.DEFAULT_FOREGROUND_USER_CHECKER;
import android.app.admin.DevicePolicyManager;
+import android.app.supervision.SupervisionManager;
import android.content.ComponentName;
import android.content.Context;
import android.hardware.biometrics.BiometricAuthenticator;
@@ -59,12 +60,18 @@ public final class ActionDisabledByAdminControllerFactory {
}
private static boolean isSupervisedDevice(Context context) {
- DevicePolicyManager devicePolicyManager =
- context.getSystemService(DevicePolicyManager.class);
- ComponentName supervisionComponent =
- devicePolicyManager.getProfileOwnerOrDeviceOwnerSupervisionComponent(
- new UserHandle(UserHandle.myUserId()));
- return supervisionComponent != null;
+ if (android.app.supervision.flags.Flags.deprecateDpmSupervisionApis()) {
+ SupervisionManager supervisionManager =
+ context.getSystemService(SupervisionManager.class);
+ return supervisionManager.isSupervisionEnabledForUser(UserHandle.myUserId());
+ } else {
+ DevicePolicyManager devicePolicyManager =
+ context.getSystemService(DevicePolicyManager.class);
+ ComponentName supervisionComponent =
+ devicePolicyManager.getProfileOwnerOrDeviceOwnerSupervisionComponent(
+ new UserHandle(UserHandle.myUserId()));
+ return supervisionComponent != null;
+ }
}
/**
diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/FakeZenModeRepository.kt b/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/FakeZenModeRepository.kt
index 496c3e6c74cc..9aaefe47fda2 100644
--- a/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/FakeZenModeRepository.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/FakeZenModeRepository.kt
@@ -37,7 +37,8 @@ class FakeZenModeRepository : ZenModeRepository {
override val globalZenMode: StateFlow<Int>
get() = mutableZenMode.asStateFlow()
- private val mutableModesFlow: MutableStateFlow<List<ZenMode>> = MutableStateFlow(listOf())
+ private val mutableModesFlow: MutableStateFlow<List<ZenMode>> =
+ MutableStateFlow(listOf(TestModeBuilder.MANUAL_DND))
override val modes: Flow<List<ZenMode>>
get() = mutableModesFlow.asStateFlow()
@@ -65,8 +66,11 @@ class FakeZenModeRepository : ZenModeRepository {
mutableModesFlow.value += mode
}
- fun addMode(id: String, @AutomaticZenRule.Type type: Int = AutomaticZenRule.TYPE_UNKNOWN,
- active: Boolean = false) {
+ fun addMode(
+ id: String,
+ @AutomaticZenRule.Type type: Int = AutomaticZenRule.TYPE_UNKNOWN,
+ active: Boolean = false,
+ ) {
mutableModesFlow.value += newMode(id, type, active)
}
diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/modes/TestModeBuilder.java b/packages/SettingsLib/src/com/android/settingslib/notification/modes/TestModeBuilder.java
index abc163867248..64a2de5025de 100644
--- a/packages/SettingsLib/src/com/android/settingslib/notification/modes/TestModeBuilder.java
+++ b/packages/SettingsLib/src/com/android/settingslib/notification/modes/TestModeBuilder.java
@@ -31,7 +31,6 @@ import android.service.notification.ZenModeConfig;
import android.service.notification.ZenPolicy;
import androidx.annotation.DrawableRes;
-import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.Random;
@@ -44,22 +43,7 @@ public class TestModeBuilder {
private boolean mIsManualDnd;
public static final ZenMode EXAMPLE = new TestModeBuilder().build();
-
- public static final ZenMode MANUAL_DND_ACTIVE = manualDnd(
- INTERRUPTION_FILTER_PRIORITY, true);
-
- public static final ZenMode MANUAL_DND_INACTIVE = manualDnd(
- INTERRUPTION_FILTER_PRIORITY, false);
-
- @NonNull
- public static ZenMode manualDnd(@NotificationManager.InterruptionFilter int filter,
- boolean isActive) {
- return new TestModeBuilder()
- .makeManualDnd()
- .setInterruptionFilter(filter)
- .setActive(isActive)
- .build();
- }
+ public static final ZenMode MANUAL_DND = new TestModeBuilder().makeManualDnd().build();
public TestModeBuilder() {
// Reasonable defaults
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/applications/ApplicationsStateRoboTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/applications/ApplicationsStateRoboTest.java
index 3b18aa310c91..4e821ca50dce 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/applications/ApplicationsStateRoboTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/applications/ApplicationsStateRoboTest.java
@@ -16,6 +16,7 @@
package com.android.settingslib.applications;
+import static android.content.pm.Flags.FLAG_REMOVE_HIDDEN_MODULE_USAGE;
import static android.content.pm.Flags.FLAG_PROVIDE_INFO_OF_APK_IN_APEX;
import static android.os.UserHandle.MU_ENABLED;
import static android.os.UserHandle.USER_SYSTEM;
@@ -59,6 +60,8 @@ import android.os.Handler;
import android.os.RemoteException;
import android.os.UserHandle;
import android.os.UserManager;
+import android.platform.test.annotations.DisableFlags;
+import android.platform.test.annotations.EnableFlags;
import android.platform.test.flag.junit.SetFlagsRule;
import android.text.TextUtils;
import android.util.IconDrawableFactory;
@@ -204,6 +207,7 @@ public class ApplicationsStateRoboTest {
info.setPackageName(packageName);
info.setApkInApexPackageNames(Collections.singletonList(apexPackageName));
// will treat any app with package name that contains "hidden" as hidden module
+ // TODO(b/382016780): to be removed after flag cleanup.
info.setHidden(!TextUtils.isEmpty(packageName) && packageName.contains("hidden"));
return info;
}
@@ -414,6 +418,7 @@ public class ApplicationsStateRoboTest {
}
@Test
+ @DisableFlags({FLAG_REMOVE_HIDDEN_MODULE_USAGE})
public void onResume_shouldNotIncludeSystemHiddenModule() {
mSession.onResume();
@@ -424,6 +429,18 @@ public class ApplicationsStateRoboTest {
}
@Test
+ @EnableFlags({FLAG_REMOVE_HIDDEN_MODULE_USAGE})
+ public void onResume_shouldIncludeSystemModule() {
+ mSession.onResume();
+
+ final List<ApplicationInfo> mApplications = mApplicationsState.mApplications;
+ assertThat(mApplications).hasSize(3);
+ assertThat(mApplications.get(0).packageName).isEqualTo("test.package.1");
+ assertThat(mApplications.get(1).packageName).isEqualTo("test.hidden.module.2");
+ assertThat(mApplications.get(2).packageName).isEqualTo("test.package.3");
+ }
+
+ @Test
public void removeAndInstall_noWorkprofile_doResumeIfNeededLocked_shouldClearEntries()
throws RemoteException {
// scenario: only owner user
@@ -832,6 +849,7 @@ public class ApplicationsStateRoboTest {
mApplicationsState.mEntriesMap.clear();
ApplicationInfo appInfo = createApplicationInfo(PKG_1, /* uid= */ 0);
mApplicationsState.mApplications.add(appInfo);
+ // TODO(b/382016780): to be removed after flag cleanup.
mApplicationsState.mSystemModules.put(PKG_1, /* value= */ false);
assertThat(mApplicationsState.getEntry(PKG_1, /* userId= */ 0).info.packageName)
@@ -839,6 +857,7 @@ public class ApplicationsStateRoboTest {
}
@Test
+ @DisableFlags({FLAG_REMOVE_HIDDEN_MODULE_USAGE})
public void isHiddenModule_hasApkInApexInfo_shouldSupportHiddenApexPackage() {
mSetFlagsRule.enableFlags(FLAG_PROVIDE_INFO_OF_APK_IN_APEX);
ApplicationsState.sInstance = null;
@@ -853,6 +872,7 @@ public class ApplicationsStateRoboTest {
}
@Test
+ @DisableFlags({FLAG_REMOVE_HIDDEN_MODULE_USAGE})
public void isHiddenModule_noApkInApexInfo_onlySupportHiddenModule() {
mSetFlagsRule.disableFlags(FLAG_PROVIDE_INFO_OF_APK_IN_APEX);
ApplicationsState.sInstance = null;
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HearingAidDeviceManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HearingAidDeviceManagerTest.java
index 21dde1fd9411..a215464f66c2 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HearingAidDeviceManagerTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HearingAidDeviceManagerTest.java
@@ -50,6 +50,9 @@ import android.media.AudioDeviceInfo;
import android.media.AudioManager;
import android.media.audiopolicy.AudioProductStrategy;
import android.os.Parcel;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
import android.util.FeatureFlagUtils;
import androidx.test.core.app.ApplicationProvider;
@@ -72,6 +75,8 @@ import java.util.List;
public class HearingAidDeviceManagerTest {
@Rule
public MockitoRule mMockitoRule = MockitoJUnit.rule();
+ @Rule
+ public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
private static final long HISYNCID1 = 10;
private static final long HISYNCID2 = 11;
@@ -736,6 +741,7 @@ public class HearingAidDeviceManagerTest {
@Test
public void onActiveDeviceChanged_connected_callSetStrategies() {
+ when(mCachedDevice1.isConnectedHearingAidDevice()).thenReturn(true);
when(mHelper.getMatchedHearingDeviceAttributesForOutput(mCachedDevice1)).thenReturn(
mHearingDeviceAttribute);
when(mCachedDevice1.isActiveDevice(BluetoothProfile.HEARING_AID)).thenReturn(true);
@@ -750,6 +756,7 @@ public class HearingAidDeviceManagerTest {
@Test
public void onActiveDeviceChanged_disconnected_callSetStrategiesWithAutoValue() {
+ when(mCachedDevice1.isConnectedHearingAidDevice()).thenReturn(false);
when(mHelper.getMatchedHearingDeviceAttributesForOutput(mCachedDevice1)).thenReturn(
mHearingDeviceAttribute);
when(mCachedDevice1.isActiveDevice(BluetoothProfile.HEARING_AID)).thenReturn(false);
@@ -952,6 +959,38 @@ public class HearingAidDeviceManagerTest {
ConnectionStatus.CONNECTED);
}
+ @Test
+ @RequiresFlagsEnabled(
+ com.android.settingslib.flags.Flags.FLAG_HEARING_DEVICES_INPUT_ROUTING_CONTROL)
+ public void onActiveDeviceChanged_activeHearingAidProfile_callSetInputDeviceForCalls() {
+ when(mCachedDevice1.isConnectedHearingAidDevice()).thenReturn(true);
+ when(mCachedDevice1.isActiveDevice(BluetoothProfile.HEARING_AID)).thenReturn(true);
+ when(mDevice1.isMicrophonePreferredForCalls()).thenReturn(true);
+ doReturn(true).when(mHelper).setPreferredDeviceRoutingStrategies(anyList(), any(),
+ anyInt());
+
+ mHearingAidDeviceManager.onActiveDeviceChanged(mCachedDevice1);
+
+ verify(mHelper).setPreferredInputDeviceForCalls(
+ eq(mCachedDevice1), eq(HearingAidAudioRoutingConstants.RoutingValue.AUTO));
+
+ }
+
+ @Test
+ @RequiresFlagsEnabled(
+ com.android.settingslib.flags.Flags.FLAG_HEARING_DEVICES_INPUT_ROUTING_CONTROL)
+ public void onActiveDeviceChanged_notActiveHearingAidProfile_callClearInputDeviceForCalls() {
+ when(mCachedDevice1.isConnectedHearingAidDevice()).thenReturn(true);
+ when(mCachedDevice1.isActiveDevice(BluetoothProfile.HEARING_AID)).thenReturn(false);
+ when(mDevice1.isMicrophonePreferredForCalls()).thenReturn(true);
+ doReturn(true).when(mHelper).setPreferredDeviceRoutingStrategies(anyList(), any(),
+ anyInt());
+
+ mHearingAidDeviceManager.onActiveDeviceChanged(mCachedDevice1);
+
+ verify(mHelper).clearPreferredInputDeviceForCalls();
+ }
+
private HearingAidInfo getLeftAshaHearingAidInfo(long hiSyncId) {
return new HearingAidInfo.Builder()
.setAshaDeviceSide(HearingAidInfo.DeviceSide.SIDE_LEFT)
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/modes/ZenModeTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/modes/ZenModeTest.java
index d08d91d18b27..6b30f159129e 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/modes/ZenModeTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/modes/ZenModeTest.java
@@ -87,7 +87,7 @@ public class ZenModeTest {
@Test
public void testBasicMethods_manualDnd() {
- ZenMode manualMode = TestModeBuilder.MANUAL_DND_INACTIVE;
+ ZenMode manualMode = TestModeBuilder.MANUAL_DND;
assertThat(manualMode.getId()).isEqualTo(ZenMode.MANUAL_DND_MODE_ID);
assertThat(manualMode.isManualDnd()).isTrue();
@@ -271,7 +271,7 @@ public class ZenModeTest {
@Test
public void setInterruptionFilter_manualDnd_throws() {
- ZenMode manualDnd = TestModeBuilder.MANUAL_DND_INACTIVE;
+ ZenMode manualDnd = TestModeBuilder.MANUAL_DND;
assertThrows(IllegalStateException.class,
() -> manualDnd.setInterruptionFilter(INTERRUPTION_FILTER_ALL));
@@ -280,24 +280,46 @@ public class ZenModeTest {
@Test
public void canEditPolicy_onlyFalseForSpecialDnd() {
assertThat(TestModeBuilder.EXAMPLE.canEditPolicy()).isTrue();
- assertThat(TestModeBuilder.MANUAL_DND_ACTIVE.canEditPolicy()).isTrue();
- assertThat(TestModeBuilder.MANUAL_DND_INACTIVE.canEditPolicy()).isTrue();
- ZenMode dndWithAlarms = TestModeBuilder.manualDnd(INTERRUPTION_FILTER_ALARMS, true);
+ ZenMode inactiveDnd = new TestModeBuilder().makeManualDnd().setActive(false).build();
+ assertThat(inactiveDnd.canEditPolicy()).isTrue();
+
+ ZenMode activeDnd = new TestModeBuilder().makeManualDnd().setActive(true).build();
+ assertThat(activeDnd.canEditPolicy()).isTrue();
+
+ ZenMode dndWithAlarms = new TestModeBuilder()
+ .makeManualDnd()
+ .setInterruptionFilter(INTERRUPTION_FILTER_ALARMS)
+ .setActive(true)
+ .build();
assertThat(dndWithAlarms.canEditPolicy()).isFalse();
- ZenMode dndWithNone = TestModeBuilder.manualDnd(INTERRUPTION_FILTER_NONE, true);
+
+ ZenMode dndWithNone = new TestModeBuilder()
+ .makeManualDnd()
+ .setInterruptionFilter(INTERRUPTION_FILTER_NONE)
+ .setActive(true)
+ .build();
assertThat(dndWithNone.canEditPolicy()).isFalse();
// Note: Backend will never return an inactive manual mode with custom filter.
- ZenMode badDndWithAlarms = TestModeBuilder.manualDnd(INTERRUPTION_FILTER_ALARMS, false);
+ ZenMode badDndWithAlarms = new TestModeBuilder()
+ .makeManualDnd()
+ .setInterruptionFilter(INTERRUPTION_FILTER_ALARMS)
+ .setActive(false)
+ .build();
assertThat(badDndWithAlarms.canEditPolicy()).isFalse();
- ZenMode badDndWithNone = TestModeBuilder.manualDnd(INTERRUPTION_FILTER_NONE, false);
+
+ ZenMode badDndWithNone = new TestModeBuilder()
+ .makeManualDnd()
+ .setInterruptionFilter(INTERRUPTION_FILTER_NONE)
+ .setActive(false)
+ .build();
assertThat(badDndWithNone.canEditPolicy()).isFalse();
}
@Test
public void canEditPolicy_whenTrue_allowsSettingPolicyAndEffects() {
- ZenMode normalDnd = TestModeBuilder.manualDnd(INTERRUPTION_FILTER_PRIORITY, true);
+ ZenMode normalDnd = new TestModeBuilder().makeManualDnd().setActive(true).build();
assertThat(normalDnd.canEditPolicy()).isTrue();
@@ -313,7 +335,11 @@ public class ZenModeTest {
@Test
public void canEditPolicy_whenFalse_preventsSettingFilterPolicyOrEffects() {
- ZenMode specialDnd = TestModeBuilder.manualDnd(INTERRUPTION_FILTER_ALARMS, true);
+ ZenMode specialDnd = new TestModeBuilder()
+ .makeManualDnd()
+ .setInterruptionFilter(INTERRUPTION_FILTER_ALARMS)
+ .setActive(true)
+ .build();
assertThat(specialDnd.canEditPolicy()).isFalse();
assertThrows(IllegalStateException.class,
@@ -324,7 +350,7 @@ public class ZenModeTest {
@Test
public void comparator_prioritizes() {
- ZenMode manualDnd = TestModeBuilder.MANUAL_DND_INACTIVE;
+ ZenMode manualDnd = TestModeBuilder.MANUAL_DND;
ZenMode driving1 = new TestModeBuilder().setName("b1").setType(TYPE_DRIVING).build();
ZenMode driving2 = new TestModeBuilder().setName("b2").setType(TYPE_DRIVING).build();
ZenMode bedtime1 = new TestModeBuilder().setName("c1").setType(TYPE_BEDTIME).build();
@@ -403,7 +429,7 @@ public class ZenModeTest {
@Test
public void getIconKey_manualDnd_isDndIcon() {
- ZenIcon.Key iconKey = TestModeBuilder.MANUAL_DND_INACTIVE.getIconKey();
+ ZenIcon.Key iconKey = TestModeBuilder.MANUAL_DND.getIconKey();
assertThat(iconKey.resPackage()).isNull();
assertThat(iconKey.resId()).isEqualTo(
diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
index dd28402d705f..7b4a2ca5de39 100644
--- a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
+++ b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
@@ -155,6 +155,7 @@ public class SecureSettings {
Settings.Secure.SCREENSAVER_COMPONENTS,
Settings.Secure.SCREENSAVER_ACTIVATE_ON_DOCK,
Settings.Secure.SCREENSAVER_ACTIVATE_ON_SLEEP,
+ Settings.Secure.SCREENSAVER_ACTIVATE_ON_POSTURED,
Settings.Secure.SCREENSAVER_HOME_CONTROLS_ENABLED,
Settings.Secure.SHOW_FIRST_CRASH_DIALOG_DEV_OPTION,
Settings.Secure.VOLUME_DIALOG_DISMISS_TIMEOUT,
diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
index b01f6229af16..b0309a8fa5a5 100644
--- a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
+++ b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
@@ -230,6 +230,7 @@ public class SecureSettingsValidators {
VALIDATORS.put(Secure.SCREENSAVER_COMPONENTS, COMMA_SEPARATED_COMPONENT_LIST_VALIDATOR);
VALIDATORS.put(Secure.SCREENSAVER_ACTIVATE_ON_DOCK, BOOLEAN_VALIDATOR);
VALIDATORS.put(Secure.SCREENSAVER_ACTIVATE_ON_SLEEP, BOOLEAN_VALIDATOR);
+ VALIDATORS.put(Secure.SCREENSAVER_ACTIVATE_ON_POSTURED, BOOLEAN_VALIDATOR);
VALIDATORS.put(Secure.SCREENSAVER_HOME_CONTROLS_ENABLED, BOOLEAN_VALIDATOR);
VALIDATORS.put(Secure.SHOW_FIRST_CRASH_DIALOG_DEV_OPTION, BOOLEAN_VALIDATOR);
VALIDATORS.put(Secure.VOLUME_DIALOG_DISMISS_TIMEOUT, NON_NEGATIVE_INTEGER_VALIDATOR);
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupAgent.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupAgent.java
index f79a60f5be96..c1c3e04d46fd 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupAgent.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupAgent.java
@@ -212,6 +212,8 @@ public class SettingsBackupAgent extends BackupAgentHelper {
private static final String ERROR_IO_EXCEPTION = "io_exception";
private static final String ERROR_FAILED_TO_RESTORE_SOFTAP_CONFIG =
"failed_to_restore_softap_config";
+ private static final String ERROR_FAILED_TO_RESTORE_WIFI_CONFIG =
+ "failed_to_restore_wifi_config";
// Name of the temporary file we use during full backup/restore. This is
@@ -1455,8 +1457,14 @@ public class SettingsBackupAgent extends BackupAgentHelper {
return baos.toByteArray();
}
- private byte[] getNewWifiConfigData() {
- return mWifiManager.retrieveBackupData();
+ @VisibleForTesting
+ byte[] getNewWifiConfigData() {
+ byte[] data = mWifiManager.retrieveBackupData();
+ if (areAgentMetricsEnabled) {
+ // We're unable to determine how many settings this includes, so we'll just log 1.
+ numberOfSettingsPerKey.put(KEY_WIFI_NEW_CONFIG, 1);
+ }
+ return data;
}
private byte[] getLocaleSettings() {
@@ -1468,11 +1476,22 @@ public class SettingsBackupAgent extends BackupAgentHelper {
return localeList.toLanguageTags().getBytes();
}
- private void restoreNewWifiConfigData(byte[] bytes) {
+ @VisibleForTesting
+ void restoreNewWifiConfigData(byte[] bytes) {
if (DEBUG_BACKUP) {
Log.v(TAG, "Applying restored wifi data");
}
- mWifiManager.restoreBackupData(bytes);
+ if (areAgentMetricsEnabled) {
+ try {
+ mWifiManager.restoreBackupData(bytes);
+ mBackupRestoreEventLogger.logItemsRestored(KEY_WIFI_NEW_CONFIG, /* count= */ 1);
+ } catch (Exception e) {
+ mBackupRestoreEventLogger.logItemsRestoreFailed(
+ KEY_WIFI_NEW_CONFIG, /* count= */ 1, ERROR_FAILED_TO_RESTORE_WIFI_CONFIG);
+ }
+ } else {
+ mWifiManager.restoreBackupData(bytes);
+ }
}
private void restoreNetworkPolicies(byte[] data) {
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java
index 5ad4b8a6dffe..1c6d6816e9b4 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java
@@ -2550,6 +2550,9 @@ class SettingsProtoDumpUtil {
dumpSetting(s, p,
Settings.Secure.SCREENSAVER_DEFAULT_COMPONENT,
SecureSettingsProto.Screensaver.DEFAULT_COMPONENT);
+ dumpSetting(s, p,
+ Settings.Secure.SCREENSAVER_ACTIVATE_ON_POSTURED,
+ SecureSettingsProto.Screensaver.ACTIVATE_ON_POSTURED);
p.end(screensaverToken);
final long searchToken = p.start(SecureSettingsProto.SEARCH);
diff --git a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsBackupAgentTest.java b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsBackupAgentTest.java
index 95dd0db40c0e..6e5b602c02c5 100644
--- a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsBackupAgentTest.java
+++ b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsBackupAgentTest.java
@@ -16,6 +16,7 @@
package com.android.providers.settings;
+import static com.android.providers.settings.SettingsBackupRestoreKeys.KEY_WIFI_NEW_CONFIG;
import static com.android.providers.settings.SettingsBackupRestoreKeys.KEY_SOFTAP_CONFIG;
import static junit.framework.Assert.assertEquals;
@@ -28,6 +29,8 @@ import static org.junit.Assert.assertArrayEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.when;
import android.annotation.Nullable;
@@ -69,6 +72,7 @@ import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
+import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
@@ -834,6 +838,74 @@ public class SettingsBackupAgentTest extends BaseSettingsProviderTest {
assertNull(getLoggingResultForDatatype(KEY_SOFTAP_CONFIG, mAgentUnderTest));
}
+ @Test
+ @EnableFlags(com.android.server.backup.Flags.FLAG_ENABLE_METRICS_SETTINGS_BACKUP_AGENTS)
+ public void getNewWifiConfigData_flagIsEnabled_numberOfSettingsInKeyAreRecorded() {
+ mAgentUnderTest.onCreate(
+ UserHandle.SYSTEM, BackupDestination.CLOUD, OperationType.BACKUP);
+ when(mWifiManager.retrieveBackupData()).thenReturn(null);
+
+ mAgentUnderTest.getNewWifiConfigData();
+
+ assertEquals(mAgentUnderTest.getNumberOfSettingsPerKey(KEY_WIFI_NEW_CONFIG), 1);
+ }
+
+ @Test
+ @DisableFlags(com.android.server.backup.Flags.FLAG_ENABLE_METRICS_SETTINGS_BACKUP_AGENTS)
+ public void getNewWifiConfigData_flagIsNotEnabled_numberOfSettingsInKeyAreNotRecorded() {
+ mAgentUnderTest.onCreate(
+ UserHandle.SYSTEM, BackupDestination.CLOUD, OperationType.BACKUP);
+ when(mWifiManager.retrieveBackupData()).thenReturn(null);
+
+ mAgentUnderTest.getNewWifiConfigData();
+
+ assertEquals(mAgentUnderTest.getNumberOfSettingsPerKey(KEY_WIFI_NEW_CONFIG), 0);
+ }
+
+ @Test
+ @EnableFlags(com.android.server.backup.Flags.FLAG_ENABLE_METRICS_SETTINGS_BACKUP_AGENTS)
+ public void
+ restoreNewWifiConfigData_flagIsEnabled_restoreIsSuccessful_successMetricsAreLogged() {
+ mAgentUnderTest.onCreate(
+ UserHandle.SYSTEM, BackupDestination.CLOUD, OperationType.RESTORE);
+ doNothing().when(mWifiManager).restoreBackupData(any());
+
+ mAgentUnderTest.restoreNewWifiConfigData(new byte[] {});
+
+ DataTypeResult loggingResult =
+ getLoggingResultForDatatype(KEY_WIFI_NEW_CONFIG, mAgentUnderTest);
+ assertNotNull(loggingResult);
+ assertEquals(loggingResult.getSuccessCount(), 1);
+ }
+
+ @Test
+ @EnableFlags(com.android.server.backup.Flags.FLAG_ENABLE_METRICS_SETTINGS_BACKUP_AGENTS)
+ public void
+ restoreNewWifiConfigData_flagIsEnabled_restoreIsNotSuccessful_failureMetricsAreLogged() {
+ mAgentUnderTest.onCreate(
+ UserHandle.SYSTEM, BackupDestination.CLOUD, OperationType.RESTORE);
+ doThrow(new RuntimeException()).when(mWifiManager).restoreBackupData(any());
+
+ mAgentUnderTest.restoreNewWifiConfigData(new byte[] {});
+
+ DataTypeResult loggingResult =
+ getLoggingResultForDatatype(KEY_WIFI_NEW_CONFIG, mAgentUnderTest);
+ assertNotNull(loggingResult);
+ assertEquals(loggingResult.getFailCount(), 1);
+ }
+
+ @Test
+ @DisableFlags(com.android.server.backup.Flags.FLAG_ENABLE_METRICS_SETTINGS_BACKUP_AGENTS)
+ public void restoreNewWifiConfigData_flagIsNotEnabled_metricsAreNotLogged() {
+ mAgentUnderTest.onCreate(
+ UserHandle.SYSTEM, BackupDestination.CLOUD, OperationType.RESTORE);
+ doNothing().when(mWifiManager).restoreBackupData(any());
+
+ mAgentUnderTest.restoreNewWifiConfigData(new byte[] {});
+
+ assertNull(getLoggingResultForDatatype(KEY_WIFI_NEW_CONFIG, mAgentUnderTest));
+ }
+
private byte[] generateBackupData(Map<String, String> keyValueData) {
int totalBytes = 0;
for (String key : keyValueData.keySet()) {
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp
index 9adc95a01216..6b2449fdaa49 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -92,7 +92,6 @@ filegroup {
"tests/src/**/systemui/shade/NotificationShadeWindowViewControllerTest.kt",
"tests/src/**/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorSceneContainerTest.kt",
"tests/src/**/systemui/statusbar/pipeline/mobile/ui/model/SignalIconModelParameterizedTest.kt",
- "tests/src/**/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorTest.kt",
"tests/src/**/systemui/biometrics/udfps/SinglePointerTouchProcessorTest.kt",
"tests/src/**/systemui/animation/back/FlingOnBackAnimationCallbackTest.kt",
"tests/src/**/systemui/education/domain/ui/view/ContextualEduDialogTest.kt",
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index 7d5fd903c01b..9982710737ce 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -424,24 +424,6 @@ flag {
}
flag {
- name: "status_bar_use_repos_for_call_chip"
- namespace: "systemui"
- description: "Use repositories as the source of truth for call notifications shown as a chip in"
- "the status bar"
- bug: "328584859"
- metadata {
- purpose: PURPOSE_BUGFIX
- }
-}
-
-flag {
- name: "status_bar_call_chip_notification_icon"
- namespace: "systemui"
- description: "Use the small icon set on the notification for the status bar call chip"
- bug: "354930838"
-}
-
-flag {
name: "status_bar_signal_policy_refactor"
namespace: "systemui"
description: "Use a settings observer for airplane mode and make StatusBarSignalPolicy startable"
@@ -1344,6 +1326,13 @@ flag {
}
flag {
+ name: "output_switcher_redesign"
+ namespace: "systemui"
+ description: "Enables visual update for Media Output Switcher"
+ bug: "388296370"
+}
+
+flag {
namespace: "systemui"
name: "enable_view_capture_tracing"
description: "Enables view capture tracing in System UI."
@@ -1806,16 +1795,6 @@ flag {
}
flag {
- name: "disable_shade_expands_on_trackpad_two_finger_swipe"
- namespace: "systemui"
- description: "Disables expansion of the shade via two finger swipe on a trackpad"
- bug: "356804470"
- metadata {
- purpose: PURPOSE_BUGFIX
- }
-}
-
-flag {
name: "keyboard_shortcut_helper_shortcut_customizer"
namespace: "systemui"
description: "An implementation of shortcut customizations through shortcut helper."
@@ -1916,6 +1895,16 @@ flag {
}
flag {
+ name: "disable_shade_trackpad_two_finger_swipe"
+ namespace: "systemui"
+ description: "Disables expansion of the shade via two finger swipe on a trackpad"
+ bug: "356804470"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+flag {
name: "notification_magic_actions_treatment"
namespace: "systemui"
description: "Special UI treatment for magic actions"
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/gesture/NestedDraggable.kt b/packages/SystemUI/compose/core/src/com/android/compose/gesture/NestedDraggable.kt
index 5f1f588bb2b5..a27bf8af1806 100644
--- a/packages/SystemUI/compose/core/src/com/android/compose/gesture/NestedDraggable.kt
+++ b/packages/SystemUI/compose/core/src/com/android/compose/gesture/NestedDraggable.kt
@@ -34,6 +34,7 @@ import androidx.compose.ui.input.nestedscroll.nestedScrollModifierNode
import androidx.compose.ui.input.pointer.AwaitPointerEventScope
import androidx.compose.ui.input.pointer.PointerEvent
import androidx.compose.ui.input.pointer.PointerEventPass
+import androidx.compose.ui.input.pointer.PointerEventType
import androidx.compose.ui.input.pointer.PointerId
import androidx.compose.ui.input.pointer.PointerInputChange
import androidx.compose.ui.input.pointer.PointerInputScope
@@ -146,21 +147,66 @@ private data class NestedDraggableElement(
private val orientation: Orientation,
private val overscrollEffect: OverscrollEffect?,
private val enabled: Boolean,
-) : ModifierNodeElement<NestedDraggableNode>() {
- override fun create(): NestedDraggableNode {
- return NestedDraggableNode(draggable, orientation, overscrollEffect, enabled)
+) : ModifierNodeElement<NestedDraggableRootNode>() {
+ override fun create(): NestedDraggableRootNode {
+ return NestedDraggableRootNode(draggable, orientation, overscrollEffect, enabled)
}
- override fun update(node: NestedDraggableNode) {
+ override fun update(node: NestedDraggableRootNode) {
node.update(draggable, orientation, overscrollEffect, enabled)
}
}
+/**
+ * A root node on top of [NestedDraggableNode] so that no [PointerInputModifierNode] is installed
+ * when this draggable is disabled.
+ */
+private class NestedDraggableRootNode(
+ draggable: NestedDraggable,
+ orientation: Orientation,
+ overscrollEffect: OverscrollEffect?,
+ enabled: Boolean,
+) : DelegatingNode() {
+ private var delegateNode =
+ if (enabled) create(draggable, orientation, overscrollEffect) else null
+
+ fun update(
+ draggable: NestedDraggable,
+ orientation: Orientation,
+ overscrollEffect: OverscrollEffect?,
+ enabled: Boolean,
+ ) {
+ // Disabled.
+ if (!enabled) {
+ delegateNode?.let { undelegate(it) }
+ delegateNode = null
+ return
+ }
+
+ // Disabled => Enabled.
+ val nullableDelegate = delegateNode
+ if (nullableDelegate == null) {
+ delegateNode = create(draggable, orientation, overscrollEffect)
+ return
+ }
+
+ // Enabled => Enabled (update).
+ nullableDelegate.update(draggable, orientation, overscrollEffect)
+ }
+
+ private fun create(
+ draggable: NestedDraggable,
+ orientation: Orientation,
+ overscrollEffect: OverscrollEffect?,
+ ): NestedDraggableNode {
+ return delegate(NestedDraggableNode(draggable, orientation, overscrollEffect))
+ }
+}
+
private class NestedDraggableNode(
private var draggable: NestedDraggable,
override var orientation: Orientation,
private var overscrollEffect: OverscrollEffect?,
- private var enabled: Boolean,
) :
DelegatingNode(),
PointerInputModifierNode,
@@ -168,17 +214,11 @@ private class NestedDraggableNode(
CompositionLocalConsumerModifierNode,
OrientationAware {
private val nestedScrollDispatcher = NestedScrollDispatcher()
- private var trackDownPositionDelegate: SuspendingPointerInputModifierNode? = null
- set(value) {
- field?.let { undelegate(it) }
- field = value?.also { delegate(it) }
- }
-
- private var detectDragsDelegate: SuspendingPointerInputModifierNode? = null
- set(value) {
- field?.let { undelegate(it) }
- field = value?.also { delegate(it) }
- }
+ private val trackWheelScroll =
+ delegate(SuspendingPointerInputModifierNode { trackWheelScroll() })
+ private val trackDownPositionDelegate =
+ delegate(SuspendingPointerInputModifierNode { trackDownPosition() })
+ private val detectDragsDelegate = delegate(SuspendingPointerInputModifierNode { detectDrags() })
/** The controller created by the nested scroll logic (and *not* the drag logic). */
private var nestedScrollController: NestedScrollController? = null
@@ -189,6 +229,7 @@ private class NestedDraggableNode(
* This is use to track the started position of a drag started on a nested scrollable.
*/
private var lastFirstDown: Offset? = null
+ private var lastEventWasScrollWheel: Boolean = false
/** The pointers currently down, in order of which they were done and mapping to their type. */
private val pointersDown = linkedMapOf<PointerId, PointerType>()
@@ -206,23 +247,25 @@ private class NestedDraggableNode(
draggable: NestedDraggable,
orientation: Orientation,
overscrollEffect: OverscrollEffect?,
- enabled: Boolean,
) {
+ if (
+ draggable == this.draggable &&
+ orientation == this.orientation &&
+ overscrollEffect == this.overscrollEffect
+ ) {
+ return
+ }
+
this.draggable = draggable
this.orientation = orientation
this.overscrollEffect = overscrollEffect
- this.enabled = enabled
- trackDownPositionDelegate?.resetPointerInputHandler()
- detectDragsDelegate?.resetPointerInputHandler()
+ trackWheelScroll.resetPointerInputHandler()
+ trackDownPositionDelegate.resetPointerInputHandler()
+ detectDragsDelegate.resetPointerInputHandler()
+
nestedScrollController?.ensureOnDragStoppedIsCalled()
nestedScrollController = null
-
- if (!enabled && trackDownPositionDelegate != null) {
- check(detectDragsDelegate != null)
- trackDownPositionDelegate = null
- detectDragsDelegate = null
- }
}
override fun onPointerEvent(
@@ -230,21 +273,15 @@ private class NestedDraggableNode(
pass: PointerEventPass,
bounds: IntSize,
) {
- if (!enabled) return
-
- if (trackDownPositionDelegate == null) {
- check(detectDragsDelegate == null)
- trackDownPositionDelegate = SuspendingPointerInputModifierNode { trackDownPosition() }
- detectDragsDelegate = SuspendingPointerInputModifierNode { detectDrags() }
- }
-
- checkNotNull(trackDownPositionDelegate).onPointerEvent(pointerEvent, pass, bounds)
- checkNotNull(detectDragsDelegate).onPointerEvent(pointerEvent, pass, bounds)
+ trackWheelScroll.onPointerEvent(pointerEvent, pass, bounds)
+ trackDownPositionDelegate.onPointerEvent(pointerEvent, pass, bounds)
+ detectDragsDelegate.onPointerEvent(pointerEvent, pass, bounds)
}
override fun onCancelPointerInput() {
- trackDownPositionDelegate?.onCancelPointerInput()
- detectDragsDelegate?.onCancelPointerInput()
+ trackWheelScroll.onCancelPointerInput()
+ trackDownPositionDelegate.onCancelPointerInput()
+ detectDragsDelegate.onCancelPointerInput()
}
/*
@@ -407,7 +444,7 @@ private class NestedDraggableNode(
val left = available - consumed
val postConsumed =
nestedScrollDispatcher.dispatchPostScroll(
- consumed = preConsumed + consumed,
+ consumed = consumed,
available = left,
source = NestedScrollSource.UserInput,
)
@@ -445,10 +482,9 @@ private class NestedDraggableNode(
val available = velocity - preConsumed
val consumed = performFling(available)
val left = available - consumed
- return nestedScrollDispatcher.dispatchPostFling(
- consumed = consumed + preConsumed,
- available = left,
- )
+ val postConsumed =
+ nestedScrollDispatcher.dispatchPostFling(consumed = consumed, available = left)
+ return preConsumed + consumed + postConsumed
}
/*
@@ -457,6 +493,13 @@ private class NestedDraggableNode(
* ===============================
*/
+ private suspend fun PointerInputScope.trackWheelScroll() {
+ awaitEachGesture {
+ val event = awaitPointerEvent(pass = PointerEventPass.Initial)
+ lastEventWasScrollWheel = event.type == PointerEventType.Scroll
+ }
+ }
+
private suspend fun PointerInputScope.trackDownPosition() {
awaitEachGesture {
try {
@@ -501,8 +544,14 @@ private class NestedDraggableNode(
}
val sign = offset.sign
- if (nestedScrollController == null && draggable.shouldConsumeNestedScroll(sign)) {
- val startedPosition = checkNotNull(lastFirstDown) { "lastFirstDown is not set" }
+ if (
+ nestedScrollController == null &&
+ // TODO(b/388231324): Remove this.
+ !lastEventWasScrollWheel &&
+ draggable.shouldConsumeNestedScroll(sign) &&
+ lastFirstDown != null
+ ) {
+ val startedPosition = checkNotNull(lastFirstDown)
// TODO(b/382665591): Ensure that there is at least one pointer down.
val pointersDownCount = pointersDown.size.coerceAtLeast(1)
diff --git a/packages/SystemUI/compose/core/tests/AndroidManifest.xml b/packages/SystemUI/compose/core/tests/AndroidManifest.xml
index 28f80d4af265..7c721b97ee47 100644
--- a/packages/SystemUI/compose/core/tests/AndroidManifest.xml
+++ b/packages/SystemUI/compose/core/tests/AndroidManifest.xml
@@ -15,6 +15,7 @@
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
package="com.android.compose.core.tests" >
<application>
@@ -23,7 +24,8 @@
<activity
android:name="androidx.activity.ComponentActivity"
android:theme="@android:style/Theme.DeviceDefault.DayNight"
- android:exported="true" />
+ android:exported="true"
+ tools:replace="android:theme" />
</application>
<instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
diff --git a/packages/SystemUI/compose/core/tests/src/com/android/compose/gesture/NestedDraggableTest.kt b/packages/SystemUI/compose/core/tests/src/com/android/compose/gesture/NestedDraggableTest.kt
index 7f70e97411f4..19d28cc2d626 100644
--- a/packages/SystemUI/compose/core/tests/src/com/android/compose/gesture/NestedDraggableTest.kt
+++ b/packages/SystemUI/compose/core/tests/src/com/android/compose/gesture/NestedDraggableTest.kt
@@ -18,30 +18,42 @@ package com.android.compose.gesture
import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.gestures.rememberScrollableState
+import androidx.compose.foundation.gestures.scrollable
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.Button
+import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
+import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.input.pointer.PointerInputChange
import androidx.compose.ui.input.pointer.PointerType
import androidx.compose.ui.platform.LocalViewConfiguration
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.ScrollWheel
+import androidx.compose.ui.test.assertTextEquals
import androidx.compose.ui.test.junit4.ComposeContentTestRule
import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onRoot
+import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performMouseInput
import androidx.compose.ui.test.performTouchInput
import androidx.compose.ui.test.swipeDown
import androidx.compose.ui.test.swipeLeft
+import androidx.compose.ui.test.swipeWithVelocity
import androidx.compose.ui.unit.Velocity
import com.google.common.truth.Truth.assertThat
import kotlin.math.ceil
@@ -690,6 +702,7 @@ class NestedDraggableTest(override val orientation: Orientation) : OrientationAw
}
@Test
+ @Ignore("b/388507816: re-enable this when the crash in HitPath is fixed")
fun pointersDown_clearedWhenDisabled() {
val draggable = TestDraggable()
var enabled by mutableStateOf(true)
@@ -710,6 +723,233 @@ class NestedDraggableTest(override val orientation: Orientation) : OrientationAw
rule.onRoot().performTouchInput { down(center) }
}
+ @Test
+ // TODO(b/388231324): Remove this.
+ fun nestedScrollWithMouseWheelIsIgnored() {
+ val draggable = TestDraggable()
+ val touchSlop =
+ rule.setContentWithTouchSlop {
+ Box(
+ Modifier.fillMaxSize()
+ .nestedDraggable(draggable, orientation)
+ .scrollable(rememberScrollableState { 0f }, orientation)
+ )
+ }
+
+ rule.onRoot().performMouseInput {
+ enter(center)
+ scroll(
+ touchSlop + 1f,
+ when (orientation) {
+ Orientation.Horizontal -> ScrollWheel.Horizontal
+ Orientation.Vertical -> ScrollWheel.Vertical
+ },
+ )
+ }
+
+ assertThat(draggable.onDragStartedCalled).isFalse()
+ }
+
+ @Test
+ fun doesNotConsumeGesturesWhenDisabled() {
+ val buttonTag = "button"
+ rule.setContent {
+ Box {
+ var count by remember { mutableStateOf(0) }
+ Button(onClick = { count++ }, Modifier.testTag(buttonTag).align(Alignment.Center)) {
+ Text("Count: $count")
+ }
+
+ Box(
+ Modifier.fillMaxSize()
+ .nestedDraggable(remember { TestDraggable() }, orientation, enabled = false)
+ )
+ }
+ }
+
+ rule.onNodeWithTag(buttonTag).assertTextEquals("Count: 0")
+
+ // Click on the root at its center, where the button is located. Clicks should go through
+ // the draggable and reach the button given that it is disabled.
+ repeat(3) { rule.onRoot().performClick() }
+ rule.onNodeWithTag(buttonTag).assertTextEquals("Count: 3")
+ }
+
+ @Test
+ fun nestedDragNotStartedWhenEnabledAfterDragStarted() {
+ val draggable = TestDraggable()
+ var enabled by mutableStateOf(false)
+ val touchSlop =
+ rule.setContentWithTouchSlop {
+ Box(
+ Modifier.fillMaxSize()
+ .nestedDraggable(draggable, orientation, enabled = enabled)
+ .scrollable(rememberScrollableState { 0f }, orientation)
+ )
+ }
+
+ rule.onRoot().performTouchInput { down(center) }
+
+ enabled = true
+ rule.waitForIdle()
+
+ rule.onRoot().performTouchInput { moveBy((touchSlop + 1f).toOffset()) }
+
+ assertThat(draggable.onDragStartedCalled).isFalse()
+ }
+
+ @Test
+ fun availableAndConsumedScrollDeltas() {
+ val totalScroll = 200f
+ val consumedByEffectPreScroll = 10f // 200f => 190f
+ val consumedByConnectionPreScroll = 20f // 190f => 170f
+ val consumedByScroll = 30f // 170f => 140f
+ val consumedByConnectionPostScroll = 40f // 140f => 100f
+
+ // Available scroll values that we will check later.
+ var availableToEffectPreScroll = 0f
+ var availableToConnectionPreScroll = 0f
+ var availableToScroll = 0f
+ var availableToConnectionPostScroll = 0f
+ var availableToEffectPostScroll = 0f
+
+ val effect =
+ TestOverscrollEffect(
+ orientation,
+ onPreScroll = {
+ availableToEffectPreScroll = it
+ consumedByEffectPreScroll
+ },
+ onPostScroll = {
+ availableToEffectPostScroll = it
+ it
+ },
+ )
+
+ val connection =
+ object : NestedScrollConnection {
+ override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
+ availableToConnectionPreScroll = available.toFloat()
+ return consumedByConnectionPreScroll.toOffset()
+ }
+
+ override fun onPostScroll(
+ consumed: Offset,
+ available: Offset,
+ source: NestedScrollSource,
+ ): Offset {
+ assertThat(consumed.toFloat()).isEqualTo(consumedByScroll)
+ availableToConnectionPostScroll = available.toFloat()
+ return consumedByConnectionPostScroll.toOffset()
+ }
+ }
+
+ val draggable =
+ TestDraggable(
+ onDrag = {
+ availableToScroll = it
+ consumedByScroll
+ }
+ )
+
+ val touchSlop =
+ rule.setContentWithTouchSlop {
+ Box(
+ Modifier.fillMaxSize()
+ .nestedScroll(connection)
+ .nestedDraggable(draggable, orientation, effect)
+ )
+ }
+
+ rule.onRoot().performTouchInput {
+ down(center)
+ moveBy((touchSlop + totalScroll).toOffset())
+ }
+
+ assertThat(availableToEffectPreScroll).isEqualTo(200f)
+ assertThat(availableToConnectionPreScroll).isEqualTo(190f)
+ assertThat(availableToScroll).isEqualTo(170f)
+ assertThat(availableToConnectionPostScroll).isEqualTo(140f)
+ assertThat(availableToEffectPostScroll).isEqualTo(100f)
+ }
+
+ @Test
+ fun availableAndConsumedVelocities() {
+ val totalVelocity = 200f
+ val consumedByEffectPreFling = 10f // 200f => 190f
+ val consumedByConnectionPreFling = 20f // 190f => 170f
+ val consumedByFling = 30f // 170f => 140f
+ val consumedByConnectionPostFling = 40f // 140f => 100f
+
+ // Available velocities that we will check later.
+ var availableToEffectPreFling = 0f
+ var availableToConnectionPreFling = 0f
+ var availableToFling = 0f
+ var availableToConnectionPostFling = 0f
+ var availableToEffectPostFling = 0f
+
+ val effect =
+ TestOverscrollEffect(
+ orientation,
+ onPreFling = {
+ availableToEffectPreFling = it
+ consumedByEffectPreFling
+ },
+ onPostFling = {
+ availableToEffectPostFling = it
+ it
+ },
+ onPostScroll = { 0f },
+ )
+
+ val connection =
+ object : NestedScrollConnection {
+ override suspend fun onPreFling(available: Velocity): Velocity {
+ availableToConnectionPreFling = available.toFloat()
+ return consumedByConnectionPreFling.toVelocity()
+ }
+
+ override suspend fun onPostFling(
+ consumed: Velocity,
+ available: Velocity,
+ ): Velocity {
+ assertThat(consumed.toFloat()).isEqualTo(consumedByFling)
+ availableToConnectionPostFling = available.toFloat()
+ return consumedByConnectionPostFling.toVelocity()
+ }
+ }
+
+ val draggable =
+ TestDraggable(
+ onDragStopped = { velocity, _ ->
+ availableToFling = velocity
+ consumedByFling
+ },
+ onDrag = { 0f },
+ )
+
+ rule.setContent {
+ Box(
+ Modifier.fillMaxSize()
+ .nestedScroll(connection)
+ .nestedDraggable(draggable, orientation, effect)
+ )
+ }
+
+ rule.onRoot().performTouchInput {
+ when (orientation) {
+ Orientation.Horizontal -> swipeWithVelocity(topLeft, topRight, totalVelocity)
+ Orientation.Vertical -> swipeWithVelocity(topLeft, bottomLeft, totalVelocity)
+ }
+ }
+
+ assertThat(availableToEffectPreFling).isWithin(1f).of(200f)
+ assertThat(availableToConnectionPreFling).isWithin(1f).of(190f)
+ assertThat(availableToFling).isWithin(1f).of(170f)
+ assertThat(availableToConnectionPostFling).isWithin(1f).of(140f)
+ assertThat(availableToEffectPostFling).isWithin(1f).of(100f)
+ }
+
private fun ComposeContentTestRule.setContentWithTouchSlop(
content: @Composable () -> Unit
): Float {
diff --git a/packages/SystemUI/compose/core/tests/src/com/android/compose/gesture/TestOverscrollEffect.kt b/packages/SystemUI/compose/core/tests/src/com/android/compose/gesture/TestOverscrollEffect.kt
index 8bf9c21639f4..0659f9198730 100644
--- a/packages/SystemUI/compose/core/tests/src/com/android/compose/gesture/TestOverscrollEffect.kt
+++ b/packages/SystemUI/compose/core/tests/src/com/android/compose/gesture/TestOverscrollEffect.kt
@@ -24,6 +24,8 @@ import androidx.compose.ui.unit.Velocity
class TestOverscrollEffect(
override val orientation: Orientation,
+ private val onPreScroll: (Float) -> Float = { 0f },
+ private val onPreFling: suspend (Float) -> Float = { 0f },
private val onPostFling: suspend (Float) -> Float = { it },
private val onPostScroll: (Float) -> Float,
) : OverscrollEffect, OrientationAware {
@@ -36,19 +38,23 @@ class TestOverscrollEffect(
source: NestedScrollSource,
performScroll: (Offset) -> Offset,
): Offset {
- val consumedByScroll = performScroll(delta)
- val available = delta - consumedByScroll
- val consumedByEffect = onPostScroll(available.toFloat()).toOffset()
- return consumedByScroll + consumedByEffect
+ val consumedByPreScroll = onPreScroll(delta.toFloat()).toOffset()
+ val availableToScroll = delta - consumedByPreScroll
+ val consumedByScroll = performScroll(availableToScroll)
+ val availableToPostScroll = availableToScroll - consumedByScroll
+ val consumedByPostScroll = onPostScroll(availableToPostScroll.toFloat()).toOffset()
+ return consumedByPreScroll + consumedByScroll + consumedByPostScroll
}
override suspend fun applyToFling(
velocity: Velocity,
performFling: suspend (Velocity) -> Velocity,
) {
- val consumedByFling = performFling(velocity)
- val available = velocity - consumedByFling
- onPostFling(available.toFloat())
+ val consumedByPreFling = onPreFling(velocity.toFloat()).toVelocity()
+ val availableToFling = velocity - consumedByPreFling
+ val consumedByFling = performFling(availableToFling)
+ val availableToPostFling = availableToFling - consumedByFling
+ onPostFling(availableToPostFling.toFloat())
applyToFlingDone = true
}
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt
index 9c53afecad11..a2a91fcd5d52 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt
@@ -78,6 +78,18 @@ object TransitionDuration {
const val EDIT_MODE_TO_HUB_GRID_END_MS =
EDIT_MODE_TO_HUB_GRID_DELAY_MS + EDIT_MODE_TO_HUB_CONTENT_MS
const val HUB_TO_EDIT_MODE_CONTENT_MS = 250
+ const val TO_GLANCEABLE_HUB_DURATION_MS = 1000
+}
+
+val sceneTransitionsV2 = transitions {
+ to(CommunalScenes.Communal) {
+ spec = tween(durationMillis = TransitionDuration.TO_GLANCEABLE_HUB_DURATION_MS)
+ fade(AllElements)
+ }
+ to(CommunalScenes.Blank) {
+ spec = tween(durationMillis = TO_GONE_DURATION.toInt(DurationUnit.MILLISECONDS))
+ fade(AllElements)
+ }
}
val sceneTransitions = transitions {
@@ -157,7 +169,7 @@ fun CommunalContainer(
MutableSceneTransitionLayoutState(
initialScene = currentSceneKey,
canChangeScene = { _ -> viewModel.canChangeScene() },
- transitions = sceneTransitions,
+ transitions = if (viewModel.v2FlagEnabled()) sceneTransitionsV2 else sceneTransitions,
)
}
val isUiBlurred by viewModel.isUiBlurred.collectAsStateWithLifecycle()
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt
index 6d24fc16df23..aa8b4ae9000d 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt
@@ -48,8 +48,8 @@ import com.android.systemui.shade.ui.composable.Shade
val SceneContainerTransitions = transitions {
interruptionHandler = SceneContainerInterruptionHandler
- // Overscroll progress starts linearly with some resistance (3f) and slowly approaches 0.2f
- defaultSwipeSpec = spring(stiffness = 300f, dampingRatio = 0.8f, visibilityThreshold = 0.5f)
+ defaultMotionSpatialSpec =
+ spring(stiffness = 300f, dampingRatio = 0.8f, visibilityThreshold = 0.5f)
// Scene transitions
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromDreamToCommunalTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromDreamToCommunalTransition.kt
index 93c10b6224ab..6ca9c7934979 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromDreamToCommunalTransition.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromDreamToCommunalTransition.kt
@@ -17,17 +17,12 @@
package com.android.systemui.scene.ui.composable.transitions
import androidx.compose.animation.core.tween
-import com.android.compose.animation.scene.Edge
import com.android.compose.animation.scene.TransitionBuilder
import com.android.systemui.communal.ui.compose.AllElements
-import com.android.systemui.communal.ui.compose.Communal
fun TransitionBuilder.dreamToCommunalTransition() {
spec = tween(durationMillis = 1000)
- // Translate communal hub grid from the end direction.
- translate(Communal.Elements.Grid, Edge.End)
-
- // Fade all communal hub elements.
- timestampRange(startMillis = 167, endMillis = 334) { fade(AllElements) }
+ // Fade in all communal hub elements.
+ fade(AllElements)
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToSplitShadeTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToSplitShadeTransition.kt
index ce7a85b19fb4..e30e7d3ee34c 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToSplitShadeTransition.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToSplitShadeTransition.kt
@@ -30,7 +30,7 @@ import kotlin.time.Duration.Companion.milliseconds
fun TransitionBuilder.goneToSplitShadeTransition(durationScale: Double = 1.0) {
spec = tween(durationMillis = (DefaultDuration * durationScale).inWholeMilliseconds.toInt())
- swipeSpec =
+ motionSpatialSpec =
spring(
stiffness = Spring.StiffnessMediumLow,
visibilityThreshold = Shade.Dimensions.ScrimVisibilityThreshold,
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToCommunalTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToCommunalTransition.kt
index 826a2550cca3..de9a78c497f8 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToCommunalTransition.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToCommunalTransition.kt
@@ -17,21 +17,12 @@
package com.android.systemui.scene.ui.composable.transitions
import androidx.compose.animation.core.tween
-import com.android.compose.animation.scene.Edge
import com.android.compose.animation.scene.TransitionBuilder
import com.android.systemui.communal.ui.compose.AllElements
-import com.android.systemui.communal.ui.compose.Communal
-import com.android.systemui.scene.shared.model.Scenes
fun TransitionBuilder.lockscreenToCommunalTransition() {
spec = tween(durationMillis = 1000)
- // Translate lockscreen to the start direction.
- translate(Scenes.Lockscreen.rootElementKey, Edge.Start)
-
- // Translate communal hub grid from the end direction.
- translate(Communal.Elements.Grid, Edge.End)
-
- // Fade all communal hub elements.
- timestampRange(startMillis = 167, endMillis = 334) { fade(AllElements) }
+ // Fade all communal hub elements in.
+ fade(AllElements)
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToSplitShadeTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToSplitShadeTransition.kt
index 1f7a7380bbc6..1a243ca48157 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToSplitShadeTransition.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToSplitShadeTransition.kt
@@ -29,7 +29,7 @@ import kotlin.time.Duration.Companion.milliseconds
fun TransitionBuilder.lockscreenToSplitShadeTransition(durationScale: Double = 1.0) {
spec = tween(durationMillis = (DefaultDuration * durationScale).inWholeMilliseconds.toInt())
- swipeSpec =
+ motionSpatialSpec =
spring(
stiffness = Spring.StiffnessMediumLow,
visibilityThreshold = Shade.Dimensions.ScrimVisibilityThreshold,
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromNotificationsShadeToQuickSettingsShadeTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromNotificationsShadeToQuickSettingsShadeTransition.kt
index 24f285e81da2..a9af95bdcb8a 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromNotificationsShadeToQuickSettingsShadeTransition.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromNotificationsShadeToQuickSettingsShadeTransition.kt
@@ -27,7 +27,7 @@ fun TransitionBuilder.notificationsShadeToQuickSettingsShadeTransition(
durationScale: Double = 1.0
) {
spec = tween(durationMillis = (DefaultDuration * durationScale).inWholeMilliseconds.toInt())
- swipeSpec =
+ motionSpatialSpec =
spring(
stiffness = Spring.StiffnessMediumLow,
visibilityThreshold = Shade.Dimensions.ScrimVisibilityThreshold,
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToNotificationsShadeTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToNotificationsShadeTransition.kt
index 3d62151baf2f..ddea5854d67e 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToNotificationsShadeTransition.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToNotificationsShadeTransition.kt
@@ -31,7 +31,7 @@ import kotlin.time.Duration.Companion.milliseconds
fun TransitionBuilder.toNotificationsShadeTransition(durationScale: Double = 1.0) {
spec = tween(durationMillis = (DefaultDuration * durationScale).inWholeMilliseconds.toInt())
- swipeSpec =
+ motionSpatialSpec =
spring(
stiffness = Spring.StiffnessMediumLow,
visibilityThreshold = Shade.Dimensions.ScrimVisibilityThreshold,
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToQuickSettingsShadeTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToQuickSettingsShadeTransition.kt
index e78bc6afcc4f..e477a41ac608 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToQuickSettingsShadeTransition.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToQuickSettingsShadeTransition.kt
@@ -28,7 +28,7 @@ import kotlin.time.Duration.Companion.milliseconds
fun TransitionBuilder.toQuickSettingsShadeTransition(durationScale: Double = 1.0) {
spec = tween(durationMillis = (DefaultDuration * durationScale).inWholeMilliseconds.toInt())
- swipeSpec =
+ motionSpatialSpec =
spring(
stiffness = Spring.StiffnessMediumLow,
visibilityThreshold = Shade.Dimensions.ScrimVisibilityThreshold,
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToShadeTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToShadeTransition.kt
index bfae4897dc68..4db4934cf271 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToShadeTransition.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToShadeTransition.kt
@@ -32,7 +32,7 @@ import kotlin.time.Duration.Companion.milliseconds
fun TransitionBuilder.toShadeTransition(durationScale: Double = 1.0) {
spec = tween(durationMillis = (DefaultDuration * durationScale).inWholeMilliseconds.toInt())
- swipeSpec =
+ motionSpatialSpec =
spring(
stiffness = Spring.StiffnessMediumLow,
visibilityThreshold = Shade.Dimensions.ScrimVisibilityThreshold,
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt
index b41c55858c75..2ca846424d93 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt
@@ -429,6 +429,58 @@ internal class Swipes(val upOrLeft: Swipe.Resolved, val downOrRight: Swipe.Resol
}
/**
+ * Finds the best matching [UserActionResult] for the given [swipe] within this [Content].
+ * Prioritizes actions with matching [Swipe.Resolved.fromSource].
+ *
+ * @param swipe The swipe to match against.
+ * @return The best matching [UserActionResult], or `null` if no match is found.
+ */
+ private fun Content.findActionResultBestMatch(swipe: Swipe.Resolved): UserActionResult? {
+ if (!areSwipesAllowed()) {
+ return null
+ }
+
+ var bestPoints = Int.MIN_VALUE
+ var bestMatch: UserActionResult? = null
+ userActions.forEach { (actionSwipe, actionResult) ->
+ if (
+ actionSwipe !is Swipe.Resolved ||
+ // The direction must match.
+ actionSwipe.direction != swipe.direction ||
+ // The number of pointers down must match.
+ actionSwipe.pointerCount != swipe.pointerCount ||
+ // The action requires a specific fromSource.
+ (actionSwipe.fromSource != null &&
+ actionSwipe.fromSource != swipe.fromSource) ||
+ // The action requires a specific pointerType.
+ (actionSwipe.pointersType != null &&
+ actionSwipe.pointersType != swipe.pointersType)
+ ) {
+ // This action is not eligible.
+ return@forEach
+ }
+
+ val sameFromSource = actionSwipe.fromSource == swipe.fromSource
+ val samePointerType = actionSwipe.pointersType == swipe.pointersType
+ // Prioritize actions with a perfect match.
+ if (sameFromSource && samePointerType) {
+ return actionResult
+ }
+
+ var points = 0
+ if (sameFromSource) points++
+ if (samePointerType) points++
+
+ // Otherwise, keep track of the best eligible action.
+ if (points > bestPoints) {
+ bestPoints = points
+ bestMatch = actionResult
+ }
+ }
+ return bestMatch
+ }
+
+ /**
* Update the swipes results.
*
* Usually we don't want to update them while doing a drag, because this could change the target
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt
index ff8efc28aa21..d50304d433f9 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt
@@ -34,7 +34,7 @@ import com.android.internal.jank.Cuj.CujType
/** The transitions configuration of a [SceneTransitionLayout]. */
class SceneTransitions
internal constructor(
- internal val defaultSwipeSpec: SpringSpec<Float>,
+ internal val defaultMotionSpatialSpec: SpringSpec<Float>,
internal val transitionSpecs: List<TransitionSpecImpl>,
internal val interruptionHandler: InterruptionHandler,
) {
@@ -132,7 +132,7 @@ internal constructor(
val Empty =
SceneTransitions(
- defaultSwipeSpec = DefaultSwipeSpec,
+ defaultMotionSpatialSpec = DefaultSwipeSpec,
transitionSpecs = emptyList(),
interruptionHandler = DefaultInterruptionHandler,
)
@@ -194,9 +194,9 @@ internal interface TransformationSpec {
* The [SpringSpec] used to animate the associated transition progress when the transition was
* started by a swipe and is now animating back to a scene because the user lifted their finger.
*
- * If `null`, then the [SceneTransitions.defaultSwipeSpec] will be used.
+ * If `null`, then the [SceneTransitions.defaultMotionSpatialSpec] will be used.
*/
- val swipeSpec: SpringSpec<Float>?
+ val motionSpatialSpec: AnimationSpec<Float>?
/**
* The distance it takes for this transition to animate from 0% to 100% when it is driven by a
@@ -213,7 +213,7 @@ internal interface TransformationSpec {
internal val Empty =
TransformationSpecImpl(
progressSpec = snap(),
- swipeSpec = null,
+ motionSpatialSpec = null,
distance = null,
transformationMatchers = emptyList(),
)
@@ -246,7 +246,7 @@ internal class TransitionSpecImpl(
val reverse = transformationSpec.invoke(transition)
TransformationSpecImpl(
progressSpec = reverse.progressSpec,
- swipeSpec = reverse.swipeSpec,
+ motionSpatialSpec = reverse.motionSpatialSpec,
distance = reverse.distance,
transformationMatchers =
reverse.transformationMatchers.map {
@@ -276,7 +276,7 @@ internal class TransitionSpecImpl(
*/
internal class TransformationSpecImpl(
override val progressSpec: AnimationSpec<Float>,
- override val swipeSpec: SpringSpec<Float>?,
+ override val motionSpatialSpec: SpringSpec<Float>?,
override val distance: UserActionDistance?,
override val transformationMatchers: List<TransformationMatcher>,
) : TransformationSpec {
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeAnimation.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeAnimation.kt
index ba92f9bea07d..2bfa0199f30b 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeAnimation.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeAnimation.kt
@@ -365,10 +365,10 @@ internal class SwipeAnimation<T : ContentKey>(
return 0f
}
- val swipeSpec =
+ val motionSpatialSpec =
spec
- ?: contentTransition.transformationSpec.swipeSpec
- ?: layoutState.transitions.defaultSwipeSpec
+ ?: contentTransition.transformationSpec.motionSpatialSpec
+ ?: layoutState.transitions.defaultMotionSpatialSpec
val velocityConsumed = CompletableDeferred<Float>()
@@ -376,7 +376,7 @@ internal class SwipeAnimation<T : ContentKey>(
val result =
animatable.animateTo(
targetValue = targetOffset,
- animationSpec = swipeSpec,
+ animationSpec = motionSpatialSpec,
initialVelocity = initialVelocity,
)
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt
index 3f6bce724b1b..e2212113404d 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt
@@ -37,11 +37,7 @@ internal fun Modifier.swipeToScene(
draggableHandler: DraggableHandlerImpl,
swipeDetector: SwipeDetector,
): Modifier {
- return if (draggableHandler.enabled()) {
- this.then(SwipeToSceneElement(draggableHandler, swipeDetector))
- } else {
- this
- }
+ return then(SwipeToSceneElement(draggableHandler, swipeDetector, draggableHandler.enabled()))
}
private fun DraggableHandlerImpl.enabled(): Boolean {
@@ -61,84 +57,62 @@ internal fun Content.shouldEnableSwipes(orientation: Orientation): Boolean {
return userActions.keys.any { it is Swipe.Resolved && it.direction.orientation == orientation }
}
-/**
- * Finds the best matching [UserActionResult] for the given [swipe] within this [Content].
- * Prioritizes actions with matching [Swipe.Resolved.fromSource].
- *
- * @param swipe The swipe to match against.
- * @return The best matching [UserActionResult], or `null` if no match is found.
- */
-internal fun Content.findActionResultBestMatch(swipe: Swipe.Resolved): UserActionResult? {
- if (!areSwipesAllowed()) {
- return null
- }
-
- var bestPoints = Int.MIN_VALUE
- var bestMatch: UserActionResult? = null
- userActions.forEach { (actionSwipe, actionResult) ->
- if (
- actionSwipe !is Swipe.Resolved ||
- // The direction must match.
- actionSwipe.direction != swipe.direction ||
- // The number of pointers down must match.
- actionSwipe.pointerCount != swipe.pointerCount ||
- // The action requires a specific fromSource.
- (actionSwipe.fromSource != null && actionSwipe.fromSource != swipe.fromSource) ||
- // The action requires a specific pointerType.
- (actionSwipe.pointersType != null && actionSwipe.pointersType != swipe.pointersType)
- ) {
- // This action is not eligible.
- return@forEach
- }
-
- val sameFromSource = actionSwipe.fromSource == swipe.fromSource
- val samePointerType = actionSwipe.pointersType == swipe.pointersType
- // Prioritize actions with a perfect match.
- if (sameFromSource && samePointerType) {
- return actionResult
- }
-
- var points = 0
- if (sameFromSource) points++
- if (samePointerType) points++
-
- // Otherwise, keep track of the best eligible action.
- if (points > bestPoints) {
- bestPoints = points
- bestMatch = actionResult
- }
- }
- return bestMatch
-}
-
private data class SwipeToSceneElement(
val draggableHandler: DraggableHandlerImpl,
val swipeDetector: SwipeDetector,
+ val enabled: Boolean,
) : ModifierNodeElement<SwipeToSceneRootNode>() {
override fun create(): SwipeToSceneRootNode =
- SwipeToSceneRootNode(draggableHandler, swipeDetector)
+ SwipeToSceneRootNode(draggableHandler, swipeDetector, enabled)
override fun update(node: SwipeToSceneRootNode) {
- node.update(draggableHandler, swipeDetector)
+ node.update(draggableHandler, swipeDetector, enabled)
}
}
private class SwipeToSceneRootNode(
draggableHandler: DraggableHandlerImpl,
swipeDetector: SwipeDetector,
+ enabled: Boolean,
) : DelegatingNode() {
- private var delegateNode = delegate(SwipeToSceneNode(draggableHandler, swipeDetector))
+ private var delegateNode = if (enabled) create(draggableHandler, swipeDetector) else null
+
+ fun update(
+ draggableHandler: DraggableHandlerImpl,
+ swipeDetector: SwipeDetector,
+ enabled: Boolean,
+ ) {
+ // Disabled.
+ if (!enabled) {
+ delegateNode?.let { undelegate(it) }
+ delegateNode = null
+ return
+ }
+
+ // Disabled => Enabled.
+ val nullableDelegate = delegateNode
+ if (nullableDelegate == null) {
+ delegateNode = create(draggableHandler, swipeDetector)
+ return
+ }
- fun update(draggableHandler: DraggableHandlerImpl, swipeDetector: SwipeDetector) {
- if (draggableHandler == delegateNode.draggableHandler) {
+ // Enabled => Enabled (update).
+ if (draggableHandler == nullableDelegate.draggableHandler) {
// Simple update, just update the swipe detector directly and keep the node.
- delegateNode.swipeDetector = swipeDetector
+ nullableDelegate.swipeDetector = swipeDetector
} else {
// The draggableHandler changed, force recreate the underlying SwipeToSceneNode.
- undelegate(delegateNode)
- delegateNode = delegate(SwipeToSceneNode(draggableHandler, swipeDetector))
+ undelegate(nullableDelegate)
+ delegateNode = create(draggableHandler, swipeDetector)
}
}
+
+ private fun create(
+ draggableHandler: DraggableHandlerImpl,
+ swipeDetector: SwipeDetector,
+ ): SwipeToSceneNode {
+ return delegate(SwipeToSceneNode(draggableHandler, swipeDetector))
+ }
}
private class SwipeToSceneNode(
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt
index 998054ef6c9e..776d553ee49c 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt
@@ -40,7 +40,7 @@ interface SceneTransitionsBuilder {
* The default [AnimationSpec] used when after the user lifts their finger after starting a
* swipe to transition, to animate back into one of the 2 scenes we are transitioning to.
*/
- var defaultSwipeSpec: SpringSpec<Float>
+ var defaultMotionSpatialSpec: SpringSpec<Float>
/**
* The [InterruptionHandler] used when transitions are interrupted. Defaults to
@@ -145,9 +145,9 @@ interface TransitionBuilder : BaseTransitionBuilder {
* The [SpringSpec] used to animate the associated transition progress when the transition was
* started by a swipe and is now animating back to a scene because the user lifted their finger.
*
- * If `null`, then the [SceneTransitionsBuilder.defaultSwipeSpec] will be used.
+ * If `null`, then the [SceneTransitionsBuilder.defaultMotionSpatialSpec] will be used.
*/
- var swipeSpec: SpringSpec<Float>?
+ var motionSpatialSpec: SpringSpec<Float>?
/** The CUJ associated to this transitions. */
@CujType var cuj: Int?
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt
index 7ca521513714..9a9b05eb3c1d 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt
@@ -41,11 +41,15 @@ import com.android.internal.jank.Cuj.CujType
internal fun transitionsImpl(builder: SceneTransitionsBuilder.() -> Unit): SceneTransitions {
val impl = SceneTransitionsBuilderImpl().apply(builder)
- return SceneTransitions(impl.defaultSwipeSpec, impl.transitionSpecs, impl.interruptionHandler)
+ return SceneTransitions(
+ defaultMotionSpatialSpec = impl.defaultMotionSpatialSpec,
+ transitionSpecs = impl.transitionSpecs,
+ interruptionHandler = impl.interruptionHandler,
+ )
}
private class SceneTransitionsBuilderImpl : SceneTransitionsBuilder {
- override var defaultSwipeSpec: SpringSpec<Float> = SceneTransitions.DefaultSwipeSpec
+ override var defaultMotionSpatialSpec: SpringSpec<Float> = SceneTransitions.DefaultSwipeSpec
override var interruptionHandler: InterruptionHandler = DefaultInterruptionHandler
val transitionSpecs = mutableListOf<TransitionSpecImpl>()
@@ -105,7 +109,7 @@ private class SceneTransitionsBuilderImpl : SceneTransitionsBuilder {
val impl = TransitionBuilderImpl(transition).apply(builder)
return TransformationSpecImpl(
progressSpec = impl.spec,
- swipeSpec = impl.swipeSpec,
+ motionSpatialSpec = impl.motionSpatialSpec,
distance = impl.distance,
transformationMatchers = impl.transformationMatchers,
)
@@ -209,7 +213,7 @@ internal abstract class BaseTransitionBuilderImpl : BaseTransitionBuilder {
internal class TransitionBuilderImpl(override val transition: TransitionState.Transition) :
BaseTransitionBuilderImpl(), TransitionBuilder {
override var spec: AnimationSpec<Float> = spring(stiffness = Spring.StiffnessLow)
- override var swipeSpec: SpringSpec<Float>? = null
+ override var motionSpatialSpec: SpringSpec<Float>? = null
override var distance: UserActionDistance? = null
override var cuj: Int? = null
private val durationMillis: Int by lazy {
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/state/TransitionState.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/state/TransitionState.kt
index 712af56ee1bc..097722665f8e 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/state/TransitionState.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/state/TransitionState.kt
@@ -390,10 +390,10 @@ sealed interface TransitionState {
fun create(): Animatable<Float, AnimationVector1D> {
val animatable = Animatable(1f, visibilityThreshold = ProgressVisibilityThreshold)
layoutImpl.animationScope.launch {
- val swipeSpec = layoutImpl.state.transitions.defaultSwipeSpec
+ val motionSpatialSpec = layoutImpl.state.transitions.defaultMotionSpatialSpec
val progressSpec =
spring(
- stiffness = swipeSpec.stiffness,
+ stiffness = motionSpatialSpec.stiffness,
dampingRatio = Spring.DampingRatioNoBouncy,
visibilityThreshold = ProgressVisibilityThreshold,
)
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/reveal/ContainerReveal.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/reveal/ContainerReveal.kt
index 7c4dbf153013..00cd0ca564b1 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/reveal/ContainerReveal.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/reveal/ContainerReveal.kt
@@ -104,7 +104,7 @@ fun TransitionBuilder.verticalContainerReveal(
val alphaSpec = spring<Float>(stiffness = 1200f, dampingRatio = 0.99f)
// The spring animating the progress when releasing the finger.
- swipeSpec =
+ motionSpatialSpec =
spring(
stiffness = Spring.StiffnessMediumLow,
dampingRatio = Spring.DampingRatioNoBouncy,
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt
index dbac62ffb713..5a9edba26d13 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt
@@ -137,13 +137,6 @@ class DraggableHandlerTest {
var pointerInfoOwner: () -> PointersInfo = { pointersDown() }
- fun nestedScrollConnection() =
- NestedScrollHandlerImpl(
- draggableHandler = draggableHandler,
- pointersInfoOwner = { pointerInfoOwner() },
- )
- .connection
-
val velocityThreshold = draggableHandler.velocityThreshold
fun down(fractionOfScreen: Float) =
@@ -607,57 +600,6 @@ class DraggableHandlerTest {
}
@Test
- fun nestedScrollUseFromSourceInfo() = runGestureTest {
- // Start at scene C.
- navigateToSceneC()
- val nestedScroll = nestedScrollConnection()
-
- // Drag from the **top** of the screen
- pointerInfoOwner = { pointersDown() }
- assertIdle(currentScene = SceneC)
-
- nestedScroll.scroll(available = upOffset(fractionOfScreen = 0.1f))
- assertTransition(
- currentScene = SceneC,
- fromScene = SceneC,
- // userAction: Swipe.Up to SceneB
- toScene = SceneB,
- progress = 0.1f,
- )
-
- // Reset to SceneC
- nestedScroll.preFling(Velocity.Zero)
- advanceUntilIdle()
-
- // Drag from the **bottom** of the screen
- pointerInfoOwner = { pointersDown(startedPosition = Offset(0f, SCREEN_SIZE)) }
- assertIdle(currentScene = SceneC)
-
- nestedScroll.scroll(available = upOffset(fractionOfScreen = 0.1f))
- assertTransition(
- currentScene = SceneC,
- fromScene = SceneC,
- // userAction: Swipe.Up(fromSource = Edge.Bottom) to SceneA
- toScene = SceneA,
- progress = 0.1f,
- )
- }
-
- @Test
- fun ignoreMouseWheel() = runGestureTest {
- // Start at scene C.
- navigateToSceneC()
- val nestedScroll = nestedScrollConnection()
-
- // Use mouse wheel
- pointerInfoOwner = { PointersInfo.MouseWheel }
- assertIdle(currentScene = SceneC)
-
- nestedScroll.scroll(available = upOffset(fractionOfScreen = 0.1f))
- assertIdle(currentScene = SceneC)
- }
-
- @Test
fun transitionIsImmediatelyUpdatedWhenReleasingFinger() = runGestureTest {
// Swipe up from the middle to transition to scene B.
val middle = pointersDown(startedPosition = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f))
@@ -689,28 +631,10 @@ class DraggableHandlerTest {
}
@Test
- fun scrollKeepPriorityEvenIfWeCanNoLongerScrollOnThatDirection() = runGestureTest {
- val nestedScroll = nestedScrollConnection()
-
- // Overscroll is disabled, it will scroll up to 100%
- nestedScroll.scroll(available = upOffset(fractionOfScreen = 2f))
- assertTransition(fromScene = SceneA, toScene = SceneB, progress = 1f)
-
- // We need to maintain scroll priority even if the scene transition can no longer consume
- // the scroll gesture.
- nestedScroll.scroll(available = upOffset(fractionOfScreen = 0.1f))
- assertTransition(fromScene = SceneA, toScene = SceneB, progress = 1f)
-
- // A scroll gesture in the opposite direction allows us to return to the previous scene.
- nestedScroll.scroll(available = downOffset(fractionOfScreen = 0.5f))
- assertTransition(fromScene = SceneA, toScene = SceneB, progress = 0.5f)
- }
-
- @Test
fun overscroll_releaseBetween0And100Percent_up() = runGestureTest {
// Make scene B overscrollable.
layoutState.transitions = transitions {
- defaultSwipeSpec = spring(dampingRatio = Spring.DampingRatioNoBouncy)
+ defaultMotionSpatialSpec = spring(dampingRatio = Spring.DampingRatioNoBouncy)
from(SceneA, to = SceneB) {}
}
@@ -739,7 +663,7 @@ class DraggableHandlerTest {
fun overscroll_releaseBetween0And100Percent_down() = runGestureTest {
// Make scene C overscrollable.
layoutState.transitions = transitions {
- defaultSwipeSpec = spring(dampingRatio = Spring.DampingRatioNoBouncy)
+ defaultMotionSpatialSpec = spring(dampingRatio = Spring.DampingRatioNoBouncy)
from(SceneA, to = SceneC) {}
}
@@ -944,33 +868,4 @@ class DraggableHandlerTest {
assertThat(layoutState.transitionState).hasCurrentScene(SceneA)
assertThat(layoutState.transitionState).hasCurrentOverlays(OverlayB)
}
-
- @Test
- fun replaceOverlayNestedScroll() = runGestureTest {
- layoutState.showOverlay(OverlayA, animationScope = testScope)
- advanceUntilIdle()
-
- // Initial state.
- assertThat(layoutState.transitionState).isIdle()
- assertThat(layoutState.transitionState).hasCurrentScene(SceneA)
- assertThat(layoutState.transitionState).hasCurrentOverlays(OverlayA)
-
- // Swipe down to replace overlay A by overlay B.
-
- val nestedScroll = nestedScrollConnection()
- nestedScroll.scroll(downOffset(0.1f))
- val transition = assertThat(layoutState.transitionState).isReplaceOverlayTransition()
- assertThat(transition).hasCurrentScene(SceneA)
- assertThat(transition).hasFromOverlay(OverlayA)
- assertThat(transition).hasToOverlay(OverlayB)
- assertThat(transition).hasCurrentOverlays(OverlayA)
- assertThat(transition).hasProgress(0.1f)
-
- nestedScroll.preFling(Velocity(0f, velocityThreshold))
- advanceUntilIdle()
- // Commit the gesture. The overlays are instantly swapped in the set of current overlays.
- assertThat(layoutState.transitionState).isIdle()
- assertThat(layoutState.transitionState).hasCurrentScene(SceneA)
- assertThat(layoutState.transitionState).hasCurrentOverlays(OverlayB)
- }
}
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt
index 53495be7b02a..005146997813 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt
@@ -618,7 +618,7 @@ class ElementTest {
fun layoutGetsCurrentTransitionStateFromComposition() {
val state =
rule.runOnUiThread {
- MutableSceneTransitionLayoutStateImpl(
+ MutableSceneTransitionLayoutState(
SceneA,
transitions {
from(SceneA, to = SceneB) {
@@ -1126,7 +1126,7 @@ class ElementTest {
val state =
rule.runOnUiThread {
- MutableSceneTransitionLayoutStateImpl(
+ MutableSceneTransitionLayoutState(
SceneA,
transitions {
from(SceneA, to = SceneB) { spec = tween(duration, easing = LinearEasing) }
@@ -1331,7 +1331,7 @@ class ElementTest {
val fooSize = 100.dp
val state =
rule.runOnUiThread {
- MutableSceneTransitionLayoutStateImpl(
+ MutableSceneTransitionLayoutState(
SceneA,
transitions {
from(SceneA, to = SceneB) { spec = tween(duration, easing = LinearEasing) }
@@ -1439,7 +1439,7 @@ class ElementTest {
@Test
fun targetStateIsSetEvenWhenNotPlaced() {
// Start directly at A => B but with progress < 0f to overscroll on A.
- val state = rule.runOnUiThread { MutableSceneTransitionLayoutStateImpl(SceneA) }
+ val state = rule.runOnUiThread { MutableSceneTransitionLayoutState(SceneA) }
lateinit var layoutImpl: SceneTransitionLayoutImpl
val scope =
@@ -1473,7 +1473,7 @@ class ElementTest {
fun lastAlphaIsNotSetByOutdatedLayer() {
val state =
rule.runOnUiThread {
- MutableSceneTransitionLayoutStateImpl(
+ MutableSceneTransitionLayoutState(
SceneA,
transitions { from(SceneA, to = SceneB) { fade(TestElements.Foo) } },
)
@@ -1537,7 +1537,7 @@ class ElementTest {
fun fadingElementsDontAppearInstantly() {
val state =
rule.runOnUiThread {
- MutableSceneTransitionLayoutStateImpl(
+ MutableSceneTransitionLayoutState(
SceneA,
transitions { from(SceneA, to = SceneB) { fade(TestElements.Foo) } },
)
@@ -1583,7 +1583,7 @@ class ElementTest {
@Test
fun lastPlacementValuesAreClearedOnNestedElements() {
- val state = rule.runOnIdle { MutableSceneTransitionLayoutStateImpl(SceneA) }
+ val state = rule.runOnIdle { MutableSceneTransitionLayoutState(SceneA) }
@Composable
fun ContentScope.NestedFooBar() {
@@ -1658,7 +1658,7 @@ class ElementTest {
fun currentTransitionSceneIsUsedToComputeElementValues() {
val state =
rule.runOnIdle {
- MutableSceneTransitionLayoutStateImpl(
+ MutableSceneTransitionLayoutState(
SceneA,
transitions {
from(SceneB, to = SceneC) {
@@ -1709,7 +1709,7 @@ class ElementTest {
@Test
fun interruptionDeltasAreProperlyCleaned() {
- val state = rule.runOnIdle { MutableSceneTransitionLayoutStateImpl(SceneA) }
+ val state = rule.runOnIdle { MutableSceneTransitionLayoutState(SceneA) }
@Composable
fun ContentScope.Foo(offset: Dp) {
@@ -1780,7 +1780,7 @@ class ElementTest {
fun transparentElementIsNotImpactingInterruption() {
val state =
rule.runOnIdle {
- MutableSceneTransitionLayoutStateImpl(
+ MutableSceneTransitionLayoutState(
SceneA,
transitions {
from(SceneA, to = SceneB) {
@@ -1856,7 +1856,7 @@ class ElementTest {
@Test
fun replacedTransitionDoesNotTriggerInterruption() {
- val state = rule.runOnIdle { MutableSceneTransitionLayoutStateImpl(SceneA) }
+ val state = rule.runOnIdle { MutableSceneTransitionLayoutState(SceneA) }
@Composable
fun ContentScope.Foo(modifier: Modifier = Modifier) {
@@ -2027,7 +2027,7 @@ class ElementTest {
): SceneTransitionLayoutImpl {
val state =
rule.runOnIdle {
- MutableSceneTransitionLayoutStateImpl(
+ MutableSceneTransitionLayoutState(
from,
transitions { from(from, to = to, preview = preview, builder = transition) },
)
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt
index 9135fdd15b3a..0355a30d5c73 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt
@@ -40,6 +40,7 @@ import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.platform.LocalViewConfiguration
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.ScrollWheel
+import androidx.compose.ui.test.TouchInjectionScope
import androidx.compose.ui.test.assertPositionInRootIsEqualTo
import androidx.compose.ui.test.assertTextEquals
import androidx.compose.ui.test.junit4.createComposeRule
@@ -55,6 +56,8 @@ import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.compose.animation.scene.TestOverlays.OverlayA
+import com.android.compose.animation.scene.TestOverlays.OverlayB
import com.android.compose.animation.scene.TestScenes.SceneA
import com.android.compose.animation.scene.TestScenes.SceneB
import com.android.compose.animation.scene.TestScenes.SceneC
@@ -936,4 +939,205 @@ class SwipeToSceneTest {
assertThat(state.transitionState).isIdle()
assertThat(state.transitionState).hasCurrentScene(SceneC)
}
+
+ @Test
+ fun swipeToSceneNodeIsKeptWhenDisabled() {
+ var hasHorizontalActions by mutableStateOf(false)
+ val state = rule.runOnUiThread { MutableSceneTransitionLayoutState(SceneA) }
+ var touchSlop = 0f
+ rule.setContent {
+ touchSlop = LocalViewConfiguration.current.touchSlop
+ SceneTransitionLayout(state) {
+ scene(
+ SceneA,
+ userActions =
+ buildList {
+ add(Swipe.Down to SceneB)
+
+ if (hasHorizontalActions) {
+ add(Swipe.Left to SceneC)
+ }
+ }
+ .toMap(),
+ ) {
+ Box(Modifier.fillMaxSize())
+ }
+ scene(SceneB) { Box(Modifier.fillMaxSize()) }
+ }
+ }
+
+ // Swipe down to start a transition to B.
+ rule.onRoot().performTouchInput {
+ down(middle)
+ moveBy(Offset(0f, touchSlop))
+ }
+
+ assertThat(state.transitionState).isSceneTransition()
+
+ // Add new horizontal user actions. This should not stop the current transition, even if a
+ // new horizontal Modifier.swipeToScene() handler is introduced where the vertical one was.
+ hasHorizontalActions = true
+ rule.waitForIdle()
+ assertThat(state.transitionState).isSceneTransition()
+ }
+
+ @Test
+ fun nestedScroll_useFromSourceInfo() {
+ val state = rule.runOnUiThread { MutableSceneTransitionLayoutState(SceneA) }
+ var touchSlop = 0f
+ rule.setContent {
+ touchSlop = LocalViewConfiguration.current.touchSlop
+ SceneTransitionLayout(state) {
+ scene(
+ SceneA,
+ userActions =
+ mapOf(Swipe.Down to SceneB, Swipe.Down(fromSource = Edge.Top) to SceneC),
+ ) {
+ // Use a fullscreen nested scrollable to use the nested scroll connection.
+ Box(
+ Modifier.fillMaxSize()
+ .scrollable(rememberScrollableState { 0f }, Orientation.Vertical)
+ )
+ }
+ scene(SceneB) { Box(Modifier.fillMaxSize()) }
+ scene(SceneC) { Box(Modifier.fillMaxSize()) }
+ }
+ }
+
+ // Swiping down from the middle of the screen leads to B.
+ rule.onRoot().performTouchInput {
+ down(center)
+ moveBy(Offset(0f, touchSlop + 1f))
+ }
+
+ var transition = assertThat(state.transitionState).isSceneTransition()
+ assertThat(transition).hasFromScene(SceneA)
+ assertThat(transition).hasToScene(SceneB)
+
+ // Release finger and wait to settle back to A.
+ rule.onRoot().performTouchInput { up() }
+ rule.waitForIdle()
+ assertThat(state.transitionState).isIdle()
+ assertThat(state.transitionState).hasCurrentScene(SceneA)
+
+ // Swiping down from the top of the screen leads to B.
+ rule.onRoot().performTouchInput {
+ down(center.copy(y = 0f))
+ moveBy(Offset(0f, touchSlop + 1f))
+ }
+
+ transition = assertThat(state.transitionState).isSceneTransition()
+ assertThat(transition).hasFromScene(SceneA)
+ assertThat(transition).hasToScene(SceneC)
+ }
+
+ @Test
+ fun nestedScroll_ignoreMouseWheel() {
+ val state = rule.runOnUiThread { MutableSceneTransitionLayoutState(SceneA) }
+ var touchSlop = 0f
+ rule.setContent {
+ touchSlop = LocalViewConfiguration.current.touchSlop
+ SceneTransitionLayout(state) {
+ scene(SceneA, userActions = mapOf(Swipe.Down to SceneB)) {
+ // Use a fullscreen nested scrollable to use the nested scroll connection.
+ Box(
+ Modifier.fillMaxSize()
+ .scrollable(rememberScrollableState { 0f }, Orientation.Vertical)
+ )
+ }
+ scene(SceneB) { Box(Modifier.fillMaxSize()) }
+ }
+ }
+
+ rule.onRoot().performMouseInput {
+ scroll(-touchSlop - 1f, scrollWheel = ScrollWheel.Vertical)
+ }
+ assertThat(state.transitionState).isIdle()
+ }
+
+ @Test
+ fun nestedScroll_keepPriorityEvenIfWeCanNoLongerScrollOnThatDirection() {
+ val state = rule.runOnUiThread { MutableSceneTransitionLayoutState(SceneA) }
+ var touchSlop = 0f
+ rule.setContent {
+ touchSlop = LocalViewConfiguration.current.touchSlop
+ SceneTransitionLayout(state) {
+ scene(SceneA, userActions = mapOf(Swipe.Down to SceneB)) {
+ // Use a fullscreen nested scrollable to use the nested scroll connection.
+ Box(
+ Modifier.fillMaxSize()
+ .scrollable(rememberScrollableState { 0f }, Orientation.Vertical)
+ )
+ }
+ scene(SceneB) { Box(Modifier.fillMaxSize()) }
+ }
+ }
+
+ fun TouchInjectionScope.height() = bottom
+ fun TouchInjectionScope.halfHeight() = height() / 2f
+
+ rule.onRoot().performTouchInput {
+ down(center.copy(y = 0f))
+ moveBy(Offset(0f, touchSlop + halfHeight()))
+ }
+ val transition = assertThat(state.transitionState).isSceneTransition()
+ assertThat(transition).hasProgress(0.5f, tolerance = 0.01f)
+
+ // The progress should never go above 100%.
+ rule.onRoot().performTouchInput { moveBy(Offset(0f, height())) }
+ assertThat(transition).hasProgress(1f, tolerance = 0.01f)
+
+ // Because the overscroll effect of scene B is not attached, swiping in the opposite
+ // direction will directly decrease the progress.
+ rule.onRoot().performTouchInput { moveBy(Offset(0f, -halfHeight())) }
+ assertThat(transition).hasProgress(0.5f, tolerance = 0.01f)
+ }
+
+ @Test
+ fun nestedScroll_replaceOverlay() {
+ val state =
+ rule.runOnUiThread {
+ MutableSceneTransitionLayoutState(SceneA, initialOverlays = setOf(OverlayA))
+ }
+ var touchSlop = 0f
+ rule.setContent {
+ touchSlop = LocalViewConfiguration.current.touchSlop
+ SceneTransitionLayout(state) {
+ scene(SceneA) { Box(Modifier.fillMaxSize()) }
+ overlay(
+ OverlayA,
+ mapOf(Swipe.Down to UserActionResult.ReplaceByOverlay(OverlayB)),
+ ) {
+ Box(
+ Modifier.fillMaxSize()
+ .scrollable(rememberScrollableState { 0f }, Orientation.Vertical)
+ )
+ }
+ overlay(OverlayB) { Box(Modifier.fillMaxSize()) }
+ }
+ }
+
+ // Swipe down 100% to replace A by B.
+ rule.onRoot().performTouchInput {
+ down(center.copy(y = 0f))
+ moveBy(Offset(0f, touchSlop + bottom))
+ }
+
+ val transition = assertThat(state.transitionState).isReplaceOverlayTransition()
+ assertThat(transition).hasCurrentScene(SceneA)
+ assertThat(transition).hasFromOverlay(OverlayA)
+ assertThat(transition).hasToOverlay(OverlayB)
+ assertThat(transition).hasCurrentOverlays(OverlayA)
+ assertThat(transition).hasProgress(1f, tolerance = 0.01f)
+
+ // Commit the gesture. The overlays are instantly swapped in the set of current overlays.
+ rule.onRoot().performTouchInput { up() }
+ assertThat(transition).hasCurrentScene(SceneA)
+ assertThat(transition).hasCurrentOverlays(OverlayB)
+
+ rule.waitForIdle()
+ assertThat(state.transitionState).isIdle()
+ assertThat(state.transitionState).hasCurrentScene(SceneA)
+ assertThat(state.transitionState).hasCurrentOverlays(OverlayB)
+ }
}
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/TransitionDslTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/TransitionDslTest.kt
index cb87fe849a81..aada4a50c89c 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/TransitionDslTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/TransitionDslTest.kt
@@ -276,22 +276,22 @@ class TransitionDslTest {
val defaultSpec = spring<Float>(stiffness = 1f)
val specFromAToC = spring<Float>(stiffness = 2f)
val transitions = transitions {
- defaultSwipeSpec = defaultSpec
+ defaultMotionSpatialSpec = defaultSpec
from(SceneA, to = SceneB) {
// Default swipe spec.
}
- from(SceneA, to = SceneC) { swipeSpec = specFromAToC }
+ from(SceneA, to = SceneC) { motionSpatialSpec = specFromAToC }
}
- assertThat(transitions.defaultSwipeSpec).isSameInstanceAs(defaultSpec)
+ assertThat(transitions.defaultMotionSpatialSpec).isSameInstanceAs(defaultSpec)
// A => B does not have a custom spec.
assertThat(
transitions
.transitionSpec(from = SceneA, to = SceneB, key = null)
.transformationSpec(aToB())
- .swipeSpec
+ .motionSpatialSpec
)
.isNull()
@@ -300,7 +300,7 @@ class TransitionDslTest {
transitions
.transitionSpec(from = SceneA, to = SceneC, key = null)
.transformationSpec(transition(from = SceneA, to = SceneC))
- .swipeSpec
+ .motionSpatialSpec
)
.isSameInstanceAs(specFromAToC)
}
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockController.kt
index aed3a2df0436..e69fa994931d 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockController.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockController.kt
@@ -50,7 +50,6 @@ class FlexClockController(private val clockCtx: ClockContext) : ClockController
DEFAULT_CLOCK_ID,
clockCtx.resources.getString(R.string.clock_default_name),
clockCtx.resources.getString(R.string.clock_default_description),
- isReactiveToTone = true,
)
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/DragToInteractAnimationControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/DragToInteractAnimationControllerTest.java
index 266591028efb..6edf94939010 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/DragToInteractAnimationControllerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/DragToInteractAnimationControllerTest.java
@@ -65,7 +65,7 @@ public class DragToInteractAnimationControllerTest extends SysuiTestCase {
@Before
public void setUp() throws Exception {
final WindowManager stubWindowManager = mContext.getSystemService(WindowManager.class);
- final SecureSettings mockSecureSettings = TestUtils.mockSecureSettings();
+ final SecureSettings mockSecureSettings = TestUtils.mockSecureSettings(mContext);
final MenuViewModel stubMenuViewModel = new MenuViewModel(mContext, mAccessibilityManager,
mockSecureSettings, mHearingAidDeviceManager);
final MenuViewAppearance stubMenuViewAppearance = new MenuViewAppearance(mContext,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegateTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegateTest.java
index 241da5fbc444..15afd2559d9d 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegateTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegateTest.java
@@ -71,7 +71,7 @@ public class MenuItemAccessibilityDelegateTest extends SysuiTestCase {
private AccessibilityManager mAccessibilityManager;
@Mock
private HearingAidDeviceManager mHearingAidDeviceManager;
- private final SecureSettings mSecureSettings = TestUtils.mockSecureSettings();
+ private final SecureSettings mSecureSettings = TestUtils.mockSecureSettings(mContext);
private RecyclerView mStubListView;
private MenuView mMenuView;
private MenuViewLayer mMenuViewLayer;
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandlerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandlerTest.java
index 715c40a31632..56a97bb34172 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandlerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandlerTest.java
@@ -89,7 +89,7 @@ public class MenuListViewTouchHandlerTest extends SysuiTestCase {
@Before
public void setUp() throws Exception {
final WindowManager windowManager = mContext.getSystemService(WindowManager.class);
- final SecureSettings secureSettings = TestUtils.mockSecureSettings();
+ final SecureSettings secureSettings = TestUtils.mockSecureSettings(mContext);
final MenuViewModel stubMenuViewModel = new MenuViewModel(mContext, mAccessibilityManager,
secureSettings, mHearingAidDeviceManager);
final MenuViewAppearance stubMenuViewAppearance = new MenuViewAppearance(mContext,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/MenuViewTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/MenuViewTest.java
index cb7c205742fc..5ff7bd063427 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/MenuViewTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/MenuViewTest.java
@@ -91,7 +91,7 @@ public class MenuViewTest extends SysuiTestCase {
mSpyContext = spy(mContext);
doNothing().when(mSpyContext).startActivity(any());
- final SecureSettings secureSettings = TestUtils.mockSecureSettings();
+ final SecureSettings secureSettings = TestUtils.mockSecureSettings(mContext);
final MenuViewModel stubMenuViewModel = new MenuViewModel(mContext, mAccessibilityManager,
secureSettings, mHearingAidDeviceManager);
final WindowManager stubWindowManager = mContext.getSystemService(WindowManager.class);
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/utils/TestUtils.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/utils/TestUtils.java
index 8399fa85bfb1..aafb21209468 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/utils/TestUtils.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/utils/TestUtils.java
@@ -22,6 +22,7 @@ import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import android.content.ComponentName;
+import android.content.Context;
import android.os.SystemClock;
import android.os.UserHandle;
import android.provider.Settings;
@@ -76,8 +77,10 @@ public class TestUtils {
* Returns a mock secure settings configured to return information needed for tests.
* Currently, this only includes button targets.
*/
- public static SecureSettings mockSecureSettings() {
+ public static SecureSettings mockSecureSettings(Context context) {
SecureSettings secureSettings = mock(SecureSettings.class);
+ when(secureSettings.getRealUserHandle(UserHandle.USER_CURRENT))
+ .thenReturn(context.getUserId());
final String targets = getShortcutTargets(
Set.of(TEST_COMPONENT_A, TEST_COMPONENT_B));
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractorTest.kt
index a11dace0505c..4c329dcf2f2b 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractorTest.kt
@@ -18,12 +18,20 @@ package com.android.systemui.bluetooth.qsdialog
import android.bluetooth.BluetoothLeBroadcast
import android.bluetooth.BluetoothLeBroadcastMetadata
+import android.content.ContentResolver
+import android.content.applicationContext
import android.testing.TestableLooper
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
+import com.android.settingslib.bluetooth.BluetoothEventManager
import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast
+import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant
+import com.android.settingslib.bluetooth.LocalBluetoothProfileManager
+import com.android.settingslib.bluetooth.VolumeControlProfile
+import com.android.settingslib.volume.shared.AudioSharingLogger
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.kosmos.testDispatcher
import com.android.systemui.kosmos.testScope
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
@@ -38,10 +46,16 @@ import org.junit.runner.RunWith
import org.mockito.ArgumentCaptor
import org.mockito.Captor
import org.mockito.Mock
+import org.mockito.Mockito.never
import org.mockito.Mockito.verify
import org.mockito.junit.MockitoJUnit
import org.mockito.junit.MockitoRule
import org.mockito.kotlin.any
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.spy
+import org.mockito.kotlin.times
+import org.mockito.kotlin.whenever
@SmallTest
@RunWith(AndroidJUnit4::class)
@@ -50,8 +64,11 @@ import org.mockito.kotlin.any
class AudioSharingInteractorTest : SysuiTestCase() {
@get:Rule val mockito: MockitoRule = MockitoJUnit.rule()
private val kosmos = testKosmos()
+
@Mock private lateinit var localBluetoothLeBroadcast: LocalBluetoothLeBroadcast
+
@Mock private lateinit var bluetoothLeBroadcastMetadata: BluetoothLeBroadcastMetadata
+
@Captor private lateinit var callbackCaptor: ArgumentCaptor<BluetoothLeBroadcast.Callback>
private lateinit var underTest: AudioSharingInteractor
@@ -157,13 +174,15 @@ class AudioSharingInteractorTest : SysuiTestCase() {
fun testHandleAudioSourceWhenReady_hasProfileButAudioSharingOff_sourceNotAdded() =
with(kosmos) {
testScope.runTest {
- bluetoothTileDialogAudioSharingRepository.setInAudioSharing(false)
+ bluetoothTileDialogAudioSharingRepository.setInAudioSharing(true)
bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true)
bluetoothTileDialogAudioSharingRepository.setLeAudioBroadcastProfile(
localBluetoothLeBroadcast
)
val job = launch { underTest.handleAudioSourceWhenReady() }
runCurrent()
+ bluetoothTileDialogAudioSharingRepository.setInAudioSharing(false)
+ runCurrent()
assertThat(bluetoothTileDialogAudioSharingRepository.sourceAdded).isFalse()
job.cancel()
@@ -174,15 +193,14 @@ class AudioSharingInteractorTest : SysuiTestCase() {
fun testHandleAudioSourceWhenReady_audioSharingOnButNoPlayback_sourceNotAdded() =
with(kosmos) {
testScope.runTest {
- bluetoothTileDialogAudioSharingRepository.setInAudioSharing(true)
+ bluetoothTileDialogAudioSharingRepository.setInAudioSharing(false)
bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true)
bluetoothTileDialogAudioSharingRepository.setLeAudioBroadcastProfile(
localBluetoothLeBroadcast
)
val job = launch { underTest.handleAudioSourceWhenReady() }
runCurrent()
- verify(localBluetoothLeBroadcast)
- .registerServiceCallBack(any(), callbackCaptor.capture())
+ bluetoothTileDialogAudioSharingRepository.setInAudioSharing(true)
runCurrent()
assertThat(bluetoothTileDialogAudioSharingRepository.sourceAdded).isFalse()
@@ -194,13 +212,15 @@ class AudioSharingInteractorTest : SysuiTestCase() {
fun testHandleAudioSourceWhenReady_audioSharingOnAndPlaybackStarts_sourceAdded() =
with(kosmos) {
testScope.runTest {
- bluetoothTileDialogAudioSharingRepository.setInAudioSharing(true)
+ bluetoothTileDialogAudioSharingRepository.setInAudioSharing(false)
bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true)
bluetoothTileDialogAudioSharingRepository.setLeAudioBroadcastProfile(
localBluetoothLeBroadcast
)
val job = launch { underTest.handleAudioSourceWhenReady() }
runCurrent()
+ bluetoothTileDialogAudioSharingRepository.setInAudioSharing(true)
+ runCurrent()
verify(localBluetoothLeBroadcast)
.registerServiceCallBack(any(), callbackCaptor.capture())
runCurrent()
@@ -211,4 +231,100 @@ class AudioSharingInteractorTest : SysuiTestCase() {
job.cancel()
}
}
+
+ @Test
+ fun testHandleAudioSourceWhenReady_skipInitialValue_noAudioSharing_sourceNotAdded() =
+ with(kosmos) {
+ testScope.runTest {
+ val (broadcast, repository) = setupRepositoryImpl()
+ val interactor =
+ object :
+ AudioSharingInteractorImpl(
+ applicationContext,
+ localBluetoothManager,
+ repository,
+ testDispatcher,
+ ) {
+ override suspend fun audioSharingAvailable() = true
+ }
+ val job = launch { interactor.handleAudioSourceWhenReady() }
+ runCurrent()
+ // Verify callback registered for onBroadcastStartedOrStopped
+ verify(broadcast).registerServiceCallBack(any(), callbackCaptor.capture())
+ runCurrent()
+ // Verify source is not added
+ verify(repository, never()).addSource()
+ job.cancel()
+ }
+ }
+
+ @Test
+ fun testHandleAudioSourceWhenReady_skipInitialValue_newAudioSharing_sourceAdded() =
+ with(kosmos) {
+ testScope.runTest {
+ val (broadcast, repository) = setupRepositoryImpl()
+ val interactor =
+ object :
+ AudioSharingInteractorImpl(
+ applicationContext,
+ localBluetoothManager,
+ repository,
+ testDispatcher,
+ ) {
+ override suspend fun audioSharingAvailable() = true
+ }
+ val job = launch { interactor.handleAudioSourceWhenReady() }
+ runCurrent()
+ // Verify callback registered for onBroadcastStartedOrStopped
+ verify(broadcast).registerServiceCallBack(any(), callbackCaptor.capture())
+ // Audio sharing started, trigger onBroadcastStarted
+ whenever(broadcast.isEnabled(null)).thenReturn(true)
+ callbackCaptor.value.onBroadcastStarted(0, 0)
+ runCurrent()
+ // Verify callback registered for onBroadcastMetadataChanged
+ verify(broadcast, times(2)).registerServiceCallBack(any(), callbackCaptor.capture())
+ runCurrent()
+ // Trigger onBroadcastMetadataChanged (ready to add source)
+ callbackCaptor.value.onBroadcastMetadataChanged(0, bluetoothLeBroadcastMetadata)
+ runCurrent()
+ // Verify source added
+ verify(repository).addSource()
+ job.cancel()
+ }
+ }
+
+ private fun setupRepositoryImpl(): Pair<LocalBluetoothLeBroadcast, AudioSharingRepositoryImpl> {
+ with(kosmos) {
+ val broadcast =
+ mock<LocalBluetoothLeBroadcast> {
+ on { isProfileReady } doReturn true
+ on { isEnabled(null) } doReturn false
+ }
+ val assistant =
+ mock<LocalBluetoothLeBroadcastAssistant> { on { isProfileReady } doReturn true }
+ val volumeControl = mock<VolumeControlProfile> { on { isProfileReady } doReturn true }
+ val profileManager =
+ mock<LocalBluetoothProfileManager> {
+ on { leAudioBroadcastProfile } doReturn broadcast
+ on { leAudioBroadcastAssistantProfile } doReturn assistant
+ on { volumeControlProfile } doReturn volumeControl
+ }
+ whenever(localBluetoothManager.profileManager).thenReturn(profileManager)
+ whenever(localBluetoothManager.eventManager).thenReturn(mock<BluetoothEventManager> {})
+
+ val repository =
+ AudioSharingRepositoryImpl(
+ localBluetoothManager,
+ com.android.settingslib.volume.data.repository.AudioSharingRepositoryImpl(
+ mock<ContentResolver> {},
+ localBluetoothManager,
+ testScope.backgroundScope,
+ testScope.testScheduler,
+ mock<AudioSharingLogger> {},
+ ),
+ testDispatcher,
+ )
+ return Pair(broadcast, spy(repository))
+ }
+ }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingRepositoryTest.kt
index acfe9dd45f75..f0746064f67f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingRepositoryTest.kt
@@ -111,6 +111,28 @@ class AudioSharingRepositoryTest : SysuiTestCase() {
}
@Test
+ fun testStopAudioSharing() =
+ with(kosmos) {
+ testScope.runTest {
+ whenever(localBluetoothManager.profileManager).thenReturn(profileManager)
+ whenever(profileManager.leAudioBroadcastProfile).thenReturn(leAudioBroadcastProfile)
+ audioSharingRepository.setAudioSharingAvailable(true)
+ underTest.stopAudioSharing()
+ verify(leAudioBroadcastProfile).stopLatestBroadcast()
+ }
+ }
+
+ @Test
+ fun testStopAudioSharing_flagOff_doNothing() =
+ with(kosmos) {
+ testScope.runTest {
+ audioSharingRepository.setAudioSharingAvailable(false)
+ underTest.stopAudioSharing()
+ verify(leAudioBroadcastProfile, never()).stopLatestBroadcast()
+ }
+ }
+
+ @Test
fun testAddSource_flagOff_doesNothing() =
with(kosmos) {
testScope.runTest {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractorTest.kt
index 44f9720cb9e4..ad0337e5ce86 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractorTest.kt
@@ -15,14 +15,15 @@
*/
package com.android.systemui.bluetooth.qsdialog
-import androidx.test.ext.junit.runners.AndroidJUnit4
import android.testing.TestableLooper
+import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.kosmos.testDispatcher
import com.android.systemui.kosmos.testScope
import com.android.systemui.statusbar.phone.SystemUIDialog
import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest
@@ -48,6 +49,7 @@ class DeviceItemActionInteractorTest : SysuiTestCase() {
private lateinit var notConnectedDeviceItem: DeviceItem
private lateinit var connectedMediaDeviceItem: DeviceItem
private lateinit var connectedOtherDeviceItem: DeviceItem
+ private lateinit var audioSharingDeviceItem: DeviceItem
@Mock private lateinit var dialog: SystemUIDialog
@Before
@@ -59,7 +61,7 @@ class DeviceItemActionInteractorTest : SysuiTestCase() {
deviceName = DEVICE_NAME,
connectionSummary = DEVICE_CONNECTION_SUMMARY,
iconWithDescription = null,
- background = null
+ background = null,
)
notConnectedDeviceItem =
DeviceItem(
@@ -68,7 +70,7 @@ class DeviceItemActionInteractorTest : SysuiTestCase() {
deviceName = DEVICE_NAME,
connectionSummary = DEVICE_CONNECTION_SUMMARY,
iconWithDescription = null,
- background = null
+ background = null,
)
connectedMediaDeviceItem =
DeviceItem(
@@ -77,7 +79,7 @@ class DeviceItemActionInteractorTest : SysuiTestCase() {
deviceName = DEVICE_NAME,
connectionSummary = DEVICE_CONNECTION_SUMMARY,
iconWithDescription = null,
- background = null
+ background = null,
)
connectedOtherDeviceItem =
DeviceItem(
@@ -86,7 +88,16 @@ class DeviceItemActionInteractorTest : SysuiTestCase() {
deviceName = DEVICE_NAME,
connectionSummary = DEVICE_CONNECTION_SUMMARY,
iconWithDescription = null,
- background = null
+ background = null,
+ )
+ audioSharingDeviceItem =
+ DeviceItem(
+ type = DeviceItemType.AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE,
+ cachedBluetoothDevice = kosmos.cachedBluetoothDevice,
+ deviceName = DEVICE_NAME,
+ connectionSummary = DEVICE_CONNECTION_SUMMARY,
+ iconWithDescription = null,
+ background = null,
)
actionInteractorImpl = kosmos.deviceItemActionInteractorImpl
}
@@ -135,6 +146,29 @@ class DeviceItemActionInteractorTest : SysuiTestCase() {
}
}
+ @Test
+ fun onActionIconClick_onIntent() {
+ with(kosmos) {
+ testScope.runTest {
+ var onIntentCalledOnAddress = ""
+ whenever(cachedBluetoothDevice.address).thenReturn(DEVICE_ADDRESS)
+ actionInteractorImpl.onActionIconClick(connectedMediaDeviceItem) {
+ onIntentCalledOnAddress = connectedMediaDeviceItem.cachedBluetoothDevice.address
+ }
+ assertThat(onIntentCalledOnAddress).isEqualTo(DEVICE_ADDRESS)
+ }
+ }
+ }
+
+ @Test(expected = IllegalArgumentException::class)
+ fun onActionIconClick_audioSharingDeviceType_throwException() {
+ with(kosmos) {
+ testScope.runTest {
+ actionInteractorImpl.onActionIconClick(audioSharingDeviceItem) {}
+ }
+ }
+ }
+
private companion object {
const val DEVICE_NAME = "device"
const val DEVICE_CONNECTION_SUMMARY = "active"
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorTest.kt
index 20d66155e5ca..6c955bf1818d 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorTest.kt
@@ -256,6 +256,22 @@ class DeviceEntryHapticsInteractorTest : SysuiTestCase() {
@EnableSceneContainer
@Test
+ fun playSuccessHaptic_onFaceAuthSuccess_whenBypassDisabled_sceneContainer() =
+ testScope.runTest {
+ underTest = kosmos.deviceEntryHapticsInteractor
+ val playSuccessHaptic by collectLastValue(underTest.playSuccessHaptic)
+
+ enrollFace()
+ kosmos.configureKeyguardBypass(isBypassAvailable = false)
+ runCurrent()
+ configureDeviceEntryFromBiometricSource(isFaceUnlock = true, bypassEnabled = false)
+ kosmos.fakeDeviceEntryFaceAuthRepository.isAuthenticated.value = true
+
+ assertThat(playSuccessHaptic).isNotNull()
+ }
+
+ @EnableSceneContainer
+ @Test
fun skipSuccessHaptic_onDeviceEntryFromSfps_whenPowerDown_sceneContainer() =
testScope.runTest {
kosmos.configureKeyguardBypass(isBypassAvailable = false)
@@ -299,6 +315,7 @@ class DeviceEntryHapticsInteractorTest : SysuiTestCase() {
private fun configureDeviceEntryFromBiometricSource(
isFpUnlock: Boolean = false,
isFaceUnlock: Boolean = false,
+ bypassEnabled: Boolean = true,
) {
// Mock DeviceEntrySourceInteractor#deviceEntryBiometricAuthSuccessState
if (isFpUnlock) {
@@ -314,11 +331,14 @@ class DeviceEntryHapticsInteractorTest : SysuiTestCase() {
)
// Mock DeviceEntrySourceInteractor#faceWakeAndUnlockMode = MODE_UNLOCK_COLLAPSING
- kosmos.sceneInteractor.setTransitionState(
- MutableStateFlow<ObservableTransitionState>(
- ObservableTransitionState.Idle(Scenes.Lockscreen)
+ // if the successful face authentication will bypass keyguard
+ if (bypassEnabled) {
+ kosmos.sceneInteractor.setTransitionState(
+ MutableStateFlow<ObservableTransitionState>(
+ ObservableTransitionState.Idle(Scenes.Lockscreen)
+ )
)
- )
+ }
}
underTest = kosmos.deviceEntryHapticsInteractor
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorParameterizedTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorParameterizedTest.kt
index 74e8257f4f08..5e023a203267 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorParameterizedTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorParameterizedTest.kt
@@ -292,8 +292,7 @@ class KeyboardTouchpadEduInteractorParameterizedTest(private val gestureType: Ge
val model by collectLastValue(repository.readGestureEduModelFlow(gestureType))
val originalValue = model!!.signalCount
- val listener = getOverviewProxyListener()
- listener.updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType)
+ updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType)
assertThat(model?.signalCount).isEqualTo(originalValue + 1)
}
@@ -306,8 +305,7 @@ class KeyboardTouchpadEduInteractorParameterizedTest(private val gestureType: Ge
val model by collectLastValue(repository.readGestureEduModelFlow(gestureType))
val originalValue = model!!.signalCount
- val listener = getOverviewProxyListener()
- listener.updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType)
+ updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType)
assertThat(model?.signalCount).isEqualTo(originalValue)
}
@@ -321,8 +319,7 @@ class KeyboardTouchpadEduInteractorParameterizedTest(private val gestureType: Ge
val model by collectLastValue(repository.readGestureEduModelFlow(gestureType))
val originalValue = model!!.signalCount
- val listener = getOverviewProxyListener()
- listener.updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType)
+ updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType)
assertThat(model?.signalCount).isEqualTo(originalValue + 1)
}
@@ -335,8 +332,7 @@ class KeyboardTouchpadEduInteractorParameterizedTest(private val gestureType: Ge
val model by collectLastValue(repository.readGestureEduModelFlow(gestureType))
val originalValue = model!!.signalCount
- val listener = getOverviewProxyListener()
- listener.updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType)
+ updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType)
assertThat(model?.signalCount).isEqualTo(originalValue)
}
@@ -347,8 +343,7 @@ class KeyboardTouchpadEduInteractorParameterizedTest(private val gestureType: Ge
val model by collectLastValue(repository.readGestureEduModelFlow(gestureType))
assertThat(model?.lastShortcutTriggeredTime).isNull()
- val listener = getOverviewProxyListener()
- listener.updateContextualEduStats(/* isTrackpadGesture= */ true, gestureType)
+ updateContextualEduStats(/* isTrackpadGesture= */ true, gestureType)
assertThat(model?.lastShortcutTriggeredTime).isEqualTo(kosmos.fakeEduClock.instant())
}
@@ -358,15 +353,14 @@ class KeyboardTouchpadEduInteractorParameterizedTest(private val gestureType: Ge
testScope.runTest {
setUpForDeviceConnection()
tutorialSchedulerRepository.setScheduledTutorialLaunchTime(
- DeviceType.TOUCHPAD,
+ getTargetDevice(gestureType),
eduClock.instant(),
)
val model by collectLastValue(repository.readGestureEduModelFlow(gestureType))
val originalValue = model!!.signalCount
eduClock.offset(initialDelayElapsedDuration)
- val listener = getOverviewProxyListener()
- listener.updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType)
+ updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType)
assertThat(model?.signalCount).isEqualTo(originalValue + 1)
}
@@ -376,33 +370,92 @@ class KeyboardTouchpadEduInteractorParameterizedTest(private val gestureType: Ge
testScope.runTest {
setUpForDeviceConnection()
tutorialSchedulerRepository.setScheduledTutorialLaunchTime(
- DeviceType.TOUCHPAD,
+ getTargetDevice(gestureType),
eduClock.instant(),
)
val model by collectLastValue(repository.readGestureEduModelFlow(gestureType))
val originalValue = model!!.signalCount
// No offset to the clock to simulate update before initial delay
- val listener = getOverviewProxyListener()
- listener.updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType)
+ updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType)
assertThat(model?.signalCount).isEqualTo(originalValue)
}
@Test
- fun dataUnchangedOnIncrementSignalCountWithoutOobeLaunchTime() =
+ fun dataUnchangedOnIncrementSignalCountWithoutOobeLaunchOrNotifyTime() =
testScope.runTest {
- // No update to OOBE launch time to simulate no OOBE is launched yet
+ // No update to OOBE launch/notify time to simulate no OOBE is launched yet
setUpForDeviceConnection()
val model by collectLastValue(repository.readGestureEduModelFlow(gestureType))
val originalValue = model!!.signalCount
- val listener = getOverviewProxyListener()
- listener.updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType)
+ updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType)
assertThat(model?.signalCount).isEqualTo(originalValue)
}
+ @Test
+ fun dataUpdatedOnIncrementSignalCountAfterNotifyTimeDelayWithoutLaunchTime() =
+ testScope.runTest {
+ setUpForDeviceConnection()
+ tutorialSchedulerRepository.setNotifiedTime(
+ getTargetDevice(gestureType),
+ eduClock.instant(),
+ )
+
+ val model by collectLastValue(repository.readGestureEduModelFlow(gestureType))
+ val originalValue = model!!.signalCount
+ eduClock.offset(initialDelayElapsedDuration)
+ updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType)
+
+ assertThat(model?.signalCount).isEqualTo(originalValue + 1)
+ }
+
+ @Test
+ fun dataUnchangedOnIncrementSignalCountBeforeLaunchTimeDelayWithNotifyTime() =
+ testScope.runTest {
+ setUpForDeviceConnection()
+ tutorialSchedulerRepository.setNotifiedTime(
+ getTargetDevice(gestureType),
+ eduClock.instant(),
+ )
+ eduClock.offset(initialDelayElapsedDuration)
+
+ tutorialSchedulerRepository.setScheduledTutorialLaunchTime(
+ getTargetDevice(gestureType),
+ eduClock.instant(),
+ )
+ val model by collectLastValue(repository.readGestureEduModelFlow(gestureType))
+ val originalValue = model!!.signalCount
+ // No offset to the clock to simulate update before initial delay of launch time
+ updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType)
+
+ assertThat(model?.signalCount).isEqualTo(originalValue)
+ }
+
+ @Test
+ fun dataUpdatedOnIncrementSignalCountAfterLaunchTimeDelayWithNotifyTime() =
+ testScope.runTest {
+ setUpForDeviceConnection()
+ tutorialSchedulerRepository.setNotifiedTime(
+ getTargetDevice(gestureType),
+ eduClock.instant(),
+ )
+ eduClock.offset(initialDelayElapsedDuration)
+
+ tutorialSchedulerRepository.setScheduledTutorialLaunchTime(
+ getTargetDevice(gestureType),
+ eduClock.instant(),
+ )
+ val model by collectLastValue(repository.readGestureEduModelFlow(gestureType))
+ val originalValue = model!!.signalCount
+ eduClock.offset(initialDelayElapsedDuration)
+ updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType)
+
+ assertThat(model?.signalCount).isEqualTo(originalValue + 1)
+ }
+
private suspend fun setUpForInitialDelayElapse() {
tutorialSchedulerRepository.setScheduledTutorialLaunchTime(
DeviceType.TOUCHPAD,
@@ -465,12 +518,18 @@ class KeyboardTouchpadEduInteractorParameterizedTest(private val gestureType: Ge
keyboardRepository.setIsAnyKeyboardConnected(true)
}
- private fun getOverviewProxyListener(): OverviewProxyListener {
+ private fun updateContextualEduStats(isTrackpadGesture: Boolean, gestureType: GestureType) {
val listenerCaptor = argumentCaptor<OverviewProxyListener>()
verify(overviewProxyService).addCallback(listenerCaptor.capture())
- return listenerCaptor.firstValue
+ listenerCaptor.firstValue.updateContextualEduStats(isTrackpadGesture, gestureType)
}
+ private fun getTargetDevice(gestureType: GestureType) =
+ when (gestureType) {
+ ALL_APPS -> DeviceType.KEYBOARD
+ else -> DeviceType.TOUCHPAD
+ }
+
companion object {
private val USER_INFOS = listOf(UserInfo(101, "Second User", 0))
diff --git a/packages/SystemUI/tests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorTest.kt
index 692b9c67f322..692b9c67f322 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorTest.kt
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/source/TestShortcuts.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/source/TestShortcuts.kt
index 7c88d76f28bd..183e4d6f624b 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/source/TestShortcuts.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/source/TestShortcuts.kt
@@ -24,6 +24,7 @@ import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_HOME
import android.os.SystemClock
import android.view.KeyEvent
import android.view.KeyEvent.ACTION_DOWN
+import android.view.KeyEvent.ACTION_UP
import android.view.KeyEvent.KEYCODE_A
import android.view.KeyEvent.META_ALT_ON
import android.view.KeyEvent.META_CTRL_ON
@@ -540,11 +541,7 @@ object TestShortcuts {
simpleShortcutCategory(System, "System apps", "Take a note"),
simpleShortcutCategory(System, "System controls", "Take screenshot"),
simpleShortcutCategory(System, "System controls", "Go back"),
- simpleShortcutCategory(
- MultiTasking,
- "Split screen",
- "Switch to full screen",
- ),
+ simpleShortcutCategory(MultiTasking, "Split screen", "Switch to full screen"),
simpleShortcutCategory(
MultiTasking,
"Split screen",
@@ -704,7 +701,7 @@ object TestShortcuts {
android.view.KeyEvent(
/* downTime = */ SystemClock.uptimeMillis(),
/* eventTime = */ SystemClock.uptimeMillis(),
- /* action = */ ACTION_DOWN,
+ /* action = */ ACTION_UP,
/* code = */ KEYCODE_A,
/* repeat = */ 0,
/* metaState = */ 0,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutCustomizationViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutCustomizationViewModelTest.kt
index 755c218f6789..d9d34f5ace7b 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutCustomizationViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutCustomizationViewModelTest.kt
@@ -92,13 +92,14 @@ class ShortcutCustomizationViewModelTest : SysuiTestCase() {
viewModel.onShortcutCustomizationRequested(standardAddShortcutRequest)
val uiState by collectLastValue(viewModel.shortcutCustomizationUiState)
- assertThat(uiState).isEqualTo(
- AddShortcutDialog(
- shortcutLabel = "Standard shortcut",
- defaultCustomShortcutModifierKey =
- ShortcutKey.Icon.ResIdIcon(R.drawable.ic_ksh_key_meta),
+ assertThat(uiState)
+ .isEqualTo(
+ AddShortcutDialog(
+ shortcutLabel = "Standard shortcut",
+ defaultCustomShortcutModifierKey =
+ ShortcutKey.Icon.ResIdIcon(R.drawable.ic_ksh_key_meta),
+ )
)
- )
}
}
@@ -137,8 +138,7 @@ class ShortcutCustomizationViewModelTest : SysuiTestCase() {
testScope.runTest {
val uiState by collectLastValue(viewModel.shortcutCustomizationUiState)
viewModel.onShortcutCustomizationRequested(standardAddShortcutRequest)
- assertThat((uiState as AddShortcutDialog).pressedKeys)
- .isEmpty()
+ assertThat((uiState as AddShortcutDialog).pressedKeys).isEmpty()
}
}
@@ -161,8 +161,7 @@ class ShortcutCustomizationViewModelTest : SysuiTestCase() {
val uiState by collectLastValue(viewModel.shortcutCustomizationUiState)
viewModel.onShortcutCustomizationRequested(allAppsShortcutAddRequest)
- assertThat((uiState as AddShortcutDialog).errorMessage)
- .isEmpty()
+ assertThat((uiState as AddShortcutDialog).errorMessage).isEmpty()
}
}
@@ -244,32 +243,34 @@ class ShortcutCustomizationViewModelTest : SysuiTestCase() {
}
@Test
- fun onKeyPressed_handlesKeyEvents_whereActionKeyIsAlsoPressed() {
+ fun onShortcutKeyCombinationSelected_handlesKeyEvents_whereActionKeyIsAlsoPressed() {
testScope.runTest {
viewModel.onShortcutCustomizationRequested(standardAddShortcutRequest)
- val isHandled = viewModel.onKeyPressed(keyDownEventWithActionKeyPressed)
+ val isHandled =
+ viewModel.onShortcutKeyCombinationSelected(keyDownEventWithActionKeyPressed)
assertThat(isHandled).isTrue()
}
}
@Test
- fun onKeyPressed_doesNotHandleKeyEvents_whenActionKeyIsNotAlsoPressed() {
+ fun onShortcutKeyCombinationSelected_doesNotHandleKeyEvents_whenActionKeyIsNotAlsoPressed() {
testScope.runTest {
viewModel.onShortcutCustomizationRequested(standardAddShortcutRequest)
- val isHandled = viewModel.onKeyPressed(keyDownEventWithoutActionKeyPressed)
+ val isHandled =
+ viewModel.onShortcutKeyCombinationSelected(keyDownEventWithoutActionKeyPressed)
assertThat(isHandled).isFalse()
}
}
@Test
- fun onKeyPressed_convertsKeyEventsAndUpdatesUiStatesPressedKey() {
+ fun onShortcutKeyCombinationSelected_convertsKeyEventsAndUpdatesUiStatesPressedKey() {
testScope.runTest {
val uiState by collectLastValue(viewModel.shortcutCustomizationUiState)
viewModel.onShortcutCustomizationRequested(standardAddShortcutRequest)
- viewModel.onKeyPressed(keyDownEventWithActionKeyPressed)
- viewModel.onKeyPressed(keyUpEventWithActionKeyPressed)
+ viewModel.onShortcutKeyCombinationSelected(keyDownEventWithActionKeyPressed)
+ viewModel.onShortcutKeyCombinationSelected(keyUpEventWithActionKeyPressed)
// Note that Action Key is excluded as it's already displayed on the UI
assertThat((uiState as AddShortcutDialog).pressedKeys)
@@ -282,8 +283,8 @@ class ShortcutCustomizationViewModelTest : SysuiTestCase() {
testScope.runTest {
val uiState by collectLastValue(viewModel.shortcutCustomizationUiState)
viewModel.onShortcutCustomizationRequested(standardAddShortcutRequest)
- viewModel.onKeyPressed(keyDownEventWithActionKeyPressed)
- viewModel.onKeyPressed(keyUpEventWithActionKeyPressed)
+ viewModel.onShortcutKeyCombinationSelected(keyDownEventWithActionKeyPressed)
+ viewModel.onShortcutKeyCombinationSelected(keyUpEventWithActionKeyPressed)
// Note that Action Key is excluded as it's already displayed on the UI
assertThat((uiState as AddShortcutDialog).pressedKeys)
@@ -292,16 +293,15 @@ class ShortcutCustomizationViewModelTest : SysuiTestCase() {
// Close the dialog and show it again
viewModel.onDialogDismissed()
viewModel.onShortcutCustomizationRequested(standardAddShortcutRequest)
- assertThat((uiState as AddShortcutDialog).pressedKeys)
- .isEmpty()
+ assertThat((uiState as AddShortcutDialog).pressedKeys).isEmpty()
}
}
private suspend fun openAddShortcutDialogAndSetShortcut() {
viewModel.onShortcutCustomizationRequested(allAppsShortcutAddRequest)
- viewModel.onKeyPressed(keyDownEventWithActionKeyPressed)
- viewModel.onKeyPressed(keyUpEventWithActionKeyPressed)
+ viewModel.onShortcutKeyCombinationSelected(keyDownEventWithActionKeyPressed)
+ viewModel.onShortcutKeyCombinationSelected(keyUpEventWithActionKeyPressed)
viewModel.onSetShortcut()
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/DoNotDisturbQuickAffordanceConfigTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/DoNotDisturbQuickAffordanceConfigTest.kt
index fde9b8ce6a50..bf49186a7f01 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/DoNotDisturbQuickAffordanceConfigTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/DoNotDisturbQuickAffordanceConfigTest.kt
@@ -28,7 +28,7 @@ import android.provider.Settings.Secure.ZEN_DURATION_PROMPT
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.settingslib.notification.modes.EnableZenModeDialog
-import com.android.settingslib.notification.modes.TestModeBuilder
+import com.android.settingslib.notification.modes.TestModeBuilder.MANUAL_DND
import com.android.systemui.SysuiTestCase
import com.android.systemui.animation.Expandable
import com.android.systemui.common.shared.model.ContentDescription
@@ -187,7 +187,7 @@ class DoNotDisturbQuickAffordanceConfigTest : SysuiTestCase() {
testScope.runTest {
val currentModes by collectLastValue(zenModeRepository.modes)
- zenModeRepository.addMode(TestModeBuilder.MANUAL_DND_ACTIVE)
+ zenModeRepository.activateMode(MANUAL_DND)
secureSettingsRepository.setInt(Settings.Secure.ZEN_DURATION, -2)
collectLastValue(underTest.lockScreenState)
runCurrent()
@@ -233,7 +233,6 @@ class DoNotDisturbQuickAffordanceConfigTest : SysuiTestCase() {
testScope.runTest {
val currentModes by collectLastValue(zenModeRepository.modes)
- zenModeRepository.addMode(TestModeBuilder.MANUAL_DND_INACTIVE)
secureSettingsRepository.setInt(Settings.Secure.ZEN_DURATION, ZEN_DURATION_FOREVER)
collectLastValue(underTest.lockScreenState)
runCurrent()
@@ -278,7 +277,6 @@ class DoNotDisturbQuickAffordanceConfigTest : SysuiTestCase() {
fun onTriggered_dndModeIsOff_settingNotFOREVERorPROMPT_dndWithDuration() =
testScope.runTest {
val currentModes by collectLastValue(zenModeRepository.modes)
- zenModeRepository.addMode(TestModeBuilder.MANUAL_DND_INACTIVE)
secureSettingsRepository.setInt(Settings.Secure.ZEN_DURATION, -900)
runCurrent()
@@ -323,7 +321,6 @@ class DoNotDisturbQuickAffordanceConfigTest : SysuiTestCase() {
fun onTriggered_dndModeIsOff_settingIsPROMPT_showDialog() =
testScope.runTest {
val expandable: Expandable = mock()
- zenModeRepository.addMode(TestModeBuilder.MANUAL_DND_INACTIVE)
secureSettingsRepository.setInt(Settings.Secure.ZEN_DURATION, ZEN_DURATION_PROMPT)
whenever(enableZenModeDialog.createDialog()).thenReturn(mock())
collectLastValue(underTest.lockScreenState)
@@ -405,10 +402,6 @@ class DoNotDisturbQuickAffordanceConfigTest : SysuiTestCase() {
testScope.runTest {
val lockScreenState by collectLastValue(underTest.lockScreenState)
- val manualDnd = TestModeBuilder.MANUAL_DND_INACTIVE
- zenModeRepository.addMode(manualDnd)
- runCurrent()
-
assertThat(lockScreenState)
.isEqualTo(
KeyguardQuickAffordanceConfig.LockScreenState.Visible(
@@ -420,7 +413,7 @@ class DoNotDisturbQuickAffordanceConfigTest : SysuiTestCase() {
)
)
- zenModeRepository.activateMode(manualDnd)
+ zenModeRepository.activateMode(MANUAL_DND)
runCurrent()
assertThat(lockScreenState)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorTest.kt
index b3417b9de36d..c44f27ef348b 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorTest.kt
@@ -46,8 +46,6 @@ import com.android.systemui.keyguard.shared.model.TransitionState.RUNNING
import com.android.systemui.keyguard.shared.model.TransitionState.STARTED
import com.android.systemui.keyguard.shared.model.TransitionStep
import com.android.systemui.kosmos.testScope
-import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest
-import com.android.systemui.power.domain.interactor.powerInteractor
import com.android.systemui.res.R
import com.android.systemui.scene.domain.interactor.sceneInteractor
import com.android.systemui.scene.shared.model.Scenes
@@ -76,7 +74,6 @@ class KeyguardInteractorTest : SysuiTestCase() {
private val configRepository by lazy { kosmos.fakeConfigurationRepository }
private val bouncerRepository by lazy { kosmos.keyguardBouncerRepository }
private val shadeRepository by lazy { kosmos.shadeRepository }
- private val powerInteractor by lazy { kosmos.powerInteractor }
private val keyguardRepository by lazy { kosmos.keyguardRepository }
private val keyguardTransitionRepository by lazy { kosmos.fakeKeyguardTransitionRepository }
@@ -444,7 +441,6 @@ class KeyguardInteractorTest : SysuiTestCase() {
repository.setDozeTransitionModel(
DozeTransitionModel(from = DozeStateModel.DOZE, to = DozeStateModel.FINISH)
)
- powerInteractor.setAwakeForTest()
advanceTimeBy(1000L)
assertThat(isAbleToDream).isEqualTo(false)
@@ -460,9 +456,6 @@ class KeyguardInteractorTest : SysuiTestCase() {
repository.setDozeTransitionModel(
DozeTransitionModel(from = DozeStateModel.DOZE, to = DozeStateModel.FINISH)
)
- powerInteractor.setAwakeForTest()
- runCurrent()
-
// After some delay, still false
advanceTimeBy(300L)
assertThat(isAbleToDream).isEqualTo(false)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToPrimaryBouncerTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToPrimaryBouncerTransitionViewModelTest.kt
index feaf06aca29a..ade7614ae853 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToPrimaryBouncerTransitionViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToPrimaryBouncerTransitionViewModelTest.kt
@@ -16,10 +16,13 @@
package com.android.systemui.keyguard.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.FLAG_BOUNCER_UI_REVAMP
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectValues
+import com.android.systemui.flags.BrokenWithSceneContainer
import com.android.systemui.flags.Flags
import com.android.systemui.flags.fakeFeatureFlagsClassic
import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
@@ -72,6 +75,28 @@ class AlternateBouncerToPrimaryBouncerTransitionViewModelTest : SysuiTestCase()
}
@Test
+ @EnableFlags(FLAG_BOUNCER_UI_REVAMP)
+ @BrokenWithSceneContainer(388068805)
+ fun notifications_areFullyVisible_whenShadeIsOpen() =
+ testScope.runTest {
+ val values by collectValues(underTest.notificationAlpha)
+ kosmos.bouncerWindowBlurTestUtil.shadeExpanded(true)
+
+ keyguardTransitionRepository.sendTransitionSteps(
+ listOf(
+ step(0f, TransitionState.STARTED),
+ step(0.1f),
+ step(0.2f),
+ step(0.3f),
+ step(1f),
+ ),
+ testScope,
+ )
+
+ values.forEach { assertThat(it).isEqualTo(1f) }
+ }
+
+ @Test
fun blurRadiusGoesToMaximumWhenShadeIsExpanded() =
testScope.runTest {
val values by collectValues(underTest.windowBlurRadius)
@@ -88,6 +113,25 @@ class AlternateBouncerToPrimaryBouncerTransitionViewModelTest : SysuiTestCase()
}
@Test
+ @EnableFlags(FLAG_BOUNCER_UI_REVAMP)
+ @BrokenWithSceneContainer(388068805)
+ fun notificationBlur_isNonZero_whenShadeIsExpanded() =
+ testScope.runTest {
+ val values by collectValues(underTest.notificationBlurRadius)
+
+ kosmos.bouncerWindowBlurTestUtil.shadeExpanded(true)
+
+ kosmos.bouncerWindowBlurTestUtil.assertTransitionToBlurRadius(
+ transitionProgress = listOf(0f, 0f, 0.1f, 0.2f, 0.3f, 1f),
+ startValue = kosmos.blurConfig.maxBlurRadiusPx / 3.0f,
+ endValue = kosmos.blurConfig.maxBlurRadiusPx / 3.0f,
+ transitionFactory = ::step,
+ actualValuesProvider = { values },
+ checkInterpolatedValues = false,
+ )
+ }
+
+ @Test
fun blurRadiusGoesFromMinToMaxWhenShadeIsNotExpanded() =
testScope.runTest {
val values by collectValues(underTest.windowBlurRadius)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModelTest.kt
index d909c5ab5f1b..914094fa39df 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModelTest.kt
@@ -16,9 +16,11 @@
package com.android.systemui.keyguard.ui.viewmodel
+import android.platform.test.annotations.EnableFlags
import android.platform.test.flag.junit.FlagsParameterization
import androidx.test.filters.SmallTest
import com.android.compose.animation.scene.ObservableTransitionState
+import com.android.systemui.Flags.FLAG_BOUNCER_UI_REVAMP
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.coroutines.collectValues
@@ -153,7 +155,7 @@ class LockscreenToPrimaryBouncerTransitionViewModelTest(flags: FlagsParameteriza
}
@Test
- @BrokenWithSceneContainer(330311871)
+ @BrokenWithSceneContainer(388068805)
fun blurRadiusIsMaxWhenShadeIsExpanded() =
testScope.runTest {
val values by collectValues(underTest.windowBlurRadius)
@@ -170,7 +172,7 @@ class LockscreenToPrimaryBouncerTransitionViewModelTest(flags: FlagsParameteriza
}
@Test
- @BrokenWithSceneContainer(330311871)
+ @BrokenWithSceneContainer(388068805)
fun blurRadiusGoesFromMinToMaxWhenShadeIsNotExpanded() =
testScope.runTest {
val values by collectValues(underTest.windowBlurRadius)
@@ -185,6 +187,44 @@ class LockscreenToPrimaryBouncerTransitionViewModelTest(flags: FlagsParameteriza
)
}
+ @Test
+ @EnableFlags(FLAG_BOUNCER_UI_REVAMP)
+ @BrokenWithSceneContainer(388068805)
+ fun notificationBlur_isNonZero_whenShadeIsExpanded() =
+ testScope.runTest {
+ val values by collectValues(underTest.notificationBlurRadius)
+ kosmos.bouncerWindowBlurTestUtil.shadeExpanded(true)
+ runCurrent()
+
+ kosmos.bouncerWindowBlurTestUtil.assertTransitionToBlurRadius(
+ transitionProgress = listOf(0f, 0f, 0.1f, 0.2f, 0.3f, 1f),
+ startValue = kosmos.blurConfig.maxBlurRadiusPx / 3.0f,
+ endValue = kosmos.blurConfig.maxBlurRadiusPx / 3.0f,
+ transitionFactory = ::step,
+ actualValuesProvider = { values },
+ checkInterpolatedValues = false,
+ )
+ }
+
+ @Test
+ @EnableFlags(FLAG_BOUNCER_UI_REVAMP)
+ @BrokenWithSceneContainer(388068805)
+ fun notifications_areFullyVisible_whenShadeIsExpanded() =
+ testScope.runTest {
+ val values by collectValues(underTest.notificationAlpha)
+ kosmos.bouncerWindowBlurTestUtil.shadeExpanded(true)
+ runCurrent()
+
+ kosmos.bouncerWindowBlurTestUtil.assertTransitionToBlurRadius(
+ transitionProgress = listOf(0f, 0f, 0.1f, 0.2f, 0.3f, 1f),
+ startValue = 1.0f,
+ endValue = 1.0f,
+ transitionFactory = ::step,
+ actualValuesProvider = { values },
+ checkInterpolatedValues = false,
+ )
+ }
+
private fun step(
value: Float,
state: TransitionState = TransitionState.RUNNING,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/EditModeButtonViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/EditModeButtonViewModelTest.kt
index a8e390c25a4d..46d98f979655 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/EditModeButtonViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/EditModeButtonViewModelTest.kt
@@ -23,11 +23,16 @@ import com.android.systemui.classifier.fakeFalsingManager
import com.android.systemui.kosmos.collectLastValue
import com.android.systemui.kosmos.runCurrent
import com.android.systemui.kosmos.runTest
+import com.android.systemui.plugins.activityStarter
import com.android.systemui.qs.panels.ui.viewmodel.toolbar.editModeButtonViewModelFactory
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
+import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doAnswer
+import org.mockito.kotlin.whenever
@RunWith(AndroidJUnit4::class)
@SmallTest
@@ -36,6 +41,15 @@ class EditModeButtonViewModelTest : SysuiTestCase() {
val underTest = kosmos.editModeButtonViewModelFactory.create()
+ @Before
+ fun setUp() {
+ with(kosmos) {
+ whenever(activityStarter.postQSRunnableDismissingKeyguard(any())).doAnswer {
+ (it.getArgument(0) as Runnable).run()
+ }
+ }
+ }
+
@Test
fun falsingFalseTap_editModeDoesntStart() =
kosmos.runTest {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/InternetTileNewImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/InternetTileNewImplTest.kt
index eeccbdf20540..79556baed067 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/InternetTileNewImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/InternetTileNewImplTest.kt
@@ -17,6 +17,8 @@
package com.android.systemui.qs.tiles
import android.os.Handler
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
import android.platform.test.flag.junit.FlagsParameterization
import android.platform.test.flag.junit.FlagsParameterization.allCombinationsOf
import android.service.quicksettings.Tile
@@ -24,18 +26,26 @@ import android.testing.TestableLooper
import android.testing.TestableLooper.RunWithLooper
import androidx.test.filters.SmallTest
import com.android.internal.logging.MetricsLogger
+import com.android.systemui.Flags
+import com.android.systemui.Flags.FLAG_SCENE_CONTAINER
import com.android.systemui.SysuiTestCase
import com.android.systemui.classifier.FalsingManagerFake
+import com.android.systemui.flags.setFlagValue
+import com.android.systemui.keyguard.KeyguardWmStateRefactor
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.plugins.statusbar.StatusBarStateController
import com.android.systemui.qs.QSHost
import com.android.systemui.qs.QsEventLogger
import com.android.systemui.qs.flags.QSComposeFragment
+import com.android.systemui.qs.flags.QsDetailedView
import com.android.systemui.qs.logging.QSLogger
import com.android.systemui.qs.tiles.dialog.InternetDialogManager
import com.android.systemui.qs.tiles.dialog.WifiStateWorker
import com.android.systemui.res.R
+import com.android.systemui.scene.shared.flag.SceneContainerFlag
+import com.android.systemui.shade.shared.flag.DualShade
import com.android.systemui.statusbar.connectivity.AccessPointController
+import com.android.systemui.statusbar.notification.shared.NotificationThrottleHun
import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirplaneModeRepository
import com.android.systemui.statusbar.pipeline.ethernet.domain.EthernetInteractor
import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.FakeMobileIconsInteractor
@@ -256,6 +266,41 @@ class InternetTileNewImplTest(flags: FlagsParameterization) : SysuiTestCase() {
verify(wifiStateWorker, times(1)).isWifiEnabled = eq(true)
}
+ @Test
+ @DisableFlags(QsDetailedView.FLAG_NAME)
+ fun click_withQsDetailedViewDisabled() {
+ underTest.click(null)
+ looper.processAllMessages()
+
+ verify(dialogManager, times(1)).create(
+ aboveStatusBar = true,
+ accessPointController.canConfigMobileData(),
+ accessPointController.canConfigWifi(),
+ null,
+ )
+ }
+
+ @Test
+ @EnableFlags(
+ value = [
+ QsDetailedView.FLAG_NAME,
+ FLAG_SCENE_CONTAINER,
+ KeyguardWmStateRefactor.FLAG_NAME,
+ NotificationThrottleHun.FLAG_NAME,
+ DualShade.FLAG_NAME]
+ )
+ fun click_withQsDetailedViewEnabled() {
+ underTest.click(null)
+ looper.processAllMessages()
+
+ verify(dialogManager, times(0)).create(
+ aboveStatusBar = true,
+ accessPointController.canConfigMobileData(),
+ accessPointController.canConfigWifi(),
+ null,
+ )
+ }
+
companion object {
const val WIFI_SSID = "test ssid"
val ACTIVE_WIFI =
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/ScreenRecordTileTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/ScreenRecordTileTest.java
index fc1d73b62abd..3a3f5371d195 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/ScreenRecordTileTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/ScreenRecordTileTest.java
@@ -35,6 +35,7 @@ import static org.mockito.Mockito.when;
import android.app.Dialog;
import android.media.projection.StopReason;
import android.os.Handler;
+import android.platform.test.annotations.EnableFlags;
import android.platform.test.flag.junit.FlagsParameterization;
import android.service.quicksettings.Tile;
import android.testing.TestableLooper;
@@ -52,6 +53,7 @@ import com.android.systemui.plugins.qs.QSTile;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.qs.QSHost;
import com.android.systemui.qs.QsEventLogger;
+import com.android.systemui.qs.flags.QsDetailedView;
import com.android.systemui.qs.flags.QsInCompose;
import com.android.systemui.qs.logging.QSLogger;
import com.android.systemui.qs.pipeline.domain.interactor.PanelInteractor;
@@ -63,6 +65,7 @@ import com.android.systemui.statusbar.phone.KeyguardDismissUtil;
import com.android.systemui.statusbar.policy.KeyguardStateController;
import org.junit.After;
+import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -70,11 +73,11 @@ import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
-import java.util.List;
-
import platform.test.runner.parameterized.ParameterizedAndroidJunit4;
import platform.test.runner.parameterized.Parameters;
+import java.util.List;
+
@RunWith(ParameterizedAndroidJunit4.class)
@TestableLooper.RunWithLooper(setAsMainLooper = true)
@SmallTest
@@ -82,7 +85,8 @@ public class ScreenRecordTileTest extends SysuiTestCase {
@Parameters(name = "{0}")
public static List<FlagsParameterization> getParams() {
- return allCombinationsOf(FLAG_QS_CUSTOM_TILE_CLICK_GUARANTEED_BUG_FIX);
+ return allCombinationsOf(FLAG_QS_CUSTOM_TILE_CLICK_GUARANTEED_BUG_FIX,
+ QsDetailedView.FLAG_NAME);
}
@Mock
@@ -336,6 +340,30 @@ public class ScreenRecordTileTest extends SysuiTestCase {
.notifyPermissionRequestDisplayed(mContext.getUserId());
}
+ @Test
+ @EnableFlags(QsDetailedView.FLAG_NAME)
+ public void testNotStartingAndRecording_returnDetailsViewModel() {
+ when(mController.isStarting()).thenReturn(false);
+ when(mController.isRecording()).thenReturn(false);
+ mTile.getDetailsViewModel(Assert::assertNotNull);
+ }
+
+ @Test
+ @EnableFlags(QsDetailedView.FLAG_NAME)
+ public void testStarting_notReturnDetailsViewModel() {
+ when(mController.isStarting()).thenReturn(true);
+ when(mController.isRecording()).thenReturn(false);
+ mTile.getDetailsViewModel(Assert::assertNull);
+ }
+
+ @Test
+ @EnableFlags(QsDetailedView.FLAG_NAME)
+ public void testRecording_notReturnDetailsViewModel() {
+ when(mController.isStarting()).thenReturn(false);
+ when(mController.isRecording()).thenReturn(true);
+ mTile.getDetailsViewModel(Assert::assertNull);
+ }
+
private QSTile.Icon createExpectedIcon(int resId) {
if (QsInCompose.isEnabled()) {
return new QSTileImpl.DrawableIconWithRes(mContext.getDrawable(resId), resId);
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractorTest.kt
index 04e094f25f5d..c8b3aba9b846 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractorTest.kt
@@ -24,6 +24,7 @@ import android.provider.Settings
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.settingslib.notification.modes.TestModeBuilder
+import com.android.settingslib.notification.modes.TestModeBuilder.MANUAL_DND
import com.android.systemui.Flags
import com.android.systemui.SysuiTestCase
import com.android.systemui.animation.Expandable
@@ -87,9 +88,9 @@ class ModesTileUserActionInteractorTest : SysuiTestCase() {
testScope.runTest {
val activeModes by collectLastValue(zenModeInteractor.activeModes)
+ zenModeRepository.activateMode(MANUAL_DND)
zenModeRepository.addModes(
listOf(
- TestModeBuilder.MANUAL_DND_ACTIVE,
TestModeBuilder().setName("Mode 1").setActive(true).build(),
TestModeBuilder().setName("Mode 2").setActive(true).build(),
)
@@ -111,7 +112,7 @@ class ModesTileUserActionInteractorTest : SysuiTestCase() {
testScope.runTest {
val dndMode by collectLastValue(zenModeInteractor.dndMode)
- zenModeRepository.addMode(TestModeBuilder.MANUAL_DND_ACTIVE)
+ zenModeRepository.activateMode(MANUAL_DND)
assertThat(dndMode?.isActive).isTrue()
underTest.handleInput(
@@ -127,7 +128,6 @@ class ModesTileUserActionInteractorTest : SysuiTestCase() {
testScope.runTest {
val dndMode by collectLastValue(zenModeInteractor.dndMode)
- zenModeRepository.addMode(TestModeBuilder.MANUAL_DND_INACTIVE)
assertThat(dndMode?.isActive).isFalse()
underTest.handleInput(
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 48edded5df18..de54e75281aa 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
@@ -575,4 +575,50 @@ class SceneInteractorTest : SysuiTestCase() {
assertThat(currentScene).isNotEqualTo(disabledScene)
}
+
+ @Test
+ fun transitionAnimations() =
+ kosmos.runTest {
+ val isVisible by collectLastValue(underTest.isVisible)
+ assertThat(isVisible).isTrue()
+
+ underTest.setVisible(false, "test")
+ assertThat(isVisible).isFalse()
+
+ underTest.onTransitionAnimationStart()
+ // One animation is active, forced visible.
+ assertThat(isVisible).isTrue()
+
+ underTest.onTransitionAnimationEnd()
+ // No more active animations, not forced visible.
+ assertThat(isVisible).isFalse()
+
+ underTest.onTransitionAnimationStart()
+ // One animation is active, forced visible.
+ assertThat(isVisible).isTrue()
+
+ underTest.onTransitionAnimationCancelled()
+ // No more active animations, not forced visible.
+ assertThat(isVisible).isFalse()
+
+ underTest.setVisible(true, "test")
+ assertThat(isVisible).isTrue()
+
+ underTest.onTransitionAnimationStart()
+ underTest.onTransitionAnimationStart()
+ // Two animations are active, forced visible.
+ assertThat(isVisible).isTrue()
+
+ underTest.setVisible(false, "test")
+ // Two animations are active, forced visible.
+ assertThat(isVisible).isTrue()
+
+ underTest.onTransitionAnimationEnd()
+ // One animation is still active, forced visible.
+ assertThat(isVisible).isTrue()
+
+ underTest.onTransitionAnimationEnd()
+ // No more active animations, not forced visible.
+ assertThat(isVisible).isFalse()
+ }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
index 06dd046564df..51f056aa18da 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
@@ -36,6 +36,8 @@ import com.android.keyguard.AuthInteractionProperties
import com.android.keyguard.keyguardUpdateMonitor
import com.android.systemui.Flags
import com.android.systemui.SysuiTestCase
+import com.android.systemui.animation.ActivityTransitionAnimator
+import com.android.systemui.animation.activityTransitionAnimator
import com.android.systemui.authentication.data.repository.FakeAuthenticationRepository
import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository
import com.android.systemui.authentication.domain.interactor.authenticationInteractor
@@ -141,6 +143,7 @@ import org.mockito.Mockito.never
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.argumentCaptor
import org.mockito.kotlin.whenever
@SmallTest
@@ -169,6 +172,7 @@ class SceneContainerStartableTest : SysuiTestCase() {
private val uiEventLoggerFake = kosmos.uiEventLoggerFake
private val msdlPlayer = kosmos.fakeMSDLPlayer
private val authInteractionProperties = AuthInteractionProperties()
+ private val mockActivityTransitionAnimator = mock<ActivityTransitionAnimator>()
private lateinit var underTest: SceneContainerStartable
@@ -177,6 +181,8 @@ class SceneContainerStartableTest : SysuiTestCase() {
MockitoAnnotations.initMocks(this)
whenever(kosmos.keyguardUpdateMonitor.isUnlockingWithBiometricAllowed(anyBoolean()))
.thenReturn(true)
+ kosmos.activityTransitionAnimator = mockActivityTransitionAnimator
+
underTest = kosmos.sceneContainerStartable
}
@@ -2716,6 +2722,27 @@ class SceneContainerStartableTest : SysuiTestCase() {
assertThat(currentOverlays).isEmpty()
}
+ @Test
+ fun hydrateActivityTransitionAnimationState() =
+ kosmos.runTest {
+ underTest.start()
+
+ val isVisible by collectLastValue(sceneInteractor.isVisible)
+ assertThat(isVisible).isTrue()
+
+ sceneInteractor.setVisible(false, "reason")
+ assertThat(isVisible).isFalse()
+
+ val argumentCaptor = argumentCaptor<ActivityTransitionAnimator.Listener>()
+ verify(mockActivityTransitionAnimator).addListener(argumentCaptor.capture())
+
+ val listeners = argumentCaptor.allValues
+ listeners.forEach { it.onTransitionAnimationStart() }
+ assertThat(isVisible).isTrue()
+ listeners.forEach { it.onTransitionAnimationEnd() }
+ assertThat(isVisible).isFalse()
+ }
+
private fun TestScope.emulateSceneTransition(
transitionStateFlow: MutableStateFlow<ObservableTransitionState>,
toScene: SceneKey,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
index 789ca5158dbf..62c360400582 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
@@ -85,7 +85,6 @@ import com.android.systemui.keyguard.data.repository.FakeKeyguardClockRepository
import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository;
import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor;
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
-import com.android.systemui.keyguard.domain.interactor.KeyguardInteractorFactory;
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
import com.android.systemui.keyguard.domain.interactor.NaturalScrollingSettingObserver;
import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel;
@@ -335,16 +334,14 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase {
mFeatureFlags.set(Flags.QS_USER_DETAIL_SHORTCUT, false);
mMainDispatcher = getMainDispatcher();
- KeyguardInteractorFactory.WithDependencies keyguardInteractorDeps =
- KeyguardInteractorFactory.create();
- mFakeKeyguardRepository = keyguardInteractorDeps.getRepository();
+ mFakeKeyguardRepository = mKosmos.getKeyguardRepository();
mFakeKeyguardClockRepository = new FakeKeyguardClockRepository();
mKeyguardClockInteractor = mKosmos.getKeyguardClockInteractor();
- mKeyguardInteractor = keyguardInteractorDeps.getKeyguardInteractor();
+ mKeyguardInteractor = mKosmos.getKeyguardInteractor();
mShadeRepository = new FakeShadeRepository();
mShadeAnimationInteractor = new ShadeAnimationInteractorLegacyImpl(
new ShadeAnimationRepository(), mShadeRepository);
- mPowerInteractor = keyguardInteractorDeps.getPowerInteractor();
+ mPowerInteractor = mKosmos.getPowerInteractor();
when(mKeyguardTransitionInteractor.isInTransitionWhere(any(), any())).thenReturn(
MutableStateFlow(false));
when(mKeyguardTransitionInteractor.isInTransition(any(), any()))
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractorTest.kt
index a98d1a2ea4a5..d3ba3dceb4cf 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractorTest.kt
@@ -22,10 +22,13 @@ import android.view.Display
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
+import com.android.systemui.kosmos.testScope
import com.android.systemui.kosmos.useUnconfinedTestDispatcher
import com.android.systemui.scene.ui.view.mockShadeRootView
import com.android.systemui.shade.data.repository.fakeShadeDisplaysRepository
import com.android.systemui.testKosmos
+import kotlinx.coroutines.test.advanceUntilIdle
+import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -41,6 +44,7 @@ import org.mockito.kotlin.whenever
class ShadeDisplaysInteractorTest : SysuiTestCase() {
val kosmos = testKosmos().useUnconfinedTestDispatcher()
+ private val testScope = kosmos.testScope
private val shadeRootview = kosmos.mockShadeRootView
private val positionRepository = kosmos.fakeShadeDisplaysRepository
private val shadeContext = kosmos.mockedWindowContext
@@ -49,7 +53,7 @@ class ShadeDisplaysInteractorTest : SysuiTestCase() {
private val configuration = mock<Configuration>()
private val display = mock<Display>()
- private val underTest = kosmos.shadeDisplaysInteractor
+ private val underTest by lazy { kosmos.shadeDisplaysInteractor }
@Before
fun setup() {
@@ -84,12 +88,14 @@ class ShadeDisplaysInteractorTest : SysuiTestCase() {
}
@Test
- fun start_shadeInWrongPosition_logsStartToLatencyTracker() {
- whenever(display.displayId).thenReturn(0)
- positionRepository.setDisplayId(1)
+ fun start_shadeInWrongPosition_logsStartToLatencyTracker() =
+ testScope.runTest {
+ whenever(display.displayId).thenReturn(0)
+ positionRepository.setDisplayId(1)
- underTest.start()
+ underTest.start()
+ advanceUntilIdle()
- verify(latencyTracker).onShadeDisplayChanging(eq(1))
- }
+ verify(latencyTracker).onShadeDisplayChanging(eq(1))
+ }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeExpandedStateInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeExpandedStateInteractorTest.kt
new file mode 100644
index 000000000000..58396e7cef82
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeExpandedStateInteractorTest.kt
@@ -0,0 +1,77 @@
+/*
+ * 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.shade.domain.interactor
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.flags.EnableSceneContainer
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.kosmos.useUnconfinedTestDispatcher
+import com.android.systemui.shade.domain.interactor.ShadeExpandedStateInteractorImpl.NotificationElement
+import com.android.systemui.shade.domain.interactor.ShadeExpandedStateInteractorImpl.QSElement
+import com.android.systemui.shade.shadeTestUtil
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+@EnableSceneContainer
+class ShadeExpandedStateInteractorTest : SysuiTestCase() {
+ private val kosmos = testKosmos().useUnconfinedTestDispatcher()
+
+ private val testScope = kosmos.testScope
+ private val shadeTestUtil by lazy { kosmos.shadeTestUtil }
+
+ private val underTest: ShadeExpandedStateInteractor by lazy {
+ kosmos.shadeExpandedStateInteractor
+ }
+
+ @Test
+ fun expandedElement_qsExpanded_returnsQSElement() =
+ testScope.runTest {
+ shadeTestUtil.setShadeAndQsExpansion(shadeExpansion = 0f, qsExpansion = 1f)
+ val currentlyExpandedElement = underTest.currentlyExpandedElement
+
+ val element = currentlyExpandedElement.value
+
+ assertThat(element).isInstanceOf(QSElement::class.java)
+ }
+
+ @Test
+ fun expandedElement_shadeExpanded_returnsShade() =
+ testScope.runTest {
+ shadeTestUtil.setShadeAndQsExpansion(shadeExpansion = 1f, qsExpansion = 0f)
+
+ val element = underTest.currentlyExpandedElement.value
+
+ assertThat(element).isInstanceOf(NotificationElement::class.java)
+ }
+
+ @Test
+ fun expandedElement_noneExpanded_returnsNull() =
+ testScope.runTest {
+ shadeTestUtil.setShadeAndQsExpansion(shadeExpansion = 0f, qsExpansion = 0f)
+
+ val element = underTest.currentlyExpandedElement.value
+
+ assertThat(element).isNull()
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java
index 20474c842b51..deaf57999b21 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java
@@ -526,7 +526,7 @@ public class NotificationLockscreenUserManagerTest extends SysuiTestCase {
mLockscreenUserManager.mLastLockTime
.set(mSensitiveNotifPostTime - TimeUnit.DAYS.toMillis(1));
// Device is not currently locked
- when(mKeyguardManager.isDeviceLocked()).thenReturn(false);
+ mLockscreenUserManager.mLocked.set(false);
// Sensitive Content notifications are always redacted
assertEquals(REDACTION_TYPE_NONE,
@@ -540,7 +540,7 @@ public class NotificationLockscreenUserManagerTest extends SysuiTestCase {
mSettings.putIntForUser(LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS, 1,
mCurrentUser.id);
changeSetting(LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS);
- when(mKeyguardManager.isDeviceLocked()).thenReturn(true);
+ mLockscreenUserManager.mLocked.set(true);
// Device was locked after this notification arrived
mLockscreenUserManager.mLastLockTime
.set(mSensitiveNotifPostTime + TimeUnit.DAYS.toMillis(1));
@@ -560,7 +560,7 @@ public class NotificationLockscreenUserManagerTest extends SysuiTestCase {
// Device has been locked for 1 second before the notification came in, which is too short
mLockscreenUserManager.mLastLockTime
.set(mSensitiveNotifPostTime - TimeUnit.SECONDS.toMillis(1));
- when(mKeyguardManager.isDeviceLocked()).thenReturn(true);
+ mLockscreenUserManager.mLocked.set(true);
// Sensitive Content notifications are always redacted
assertEquals(REDACTION_TYPE_NONE,
@@ -577,7 +577,7 @@ public class NotificationLockscreenUserManagerTest extends SysuiTestCase {
// Claim the device was last locked 1 day ago
mLockscreenUserManager.mLastLockTime
.set(mSensitiveNotifPostTime - TimeUnit.DAYS.toMillis(1));
- when(mKeyguardManager.isDeviceLocked()).thenReturn(true);
+ mLockscreenUserManager.mLocked.set(true);
// Sensitive Content notifications are always redacted
assertEquals(REDACTION_TYPE_NONE,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationShadeDepthControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationShadeDepthControllerTest.kt
index 2d7dc2e63650..0a0564994e69 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationShadeDepthControllerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationShadeDepthControllerTest.kt
@@ -43,6 +43,7 @@ import com.android.systemui.testKosmos
import com.android.systemui.util.WallpaperController
import com.android.systemui.util.mockito.eq
import com.android.systemui.window.domain.interactor.WindowRootViewBlurInteractor
+import com.android.wm.shell.appzoomout.AppZoomOut
import com.google.common.truth.Truth.assertThat
import java.util.function.Consumer
import org.junit.Before
@@ -65,6 +66,7 @@ import org.mockito.Mockito.reset
import org.mockito.Mockito.verify
import org.mockito.Mockito.`when`
import org.mockito.junit.MockitoJUnit
+import java.util.Optional
@RunWith(AndroidJUnit4::class)
@RunWithLooper
@@ -82,6 +84,7 @@ class NotificationShadeDepthControllerTest : SysuiTestCase() {
@Mock private lateinit var wallpaperController: WallpaperController
@Mock private lateinit var notificationShadeWindowController: NotificationShadeWindowController
@Mock private lateinit var dumpManager: DumpManager
+ @Mock private lateinit var appZoomOutOptional: Optional<AppZoomOut>
@Mock private lateinit var root: View
@Mock private lateinit var viewRootImpl: ViewRootImpl
@Mock private lateinit var windowToken: IBinder
@@ -128,6 +131,7 @@ class NotificationShadeDepthControllerTest : SysuiTestCase() {
ResourcesSplitShadeStateController(),
windowRootViewBlurInteractor,
applicationScope,
+ appZoomOutOptional,
dumpManager,
configurationController,
)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/StatusBarSignalPolicyTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/StatusBarSignalPolicyTest.kt
index bb9141afe404..5f73ac45d12f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/StatusBarSignalPolicyTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/StatusBarSignalPolicyTest.kt
@@ -45,8 +45,11 @@ import kotlin.test.Test
import org.junit.Before
import org.junit.runner.RunWith
import org.mockito.Mockito.verify
+import org.mockito.kotlin.any
import org.mockito.kotlin.clearInvocations
+import org.mockito.kotlin.eq
import org.mockito.kotlin.mock
+import org.mockito.kotlin.never
import org.mockito.kotlin.verifyNoMoreInteractions
@SmallTest
@@ -106,10 +109,10 @@ class StatusBarSignalPolicyTest : SysuiTestCase() {
// Make sure the legacy code path does not change airplane mode when the refactor
// flag is enabled.
underTest.setIsAirplaneMode(IconState(true, TelephonyIcons.FLIGHT_MODE_ICON, ""))
- verifyNoMoreInteractions(statusBarIconController)
+ verify(statusBarIconController, never()).setIconVisibility(eq(slotAirplane), any())
underTest.setIsAirplaneMode(IconState(false, TelephonyIcons.FLIGHT_MODE_ICON, ""))
- verifyNoMoreInteractions(statusBarIconController)
+ verify(statusBarIconController, never()).setIconVisibility(eq(slotAirplane), any())
}
@Test
@@ -144,10 +147,10 @@ class StatusBarSignalPolicyTest : SysuiTestCase() {
// Make sure changing airplane mode from airplaneModeRepository does nothing
// if the StatusBarSignalPolicyRefactor is not enabled.
airplaneModeInteractor.setIsAirplaneMode(true)
- verifyNoMoreInteractions(statusBarIconController)
+ verify(statusBarIconController, never()).setIconVisibility(eq(slotAirplane), any())
airplaneModeInteractor.setIsAirplaneMode(false)
- verifyNoMoreInteractions(statusBarIconController)
+ verify(statusBarIconController, never()).setIconVisibility(eq(slotAirplane), any())
}
@Test
@@ -196,7 +199,7 @@ class StatusBarSignalPolicyTest : SysuiTestCase() {
underTest.setEthernetIndicators(
IconState(/* visible= */ true, /* icon= */ 1, /* contentDescription= */ "Ethernet")
)
- verifyNoMoreInteractions(statusBarIconController)
+ verify(statusBarIconController, never()).setIconVisibility(eq(slotEthernet), any())
underTest.setEthernetIndicators(
IconState(
@@ -205,7 +208,7 @@ class StatusBarSignalPolicyTest : SysuiTestCase() {
/* contentDescription= */ "No ethernet",
)
)
- verifyNoMoreInteractions(statusBarIconController)
+ verify(statusBarIconController, never()).setIconVisibility(eq(slotEthernet), any())
}
@Test
@@ -217,13 +220,13 @@ class StatusBarSignalPolicyTest : SysuiTestCase() {
clearInvocations(statusBarIconController)
connectivityRepository.fake.setEthernetConnected(default = true, validated = true)
- verifyNoMoreInteractions(statusBarIconController)
+ verify(statusBarIconController, never()).setIconVisibility(eq(slotEthernet), any())
connectivityRepository.fake.setEthernetConnected(default = false, validated = false)
- verifyNoMoreInteractions(statusBarIconController)
+ verify(statusBarIconController, never()).setIconVisibility(eq(slotEthernet), any())
connectivityRepository.fake.setEthernetConnected(default = true, validated = false)
- verifyNoMoreInteractions(statusBarIconController)
+ verify(statusBarIconController, never()).setIconVisibility(eq(slotEthernet), any())
}
@Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt
index 0061c4142e8b..75d000b63d62 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt
@@ -22,7 +22,6 @@ import android.platform.test.annotations.EnableFlags
import android.view.View
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
-import com.android.systemui.Flags.FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON
import com.android.systemui.SysuiTestCase
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.coroutines.collectLastValue
@@ -126,65 +125,8 @@ class CallChipViewModelTest : SysuiTestCase() {
}
@Test
- @DisableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON)
- fun chip_positiveStartTime_notifIconFlagOff_iconIsPhone() =
- testScope.runTest {
- val latest by collectLastValue(underTest.chip)
-
- repo.setOngoingCallState(
- inCallModel(startTimeMs = 1000, notificationIcon = createStatusBarIconViewOrNull())
- )
-
- assertThat((latest as OngoingActivityChipModel.Shown).icon)
- .isInstanceOf(OngoingActivityChipModel.ChipIcon.SingleColorIcon::class.java)
- val icon =
- (((latest as OngoingActivityChipModel.Shown).icon)
- as OngoingActivityChipModel.ChipIcon.SingleColorIcon)
- .impl as Icon.Resource
- assertThat(icon.res).isEqualTo(com.android.internal.R.drawable.ic_phone)
- assertThat(icon.contentDescription).isNotNull()
- }
-
- @Test
- @EnableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON)
- @DisableFlags(StatusBarConnectedDisplays.FLAG_NAME)
- fun chip_positiveStartTime_notifIconFlagOn_iconIsNotifIcon() =
- testScope.runTest {
- val latest by collectLastValue(underTest.chip)
-
- val notifIcon = createStatusBarIconViewOrNull()
- repo.setOngoingCallState(inCallModel(startTimeMs = 1000, notificationIcon = notifIcon))
-
- assertThat((latest as OngoingActivityChipModel.Shown).icon)
- .isInstanceOf(OngoingActivityChipModel.ChipIcon.StatusBarView::class.java)
- val actualIcon =
- (((latest as OngoingActivityChipModel.Shown).icon)
- as OngoingActivityChipModel.ChipIcon.StatusBarView)
- .impl
- assertThat(actualIcon).isEqualTo(notifIcon)
- }
-
- @Test
- @EnableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON, StatusBarConnectedDisplays.FLAG_NAME)
- fun chip_positiveStartTime_notifIconFlagOn_cdFlagOn_iconIsNotifKeyIcon() =
- testScope.runTest {
- val latest by collectLastValue(underTest.chip)
-
- repo.setOngoingCallState(
- inCallModel(
- startTimeMs = 1000,
- notificationIcon = createStatusBarIconViewOrNull(),
- notificationKey = "notifKey",
- )
- )
-
- assertThat((latest as OngoingActivityChipModel.Shown).icon)
- .isEqualTo(OngoingActivityChipModel.ChipIcon.StatusBarNotificationIcon("notifKey"))
- }
-
- @Test
- @EnableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON, StatusBarConnectedDisplays.FLAG_NAME)
- fun chip_positiveStartTime_notifIconAndConnectedDisplaysFlagOn_iconIsNotifIcon() =
+ @EnableFlags(StatusBarConnectedDisplays.FLAG_NAME)
+ fun chip_positiveStartTime_connectedDisplaysFlagOn_iconIsNotifIcon() =
testScope.runTest {
val latest by collectLastValue(underTest.chip)
@@ -205,29 +147,8 @@ class CallChipViewModelTest : SysuiTestCase() {
}
@Test
- @DisableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON)
- fun chip_zeroStartTime_notifIconFlagOff_iconIsPhone() =
- testScope.runTest {
- val latest by collectLastValue(underTest.chip)
-
- repo.setOngoingCallState(
- inCallModel(startTimeMs = 0, notificationIcon = createStatusBarIconViewOrNull())
- )
-
- assertThat((latest as OngoingActivityChipModel.Shown).icon)
- .isInstanceOf(OngoingActivityChipModel.ChipIcon.SingleColorIcon::class.java)
- val icon =
- (((latest as OngoingActivityChipModel.Shown).icon)
- as OngoingActivityChipModel.ChipIcon.SingleColorIcon)
- .impl as Icon.Resource
- assertThat(icon.res).isEqualTo(com.android.internal.R.drawable.ic_phone)
- assertThat(icon.contentDescription).isNotNull()
- }
-
- @Test
- @EnableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON)
@DisableFlags(StatusBarConnectedDisplays.FLAG_NAME)
- fun chip_zeroStartTime_notifIconFlagOn_cdFlagOff_iconIsNotifIcon() =
+ fun chip_zeroStartTime_cdFlagOff_iconIsNotifIcon() =
testScope.runTest {
val latest by collectLastValue(underTest.chip)
@@ -244,8 +165,8 @@ class CallChipViewModelTest : SysuiTestCase() {
}
@Test
- @EnableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON, StatusBarConnectedDisplays.FLAG_NAME)
- fun chip_zeroStartTime_notifIconFlagOn_cdFlagOn_iconIsNotifKeyIcon() =
+ @EnableFlags(StatusBarConnectedDisplays.FLAG_NAME)
+ fun chip_zeroStartTime_cdFlagOn_iconIsNotifKeyIcon() =
testScope.runTest {
val latest by collectLastValue(underTest.chip)
@@ -262,7 +183,6 @@ class CallChipViewModelTest : SysuiTestCase() {
}
@Test
- @EnableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON)
@DisableFlags(StatusBarConnectedDisplays.FLAG_NAME)
fun chip_notifIconFlagOn_butNullNotifIcon_cdFlagOff_iconIsPhone() =
testScope.runTest {
@@ -281,7 +201,7 @@ class CallChipViewModelTest : SysuiTestCase() {
}
@Test
- @EnableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON, StatusBarConnectedDisplays.FLAG_NAME)
+ @EnableFlags(StatusBarConnectedDisplays.FLAG_NAME)
fun chip_notifIconFlagOn_butNullNotifIcon_iconNotifKey() =
testScope.runTest {
val latest by collectLastValue(underTest.chip)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt
index e561e3ea27d7..902db5e10589 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt
@@ -501,7 +501,7 @@ class NotifChipsViewModelTest : SysuiTestCase() {
@Test
@DisableFlags(FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY)
- fun chips_hasHeadsUpByUser_onlyShowsIcon() =
+ fun chips_hasHeadsUpBySystem_showsTime() =
kosmos.runTest {
val latest by collectLastValue(underTest.chips)
@@ -523,7 +523,99 @@ class NotifChipsViewModelTest : SysuiTestCase() {
)
)
- // WHEN there's a HUN pinned by a user
+ // WHEN there's a HUN pinned by the system
+ kosmos.headsUpNotificationRepository.setNotifications(
+ UnconfinedFakeHeadsUpRowRepository(
+ key = "notif",
+ pinnedStatus = MutableStateFlow(PinnedStatus.PinnedBySystem),
+ )
+ )
+
+ // THEN the chip keeps showing time
+ // (In real life the chip won't show at all, but that's handled in a different part of
+ // the system. What we know here is that the chip shouldn't shrink to icon only.)
+ assertThat(latest!![0])
+ .isInstanceOf(OngoingActivityChipModel.Shown.ShortTimeDelta::class.java)
+ }
+
+ @Test
+ @DisableFlags(FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY)
+ fun chips_hasHeadsUpByUser_forOtherNotif_showsTime() =
+ kosmos.runTest {
+ val latest by collectLastValue(underTest.chips)
+
+ val promotedContentBuilder =
+ PromotedNotificationContentModel.Builder("notif").apply {
+ this.time =
+ PromotedNotificationContentModel.When(
+ time = 6543L,
+ mode = PromotedNotificationContentModel.When.Mode.BasicTime,
+ )
+ }
+ val otherPromotedContentBuilder =
+ PromotedNotificationContentModel.Builder("other notif").apply {
+ this.time =
+ PromotedNotificationContentModel.When(
+ time = 654321L,
+ mode = PromotedNotificationContentModel.When.Mode.BasicTime,
+ )
+ }
+ val icon = createStatusBarIconViewOrNull()
+ val otherIcon = createStatusBarIconViewOrNull()
+ setNotifs(
+ listOf(
+ activeNotificationModel(
+ key = "notif",
+ statusBarChipIcon = icon,
+ promotedContent = promotedContentBuilder.build(),
+ ),
+ activeNotificationModel(
+ key = "other notif",
+ statusBarChipIcon = otherIcon,
+ promotedContent = otherPromotedContentBuilder.build(),
+ ),
+ )
+ )
+
+ // WHEN there's a HUN pinned for the "other notif" chip
+ kosmos.headsUpNotificationRepository.setNotifications(
+ UnconfinedFakeHeadsUpRowRepository(
+ key = "other notif",
+ pinnedStatus = MutableStateFlow(PinnedStatus.PinnedByUser),
+ )
+ )
+
+ // THEN the "notif" chip keeps showing time
+ val chip = latest!![0]
+ assertThat(chip).isInstanceOf(OngoingActivityChipModel.Shown.ShortTimeDelta::class.java)
+ assertIsNotifChip(chip, icon, "notif")
+ }
+
+ @Test
+ @DisableFlags(FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY)
+ fun chips_hasHeadsUpByUser_forThisNotif_onlyShowsIcon() =
+ kosmos.runTest {
+ val latest by collectLastValue(underTest.chips)
+
+ val promotedContentBuilder =
+ PromotedNotificationContentModel.Builder("notif").apply {
+ this.time =
+ PromotedNotificationContentModel.When(
+ time = 6543L,
+ mode = PromotedNotificationContentModel.When.Mode.BasicTime,
+ )
+ }
+ setNotifs(
+ listOf(
+ activeNotificationModel(
+ key = "notif",
+ statusBarChipIcon = mock<StatusBarIconView>(),
+ promotedContent = promotedContentBuilder.build(),
+ )
+ )
+ )
+
+ // WHEN this notification is pinned by the user
kosmos.headsUpNotificationRepository.setNotifications(
UnconfinedFakeHeadsUpRowRepository(
key = "notif",
@@ -531,6 +623,7 @@ class NotifChipsViewModelTest : SysuiTestCase() {
)
)
+ // THEN the chip shrinks to icon only
assertThat(latest!![0])
.isInstanceOf(OngoingActivityChipModel.Shown.IconOnly::class.java)
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/layout/StatusBarContentInsetsProviderTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/layout/StatusBarContentInsetsProviderTest.kt
index c9c961791e89..49b95d92129c 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/layout/StatusBarContentInsetsProviderTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/layout/StatusBarContentInsetsProviderTest.kt
@@ -45,7 +45,7 @@ import com.google.common.truth.Truth.assertWithMessage
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
-import org.mockito.ArgumentMatchers.any
+import org.mockito.kotlin.any
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.mock
import org.mockito.kotlin.whenever
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/layout/ui/viewmodel/StatusBarContentInsetsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/layout/ui/viewmodel/StatusBarContentInsetsViewModelTest.kt
new file mode 100644
index 000000000000..72001758d01f
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/layout/ui/viewmodel/StatusBarContentInsetsViewModelTest.kt
@@ -0,0 +1,77 @@
+/*
+ * 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.layout.ui.viewmodel
+
+import android.content.res.Configuration
+import android.platform.test.annotations.EnableFlags
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.collectValues
+import com.android.systemui.kosmos.runTest
+import com.android.systemui.kosmos.useUnconfinedTestDispatcher
+import com.android.systemui.statusbar.core.StatusBarConnectedDisplays
+import com.android.systemui.statusbar.layout.statusBarContentInsetsProvider
+import com.android.systemui.statusbar.policy.configurationController
+import com.android.systemui.statusbar.policy.fake
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+@EnableFlags(StatusBarConnectedDisplays.FLAG_NAME)
+class StatusBarContentInsetsViewModelTest : SysuiTestCase() {
+ private val kosmos = testKosmos().useUnconfinedTestDispatcher()
+ private val configuration = Configuration()
+
+ private val Kosmos.underTest by Kosmos.Fixture { statusBarContentInsetsViewModel }
+
+ @Test
+ fun contentArea_onMaxBoundsChanged_emitsNewValue() =
+ kosmos.runTest {
+ statusBarContentInsetsProvider.start()
+
+ val values by collectValues(underTest.contentArea)
+
+ // WHEN the content area changes
+ configurationController.fake.notifyLayoutDirectionChanged(isRtl = true)
+ configurationController.fake.notifyDensityOrFontScaleChanged()
+
+ // THEN the flow emits the new bounds
+ assertThat(values[0]).isNotEqualTo(values[1])
+ }
+
+ @Test
+ fun contentArea_onDensityOrFontScaleChanged_emitsLastBounds() =
+ kosmos.runTest {
+ configuration.densityDpi = 12
+ statusBarContentInsetsProvider.start()
+
+ val values by collectValues(underTest.contentArea)
+
+ // WHEN a change happens but it doesn't affect content area
+ configuration.densityDpi = 20
+ configurationController.onConfigurationChanged(configuration)
+ configurationController.fake.notifyDensityOrFontScaleChanged()
+
+ // THEN it still has the last bounds
+ assertThat(values).hasSize(1)
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt
index a3ffd91b1728..609885d0214b 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt
@@ -458,7 +458,7 @@ class HeadsUpCoordinatorTest : SysuiTestCase() {
@Test
@EnableFlags(StatusBarNotifChips.FLAG_NAME)
- fun showPromotedNotification_hasNotifEntry_shownAsHUN() =
+ fun onPromotedNotificationChipTapped_hasNotifEntry_shownAsHUN() =
testScope.runTest {
whenever(notifCollection.getEntry(entry.key)).thenReturn(entry)
@@ -473,7 +473,7 @@ class HeadsUpCoordinatorTest : SysuiTestCase() {
@Test
@EnableFlags(StatusBarNotifChips.FLAG_NAME)
- fun showPromotedNotification_noNotifEntry_noHUN() =
+ fun onPromotedNotificationChipTapped_noNotifEntry_noHUN() =
testScope.runTest {
whenever(notifCollection.getEntry(entry.key)).thenReturn(null)
@@ -488,7 +488,7 @@ class HeadsUpCoordinatorTest : SysuiTestCase() {
@Test
@EnableFlags(StatusBarNotifChips.FLAG_NAME)
- fun showPromotedNotification_shownAsHUNEvenIfEntryShouldNot() =
+ fun onPromotedNotificationChipTapped_shownAsHUNEvenIfEntryShouldNot() =
testScope.runTest {
whenever(notifCollection.getEntry(entry.key)).thenReturn(entry)
@@ -511,7 +511,7 @@ class HeadsUpCoordinatorTest : SysuiTestCase() {
@Test
@EnableFlags(StatusBarNotifChips.FLAG_NAME)
- fun showPromotedNotification_atSameTimeAsOnAdded_promotedShownAsHUN() =
+ fun onPromotedNotificationChipTapped_atSameTimeAsOnAdded_promotedShownAsHUN() =
testScope.runTest {
// First, the promoted notification appears as not heads up
val promotedEntry = NotificationEntryBuilder().setPkg("promotedPackage").build()
@@ -548,6 +548,33 @@ class HeadsUpCoordinatorTest : SysuiTestCase() {
}
@Test
+ @EnableFlags(StatusBarNotifChips.FLAG_NAME)
+ fun onPromotedNotificationChipTapped_chipTappedTwice_hunHiddenOnSecondTap() =
+ testScope.runTest {
+ whenever(notifCollection.getEntry(entry.key)).thenReturn(entry)
+
+ // WHEN chip tapped first
+ statusBarNotificationChipsInteractor.onPromotedNotificationChipTapped(entry.key)
+ executor.advanceClockToLast()
+ executor.runAllReady()
+ beforeFinalizeFilterListener.onBeforeFinalizeFilter(listOf(entry))
+
+ // THEN HUN is shown
+ finishBind(entry)
+ verify(headsUpManager).showNotification(entry, isPinnedByUser = true)
+ addHUN(entry)
+
+ // WHEN chip is tapped again
+ statusBarNotificationChipsInteractor.onPromotedNotificationChipTapped(entry.key)
+ executor.advanceClockToLast()
+ executor.runAllReady()
+ beforeFinalizeFilterListener.onBeforeFinalizeFilter(listOf(entry))
+
+ // THEN HUN is hidden
+ verify(headsUpManager).removeNotification(eq(entry.key), eq(false), any())
+ }
+
+ @Test
fun testTransferIsolatedChildAlert_withGroupAlertSummary() {
setShouldHeadsUp(groupSummary)
whenever(notifPipeline.allNotifs).thenReturn(listOf(groupSummary, groupSibling1))
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractorTest.kt
index 22ef408e266c..fae7d515d305 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractorTest.kt
@@ -32,6 +32,7 @@ import com.android.systemui.shade.data.repository.fakeShadeRepository
import com.android.systemui.shade.shadeTestUtil
import com.android.systemui.statusbar.notification.data.repository.FakeHeadsUpRowRepository
import com.android.systemui.statusbar.notification.data.repository.notificationsKeyguardViewStateRepository
+import com.android.systemui.statusbar.notification.domain.model.TopPinnedState
import com.android.systemui.statusbar.notification.headsup.PinnedStatus
import com.android.systemui.statusbar.notification.stack.data.repository.headsUpNotificationRepository
import com.android.systemui.statusbar.notification.stack.domain.interactor.headsUpNotificationInteractor
@@ -412,46 +413,53 @@ class HeadsUpNotificationInteractorTest : SysuiTestCase() {
@Test
fun statusBarHeadsUpState_pinnedBySystem() =
testScope.runTest {
- val statusBarHeadsUpState by collectLastValue(underTest.statusBarHeadsUpState)
+ val state by collectLastValue(underTest.statusBarHeadsUpState)
+ val status by collectLastValue(underTest.statusBarHeadsUpStatus)
headsUpRepository.setNotifications(
FakeHeadsUpRowRepository(key = "key 0", pinnedStatus = PinnedStatus.PinnedBySystem)
)
runCurrent()
- assertThat(statusBarHeadsUpState).isEqualTo(PinnedStatus.PinnedBySystem)
+ assertThat(state).isEqualTo(TopPinnedState.Pinned("key 0", PinnedStatus.PinnedBySystem))
+ assertThat(status).isEqualTo(PinnedStatus.PinnedBySystem)
}
@Test
fun statusBarHeadsUpState_pinnedByUser() =
testScope.runTest {
- val statusBarHeadsUpState by collectLastValue(underTest.statusBarHeadsUpState)
+ val state by collectLastValue(underTest.statusBarHeadsUpState)
+ val status by collectLastValue(underTest.statusBarHeadsUpStatus)
headsUpRepository.setNotifications(
FakeHeadsUpRowRepository(key = "key 0", pinnedStatus = PinnedStatus.PinnedByUser)
)
runCurrent()
- assertThat(statusBarHeadsUpState).isEqualTo(PinnedStatus.PinnedByUser)
+ assertThat(state).isEqualTo(TopPinnedState.Pinned("key 0", PinnedStatus.PinnedByUser))
+ assertThat(status).isEqualTo(PinnedStatus.PinnedByUser)
}
@Test
fun statusBarHeadsUpState_withoutPinnedNotifications_notPinned() =
testScope.runTest {
- val statusBarHeadsUpState by collectLastValue(underTest.statusBarHeadsUpState)
+ val state by collectLastValue(underTest.statusBarHeadsUpState)
+ val status by collectLastValue(underTest.statusBarHeadsUpStatus)
headsUpRepository.setNotifications(
FakeHeadsUpRowRepository(key = "key 0", PinnedStatus.NotPinned)
)
runCurrent()
- assertThat(statusBarHeadsUpState).isEqualTo(PinnedStatus.NotPinned)
+ assertThat(state).isEqualTo(TopPinnedState.NothingPinned)
+ assertThat(status).isEqualTo(PinnedStatus.NotPinned)
}
@Test
fun statusBarHeadsUpState_whenShadeExpanded_false() =
testScope.runTest {
- val statusBarHeadsUpState by collectLastValue(underTest.statusBarHeadsUpState)
+ val state by collectLastValue(underTest.statusBarHeadsUpState)
+ val status by collectLastValue(underTest.statusBarHeadsUpStatus)
// WHEN a row is pinned
headsUpRepository.setNotifications(fakeHeadsUpRowRepository("key 0", isPinned = true))
@@ -463,13 +471,15 @@ class HeadsUpNotificationInteractorTest : SysuiTestCase() {
// should emit `false`.
kosmos.fakeShadeRepository.setLegacyShadeExpansion(1.0f)
- assertThat(statusBarHeadsUpState!!.isPinned).isFalse()
+ assertThat(state).isEqualTo(TopPinnedState.NothingPinned)
+ assertThat(status!!.isPinned).isFalse()
}
@Test
fun statusBarHeadsUpState_notificationsAreHidden_false() =
testScope.runTest {
- val statusBarHeadsUpState by collectLastValue(underTest.statusBarHeadsUpState)
+ val state by collectLastValue(underTest.statusBarHeadsUpState)
+ val status by collectLastValue(underTest.statusBarHeadsUpStatus)
// WHEN a row is pinned
headsUpRepository.setNotifications(fakeHeadsUpRowRepository("key 0", isPinned = true))
@@ -477,13 +487,15 @@ class HeadsUpNotificationInteractorTest : SysuiTestCase() {
// AND the notifications are hidden
keyguardViewStateRepository.areNotificationsFullyHidden.value = true
- assertThat(statusBarHeadsUpState!!.isPinned).isFalse()
+ assertThat(state).isEqualTo(TopPinnedState.NothingPinned)
+ assertThat(status!!.isPinned).isFalse()
}
@Test
fun statusBarHeadsUpState_onLockScreen_false() =
testScope.runTest {
- val statusBarHeadsUpState by collectLastValue(underTest.statusBarHeadsUpState)
+ val state by collectLastValue(underTest.statusBarHeadsUpState)
+ val status by collectLastValue(underTest.statusBarHeadsUpStatus)
// WHEN a row is pinned
headsUpRepository.setNotifications(fakeHeadsUpRowRepository("key 0", isPinned = true))
@@ -494,13 +506,15 @@ class HeadsUpNotificationInteractorTest : SysuiTestCase() {
testSetup = true,
)
- assertThat(statusBarHeadsUpState!!.isPinned).isFalse()
+ assertThat(state).isEqualTo(TopPinnedState.NothingPinned)
+ assertThat(status!!.isPinned).isFalse()
}
@Test
fun statusBarHeadsUpState_onByPassLockScreen_true() =
testScope.runTest {
- val statusBarHeadsUpState by collectLastValue(underTest.statusBarHeadsUpState)
+ val state by collectLastValue(underTest.statusBarHeadsUpState)
+ val status by collectLastValue(underTest.statusBarHeadsUpStatus)
// WHEN a row is pinned
headsUpRepository.setNotifications(fakeHeadsUpRowRepository("key 0", isPinned = true))
@@ -513,13 +527,15 @@ class HeadsUpNotificationInteractorTest : SysuiTestCase() {
// AND bypass is enabled
faceAuthRepository.isBypassEnabled.value = true
- assertThat(statusBarHeadsUpState!!.isPinned).isTrue()
+ assertThat(state).isInstanceOf(TopPinnedState.Pinned::class.java)
+ assertThat(status!!.isPinned).isTrue()
}
@Test
fun statusBarHeadsUpState_onByPassLockScreen_withoutNotifications_false() =
testScope.runTest {
- val statusBarHeadsUpState by collectLastValue(underTest.statusBarHeadsUpState)
+ val state by collectLastValue(underTest.statusBarHeadsUpState)
+ val status by collectLastValue(underTest.statusBarHeadsUpStatus)
// WHEN no pinned rows
// AND the lock screen is shown
@@ -530,7 +546,8 @@ class HeadsUpNotificationInteractorTest : SysuiTestCase() {
// AND bypass is enabled
faceAuthRepository.isBypassEnabled.value = true
- assertThat(statusBarHeadsUpState!!.isPinned).isFalse()
+ assertThat(state).isEqualTo(TopPinnedState.NothingPinned)
+ assertThat(status!!.isPinned).isFalse()
}
private fun fakeHeadsUpRowRepository(key: String, isPinned: Boolean = false) =
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerTest.java
index 72a91bc12f8d..14bbd38ece2c 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerTest.java
@@ -279,7 +279,7 @@ public class NotificationChildrenContainerTest extends SysuiTestCase {
notification);
RemoteViews headerRemoteViews;
if (lowPriority) {
- headerRemoteViews = builder.makeLowPriorityContentView(true, false);
+ headerRemoteViews = builder.makeLowPriorityContentView(true);
} else {
headerRemoteViews = builder.makeNotificationGroupHeader();
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt
index 459778868ccd..a045b37a8119 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt
@@ -70,6 +70,7 @@ import com.android.systemui.shade.shared.flag.DualShade
import com.android.systemui.statusbar.notification.stack.domain.interactor.sharedNotificationContainerInteractor
import com.android.systemui.statusbar.notification.stack.ui.viewmodel.SharedNotificationContainerViewModel.HorizontalPosition
import com.android.systemui.testKosmos
+import com.android.systemui.window.ui.viewmodel.fakeBouncerTransitions
import com.google.common.collect.Range
import com.google.common.truth.Truth.assertThat
import kotlin.test.assertIs
@@ -1395,6 +1396,19 @@ class SharedNotificationContainerViewModelTest(flags: FlagsParameterization) : S
assertThat(stackAbsoluteBottom).isEqualTo(100F)
}
+ @Test
+ fun blurRadius_emitsValues_fromPrimaryBouncerTransitions() =
+ testScope.runTest {
+ val blurRadius by collectLastValue(underTest.blurRadius)
+ assertThat(blurRadius).isEqualTo(0.0f)
+
+ kosmos.fakeBouncerTransitions.first().notificationBlurRadius.value = 30.0f
+ assertThat(blurRadius).isEqualTo(30.0f)
+
+ kosmos.fakeBouncerTransitions.last().notificationBlurRadius.value = 40.0f
+ assertThat(blurRadius).isEqualTo(40.0f)
+ }
+
private suspend fun TestScope.showLockscreen() {
shadeTestUtil.setQsExpansion(0f)
shadeTestUtil.setLockscreenShadeExpansion(0f)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java
index 9eafcdbadfa5..e2330f448a1b 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java
@@ -34,6 +34,7 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.hardware.biometrics.BiometricSourceType;
+import android.hardware.fingerprint.FingerprintManager;
import android.os.Handler;
import android.os.PowerManager;
import android.os.UserHandle;
@@ -431,9 +432,9 @@ public class BiometricsUnlockControllerTest extends SysuiTestCase {
}
@Test
- public void onUdfpsConsecutivelyFailedThreeTimes_showPrimaryBouncer() {
- // GIVEN UDFPS is supported
- when(mUpdateMonitor.isUdfpsSupported()).thenReturn(true);
+ public void onOpticalUdfpsConsecutivelyFailedThreeTimes_showPrimaryBouncer() {
+ // GIVEN optical UDFPS is supported
+ when(mUpdateMonitor.isOpticalUdfpsSupported()).thenReturn(true);
// WHEN udfps fails once - then don't show the bouncer yet
mBiometricUnlockController.onBiometricAuthFailed(BiometricSourceType.FINGERPRINT);
@@ -451,6 +452,25 @@ public class BiometricsUnlockControllerTest extends SysuiTestCase {
}
@Test
+ public void onUltrasonicUdfpsLockout_showPrimaryBouncer() {
+ // GIVEN ultrasonic UDFPS is supported
+ when(mUpdateMonitor.isOpticalUdfpsSupported()).thenReturn(false);
+
+ // WHEN udfps fails three times, don't show bouncer
+ mBiometricUnlockController.onBiometricAuthFailed(BiometricSourceType.FINGERPRINT);
+ mBiometricUnlockController.onBiometricAuthFailed(BiometricSourceType.FINGERPRINT);
+ mBiometricUnlockController.onBiometricAuthFailed(BiometricSourceType.FINGERPRINT);
+ verify(mStatusBarKeyguardViewManager, never()).showPrimaryBouncer(anyBoolean());
+
+ // WHEN lockout is received
+ mBiometricUnlockController.onBiometricError(FingerprintManager.FINGERPRINT_ERROR_LOCKOUT,
+ "Lockout", BiometricSourceType.FINGERPRINT);
+
+ // THEN show bouncer
+ verify(mStatusBarKeyguardViewManager).showPrimaryBouncer(true);
+ }
+
+ @Test
public void onFinishedGoingToSleep_authenticatesWhenPending() {
when(mUpdateMonitor.isGoingToSleep()).thenReturn(true);
mBiometricUnlockController.mWakefulnessObserver.onFinishedGoingToSleep();
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.kt
index ffd349d744a8..43ad042ecf78 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.kt
@@ -53,7 +53,7 @@ import com.android.systemui.statusbar.CommandQueue
import com.android.systemui.statusbar.StatusBarState
import com.android.systemui.statusbar.data.repository.StatusBarContentInsetsProviderStore
import com.android.systemui.statusbar.events.SystemStatusAnimationScheduler
-import com.android.systemui.statusbar.layout.statusBarContentInsetsProvider
+import com.android.systemui.statusbar.layout.mockStatusBarContentInsetsProvider
import com.android.systemui.statusbar.phone.ui.StatusBarIconController
import com.android.systemui.statusbar.phone.ui.TintedIconManager
import com.android.systemui.statusbar.policy.BatteryController
@@ -153,7 +153,8 @@ class KeyguardStatusBarViewControllerTest : SysuiTestCase() {
shadeViewStateProvider = TestShadeViewStateProvider()
Mockito.`when`(
- kosmos.statusBarContentInsetsProvider.getStatusBarContentInsetsForCurrentRotation()
+ kosmos.mockStatusBarContentInsetsProvider
+ .getStatusBarContentInsetsForCurrentRotation()
)
.thenReturn(Insets.of(0, 0, 0, 0))
@@ -162,7 +163,7 @@ class KeyguardStatusBarViewControllerTest : SysuiTestCase() {
Mockito.`when`(iconManagerFactory.create(ArgumentMatchers.any(), ArgumentMatchers.any()))
.thenReturn(iconManager)
Mockito.`when`(statusBarContentInsetsProviderStore.defaultDisplay)
- .thenReturn(kosmos.statusBarContentInsetsProvider)
+ .thenReturn(kosmos.mockStatusBarContentInsetsProvider)
allowTestableLooperAsMainThread()
looper.runWithLooper {
keyguardStatusBarView =
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerViaRepoTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerTest.kt
index cf512cdee800..b98409906f8d 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerViaRepoTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerTest.kt
@@ -28,9 +28,7 @@ import android.view.View
import android.widget.LinearLayout
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
-import com.android.systemui.Flags.FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON
import com.android.systemui.Flags.FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS
-import com.android.systemui.Flags.FLAG_STATUS_BAR_USE_REPOS_FOR_CALL_CHIP
import com.android.systemui.SysuiTestCase
import com.android.systemui.concurrency.fakeExecutor
import com.android.systemui.dump.DumpManager
@@ -42,7 +40,6 @@ import com.android.systemui.res.R
import com.android.systemui.statusbar.StatusBarIconView
import com.android.systemui.statusbar.data.repository.fakeStatusBarModeRepository
import com.android.systemui.statusbar.gesture.SwipeStatusBarAwayGestureHandler
-import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection
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
@@ -76,9 +73,8 @@ import org.mockito.kotlin.whenever
@RunWith(AndroidJUnit4::class)
@TestableLooper.RunWithLooper
@OptIn(ExperimentalCoroutinesApi::class)
-@EnableFlags(FLAG_STATUS_BAR_USE_REPOS_FOR_CALL_CHIP)
@DisableFlags(StatusBarChipsModernization.FLAG_NAME)
-class OngoingCallControllerViaRepoTest : SysuiTestCase() {
+class OngoingCallControllerTest : SysuiTestCase() {
private val kosmos = Kosmos()
private val clock = kosmos.fakeSystemClock
@@ -114,7 +110,6 @@ class OngoingCallControllerViaRepoTest : SysuiTestCase() {
testScope.backgroundScope,
context,
ongoingCallRepository,
- mock<CommonNotifCollection>(),
kosmos.activeNotificationsInteractor,
clock,
mockActivityStarter,
@@ -162,28 +157,7 @@ class OngoingCallControllerViaRepoTest : SysuiTestCase() {
}
@Test
- @DisableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON)
- fun interactorHasOngoingCallNotif_notifIconFlagOff_repoHasNoNotifIcon() =
- testScope.runTest {
- val icon = mock<StatusBarIconView>()
- setNotifOnRepo(
- activeNotificationModel(
- key = "ongoingNotif",
- callType = CallType.Ongoing,
- uid = CALL_UID,
- statusBarChipIcon = icon,
- whenTime = 567,
- )
- )
-
- val repoState = ongoingCallRepository.ongoingCallState.value
- assertThat(repoState).isInstanceOf(OngoingCallModel.InCall::class.java)
- assertThat((repoState as OngoingCallModel.InCall).notificationIconView).isNull()
- }
-
- @Test
- @EnableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON)
- fun interactorHasOngoingCallNotif_notifIconFlagOn_repoHasNotifIcon() =
+ fun interactorHasOngoingCallNotif_repoHasNotifIcon() =
testScope.runTest {
val icon = mock<StatusBarIconView>()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerViaListenerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerViaListenerTest.kt
deleted file mode 100644
index cd3539d6b9a5..000000000000
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerViaListenerTest.kt
+++ /dev/null
@@ -1,694 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.statusbar.phone.ongoingcall
-
-import android.app.ActivityManager
-import android.app.IActivityManager
-import android.app.IUidObserver
-import android.app.Notification
-import android.app.PendingIntent
-import android.app.Person
-import android.platform.test.annotations.DisableFlags
-import android.platform.test.annotations.EnableFlags
-import android.service.notification.NotificationListenerService.REASON_USER_STOPPED
-import android.testing.TestableLooper
-import android.view.LayoutInflater
-import android.view.View
-import android.widget.LinearLayout
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.SmallTest
-import com.android.systemui.Flags.FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS
-import com.android.systemui.Flags.FLAG_STATUS_BAR_USE_REPOS_FOR_CALL_CHIP
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.dump.DumpManager
-import com.android.systemui.kosmos.Kosmos
-import com.android.systemui.log.logcatLogBuffer
-import com.android.systemui.plugins.ActivityStarter
-import com.android.systemui.res.R
-import com.android.systemui.statusbar.data.repository.FakeStatusBarModeRepository
-import com.android.systemui.statusbar.gesture.SwipeStatusBarAwayGestureHandler
-import com.android.systemui.statusbar.notification.collection.NotificationEntry
-import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
-import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection
-import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener
-import com.android.systemui.statusbar.notification.domain.interactor.activeNotificationsInteractor
-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.window.StatusBarWindowController
-import com.android.systemui.statusbar.window.StatusBarWindowControllerStore
-import com.android.systemui.util.concurrency.FakeExecutor
-import com.android.systemui.util.mockito.any
-import com.android.systemui.util.time.FakeSystemClock
-import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.TestScope
-import kotlinx.coroutines.test.runCurrent
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.ArgumentCaptor
-import org.mockito.ArgumentMatchers.anyBoolean
-import org.mockito.ArgumentMatchers.anyString
-import org.mockito.ArgumentMatchers.nullable
-import org.mockito.Mock
-import org.mockito.Mockito.eq
-import org.mockito.Mockito.mock
-import org.mockito.Mockito.never
-import org.mockito.Mockito.reset
-import org.mockito.Mockito.times
-import org.mockito.Mockito.verify
-import org.mockito.Mockito.`when`
-import org.mockito.MockitoAnnotations
-import org.mockito.kotlin.whenever
-
-private const val CALL_UID = 900
-
-// A process state that represents the process being visible to the user.
-private const val PROC_STATE_VISIBLE = ActivityManager.PROCESS_STATE_TOP
-
-// A process state that represents the process being invisible to the user.
-private const val PROC_STATE_INVISIBLE = ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE
-
-@SmallTest
-@RunWith(AndroidJUnit4::class)
-@TestableLooper.RunWithLooper
-@OptIn(ExperimentalCoroutinesApi::class)
-@DisableFlags(FLAG_STATUS_BAR_USE_REPOS_FOR_CALL_CHIP, StatusBarChipsModernization.FLAG_NAME)
-class OngoingCallControllerViaListenerTest : SysuiTestCase() {
- private val kosmos = Kosmos()
-
- private val clock = FakeSystemClock()
- private val mainExecutor = FakeExecutor(clock)
- private val testScope = TestScope()
- private val statusBarModeRepository = FakeStatusBarModeRepository()
- private val ongoingCallRepository = kosmos.ongoingCallRepository
-
- private lateinit var controller: OngoingCallController
- private lateinit var notifCollectionListener: NotifCollectionListener
-
- @Mock
- private lateinit var mockSwipeStatusBarAwayGestureHandler: SwipeStatusBarAwayGestureHandler
- @Mock private lateinit var mockOngoingCallListener: OngoingCallListener
- @Mock private lateinit var mockActivityStarter: ActivityStarter
- @Mock private lateinit var mockIActivityManager: IActivityManager
- @Mock private lateinit var mockStatusBarWindowController: StatusBarWindowController
- @Mock private lateinit var mockStatusBarWindowControllerStore: StatusBarWindowControllerStore
-
- private lateinit var chipView: View
-
- @Before
- fun setUp() {
- allowTestableLooperAsMainThread()
- TestableLooper.get(this).runWithLooper {
- chipView =
- LayoutInflater.from(mContext).inflate(R.layout.ongoing_activity_chip_primary, null)
- }
-
- MockitoAnnotations.initMocks(this)
- val notificationCollection = mock(CommonNotifCollection::class.java)
- whenever(mockStatusBarWindowControllerStore.defaultDisplay)
- .thenReturn(mockStatusBarWindowController)
-
- controller =
- OngoingCallController(
- testScope.backgroundScope,
- context,
- ongoingCallRepository,
- notificationCollection,
- kosmos.activeNotificationsInteractor,
- clock,
- mockActivityStarter,
- mainExecutor,
- mockIActivityManager,
- DumpManager(),
- mockStatusBarWindowControllerStore,
- mockSwipeStatusBarAwayGestureHandler,
- statusBarModeRepository,
- logcatLogBuffer("OngoingCallControllerViaListenerTest"),
- )
- controller.start()
- controller.addCallback(mockOngoingCallListener)
- controller.setChipView(chipView)
- onTeardown { controller.tearDownChipView() }
-
- val collectionListenerCaptor = ArgumentCaptor.forClass(NotifCollectionListener::class.java)
- verify(notificationCollection).addCollectionListener(collectionListenerCaptor.capture())
- notifCollectionListener = collectionListenerCaptor.value!!
-
- `when`(mockIActivityManager.getUidProcessState(eq(CALL_UID), nullable(String::class.java)))
- .thenReturn(PROC_STATE_INVISIBLE)
- }
-
- @Test
- fun onEntryUpdated_isOngoingCallNotif_listenerAndRepoNotified() {
- val notification = NotificationEntryBuilder(createOngoingCallNotifEntry())
- notification.modifyNotification(context).setWhen(567)
- notifCollectionListener.onEntryUpdated(notification.build())
-
- verify(mockOngoingCallListener).onOngoingCallStateChanged(anyBoolean())
- val repoState = ongoingCallRepository.ongoingCallState.value
- assertThat(repoState).isInstanceOf(OngoingCallModel.InCall::class.java)
- assertThat((repoState as OngoingCallModel.InCall).startTimeMs).isEqualTo(567)
- }
-
- @Test
- fun onEntryUpdated_isOngoingCallNotif_windowControllerUpdated() {
- notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
-
- verify(mockStatusBarWindowController).setOngoingProcessRequiresStatusBarVisible(true)
- }
-
- @Test
- fun onEntryUpdated_notOngoingCallNotif_listenerAndRepoNotNotified() {
- notifCollectionListener.onEntryUpdated(createNotCallNotifEntry())
-
- verify(mockOngoingCallListener, never()).onOngoingCallStateChanged(anyBoolean())
- assertThat(ongoingCallRepository.ongoingCallState.value)
- .isInstanceOf(OngoingCallModel.NoCall::class.java)
- }
-
- @Test
- fun onEntryUpdated_ongoingCallNotifThenScreeningCallNotif_listenerNotifiedTwice() {
- notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
- notifCollectionListener.onEntryUpdated(createScreeningCallNotifEntry())
-
- verify(mockOngoingCallListener, times(2)).onOngoingCallStateChanged(anyBoolean())
- }
-
- @Test
- fun onEntryUpdated_ongoingCallNotifThenScreeningCallNotif_repoUpdated() {
- notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
- assertThat(ongoingCallRepository.ongoingCallState.value)
- .isInstanceOf(OngoingCallModel.InCall::class.java)
-
- notifCollectionListener.onEntryUpdated(createScreeningCallNotifEntry())
-
- assertThat(ongoingCallRepository.ongoingCallState.value)
- .isInstanceOf(OngoingCallModel.NoCall::class.java)
- }
-
- /** Regression test for b/191472854. */
- @Test
- fun onEntryUpdated_notifHasNullContentIntent_noCrash() {
- notifCollectionListener.onEntryUpdated(
- createCallNotifEntry(ongoingCallStyle, nullContentIntent = true)
- )
- }
-
- /** Regression test for b/192379214. */
- @Test
- @DisableFlags(android.app.Flags.FLAG_SORT_SECTION_BY_TIME, FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS)
- fun onEntryUpdated_notificationWhenIsZero_timeHidden() {
- val notification = NotificationEntryBuilder(createOngoingCallNotifEntry())
- notification.modifyNotification(context).setWhen(0)
-
- notifCollectionListener.onEntryUpdated(notification.build())
- chipView.measure(
- View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
- View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
- )
-
- assertThat(chipView.findViewById<View>(R.id.ongoing_activity_chip_time)?.measuredWidth)
- .isEqualTo(0)
- }
-
- @Test
- @EnableFlags(android.app.Flags.FLAG_SORT_SECTION_BY_TIME)
- @DisableFlags(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS)
- fun onEntryUpdated_notificationWhenIsZero_timeShown() {
- val notification = NotificationEntryBuilder(createOngoingCallNotifEntry())
- notification.modifyNotification(context).setWhen(0)
-
- notifCollectionListener.onEntryUpdated(notification.build())
- chipView.measure(
- View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
- View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
- )
-
- assertThat(chipView.findViewById<View>(R.id.ongoing_activity_chip_time)?.measuredWidth)
- .isGreaterThan(0)
- }
-
- @Test
- @DisableFlags(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS)
- fun onEntryUpdated_notificationWhenIsValid_timeShown() {
- val notification = NotificationEntryBuilder(createOngoingCallNotifEntry())
- notification.modifyNotification(context).setWhen(clock.currentTimeMillis())
-
- notifCollectionListener.onEntryUpdated(notification.build())
- chipView.measure(
- View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
- View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
- )
-
- assertThat(chipView.findViewById<View>(R.id.ongoing_activity_chip_time)?.measuredWidth)
- .isGreaterThan(0)
- }
-
- /** Regression test for b/194731244. */
- @Test
- fun onEntryUpdated_calledManyTimes_uidObserverOnlyRegisteredOnce() {
- for (i in 0 until 4) {
- // Re-create the notification each time so that it's considered a different object and
- // will re-trigger the whole flow.
- notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
- }
-
- verify(mockIActivityManager, times(1)).registerUidObserver(any(), any(), any(), any())
- }
-
- /** Regression test for b/216248574. */
- @Test
- fun entryUpdated_getUidProcessStateThrowsException_noCrash() {
- `when`(mockIActivityManager.getUidProcessState(eq(CALL_UID), nullable(String::class.java)))
- .thenThrow(SecurityException())
-
- // No assert required, just check no crash
- notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
- }
-
- /** Regression test for b/216248574. */
- @Test
- fun entryUpdated_registerUidObserverThrowsException_noCrash() {
- `when`(
- mockIActivityManager.registerUidObserver(
- any(),
- any(),
- any(),
- nullable(String::class.java),
- )
- )
- .thenThrow(SecurityException())
-
- // No assert required, just check no crash
- notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
- }
-
- /** Regression test for b/216248574. */
- @Test
- fun entryUpdated_packageNameProvidedToActivityManager() {
- notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
-
- val packageNameCaptor = ArgumentCaptor.forClass(String::class.java)
- verify(mockIActivityManager)
- .registerUidObserver(any(), any(), any(), packageNameCaptor.capture())
- assertThat(packageNameCaptor.value).isNotNull()
- }
-
- /**
- * If a call notification is never added before #onEntryRemoved is called, then the listener
- * should never be notified.
- */
- @Test
- fun onEntryRemoved_callNotifNeverAddedBeforehand_listenerNotNotified() {
- notifCollectionListener.onEntryRemoved(createOngoingCallNotifEntry(), REASON_USER_STOPPED)
-
- verify(mockOngoingCallListener, never()).onOngoingCallStateChanged(anyBoolean())
- }
-
- @Test
- fun onEntryRemoved_callNotifAddedThenRemoved_listenerNotified() {
- val ongoingCallNotifEntry = createOngoingCallNotifEntry()
- notifCollectionListener.onEntryAdded(ongoingCallNotifEntry)
- reset(mockOngoingCallListener)
-
- notifCollectionListener.onEntryRemoved(ongoingCallNotifEntry, REASON_USER_STOPPED)
-
- verify(mockOngoingCallListener).onOngoingCallStateChanged(anyBoolean())
- }
-
- @Test
- fun onEntryRemoved_callNotifAddedThenRemoved_repoUpdated() {
- val ongoingCallNotifEntry = createOngoingCallNotifEntry()
- notifCollectionListener.onEntryAdded(ongoingCallNotifEntry)
- assertThat(ongoingCallRepository.ongoingCallState.value)
- .isInstanceOf(OngoingCallModel.InCall::class.java)
-
- notifCollectionListener.onEntryRemoved(ongoingCallNotifEntry, REASON_USER_STOPPED)
-
- assertThat(ongoingCallRepository.ongoingCallState.value)
- .isInstanceOf(OngoingCallModel.NoCall::class.java)
- }
-
- @Test
- fun onEntryUpdated_callNotifAddedThenRemoved_windowControllerUpdated() {
- val ongoingCallNotifEntry = createOngoingCallNotifEntry()
- notifCollectionListener.onEntryAdded(ongoingCallNotifEntry)
-
- notifCollectionListener.onEntryRemoved(ongoingCallNotifEntry, REASON_USER_STOPPED)
-
- verify(mockStatusBarWindowController).setOngoingProcessRequiresStatusBarVisible(false)
- }
-
- /** Regression test for b/188491504. */
- @Test
- fun onEntryRemoved_removedNotifHasSameKeyAsAddedNotif_listenerNotified() {
- val ongoingCallNotifEntry = createOngoingCallNotifEntry()
- notifCollectionListener.onEntryAdded(ongoingCallNotifEntry)
- reset(mockOngoingCallListener)
-
- // Create another notification based on the ongoing call one, but remove the features that
- // made it a call notification.
- val removedEntryBuilder = NotificationEntryBuilder(ongoingCallNotifEntry)
- removedEntryBuilder.modifyNotification(context).style = null
-
- notifCollectionListener.onEntryRemoved(removedEntryBuilder.build(), REASON_USER_STOPPED)
-
- verify(mockOngoingCallListener).onOngoingCallStateChanged(anyBoolean())
- }
-
- /** Regression test for b/188491504. */
- @Test
- fun onEntryRemoved_removedNotifHasSameKeyAsAddedNotif_repoUpdated() {
- val ongoingCallNotifEntry = createOngoingCallNotifEntry()
- notifCollectionListener.onEntryAdded(ongoingCallNotifEntry)
-
- // Create another notification based on the ongoing call one, but remove the features that
- // made it a call notification.
- val removedEntryBuilder = NotificationEntryBuilder(ongoingCallNotifEntry)
- removedEntryBuilder.modifyNotification(context).style = null
-
- notifCollectionListener.onEntryRemoved(removedEntryBuilder.build(), REASON_USER_STOPPED)
-
- assertThat(ongoingCallRepository.ongoingCallState.value)
- .isInstanceOf(OngoingCallModel.NoCall::class.java)
- }
-
- @Test
- fun onEntryRemoved_notifKeyDoesNotMatchOngoingCallNotif_listenerNotNotified() {
- notifCollectionListener.onEntryAdded(createOngoingCallNotifEntry())
- reset(mockOngoingCallListener)
-
- notifCollectionListener.onEntryRemoved(createNotCallNotifEntry(), REASON_USER_STOPPED)
-
- verify(mockOngoingCallListener, never()).onOngoingCallStateChanged(anyBoolean())
- }
-
- @Test
- fun onEntryRemoved_notifKeyDoesNotMatchOngoingCallNotif_repoNotUpdated() {
- notifCollectionListener.onEntryAdded(createOngoingCallNotifEntry())
-
- notifCollectionListener.onEntryRemoved(createNotCallNotifEntry(), REASON_USER_STOPPED)
-
- assertThat(ongoingCallRepository.ongoingCallState.value)
- .isInstanceOf(OngoingCallModel.InCall::class.java)
- }
-
- @Test
- fun hasOngoingCall_noOngoingCallNotifSent_returnsFalse() {
- assertThat(controller.hasOngoingCall()).isFalse()
- }
-
- @Test
- fun hasOngoingCall_unrelatedNotifSent_returnsFalse() {
- notifCollectionListener.onEntryUpdated(createNotCallNotifEntry())
-
- assertThat(controller.hasOngoingCall()).isFalse()
- }
-
- @Test
- fun hasOngoingCall_screeningCallNotifSent_returnsFalse() {
- notifCollectionListener.onEntryUpdated(createScreeningCallNotifEntry())
-
- assertThat(controller.hasOngoingCall()).isFalse()
- }
-
- @Test
- fun hasOngoingCall_ongoingCallNotifSentAndCallAppNotVisible_returnsTrue() {
- `when`(mockIActivityManager.getUidProcessState(eq(CALL_UID), nullable(String::class.java)))
- .thenReturn(PROC_STATE_INVISIBLE)
-
- notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
-
- assertThat(controller.hasOngoingCall()).isTrue()
- }
-
- @Test
- fun hasOngoingCall_ongoingCallNotifSentButCallAppVisible_returnsFalse() {
- `when`(mockIActivityManager.getUidProcessState(eq(CALL_UID), nullable(String::class.java)))
- .thenReturn(PROC_STATE_VISIBLE)
-
- notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
-
- assertThat(controller.hasOngoingCall()).isFalse()
- }
-
- @Test
- fun hasOngoingCall_ongoingCallNotifSentButInvalidChipView_returnsFalse() {
- val invalidChipView = LinearLayout(context)
- controller.setChipView(invalidChipView)
-
- notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
-
- assertThat(controller.hasOngoingCall()).isFalse()
- }
-
- @Test
- fun hasOngoingCall_ongoingCallNotifSentThenRemoved_returnsFalse() {
- val ongoingCallNotifEntry = createOngoingCallNotifEntry()
-
- notifCollectionListener.onEntryUpdated(ongoingCallNotifEntry)
- notifCollectionListener.onEntryRemoved(ongoingCallNotifEntry, REASON_USER_STOPPED)
-
- assertThat(controller.hasOngoingCall()).isFalse()
- }
-
- @Test
- fun hasOngoingCall_ongoingCallNotifSentThenScreeningCallNotifSent_returnsFalse() {
- notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
- notifCollectionListener.onEntryUpdated(createScreeningCallNotifEntry())
-
- assertThat(controller.hasOngoingCall()).isFalse()
- }
-
- @Test
- fun hasOngoingCall_ongoingCallNotifSentThenUnrelatedNotifSent_returnsTrue() {
- notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
- notifCollectionListener.onEntryUpdated(createNotCallNotifEntry())
-
- assertThat(controller.hasOngoingCall()).isTrue()
- }
-
- /**
- * This test fakes a theme change during an ongoing call.
- *
- * When a theme change happens, [CollapsedStatusBarFragment] and its views get re-created, so
- * [OngoingCallController.setChipView] gets called with a new view. If there's an active ongoing
- * call when the theme changes, the new view needs to be updated with the call information.
- */
- @Test
- fun setChipView_whenHasOngoingCallIsTrue_listenerNotifiedWithNewView() {
- // Start an ongoing call.
- notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
- reset(mockOngoingCallListener)
-
- lateinit var newChipView: View
- TestableLooper.get(this).runWithLooper {
- newChipView =
- LayoutInflater.from(mContext).inflate(R.layout.ongoing_activity_chip_primary, null)
- }
-
- // Change the chip view associated with the controller.
- controller.setChipView(newChipView)
-
- verify(mockOngoingCallListener).onOngoingCallStateChanged(anyBoolean())
- }
-
- @Test
- fun callProcessChangesToVisible_listenerNotified() {
- // Start the call while the process is invisible.
- `when`(mockIActivityManager.getUidProcessState(eq(CALL_UID), nullable(String::class.java)))
- .thenReturn(PROC_STATE_INVISIBLE)
- notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
- reset(mockOngoingCallListener)
-
- val captor = ArgumentCaptor.forClass(IUidObserver.Stub::class.java)
- verify(mockIActivityManager)
- .registerUidObserver(captor.capture(), any(), any(), nullable(String::class.java))
- val uidObserver = captor.value
-
- // Update the process to visible.
- uidObserver.onUidStateChanged(CALL_UID, PROC_STATE_VISIBLE, 0, 0)
- mainExecutor.advanceClockToLast()
- mainExecutor.runAllReady()
-
- verify(mockOngoingCallListener).onOngoingCallStateChanged(anyBoolean())
- }
-
- @Test
- fun callProcessChangesToInvisible_listenerNotified() {
- // Start the call while the process is visible.
- `when`(mockIActivityManager.getUidProcessState(eq(CALL_UID), nullable(String::class.java)))
- .thenReturn(PROC_STATE_VISIBLE)
- notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
- reset(mockOngoingCallListener)
-
- val captor = ArgumentCaptor.forClass(IUidObserver.Stub::class.java)
- verify(mockIActivityManager)
- .registerUidObserver(captor.capture(), any(), any(), nullable(String::class.java))
- val uidObserver = captor.value
-
- // Update the process to invisible.
- uidObserver.onUidStateChanged(CALL_UID, PROC_STATE_INVISIBLE, 0, 0)
- mainExecutor.advanceClockToLast()
- mainExecutor.runAllReady()
-
- verify(mockOngoingCallListener).onOngoingCallStateChanged(anyBoolean())
- }
-
- /** Regression test for b/212467440. */
- @Test
- @DisableFlags(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS)
- fun chipClicked_activityStarterTriggeredWithUnmodifiedIntent() {
- val notifEntry = createOngoingCallNotifEntry()
- val pendingIntent = notifEntry.sbn.notification.contentIntent
- notifCollectionListener.onEntryUpdated(notifEntry)
-
- chipView.performClick()
-
- // Ensure that the sysui didn't modify the notification's intent -- see b/212467440.
- verify(mockActivityStarter).postStartActivityDismissingKeyguard(eq(pendingIntent), any())
- }
-
- @Test
- @DisableFlags(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS)
- fun callNotificationAdded_chipIsClickable() {
- notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
-
- assertThat(chipView.hasOnClickListeners()).isTrue()
- }
-
- @Test
- @EnableFlags(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS)
- fun callNotificationAdded_newChipsEnabled_chipNotClickable() {
- notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
-
- assertThat(chipView.hasOnClickListeners()).isFalse()
- }
-
- @Test
- @DisableFlags(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS)
- fun fullscreenIsTrue_chipStillClickable() {
- notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
- statusBarModeRepository.defaultDisplay.isInFullscreenMode.value = true
- testScope.runCurrent()
-
- assertThat(chipView.hasOnClickListeners()).isTrue()
- }
-
- // Swipe gesture tests
-
- @Test
- fun callStartedInImmersiveMode_swipeGestureCallbackAdded() {
- statusBarModeRepository.defaultDisplay.isInFullscreenMode.value = true
- testScope.runCurrent()
-
- notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
-
- verify(mockSwipeStatusBarAwayGestureHandler)
- .addOnGestureDetectedCallback(anyString(), any())
- }
-
- @Test
- fun callStartedNotInImmersiveMode_swipeGestureCallbackNotAdded() {
- statusBarModeRepository.defaultDisplay.isInFullscreenMode.value = false
- testScope.runCurrent()
-
- notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
-
- verify(mockSwipeStatusBarAwayGestureHandler, never())
- .addOnGestureDetectedCallback(anyString(), any())
- }
-
- @Test
- fun transitionToImmersiveMode_swipeGestureCallbackAdded() {
- notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
-
- statusBarModeRepository.defaultDisplay.isInFullscreenMode.value = true
- testScope.runCurrent()
-
- verify(mockSwipeStatusBarAwayGestureHandler)
- .addOnGestureDetectedCallback(anyString(), any())
- }
-
- @Test
- fun transitionOutOfImmersiveMode_swipeGestureCallbackRemoved() {
- statusBarModeRepository.defaultDisplay.isInFullscreenMode.value = true
- notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
- reset(mockSwipeStatusBarAwayGestureHandler)
-
- statusBarModeRepository.defaultDisplay.isInFullscreenMode.value = false
- testScope.runCurrent()
-
- verify(mockSwipeStatusBarAwayGestureHandler).removeOnGestureDetectedCallback(anyString())
- }
-
- @Test
- fun callEndedWhileInImmersiveMode_swipeGestureCallbackRemoved() {
- statusBarModeRepository.defaultDisplay.isInFullscreenMode.value = true
- testScope.runCurrent()
- val ongoingCallNotifEntry = createOngoingCallNotifEntry()
- notifCollectionListener.onEntryAdded(ongoingCallNotifEntry)
- reset(mockSwipeStatusBarAwayGestureHandler)
-
- notifCollectionListener.onEntryRemoved(ongoingCallNotifEntry, REASON_USER_STOPPED)
-
- verify(mockSwipeStatusBarAwayGestureHandler).removeOnGestureDetectedCallback(anyString())
- }
-
- // TODO(b/195839150): Add test
- // swipeGesturedTriggeredPreviously_entersImmersiveModeAgain_callbackNotAdded(). That's
- // difficult to add now because we have no way to trigger [SwipeStatusBarAwayGestureHandler]'s
- // callbacks in test.
-
- // END swipe gesture tests
-
- private fun createOngoingCallNotifEntry() = createCallNotifEntry(ongoingCallStyle)
-
- private fun createScreeningCallNotifEntry() = createCallNotifEntry(screeningCallStyle)
-
- private fun createCallNotifEntry(
- callStyle: Notification.CallStyle,
- nullContentIntent: Boolean = false,
- ): NotificationEntry {
- val notificationEntryBuilder = NotificationEntryBuilder()
- notificationEntryBuilder.modifyNotification(context).style = callStyle
- notificationEntryBuilder.setUid(CALL_UID)
-
- if (nullContentIntent) {
- notificationEntryBuilder.modifyNotification(context).setContentIntent(null)
- } else {
- val contentIntent = mock(PendingIntent::class.java)
- notificationEntryBuilder.modifyNotification(context).setContentIntent(contentIntent)
- }
-
- return notificationEntryBuilder.build()
- }
-
- private fun createNotCallNotifEntry() = NotificationEntryBuilder().build()
-}
-
-private val person = Person.Builder().setName("name").build()
-private val hangUpIntent = mock(PendingIntent::class.java)
-
-private val ongoingCallStyle = Notification.CallStyle.forOngoingCall(person, hangUpIntent)
-private val screeningCallStyle =
- Notification.CallStyle.forScreeningCall(
- person,
- hangUpIntent,
- /* answerIntent= */ mock(PendingIntent::class.java),
- )
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/FakeHomeStatusBarViewModel.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/FakeHomeStatusBarViewModel.kt
index a9db0b70dd4d..6feada1c9769 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/FakeHomeStatusBarViewModel.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/FakeHomeStatusBarViewModel.kt
@@ -31,7 +31,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
class FakeHomeStatusBarViewModel(
override val operatorNameViewModel: StatusBarOperatorNameViewModel
) : HomeStatusBarViewModel {
- private val areNotificationLightsOut = MutableStateFlow(false)
+ override val areNotificationsLightsOut = MutableStateFlow(false)
override val isTransitioningFromLockscreenToOccluded = MutableStateFlow(false)
@@ -77,14 +77,14 @@ class FakeHomeStatusBarViewModel(
override val iconBlockList: MutableStateFlow<List<String>> = MutableStateFlow(listOf())
- override fun areNotificationsLightsOut(displayId: Int): Flow<Boolean> = areNotificationLightsOut
+ override val contentArea = MutableStateFlow(Rect(0, 0, 1, 1))
val darkRegions = mutableListOf<Rect>()
var darkIconTint = Color.BLACK
var lightIconTint = Color.WHITE
- override fun areaTint(displayId: Int): Flow<StatusBarTintColor> =
+ override val areaTint: Flow<StatusBarTintColor> =
MutableStateFlow(
StatusBarTintColor { viewBounds ->
if (DarkIconDispatcher.isInAreas(darkRegions, viewBounds)) {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelImplTest.kt
index e91875cd0885..e95bc3378423 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelImplTest.kt
@@ -22,13 +22,17 @@ import android.app.StatusBarManager.DISABLE_CLOCK
import android.app.StatusBarManager.DISABLE_NONE
import android.app.StatusBarManager.DISABLE_NOTIFICATION_ICONS
import android.app.StatusBarManager.DISABLE_SYSTEM_INFO
+import android.content.testableContext
import android.graphics.Rect
import android.platform.test.annotations.DisableFlags
import android.platform.test.annotations.EnableFlags
+import android.view.Display.DEFAULT_DISPLAY
import android.view.View
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
+import com.android.systemui.display.data.repository.displayRepository
+import com.android.systemui.display.data.repository.fake
import com.android.systemui.flags.DisableSceneContainer
import com.android.systemui.flags.EnableSceneContainer
import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
@@ -59,7 +63,6 @@ import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel
import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipsViewModelTest.Companion.assertIsScreenRecordChip
import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipsViewModelTest.Companion.assertIsShareToAppChip
import com.android.systemui.statusbar.data.model.StatusBarMode
-import com.android.systemui.statusbar.data.repository.FakeStatusBarModeRepository.Companion.DISPLAY_ID
import com.android.systemui.statusbar.data.repository.fakeStatusBarModeRepository
import com.android.systemui.statusbar.disableflags.data.repository.fakeDisableFlagsRepository
import com.android.systemui.statusbar.disableflags.shared.model.DisableFlagsModel
@@ -85,6 +88,7 @@ import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.emptyFlow
+import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import org.junit.Before
import org.junit.Test
@@ -104,6 +108,9 @@ class HomeStatusBarViewModelImplTest : SysuiTestCase() {
setUpPackageManagerForMediaProjection(kosmos)
}
+ @Before
+ fun addDisplays() = runBlocking { kosmos.displayRepository.fake.addDisplay(DEFAULT_DISPLAY) }
+
@Test
fun isTransitioningFromLockscreenToOccluded_started_isTrue() =
kosmos.runTest {
@@ -363,7 +370,7 @@ class HomeStatusBarViewModelImplTest : SysuiTestCase() {
activeNotificationListRepository.activeNotifications.value =
activeNotificationsStore(testNotifications)
- val actual by collectLastValue(underTest.areNotificationsLightsOut(DISPLAY_ID))
+ val actual by collectLastValue(underTest.areNotificationsLightsOut)
assertThat(actual).isTrue()
}
@@ -377,7 +384,7 @@ class HomeStatusBarViewModelImplTest : SysuiTestCase() {
activeNotificationListRepository.activeNotifications.value =
activeNotificationsStore(emptyList())
- val actual by collectLastValue(underTest.areNotificationsLightsOut(DISPLAY_ID))
+ val actual by collectLastValue(underTest.areNotificationsLightsOut)
assertThat(actual).isFalse()
}
@@ -391,7 +398,7 @@ class HomeStatusBarViewModelImplTest : SysuiTestCase() {
activeNotificationListRepository.activeNotifications.value =
activeNotificationsStore(emptyList())
- val actual by collectLastValue(underTest.areNotificationsLightsOut(DISPLAY_ID))
+ val actual by collectLastValue(underTest.areNotificationsLightsOut)
assertThat(actual).isFalse()
}
@@ -405,7 +412,7 @@ class HomeStatusBarViewModelImplTest : SysuiTestCase() {
activeNotificationListRepository.activeNotifications.value =
activeNotificationsStore(testNotifications)
- val actual by collectLastValue(underTest.areNotificationsLightsOut(DISPLAY_ID))
+ val actual by collectLastValue(underTest.areNotificationsLightsOut)
assertThat(actual).isFalse()
}
@@ -415,7 +422,7 @@ class HomeStatusBarViewModelImplTest : SysuiTestCase() {
fun areNotificationsLightsOut_requiresFlagEnabled() =
kosmos.runTest {
assertLogsWtf {
- val flow = underTest.areNotificationsLightsOut(DISPLAY_ID)
+ val flow = underTest.areNotificationsLightsOut
assertThat(flow).isEqualTo(emptyFlow<Boolean>())
}
}
@@ -1005,11 +1012,11 @@ class HomeStatusBarViewModelImplTest : SysuiTestCase() {
@Test
fun areaTint_viewIsInDarkBounds_getsDarkTint() =
kosmos.runTest {
- val displayId = 321
+ val displayId = testableContext.displayId
fakeDarkIconRepository.darkState(displayId).value =
SysuiDarkIconDispatcher.DarkChange(listOf(Rect(0, 0, 5, 5)), 0f, 0xAABBCC)
- val areaTint by collectLastValue(underTest.areaTint(displayId))
+ val areaTint by collectLastValue(underTest.areaTint)
val tint = areaTint?.tint(Rect(1, 1, 3, 3))
@@ -1019,11 +1026,11 @@ class HomeStatusBarViewModelImplTest : SysuiTestCase() {
@Test
fun areaTint_viewIsNotInDarkBounds_getsDefaultTint() =
kosmos.runTest {
- val displayId = 321
+ val displayId = testableContext.displayId
fakeDarkIconRepository.darkState(displayId).value =
SysuiDarkIconDispatcher.DarkChange(listOf(Rect(0, 0, 5, 5)), 0f, 0xAABBCC)
- val areaTint by collectLastValue(underTest.areaTint(displayId))
+ val areaTint by collectLastValue(underTest.areaTint)
val tint = areaTint?.tint(Rect(6, 6, 7, 7))
@@ -1033,11 +1040,11 @@ class HomeStatusBarViewModelImplTest : SysuiTestCase() {
@Test
fun areaTint_viewIsInDarkBounds_darkBoundsChange_viewUpdates() =
kosmos.runTest {
- val displayId = 321
+ val displayId = testableContext.displayId
fakeDarkIconRepository.darkState(displayId).value =
SysuiDarkIconDispatcher.DarkChange(listOf(Rect(0, 0, 5, 5)), 0f, 0xAABBCC)
- val areaTint by collectLastValue(underTest.areaTint(displayId))
+ val areaTint by collectLastValue(underTest.areaTint)
var tint = areaTint?.tint(Rect(1, 1, 3, 3))
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt
index c6bae197ad76..7802b921eae0 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt
@@ -33,6 +33,7 @@ import androidx.test.filters.SmallTest
import com.android.internal.R
import com.android.settingslib.notification.data.repository.updateNotificationPolicy
import com.android.settingslib.notification.modes.TestModeBuilder
+import com.android.settingslib.notification.modes.TestModeBuilder.MANUAL_DND
import com.android.settingslib.volume.shared.model.AudioStream
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
@@ -204,7 +205,7 @@ class ZenModeInteractorTest : SysuiTestCase() {
@Test
fun shouldAskForZenDuration_changesWithSetting() =
testScope.runTest {
- val manualDnd = TestModeBuilder.MANUAL_DND_ACTIVE
+ val manualDnd = TestModeBuilder().makeManualDnd().setActive(true).build()
settingsRepository.setInt(ZEN_DURATION, ZEN_DURATION_FOREVER)
runCurrent()
@@ -233,29 +234,27 @@ class ZenModeInteractorTest : SysuiTestCase() {
@Test
fun activateMode_usesCorrectDuration() =
testScope.runTest {
- val manualDnd = TestModeBuilder.MANUAL_DND_ACTIVE
- zenModeRepository.addModes(listOf(manualDnd))
settingsRepository.setInt(ZEN_DURATION, ZEN_DURATION_FOREVER)
runCurrent()
- underTest.activateMode(manualDnd)
- assertThat(zenModeRepository.getModeActiveDuration(manualDnd.id)).isNull()
+ underTest.activateMode(MANUAL_DND)
+ assertThat(zenModeRepository.getModeActiveDuration(MANUAL_DND.id)).isNull()
- zenModeRepository.deactivateMode(manualDnd.id)
+ zenModeRepository.deactivateMode(MANUAL_DND)
settingsRepository.setInt(ZEN_DURATION, 60)
runCurrent()
- underTest.activateMode(manualDnd)
- assertThat(zenModeRepository.getModeActiveDuration(manualDnd.id))
+ underTest.activateMode(MANUAL_DND)
+ assertThat(zenModeRepository.getModeActiveDuration(MANUAL_DND.id))
.isEqualTo(Duration.ofMinutes(60))
}
@Test
fun deactivateAllModes_updatesCorrectModes() =
testScope.runTest {
+ zenModeRepository.activateMode(MANUAL_DND)
zenModeRepository.addModes(
listOf(
- TestModeBuilder.MANUAL_DND_ACTIVE,
TestModeBuilder().setName("Inactive").setActive(false).build(),
TestModeBuilder().setName("Active").setActive(true).build(),
)
@@ -389,12 +388,9 @@ class ZenModeInteractorTest : SysuiTestCase() {
testScope.runTest {
val dndMode by collectLastValue(underTest.dndMode)
- zenModeRepository.addMode(TestModeBuilder.MANUAL_DND_INACTIVE)
- runCurrent()
-
assertThat(dndMode!!.isActive).isFalse()
- zenModeRepository.activateMode(TestModeBuilder.MANUAL_DND_INACTIVE.id)
+ zenModeRepository.activateMode(MANUAL_DND)
runCurrent()
assertThat(dndMode!!.isActive).isTrue()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModelTest.kt
index 07d088bbb3d6..856de8ee1c80 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModelTest.kt
@@ -28,6 +28,7 @@ import android.service.notification.ZenModeConfig.ScheduleInfo
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.settingslib.notification.modes.TestModeBuilder
+import com.android.settingslib.notification.modes.TestModeBuilder.MANUAL_DND
import com.android.settingslib.notification.modes.ZenMode
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
@@ -111,7 +112,6 @@ class ModesDialogViewModelTest : SysuiTestCase() {
.setName("Disabled by other")
.setEnabled(false, /* byUser= */ false)
.build(),
- TestModeBuilder.MANUAL_DND_ACTIVE,
TestModeBuilder()
.setName("Enabled")
.setEnabled(true)
@@ -128,14 +128,14 @@ class ModesDialogViewModelTest : SysuiTestCase() {
assertThat(tiles).hasSize(3)
with(tiles?.elementAt(0)!!) {
- assertThat(this.text).isEqualTo("Disabled by other")
- assertThat(this.subtext).isEqualTo("Not set")
+ assertThat(this.text).isEqualTo("Do Not Disturb")
+ assertThat(this.subtext).isEqualTo("Off")
assertThat(this.enabled).isEqualTo(false)
}
with(tiles?.elementAt(1)!!) {
- assertThat(this.text).isEqualTo("Do Not Disturb")
- assertThat(this.subtext).isEqualTo("On")
- assertThat(this.enabled).isEqualTo(true)
+ assertThat(this.text).isEqualTo("Disabled by other")
+ assertThat(this.subtext).isEqualTo("Not set")
+ assertThat(this.enabled).isEqualTo(false)
}
with(tiles?.elementAt(2)!!) {
assertThat(this.text).isEqualTo("Enabled")
@@ -176,18 +176,24 @@ class ModesDialogViewModelTest : SysuiTestCase() {
)
runCurrent()
- assertThat(tiles).hasSize(3)
+ // Manual DND is included by default
+ assertThat(tiles).hasSize(4)
with(tiles?.elementAt(0)!!) {
+ assertThat(this.text).isEqualTo("Do Not Disturb")
+ assertThat(this.subtext).isEqualTo("Off")
+ assertThat(this.enabled).isEqualTo(false)
+ }
+ with(tiles?.elementAt(1)!!) {
assertThat(this.text).isEqualTo("Active without manual")
assertThat(this.subtext).isEqualTo("On")
assertThat(this.enabled).isEqualTo(true)
}
- with(tiles?.elementAt(1)!!) {
+ with(tiles?.elementAt(2)!!) {
assertThat(this.text).isEqualTo("Active with manual")
assertThat(this.subtext).isEqualTo("On • trigger description")
assertThat(this.enabled).isEqualTo(true)
}
- with(tiles?.elementAt(2)!!) {
+ with(tiles?.elementAt(3)!!) {
assertThat(this.text).isEqualTo("Inactive with manual")
assertThat(this.subtext).isEqualTo("Off")
assertThat(this.enabled).isEqualTo(false)
@@ -226,10 +232,11 @@ class ModesDialogViewModelTest : SysuiTestCase() {
)
runCurrent()
- assertThat(tiles).hasSize(3)
+ // Manual DND is included by default
+ assertThat(tiles).hasSize(4)
// Check that tile is initially present
- with(tiles?.elementAt(0)!!) {
+ with(tiles?.elementAt(1)!!) {
assertThat(this.text).isEqualTo("Active without manual")
assertThat(this.subtext).isEqualTo("On")
assertThat(this.enabled).isEqualTo(true)
@@ -239,8 +246,8 @@ class ModesDialogViewModelTest : SysuiTestCase() {
runCurrent()
}
// Check that tile is still present at the same location, but turned off
- assertThat(tiles).hasSize(3)
- with(tiles?.elementAt(0)!!) {
+ assertThat(tiles).hasSize(4)
+ with(tiles?.elementAt(1)!!) {
assertThat(this.text).isEqualTo("Active without manual")
assertThat(this.subtext).isEqualTo("Manage in settings")
assertThat(this.enabled).isEqualTo(false)
@@ -252,9 +259,9 @@ class ModesDialogViewModelTest : SysuiTestCase() {
runCurrent()
// Check that tile is now gone
- assertThat(tiles2).hasSize(2)
- assertThat(tiles2?.elementAt(0)!!.text).isEqualTo("Active with manual")
- assertThat(tiles2?.elementAt(1)!!.text).isEqualTo("Inactive with manual")
+ assertThat(tiles2).hasSize(3)
+ assertThat(tiles2?.elementAt(1)!!.text).isEqualTo("Active with manual")
+ assertThat(tiles2?.elementAt(2)!!.text).isEqualTo("Inactive with manual")
}
@Test
@@ -287,22 +294,23 @@ class ModesDialogViewModelTest : SysuiTestCase() {
)
runCurrent()
- assertThat(tiles).hasSize(3)
+ // Manual DND is included by default
+ assertThat(tiles).hasSize(4)
repository.removeMode("A")
runCurrent()
- assertThat(tiles).hasSize(2)
+ assertThat(tiles).hasSize(3)
repository.removeMode("B")
runCurrent()
- assertThat(tiles).hasSize(1)
+ assertThat(tiles).hasSize(2)
repository.removeMode("C")
runCurrent()
- assertThat(tiles).hasSize(0)
+ assertThat(tiles).hasSize(1)
}
@Test
@@ -353,14 +361,15 @@ class ModesDialogViewModelTest : SysuiTestCase() {
)
runCurrent()
- assertThat(tiles!!).hasSize(7)
- assertThat(tiles!![0].subtext).isEqualTo("When the going gets tough")
- assertThat(tiles!![1].subtext).isEqualTo("On • When in Rome")
- assertThat(tiles!![2].subtext).isEqualTo("Not set")
- assertThat(tiles!![3].subtext).isEqualTo("Off")
- assertThat(tiles!![4].subtext).isEqualTo("On")
- assertThat(tiles!![5].subtext).isEqualTo("Not set")
- assertThat(tiles!![6].subtext).isEqualTo(timeScheduleMode.triggerDescription)
+ // Manual DND is included by default
+ assertThat(tiles!!).hasSize(8)
+ assertThat(tiles!![1].subtext).isEqualTo("When the going gets tough")
+ assertThat(tiles!![2].subtext).isEqualTo("On • When in Rome")
+ assertThat(tiles!![3].subtext).isEqualTo("Not set")
+ assertThat(tiles!![4].subtext).isEqualTo("Off")
+ assertThat(tiles!![5].subtext).isEqualTo("On")
+ assertThat(tiles!![6].subtext).isEqualTo("Not set")
+ assertThat(tiles!![7].subtext).isEqualTo(timeScheduleMode.triggerDescription)
}
@Test
@@ -411,32 +420,33 @@ class ModesDialogViewModelTest : SysuiTestCase() {
)
runCurrent()
- assertThat(tiles!!).hasSize(7)
- with(tiles?.elementAt(0)!!) {
+ // Manual DND is included by default
+ assertThat(tiles!!).hasSize(8)
+ with(tiles?.elementAt(1)!!) {
assertThat(this.stateDescription).isEqualTo("Off")
assertThat(this.subtextDescription).isEqualTo("When the going gets tough")
}
- with(tiles?.elementAt(1)!!) {
+ with(tiles?.elementAt(2)!!) {
assertThat(this.stateDescription).isEqualTo("On")
assertThat(this.subtextDescription).isEqualTo("When in Rome")
}
- with(tiles?.elementAt(2)!!) {
+ with(tiles?.elementAt(3)!!) {
assertThat(this.stateDescription).isEqualTo("Off")
assertThat(this.subtextDescription).isEqualTo("Not set")
}
- with(tiles?.elementAt(3)!!) {
+ with(tiles?.elementAt(4)!!) {
assertThat(this.stateDescription).isEqualTo("Off")
assertThat(this.subtextDescription).isEmpty()
}
- with(tiles?.elementAt(4)!!) {
+ with(tiles?.elementAt(5)!!) {
assertThat(this.stateDescription).isEqualTo("On")
assertThat(this.subtextDescription).isEmpty()
}
- with(tiles?.elementAt(5)!!) {
+ with(tiles?.elementAt(6)!!) {
assertThat(this.stateDescription).isEqualTo("Off")
assertThat(this.subtextDescription).isEqualTo("Not set")
}
- with(tiles?.elementAt(6)!!) {
+ with(tiles?.elementAt(7)!!) {
assertThat(this.stateDescription).isEqualTo("Off")
assertThat(this.subtextDescription)
.isEqualTo(
@@ -456,31 +466,30 @@ class ModesDialogViewModelTest : SysuiTestCase() {
val tiles by collectLastValue(underTest.tiles)
val modeId = "id"
- repository.addModes(
- listOf(
- TestModeBuilder()
- .setId(modeId)
- .setName("Test")
- .setManualInvocationAllowed(true)
- .build()
- )
+ repository.addMode(
+ TestModeBuilder()
+ .setId(modeId)
+ .setName("Test")
+ .setManualInvocationAllowed(true)
+ .build()
)
runCurrent()
- assertThat(tiles).hasSize(1)
- assertThat(tiles?.elementAt(0)?.enabled).isFalse()
+ // Manual DND is included by default
+ assertThat(tiles).hasSize(2)
+ assertThat(tiles?.elementAt(1)?.enabled).isFalse()
// Trigger onClick
- tiles?.first()?.onClick?.let { it() }
+ tiles?.elementAt(1)?.onClick?.let { it() }
runCurrent()
- assertThat(tiles?.first()?.enabled).isTrue()
+ assertThat(tiles?.elementAt(1)?.enabled).isTrue()
// Trigger onClick
- tiles?.first()?.onClick?.let { it() }
+ tiles?.elementAt(1)?.onClick?.let { it() }
runCurrent()
- assertThat(tiles?.first()?.enabled).isFalse()
+ assertThat(tiles?.elementAt(1)?.enabled).isFalse()
}
@Test
@@ -489,25 +498,24 @@ class ModesDialogViewModelTest : SysuiTestCase() {
val job = Job()
val tiles by collectLastValue(underTest.tiles, context = job)
- repository.addModes(
- listOf(
- TestModeBuilder()
- .setName("Active without manual")
- .setActive(true)
- .setManualInvocationAllowed(false)
- .build()
- )
+ repository.addMode(
+ TestModeBuilder()
+ .setName("Active without manual")
+ .setActive(true)
+ .setManualInvocationAllowed(false)
+ .build()
)
runCurrent()
- assertThat(tiles).hasSize(1)
+ // Manual DND is included by default
+ assertThat(tiles).hasSize(2)
// Click tile to toggle it off
- tiles?.elementAt(0)!!.onClick()
+ tiles?.elementAt(1)!!.onClick()
runCurrent()
- assertThat(tiles).hasSize(1)
- with(tiles?.elementAt(0)!!) {
+ assertThat(tiles).hasSize(2)
+ with(tiles?.elementAt(1)!!) {
assertThat(this.text).isEqualTo("Active without manual")
assertThat(this.subtext).isEqualTo("Manage in settings")
assertThat(this.enabled).isEqualTo(false)
@@ -518,7 +526,7 @@ class ModesDialogViewModelTest : SysuiTestCase() {
}
// Check that nothing happened
- with(tiles?.elementAt(0)!!) {
+ with(tiles?.elementAt(1)!!) {
assertThat(this.text).isEqualTo("Active without manual")
assertThat(this.subtext).isEqualTo("Manage in settings")
assertThat(this.enabled).isEqualTo(false)
@@ -530,19 +538,18 @@ class ModesDialogViewModelTest : SysuiTestCase() {
testScope.runTest {
val tiles by collectLastValue(underTest.tiles)
- repository.addModes(
- listOf(
- TestModeBuilder()
- .setId("ID")
- .setName("Disabled by other")
- .setEnabled(false, /* byUser= */ false)
- .build()
- )
+ repository.addMode(
+ TestModeBuilder()
+ .setId("ID")
+ .setName("Disabled by other")
+ .setEnabled(false, /* byUser= */ false)
+ .build()
)
runCurrent()
- assertThat(tiles).hasSize(1)
- with(tiles?.elementAt(0)!!) {
+ // Manual DND is included by default
+ assertThat(tiles).hasSize(2)
+ with(tiles?.elementAt(1)!!) {
assertThat(this.text).isEqualTo("Disabled by other")
assertThat(this.subtext).isEqualTo("Not set")
assertThat(this.enabled).isEqualTo(false)
@@ -561,7 +568,7 @@ class ModesDialogViewModelTest : SysuiTestCase() {
.isEqualTo("ID")
// Check that nothing happened to the tile
- with(tiles?.elementAt(0)!!) {
+ with(tiles?.elementAt(1)!!) {
assertThat(this.text).isEqualTo("Disabled by other")
assertThat(this.subtext).isEqualTo("Not set")
assertThat(this.enabled).isEqualTo(false)
@@ -593,10 +600,11 @@ class ModesDialogViewModelTest : SysuiTestCase() {
)
runCurrent()
- assertThat(tiles).hasSize(2)
+ // Manual DND is included by default
+ assertThat(tiles).hasSize(3)
// Trigger onLongClick for A
- tiles?.first()?.onLongClick?.let { it() }
+ tiles?.elementAt(1)?.onLongClick?.let { it() }
runCurrent()
// Check that it launched the correct intent
@@ -625,9 +633,9 @@ class ModesDialogViewModelTest : SysuiTestCase() {
testScope.runTest {
val tiles by collectLastValue(underTest.tiles)
+ repository.activateMode(MANUAL_DND)
repository.addModes(
listOf(
- TestModeBuilder.MANUAL_DND_ACTIVE,
TestModeBuilder()
.setId("id1")
.setName("Inactive Mode One")
@@ -644,6 +652,7 @@ class ModesDialogViewModelTest : SysuiTestCase() {
)
runCurrent()
+ // Manual DND is included by default
assertThat(tiles).hasSize(3)
// Trigger onClick for each tile in sequence
@@ -672,19 +681,17 @@ class ModesDialogViewModelTest : SysuiTestCase() {
testScope.runTest {
val tiles by collectLastValue(underTest.tiles)
- repository.addModes(
- listOf(
- TestModeBuilder.MANUAL_DND_ACTIVE,
- TestModeBuilder()
- .setId("id1")
- .setName("Inactive Mode One")
- .setActive(false)
- .setManualInvocationAllowed(true)
- .build(),
- )
+ repository.addMode(
+ TestModeBuilder()
+ .setId("id1")
+ .setName("Inactive Mode One")
+ .setActive(false)
+ .setManualInvocationAllowed(true)
+ .build()
)
runCurrent()
+ // Manual DND is included by default
assertThat(tiles).hasSize(2)
val modeCaptor = argumentCaptor<ZenMode>()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/window/StatusBarWindowControllerImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/window/StatusBarWindowControllerImplTest.kt
index 61c719319de8..824955de83e2 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/window/StatusBarWindowControllerImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/window/StatusBarWindowControllerImplTest.kt
@@ -28,6 +28,7 @@ import com.android.systemui.statusbar.core.StatusBarRootModernization
import com.android.systemui.statusbar.policy.statusBarConfigurationController
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
+import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.any
@@ -41,13 +42,18 @@ class StatusBarWindowControllerImplTest : SysuiTestCase() {
private val kosmos =
testKosmos().also { it.statusBarWindowViewInflater = it.fakeStatusBarWindowViewInflater }
- private val underTest = kosmos.statusBarWindowControllerImpl
+ private lateinit var underTest: StatusBarWindowControllerImpl
private val fakeExecutor = kosmos.fakeExecutor
private val fakeWindowManager = kosmos.fakeWindowManager
private val mockFragmentService = kosmos.fragmentService
private val fakeStatusBarWindowViewInflater = kosmos.fakeStatusBarWindowViewInflater
private val statusBarConfigurationController = kosmos.statusBarConfigurationController
+ @Before
+ fun setUp() {
+ underTest = kosmos.statusBarWindowControllerImpl
+ }
+
@Test
@EnableFlags(StatusBarConnectedDisplays.FLAG_NAME)
fun attach_connectedDisplaysFlagEnabled_setsConfigControllerOnWindowView() {
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockConfig.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockConfig.kt
index d84d89087349..812a964bf8bc 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockConfig.kt
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockConfig.kt
@@ -30,10 +30,6 @@ data class ClockConfig(
/** Transition to AOD should move smartspace like large clock instead of small clock */
val useAlternateSmartspaceAODTransition: Boolean = false,
- /** Deprecated version of isReactiveToTone; moved to ClockPickerConfig */
- @Deprecated("TODO(b/352049256): Remove in favor of ClockPickerConfig.isReactiveToTone")
- val isReactiveToTone: Boolean = true,
-
/** True if the clock is large frame clock, which will use weather in compose. */
val useCustomClockScene: Boolean = false,
)
diff --git a/packages/SystemUI/plugin_core/processor/src/com/android/systemui/plugins/processor/ProtectedPluginProcessor.kt b/packages/SystemUI/plugin_core/processor/src/com/android/systemui/plugins/processor/ProtectedPluginProcessor.kt
index d93f7d3093b8..81156d9698d8 100644
--- a/packages/SystemUI/plugin_core/processor/src/com/android/systemui/plugins/processor/ProtectedPluginProcessor.kt
+++ b/packages/SystemUI/plugin_core/processor/src/com/android/systemui/plugins/processor/ProtectedPluginProcessor.kt
@@ -24,6 +24,7 @@ import javax.annotation.processing.RoundEnvironment
import javax.lang.model.element.Element
import javax.lang.model.element.ElementKind
import javax.lang.model.element.ExecutableElement
+import javax.lang.model.element.Modifier
import javax.lang.model.element.PackageElement
import javax.lang.model.element.TypeElement
import javax.lang.model.type.TypeKind
@@ -183,11 +184,17 @@ class ProtectedPluginProcessor : AbstractProcessor() {
// Method implementations
for (method in methods) {
val methodName = method.simpleName
+ if (methods.any { methodName.startsWith("${it.simpleName}\$") }) {
+ continue
+ }
val returnTypeName = method.returnType.toString()
val callArgs = StringBuilder()
var isFirst = true
+ val isStatic = method.modifiers.contains(Modifier.STATIC)
- line("@Override")
+ if (!isStatic) {
+ line("@Override")
+ }
parenBlock("public $returnTypeName $methodName") {
// While copying the method signature for the proxy type, we also
// accumulate arguments for the nested callsite.
@@ -202,7 +209,8 @@ class ProtectedPluginProcessor : AbstractProcessor() {
}
val isVoid = method.returnType.kind == TypeKind.VOID
- val nestedCall = "mInstance.$methodName($callArgs)"
+ val methodContainer = if (isStatic) sourceName else "mInstance"
+ val nestedCall = "$methodContainer.$methodName($callArgs)"
val callStatement =
when {
isVoid -> "$nestedCall;"
diff --git a/packages/SystemUI/res/drawable/ic_media_connecting_status_container.xml b/packages/SystemUI/res/drawable/ic_media_connecting_status_container.xml
new file mode 100644
index 000000000000..f8c0fa04cd39
--- /dev/null
+++ b/packages/SystemUI/res/drawable/ic_media_connecting_status_container.xml
@@ -0,0 +1,199 @@
+<?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.
+ -->
+<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:aapt="http://schemas.android.com/aapt">
+ <target android:name="_R_G_L_1_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="83"
+ android:propertyName="scaleX"
+ android:startOffset="1000"
+ android:valueFrom="0.45561"
+ android:valueTo="0.69699"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.3,0 0.8,0.15 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ <objectAnimator
+ android:duration="83"
+ android:propertyName="scaleY"
+ android:startOffset="1000"
+ android:valueFrom="0.6288400000000001"
+ android:valueTo="0.81618"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.3,0 0.8,0.15 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ <objectAnimator
+ android:duration="417"
+ android:propertyName="scaleX"
+ android:startOffset="1083"
+ android:valueFrom="0.69699"
+ android:valueTo="1.05905"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.05,0.7 0.1,1 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ <objectAnimator
+ android:duration="417"
+ android:propertyName="scaleY"
+ android:startOffset="1083"
+ android:valueFrom="0.81618"
+ android:valueTo="1.0972"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.05,0.7 0.1,1 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_0_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="500"
+ android:propertyName="rotation"
+ android:startOffset="0"
+ android:valueFrom="90"
+ android:valueTo="135"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ <objectAnimator
+ android:duration="500"
+ android:propertyName="rotation"
+ android:startOffset="500"
+ android:valueFrom="135"
+ android:valueTo="180"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_0_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="83"
+ android:propertyName="scaleX"
+ android:startOffset="1000"
+ android:valueFrom="0.0434"
+ android:valueTo="0.05063"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.3,0 0.8,0.15 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ <objectAnimator
+ android:duration="83"
+ android:propertyName="scaleY"
+ android:startOffset="1000"
+ android:valueFrom="0.0434"
+ android:valueTo="0.042350000000000006"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.3,0 0.8,0.15 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ <objectAnimator
+ android:duration="417"
+ android:propertyName="scaleX"
+ android:startOffset="1083"
+ android:valueFrom="0.05063"
+ android:valueTo="0.06146"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.05,0.7 0.1,1 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ <objectAnimator
+ android:duration="417"
+ android:propertyName="scaleY"
+ android:startOffset="1083"
+ android:valueFrom="0.042350000000000006"
+ android:valueTo="0.040780000000000004"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.05,0.7 0.1,1 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="time_group">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="1017"
+ android:propertyName="translateX"
+ android:startOffset="0"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <aapt:attr name="android:drawable">
+ <vector
+ android:width="88dp"
+ android:height="56dp"
+ android:viewportHeight="56"
+ android:viewportWidth="88">
+ <group android:name="_R_G">
+ <group
+ android:name="_R_G_L_1_G"
+ android:pivotX="0.493"
+ android:pivotY="0.124"
+ android:scaleX="1.05905"
+ android:scaleY="1.0972"
+ android:translateX="43.528999999999996"
+ android:translateY="27.898">
+ <path
+ android:name="_R_G_L_1_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#3d90ff"
+ android:fillType="nonZero"
+ android:pathData=" M34.47 0.63 C34.47,0.63 34.42,0.64 34.42,0.64 C33.93,12.88 24.69,21.84 13.06,21.97 C13.06,21.97 -12.54,21.97 -12.54,21.97 C-23.11,21.84 -33.38,13.11 -33.52,-0.27 C-33.52,-0.27 -33.52,-0.05 -33.52,-0.05 C-33.5,-13.21 -21.73,-21.76 -12.9,-21.76 C-12.9,-21.76 14.59,-21.76 14.59,-21.76 C24.81,-21.88 34.49,-10.58 34.47,0.63c " />
+ </group>
+ <group
+ android:name="_R_G_L_0_G"
+ android:rotation="0"
+ android:scaleX="0.06146"
+ android:scaleY="0.040780000000000004"
+ android:translateX="44"
+ android:translateY="28">
+ <path
+ android:name="_R_G_L_0_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#3d90ff"
+ android:fillType="nonZero"
+ android:pathData=" M-0.65 -437.37 C-0.65,-437.37 8.33,-437.66 8.33,-437.66 C8.33,-437.66 17.31,-437.95 17.31,-437.95 C17.31,-437.95 26.25,-438.78 26.25,-438.78 C26.25,-438.78 35.16,-439.95 35.16,-439.95 C35.16,-439.95 44.07,-441.11 44.07,-441.11 C44.07,-441.11 52.85,-443 52.85,-443 C52.85,-443 61.6,-445.03 61.6,-445.03 C61.6,-445.03 70.35,-447.09 70.35,-447.09 C70.35,-447.09 78.91,-449.83 78.91,-449.83 C78.91,-449.83 87.43,-452.67 87.43,-452.67 C87.43,-452.67 95.79,-455.97 95.79,-455.97 C95.79,-455.97 104.11,-459.35 104.11,-459.35 C104.11,-459.35 112.36,-462.93 112.36,-462.93 C112.36,-462.93 120.6,-466.51 120.6,-466.51 C120.6,-466.51 128.84,-470.09 128.84,-470.09 C128.84,-470.09 137.09,-473.67 137.09,-473.67 C137.09,-473.67 145.49,-476.84 145.49,-476.84 C145.49,-476.84 153.9,-480.01 153.9,-480.01 C153.9,-480.01 162.31,-483.18 162.31,-483.18 C162.31,-483.18 170.98,-485.54 170.98,-485.54 C170.98,-485.54 179.66,-487.85 179.66,-487.85 C179.66,-487.85 188.35,-490.15 188.35,-490.15 C188.35,-490.15 197.22,-491.58 197.22,-491.58 C197.22,-491.58 206.09,-493.01 206.09,-493.01 C206.09,-493.01 214.98,-494.28 214.98,-494.28 C214.98,-494.28 223.95,-494.81 223.95,-494.81 C223.95,-494.81 232.93,-495.33 232.93,-495.33 C232.93,-495.33 241.9,-495.5 241.9,-495.5 C241.9,-495.5 250.88,-495.13 250.88,-495.13 C250.88,-495.13 259.86,-494.75 259.86,-494.75 C259.86,-494.75 268.78,-493.78 268.78,-493.78 C268.78,-493.78 277.68,-492.52 277.68,-492.52 C277.68,-492.52 286.57,-491.26 286.57,-491.26 C286.57,-491.26 295.31,-489.16 295.31,-489.16 C295.31,-489.16 304.04,-487.04 304.04,-487.04 C304.04,-487.04 312.7,-484.65 312.7,-484.65 C312.7,-484.65 321.19,-481.72 321.19,-481.72 C321.19,-481.72 329.68,-478.78 329.68,-478.78 C329.68,-478.78 337.96,-475.31 337.96,-475.31 C337.96,-475.31 346.14,-471.59 346.14,-471.59 C346.14,-471.59 354.3,-467.82 354.3,-467.82 C354.3,-467.82 362.11,-463.38 362.11,-463.38 C362.11,-463.38 369.92,-458.93 369.92,-458.93 C369.92,-458.93 377.53,-454.17 377.53,-454.17 C377.53,-454.17 384.91,-449.04 384.91,-449.04 C384.91,-449.04 392.29,-443.91 392.29,-443.91 C392.29,-443.91 399.26,-438.24 399.26,-438.24 C399.26,-438.24 406.15,-432.48 406.15,-432.48 C406.15,-432.48 412.92,-426.57 412.92,-426.57 C412.92,-426.57 419.27,-420.22 419.27,-420.22 C419.27,-420.22 425.62,-413.87 425.62,-413.87 C425.62,-413.87 431.61,-407.18 431.61,-407.18 C431.61,-407.18 437.38,-400.29 437.38,-400.29 C437.38,-400.29 443.14,-393.39 443.14,-393.39 C443.14,-393.39 448.27,-386.01 448.27,-386.01 C448.27,-386.01 453.4,-378.64 453.4,-378.64 C453.4,-378.64 458.26,-371.09 458.26,-371.09 C458.26,-371.09 462.71,-363.28 462.71,-363.28 C462.71,-363.28 467.16,-355.47 467.16,-355.47 C467.16,-355.47 471.03,-347.37 471.03,-347.37 C471.03,-347.37 474.75,-339.19 474.75,-339.19 C474.75,-339.19 478.34,-330.95 478.34,-330.95 C478.34,-330.95 481.28,-322.46 481.28,-322.46 C481.28,-322.46 484.21,-313.97 484.21,-313.97 C484.21,-313.97 486.72,-305.35 486.72,-305.35 C486.72,-305.35 488.84,-296.62 488.84,-296.62 C488.84,-296.62 490.96,-287.88 490.96,-287.88 C490.96,-287.88 492.33,-279.01 492.33,-279.01 C492.33,-279.01 493.59,-270.11 493.59,-270.11 C493.59,-270.11 494.69,-261.2 494.69,-261.2 C494.69,-261.2 495.07,-252.22 495.07,-252.22 C495.07,-252.22 495.44,-243.24 495.44,-243.24 C495.44,-243.24 495.41,-234.27 495.41,-234.27 C495.41,-234.27 494.88,-225.29 494.88,-225.29 C494.88,-225.29 494.35,-216.32 494.35,-216.32 C494.35,-216.32 493.22,-207.42 493.22,-207.42 C493.22,-207.42 491.79,-198.55 491.79,-198.55 C491.79,-198.55 490.36,-189.68 490.36,-189.68 C490.36,-189.68 488.19,-180.96 488.19,-180.96 C488.19,-180.96 485.88,-172.28 485.88,-172.28 C485.88,-172.28 483.56,-163.6 483.56,-163.6 C483.56,-163.6 480.48,-155.16 480.48,-155.16 C480.48,-155.16 477.31,-146.75 477.31,-146.75 C477.31,-146.75 474.14,-138.34 474.14,-138.34 C474.14,-138.34 470.62,-130.07 470.62,-130.07 C470.62,-130.07 467.04,-121.83 467.04,-121.83 C467.04,-121.83 463.46,-113.59 463.46,-113.59 C463.46,-113.59 459.88,-105.35 459.88,-105.35 C459.88,-105.35 456.54,-97.01 456.54,-97.01 C456.54,-97.01 453.37,-88.6 453.37,-88.6 C453.37,-88.6 450.21,-80.19 450.21,-80.19 C450.21,-80.19 447.68,-71.57 447.68,-71.57 C447.68,-71.57 445.36,-62.89 445.36,-62.89 C445.36,-62.89 443.04,-54.21 443.04,-54.21 C443.04,-54.21 441.54,-45.35 441.54,-45.35 C441.54,-45.35 440.09,-36.48 440.09,-36.48 C440.09,-36.48 438.78,-27.6 438.78,-27.6 C438.78,-27.6 438.19,-18.63 438.19,-18.63 C438.19,-18.63 437.61,-9.66 437.61,-9.66 C437.61,-9.66 437.36,-0.69 437.36,-0.69 C437.36,-0.69 437.65,8.29 437.65,8.29 C437.65,8.29 437.95,17.27 437.95,17.27 C437.95,17.27 438.77,26.21 438.77,26.21 C438.77,26.21 439.94,35.12 439.94,35.12 C439.94,35.12 441.11,44.03 441.11,44.03 C441.11,44.03 442.99,52.81 442.99,52.81 C442.99,52.81 445.02,61.57 445.02,61.57 C445.02,61.57 447.07,70.31 447.07,70.31 C447.07,70.31 449.82,78.87 449.82,78.87 C449.82,78.87 452.65,87.4 452.65,87.4 C452.65,87.4 455.96,95.75 455.96,95.75 C455.96,95.75 459.33,104.08 459.33,104.08 C459.33,104.08 462.91,112.32 462.91,112.32 C462.91,112.32 466.49,120.57 466.49,120.57 C466.49,120.57 470.07,128.81 470.07,128.81 C470.07,128.81 473.65,137.05 473.65,137.05 C473.65,137.05 476.82,145.46 476.82,145.46 C476.82,145.46 479.99,153.87 479.99,153.87 C479.99,153.87 483.17,162.28 483.17,162.28 C483.17,162.28 485.52,170.94 485.52,170.94 C485.52,170.94 487.84,179.63 487.84,179.63 C487.84,179.63 490.14,188.31 490.14,188.31 C490.14,188.31 491.57,197.18 491.57,197.18 C491.57,197.18 493,206.06 493,206.06 C493,206.06 494.27,214.95 494.27,214.95 C494.27,214.95 494.8,223.92 494.8,223.92 C494.8,223.92 495.33,232.89 495.33,232.89 C495.33,232.89 495.5,241.86 495.5,241.86 C495.5,241.86 495.12,250.84 495.12,250.84 C495.12,250.84 494.75,259.82 494.75,259.82 C494.75,259.82 493.78,268.74 493.78,268.74 C493.78,268.74 492.52,277.64 492.52,277.64 C492.52,277.64 491.27,286.54 491.27,286.54 C491.27,286.54 489.16,295.27 489.16,295.27 C489.16,295.27 487.05,304.01 487.05,304.01 C487.05,304.01 484.66,312.66 484.66,312.66 C484.66,312.66 481.73,321.16 481.73,321.16 C481.73,321.16 478.79,329.65 478.79,329.65 C478.79,329.65 475.32,337.93 475.32,337.93 C475.32,337.93 471.6,346.11 471.6,346.11 C471.6,346.11 467.84,354.27 467.84,354.27 C467.84,354.27 463.39,362.08 463.39,362.08 C463.39,362.08 458.94,369.89 458.94,369.89 C458.94,369.89 454.19,377.5 454.19,377.5 C454.19,377.5 449.06,384.88 449.06,384.88 C449.06,384.88 443.93,392.26 443.93,392.26 C443.93,392.26 438.26,399.23 438.26,399.23 C438.26,399.23 432.5,406.12 432.5,406.12 C432.5,406.12 426.6,412.89 426.6,412.89 C426.6,412.89 420.24,419.24 420.24,419.24 C420.24,419.24 413.89,425.6 413.89,425.6 C413.89,425.6 407.2,431.59 407.2,431.59 C407.2,431.59 400.31,437.36 400.31,437.36 C400.31,437.36 393.42,443.12 393.42,443.12 C393.42,443.12 386.04,448.25 386.04,448.25 C386.04,448.25 378.66,453.38 378.66,453.38 C378.66,453.38 371.11,458.24 371.11,458.24 C371.11,458.24 363.31,462.69 363.31,462.69 C363.31,462.69 355.5,467.14 355.5,467.14 C355.5,467.14 347.4,471.02 347.4,471.02 C347.4,471.02 339.22,474.73 339.22,474.73 C339.22,474.73 330.99,478.33 330.99,478.33 C330.99,478.33 322.49,481.27 322.49,481.27 C322.49,481.27 314,484.2 314,484.2 C314,484.2 305.38,486.71 305.38,486.71 C305.38,486.71 296.65,488.83 296.65,488.83 C296.65,488.83 287.91,490.95 287.91,490.95 C287.91,490.95 279.04,492.33 279.04,492.33 C279.04,492.33 270.14,493.59 270.14,493.59 C270.14,493.59 261.23,494.69 261.23,494.69 C261.23,494.69 252.25,495.07 252.25,495.07 C252.25,495.07 243.28,495.44 243.28,495.44 C243.28,495.44 234.3,495.41 234.3,495.41 C234.3,495.41 225.33,494.88 225.33,494.88 C225.33,494.88 216.36,494.35 216.36,494.35 C216.36,494.35 207.45,493.23 207.45,493.23 C207.45,493.23 198.58,491.8 198.58,491.8 C198.58,491.8 189.71,490.37 189.71,490.37 C189.71,490.37 180.99,488.21 180.99,488.21 C180.99,488.21 172.31,485.89 172.31,485.89 C172.31,485.89 163.63,483.57 163.63,483.57 C163.63,483.57 155.19,480.5 155.19,480.5 C155.19,480.5 146.78,477.32 146.78,477.32 C146.78,477.32 138.37,474.15 138.37,474.15 C138.37,474.15 130.11,470.63 130.11,470.63 C130.11,470.63 121.86,467.06 121.86,467.06 C121.86,467.06 113.62,463.48 113.62,463.48 C113.62,463.48 105.38,459.9 105.38,459.9 C105.38,459.9 97.04,456.56 97.04,456.56 C97.04,456.56 88.63,453.39 88.63,453.39 C88.63,453.39 80.22,450.22 80.22,450.22 C80.22,450.22 71.6,447.7 71.6,447.7 C71.6,447.7 62.92,445.37 62.92,445.37 C62.92,445.37 54.24,443.05 54.24,443.05 C54.24,443.05 45.38,441.55 45.38,441.55 C45.38,441.55 36.52,440.1 36.52,440.1 C36.52,440.1 27.63,438.78 27.63,438.78 C27.63,438.78 18.66,438.2 18.66,438.2 C18.66,438.2 9.7,437.61 9.7,437.61 C9.7,437.61 0.72,437.36 0.72,437.36 C0.72,437.36 -8.26,437.65 -8.26,437.65 C-8.26,437.65 -17.24,437.95 -17.24,437.95 C-17.24,437.95 -26.18,438.77 -26.18,438.77 C-26.18,438.77 -35.09,439.94 -35.09,439.94 C-35.09,439.94 -44,441.1 -44,441.1 C-44,441.1 -52.78,442.98 -52.78,442.98 C-52.78,442.98 -61.53,445.02 -61.53,445.02 C-61.53,445.02 -70.28,447.07 -70.28,447.07 C-70.28,447.07 -78.84,449.81 -78.84,449.81 C-78.84,449.81 -87.37,452.64 -87.37,452.64 C-87.37,452.64 -95.72,455.95 -95.72,455.95 C-95.72,455.95 -104.05,459.32 -104.05,459.32 C-104.05,459.32 -112.29,462.9 -112.29,462.9 C-112.29,462.9 -120.53,466.48 -120.53,466.48 C-120.53,466.48 -128.78,470.06 -128.78,470.06 C-128.78,470.06 -137.02,473.63 -137.02,473.63 C-137.02,473.63 -145.43,476.81 -145.43,476.81 C-145.43,476.81 -153.84,479.98 -153.84,479.98 C-153.84,479.98 -162.24,483.15 -162.24,483.15 C-162.24,483.15 -170.91,485.52 -170.91,485.52 C-170.91,485.52 -179.59,487.83 -179.59,487.83 C-179.59,487.83 -188.28,490.13 -188.28,490.13 C-188.28,490.13 -197.15,491.56 -197.15,491.56 C-197.15,491.56 -206.02,492.99 -206.02,492.99 C-206.02,492.99 -214.91,494.27 -214.91,494.27 C-214.91,494.27 -223.88,494.8 -223.88,494.8 C-223.88,494.8 -232.85,495.33 -232.85,495.33 C-232.85,495.33 -241.83,495.5 -241.83,495.5 C-241.83,495.5 -250.81,495.13 -250.81,495.13 C-250.81,495.13 -259.79,494.75 -259.79,494.75 C-259.79,494.75 -268.71,493.79 -268.71,493.79 C-268.71,493.79 -277.61,492.53 -277.61,492.53 C-277.61,492.53 -286.51,491.27 -286.51,491.27 C-286.51,491.27 -295.24,489.17 -295.24,489.17 C-295.24,489.17 -303.98,487.06 -303.98,487.06 C-303.98,487.06 -312.63,484.67 -312.63,484.67 C-312.63,484.67 -321.12,481.74 -321.12,481.74 C-321.12,481.74 -329.62,478.8 -329.62,478.8 C-329.62,478.8 -337.9,475.33 -337.9,475.33 C-337.9,475.33 -346.08,471.62 -346.08,471.62 C-346.08,471.62 -354.24,467.85 -354.24,467.85 C-354.24,467.85 -362.05,463.41 -362.05,463.41 C-362.05,463.41 -369.86,458.96 -369.86,458.96 C-369.86,458.96 -377.47,454.21 -377.47,454.21 C-377.47,454.21 -384.85,449.08 -384.85,449.08 C-384.85,449.08 -392.23,443.95 -392.23,443.95 C-392.23,443.95 -399.2,438.29 -399.2,438.29 C-399.2,438.29 -406.09,432.52 -406.09,432.52 C-406.09,432.52 -412.86,426.62 -412.86,426.62 C-412.86,426.62 -419.22,420.27 -419.22,420.27 C-419.22,420.27 -425.57,413.91 -425.57,413.91 C-425.57,413.91 -431.57,407.23 -431.57,407.23 C-431.57,407.23 -437.33,400.34 -437.33,400.34 C-437.33,400.34 -443.1,393.44 -443.1,393.44 C-443.1,393.44 -448.23,386.07 -448.23,386.07 C-448.23,386.07 -453.36,378.69 -453.36,378.69 C-453.36,378.69 -458.23,371.15 -458.23,371.15 C-458.23,371.15 -462.67,363.33 -462.67,363.33 C-462.67,363.33 -467.12,355.53 -467.12,355.53 C-467.12,355.53 -471,347.43 -471,347.43 C-471,347.43 -474.72,339.25 -474.72,339.25 C-474.72,339.25 -478.32,331.02 -478.32,331.02 C-478.32,331.02 -481.25,322.52 -481.25,322.52 C-481.25,322.52 -484.19,314.03 -484.19,314.03 C-484.19,314.03 -486.71,305.42 -486.71,305.42 C-486.71,305.42 -488.82,296.68 -488.82,296.68 C-488.82,296.68 -490.94,287.95 -490.94,287.95 C-490.94,287.95 -492.32,279.07 -492.32,279.07 C-492.32,279.07 -493.58,270.18 -493.58,270.18 C-493.58,270.18 -494.69,261.27 -494.69,261.27 C-494.69,261.27 -495.07,252.29 -495.07,252.29 C-495.07,252.29 -495.44,243.31 -495.44,243.31 C-495.44,243.31 -495.42,234.33 -495.42,234.33 C-495.42,234.33 -494.89,225.36 -494.89,225.36 C-494.89,225.36 -494.36,216.39 -494.36,216.39 C-494.36,216.39 -493.23,207.49 -493.23,207.49 C-493.23,207.49 -491.8,198.61 -491.8,198.61 C-491.8,198.61 -490.37,189.74 -490.37,189.74 C-490.37,189.74 -488.22,181.02 -488.22,181.02 C-488.22,181.02 -485.9,172.34 -485.9,172.34 C-485.9,172.34 -483.58,163.66 -483.58,163.66 C-483.58,163.66 -480.51,155.22 -480.51,155.22 C-480.51,155.22 -477.34,146.81 -477.34,146.81 C-477.34,146.81 -474.17,138.41 -474.17,138.41 C-474.17,138.41 -470.65,130.14 -470.65,130.14 C-470.65,130.14 -467.07,121.9 -467.07,121.9 C-467.07,121.9 -463.49,113.65 -463.49,113.65 C-463.49,113.65 -459.91,105.41 -459.91,105.41 C-459.91,105.41 -456.57,97.07 -456.57,97.07 C-456.57,97.07 -453.4,88.66 -453.4,88.66 C-453.4,88.66 -450.23,80.25 -450.23,80.25 C-450.23,80.25 -447.7,71.64 -447.7,71.64 C-447.7,71.64 -445.38,62.96 -445.38,62.96 C-445.38,62.96 -443.06,54.28 -443.06,54.28 C-443.06,54.28 -441.56,45.42 -441.56,45.42 C-441.56,45.42 -440.1,36.55 -440.1,36.55 C-440.1,36.55 -438.78,27.67 -438.78,27.67 C-438.78,27.67 -438.2,18.7 -438.2,18.7 C-438.2,18.7 -437.62,9.73 -437.62,9.73 C-437.62,9.73 -437.36,0.76 -437.36,0.76 C-437.36,0.76 -437.66,-8.22 -437.66,-8.22 C-437.66,-8.22 -437.95,-17.2 -437.95,-17.2 C-437.95,-17.2 -438.77,-26.14 -438.77,-26.14 C-438.77,-26.14 -439.93,-35.05 -439.93,-35.05 C-439.93,-35.05 -441.1,-43.96 -441.1,-43.96 C-441.1,-43.96 -442.98,-52.75 -442.98,-52.75 C-442.98,-52.75 -445.01,-61.5 -445.01,-61.5 C-445.01,-61.5 -447.06,-70.25 -447.06,-70.25 C-447.06,-70.25 -449.8,-78.81 -449.8,-78.81 C-449.8,-78.81 -452.63,-87.33 -452.63,-87.33 C-452.63,-87.33 -455.94,-95.69 -455.94,-95.69 C-455.94,-95.69 -459.31,-104.02 -459.31,-104.02 C-459.31,-104.02 -462.89,-112.26 -462.89,-112.26 C-462.89,-112.26 -466.47,-120.5 -466.47,-120.5 C-466.47,-120.5 -470.05,-128.74 -470.05,-128.74 C-470.05,-128.74 -473.68,-137.12 -473.68,-137.12 C-473.68,-137.12 -476.85,-145.53 -476.85,-145.53 C-476.85,-145.53 -480.03,-153.94 -480.03,-153.94 C-480.03,-153.94 -483.2,-162.34 -483.2,-162.34 C-483.2,-162.34 -485.55,-171.02 -485.55,-171.02 C-485.55,-171.02 -487.86,-179.7 -487.86,-179.7 C-487.86,-179.7 -490.15,-188.39 -490.15,-188.39 C-490.15,-188.39 -491.58,-197.26 -491.58,-197.26 C-491.58,-197.26 -493.01,-206.13 -493.01,-206.13 C-493.01,-206.13 -494.28,-215.02 -494.28,-215.02 C-494.28,-215.02 -494.81,-223.99 -494.81,-223.99 C-494.81,-223.99 -495.33,-232.96 -495.33,-232.96 C-495.33,-232.96 -495.5,-241.94 -495.5,-241.94 C-495.5,-241.94 -495.12,-250.92 -495.12,-250.92 C-495.12,-250.92 -494.75,-259.9 -494.75,-259.9 C-494.75,-259.9 -493.78,-268.82 -493.78,-268.82 C-493.78,-268.82 -492.52,-277.72 -492.52,-277.72 C-492.52,-277.72 -491.26,-286.61 -491.26,-286.61 C-491.26,-286.61 -489.15,-295.35 -489.15,-295.35 C-489.15,-295.35 -487.03,-304.08 -487.03,-304.08 C-487.03,-304.08 -484.64,-312.73 -484.64,-312.73 C-484.64,-312.73 -481.7,-321.23 -481.7,-321.23 C-481.7,-321.23 -478.77,-329.72 -478.77,-329.72 C-478.77,-329.72 -475.29,-338 -475.29,-338 C-475.29,-338 -471.57,-346.18 -471.57,-346.18 C-471.57,-346.18 -467.8,-354.33 -467.8,-354.33 C-467.8,-354.33 -463.36,-362.14 -463.36,-362.14 C-463.36,-362.14 -458.91,-369.95 -458.91,-369.95 C-458.91,-369.95 -454.15,-377.56 -454.15,-377.56 C-454.15,-377.56 -449.02,-384.94 -449.02,-384.94 C-449.02,-384.94 -443.88,-392.32 -443.88,-392.32 C-443.88,-392.32 -438.22,-399.28 -438.22,-399.28 C-438.22,-399.28 -432.45,-406.18 -432.45,-406.18 C-432.45,-406.18 -426.55,-412.94 -426.55,-412.94 C-426.55,-412.94 -420.19,-419.3 -420.19,-419.3 C-420.19,-419.3 -413.84,-425.65 -413.84,-425.65 C-413.84,-425.65 -407.15,-431.64 -407.15,-431.64 C-407.15,-431.64 -400.26,-437.41 -400.26,-437.41 C-400.26,-437.41 -393.36,-443.16 -393.36,-443.16 C-393.36,-443.16 -385.98,-448.29 -385.98,-448.29 C-385.98,-448.29 -378.6,-453.43 -378.6,-453.43 C-378.6,-453.43 -371.05,-458.28 -371.05,-458.28 C-371.05,-458.28 -363.24,-462.73 -363.24,-462.73 C-363.24,-462.73 -355.43,-467.18 -355.43,-467.18 C-355.43,-467.18 -347.33,-471.05 -347.33,-471.05 C-347.33,-471.05 -339.15,-474.76 -339.15,-474.76 C-339.15,-474.76 -330.92,-478.35 -330.92,-478.35 C-330.92,-478.35 -322.42,-481.29 -322.42,-481.29 C-322.42,-481.29 -313.93,-484.23 -313.93,-484.23 C-313.93,-484.23 -305.31,-486.73 -305.31,-486.73 C-305.31,-486.73 -296.58,-488.85 -296.58,-488.85 C-296.58,-488.85 -287.85,-490.97 -287.85,-490.97 C-287.85,-490.97 -278.97,-492.34 -278.97,-492.34 C-278.97,-492.34 -270.07,-493.6 -270.07,-493.6 C-270.07,-493.6 -261.16,-494.7 -261.16,-494.7 C-261.16,-494.7 -252.18,-495.07 -252.18,-495.07 C-252.18,-495.07 -243.2,-495.44 -243.2,-495.44 C-243.2,-495.44 -234.23,-495.41 -234.23,-495.41 C-234.23,-495.41 -225.26,-494.88 -225.26,-494.88 C-225.26,-494.88 -216.29,-494.35 -216.29,-494.35 C-216.29,-494.35 -207.38,-493.22 -207.38,-493.22 C-207.38,-493.22 -198.51,-491.79 -198.51,-491.79 C-198.51,-491.79 -189.64,-490.36 -189.64,-490.36 C-189.64,-490.36 -180.92,-488.19 -180.92,-488.19 C-180.92,-488.19 -172.24,-485.87 -172.24,-485.87 C-172.24,-485.87 -163.56,-483.56 -163.56,-483.56 C-163.56,-483.56 -155.12,-480.47 -155.12,-480.47 C-155.12,-480.47 -146.72,-477.3 -146.72,-477.3 C-146.72,-477.3 -138.31,-474.13 -138.31,-474.13 C-138.31,-474.13 -130.04,-470.61 -130.04,-470.61 C-130.04,-470.61 -121.8,-467.03 -121.8,-467.03 C-121.8,-467.03 -113.55,-463.45 -113.55,-463.45 C-113.55,-463.45 -105.31,-459.87 -105.31,-459.87 C-105.31,-459.87 -96.97,-456.53 -96.97,-456.53 C-96.97,-456.53 -88.56,-453.37 -88.56,-453.37 C-88.56,-453.37 -80.15,-450.2 -80.15,-450.2 C-80.15,-450.2 -71.53,-447.68 -71.53,-447.68 C-71.53,-447.68 -62.85,-445.36 -62.85,-445.36 C-62.85,-445.36 -54.17,-443.04 -54.17,-443.04 C-54.17,-443.04 -45.31,-441.54 -45.31,-441.54 C-45.31,-441.54 -36.44,-440.09 -36.44,-440.09 C-36.44,-440.09 -27.56,-438.78 -27.56,-438.78 C-27.56,-438.78 -18.59,-438.19 -18.59,-438.19 C-18.59,-438.19 -9.62,-437.61 -9.62,-437.61 C-9.62,-437.61 -0.65,-437.37 -0.65,-437.37c " />
+ </group>
+ </group>
+ <group android:name="time_group" />
+ </vector>
+ </aapt:attr>
+</animated-vector> \ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/ic_media_pause_button.xml b/packages/SystemUI/res/drawable/ic_media_pause_button.xml
new file mode 100644
index 000000000000..6ae89f91c5ee
--- /dev/null
+++ b/packages/SystemUI/res/drawable/ic_media_pause_button.xml
@@ -0,0 +1,135 @@
+<?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.
+ -->
+<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:aapt="http://schemas.android.com/aapt">
+ <target android:name="_R_G_L_1_G_D_0_P_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="333"
+ android:propertyName="pathData"
+ android:startOffset="0"
+ android:valueFrom="M-5.06 -18 C-5.06,-18 -5.06,-1.24 -5.06,-1.24 C-5.06,-1.24 -5.06,17.7 -5.06,17.7 C-5.06,19.36 -6.41,20.7 -8.06,20.7 C-8.06,20.7 -16,20.7 -16,20.7 C-17.66,20.7 -19,19.36 -19,17.7 C-19,17.7 -19,-18 -19,-18 C-19,-19.66 -17.66,-21 -16,-21 C-16,-21 -8.06,-21 -8.06,-21 C-6.41,-21 -5.06,-19.66 -5.06,-18c "
+ android:valueTo="M-4.69 -16.69 C-4.69,-16.69 20.25,-1.25 20.31,0.25 C20.38,1.75 -4.88,16.89 -4.88,16.89 C-6.94,18.25 -8.56,19.4 -9.75,19.58 C-10.94,19.75 -12.19,18.94 -12.12,16.14 C-12.09,14.76 -12.12,15.92 -12.12,14.26 C-12.12,14.26 -11.94,-16.44 -11.94,-16.44 C-11.94,-18.09 -12.09,-19.69 -10.44,-19.69 C-10.44,-19.69 -9.5,-19.56 -9.5,-19.56 C-8.62,-19.12 -6.19,-17.44 -4.69,-16.69c "
+ android:valueType="pathType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.449,0 0,1 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_0_G_D_0_P_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="333"
+ android:propertyName="pathData"
+ android:startOffset="0"
+ android:valueFrom="M-5.06 -18 C-5.06,-18 -5.06,-0.75 -5.06,-0.75 C-5.06,-0.75 -5.06,17.7 -5.06,17.7 C-5.06,19.36 -6.41,20.7 -8.06,20.7 C-8.06,20.7 -16,20.7 -16,20.7 C-17.66,20.7 -19,19.36 -19,17.7 C-19,17.7 -19,-18 -19,-18 C-19,-19.66 -17.66,-21 -16,-21 C-16,-21 -8.06,-21 -8.06,-21 C-6.41,-21 -5.06,-19.66 -5.06,-18c "
+ android:valueTo="M-4.69 -16.69 C-4.69,-16.69 20.25,-1.25 20.31,0.25 C20.38,1.75 -4.88,16.89 -4.88,16.89 C-6.94,18.25 -8.56,19.4 -9.75,19.58 C-10.94,19.75 -12.19,18.94 -12.12,16.14 C-12.09,14.76 -12.12,15.92 -12.12,14.26 C-12.12,14.26 -11.94,-16.44 -11.94,-16.44 C-11.94,-18.09 -12.09,-19.69 -10.44,-19.69 C-10.44,-19.69 -9.5,-19.56 -9.5,-19.56 C-8.62,-19.12 -6.19,-17.44 -4.69,-16.69c "
+ android:valueType="pathType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.449,0 0,1 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_0_G_T_1">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="56"
+ android:propertyName="translateX"
+ android:startOffset="0"
+ android:valueFrom="15.485"
+ android:valueTo="12.321"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.3,0 0.8,0.15 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ <objectAnimator
+ android:duration="278"
+ android:propertyName="translateX"
+ android:startOffset="56"
+ android:valueFrom="12.321"
+ android:valueTo="7.576"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.05,0.7 0.1,1 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="time_group">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="517"
+ android:propertyName="translateX"
+ android:startOffset="0"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <aapt:attr name="android:drawable">
+ <vector
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportHeight="24"
+ android:viewportWidth="24">
+ <group android:name="_R_G">
+ <group
+ android:name="_R_G_L_1_G"
+ android:pivotX="-12.031"
+ android:scaleX="0.33299999999999996"
+ android:scaleY="0.33299999999999996"
+ android:translateX="19.524"
+ android:translateY="12.084">
+ <path
+ android:name="_R_G_L_1_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#ffffff"
+ android:fillType="nonZero"
+ android:pathData=" M-5.06 -18 C-5.06,-18 -5.06,-1.24 -5.06,-1.24 C-5.06,-1.24 -5.06,17.7 -5.06,17.7 C-5.06,19.36 -6.41,20.7 -8.06,20.7 C-8.06,20.7 -16,20.7 -16,20.7 C-17.66,20.7 -19,19.36 -19,17.7 C-19,17.7 -19,-18 -19,-18 C-19,-19.66 -17.66,-21 -16,-21 C-16,-21 -8.06,-21 -8.06,-21 C-6.41,-21 -5.06,-19.66 -5.06,-18c " />
+ </group>
+ <group
+ android:name="_R_G_L_0_G_T_1"
+ android:scaleX="0.33299999999999996"
+ android:scaleY="0.33299999999999996"
+ android:translateX="15.485"
+ android:translateY="12.084">
+ <group
+ android:name="_R_G_L_0_G"
+ android:translateX="12.031">
+ <path
+ android:name="_R_G_L_0_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#ffffff"
+ android:fillType="nonZero"
+ android:pathData=" M-5.06 -18 C-5.06,-18 -5.06,-0.75 -5.06,-0.75 C-5.06,-0.75 -5.06,17.7 -5.06,17.7 C-5.06,19.36 -6.41,20.7 -8.06,20.7 C-8.06,20.7 -16,20.7 -16,20.7 C-17.66,20.7 -19,19.36 -19,17.7 C-19,17.7 -19,-18 -19,-18 C-19,-19.66 -17.66,-21 -16,-21 C-16,-21 -8.06,-21 -8.06,-21 C-6.41,-21 -5.06,-19.66 -5.06,-18c " />
+ </group>
+ </group>
+ </group>
+ <group android:name="time_group" />
+ </vector>
+ </aapt:attr>
+</animated-vector> \ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/ic_media_pause_button_container.xml b/packages/SystemUI/res/drawable/ic_media_pause_button_container.xml
new file mode 100644
index 000000000000..571f69d51ac4
--- /dev/null
+++ b/packages/SystemUI/res/drawable/ic_media_pause_button_container.xml
@@ -0,0 +1,135 @@
+<?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.
+ -->
+<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:aapt="http://schemas.android.com/aapt">
+ <aapt:attr name="android:drawable">
+ <vector
+ android:width="88dp"
+ android:height="56dp"
+ android:viewportHeight="56"
+ android:viewportWidth="88">
+ <group android:name="_R_G">
+ <group
+ android:name="_R_G_L_0_G"
+ android:pivotX="0.493"
+ android:pivotY="0.124"
+ android:scaleX="1.05905"
+ android:scaleY="1.0972"
+ android:translateX="43.528999999999996"
+ android:translateY="27.898">
+ <path
+ android:name="_R_G_L_0_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#3d90ff"
+ android:fillType="nonZero"
+ android:pathData=" M34.49 -5.75 C34.49,-5.75 34.49,6 34.49,6 C34.49,14.84 27.32,22 18.49,22 C18.49,22 -17.5,22 -17.5,22 C-26.34,22 -33.5,14.84 -33.5,6 C-33.5,6 -33.5,-5.75 -33.5,-5.75 C-33.5,-14.59 -26.34,-21.75 -17.5,-21.75 C-17.5,-21.75 18.49,-21.75 18.49,-21.75 C27.32,-21.75 34.49,-14.59 34.49,-5.75c " />
+ </group>
+ </group>
+ <group android:name="time_group" />
+ </vector>
+ </aapt:attr>
+ <target android:name="_R_G_L_0_G_D_0_P_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="133"
+ android:propertyName="pathData"
+ android:startOffset="0"
+ android:valueFrom="M34.49 -5.75 C34.49,-5.75 34.49,6 34.49,6 C34.49,14.84 27.32,22 18.49,22 C18.49,22 -17.5,22 -17.5,22 C-26.34,22 -33.5,14.84 -33.5,6 C-33.5,6 -33.5,-5.75 -33.5,-5.75 C-33.5,-14.59 -26.34,-21.75 -17.5,-21.75 C-17.5,-21.75 18.49,-21.75 18.49,-21.75 C27.32,-21.75 34.49,-14.59 34.49,-5.75c "
+ android:valueTo="M34.49 -5.75 C34.49,-5.75 34.49,6 34.49,6 C34.49,14.84 27.32,22 18.49,22 C18.49,22 -17.5,22 -17.5,22 C-26.34,22 -33.5,14.84 -33.5,6 C-33.5,6 -33.5,-5.75 -33.5,-5.75 C-33.5,-14.59 -26.34,-21.75 -17.5,-21.75 C-17.5,-21.75 18.49,-21.75 18.49,-21.75 C27.32,-21.75 34.49,-14.59 34.49,-5.75c "
+ android:valueType="pathType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.473,0 0.065,1 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ <objectAnimator
+ android:duration="367"
+ android:propertyName="pathData"
+ android:startOffset="133"
+ android:valueFrom="M34.49 -5.75 C34.49,-5.75 34.49,6 34.49,6 C34.49,14.84 27.32,22 18.49,22 C18.49,22 -17.5,22 -17.5,22 C-26.34,22 -33.5,14.84 -33.5,6 C-33.5,6 -33.5,-5.75 -33.5,-5.75 C-33.5,-14.59 -26.34,-21.75 -17.5,-21.75 C-17.5,-21.75 18.49,-21.75 18.49,-21.75 C27.32,-21.75 34.49,-14.59 34.49,-5.75c "
+ android:valueTo="M34.47 0.63 C34.47,0.63 34.42,0.64 34.42,0.64 C33.93,12.88 24.69,21.84 13.06,21.97 C13.06,21.97 -12.54,21.97 -12.54,21.97 C-23.11,21.84 -33.38,13.11 -33.52,-0.27 C-33.52,-0.27 -33.52,-0.05 -33.52,-0.05 C-33.5,-13.21 -21.73,-21.76 -12.9,-21.76 C-12.9,-21.76 14.59,-21.76 14.59,-21.76 C24.81,-21.88 34.49,-10.58 34.47,0.63c "
+ android:valueType="pathType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.473,0 0.065,1 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_0_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="167"
+ android:propertyName="scaleX"
+ android:startOffset="0"
+ android:valueFrom="1.05905"
+ android:valueTo="1.17758"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.226,0 0.667,1 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ <objectAnimator
+ android:duration="167"
+ android:propertyName="scaleY"
+ android:startOffset="0"
+ android:valueFrom="1.0972"
+ android:valueTo="1.22"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.226,0 0.667,1 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ <objectAnimator
+ android:duration="333"
+ android:propertyName="scaleX"
+ android:startOffset="167"
+ android:valueFrom="1.17758"
+ android:valueTo="1.05905"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.213,0 0.248,1 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ <objectAnimator
+ android:duration="333"
+ android:propertyName="scaleY"
+ android:startOffset="167"
+ android:valueFrom="1.22"
+ android:valueTo="1.0972"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.213,0 0.248,1 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="time_group">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="517"
+ android:propertyName="translateX"
+ android:startOffset="0"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+</animated-vector> \ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/ic_media_play_button.xml b/packages/SystemUI/res/drawable/ic_media_play_button.xml
new file mode 100644
index 000000000000..f64690268cfe
--- /dev/null
+++ b/packages/SystemUI/res/drawable/ic_media_play_button.xml
@@ -0,0 +1,124 @@
+<?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.
+ -->
+<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:aapt="http://schemas.android.com/aapt">
+ <target android:name="_R_G_L_1_G_D_0_P_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="333"
+ android:propertyName="pathData"
+ android:startOffset="0"
+ android:valueFrom="M-4.69 -16.69 C-4.69,-16.69 20.25,-1.25 20.31,0.25 C20.38,1.75 -4.88,16.89 -4.88,16.89 C-6.94,18.25 -8.56,19.4 -9.75,19.58 C-10.94,19.75 -12.19,18.94 -12.12,16.14 C-12.09,14.76 -12.12,15.92 -12.12,14.26 C-12.12,14.26 -11.94,-16.44 -11.94,-16.44 C-11.94,-18.09 -12.09,-19.69 -10.44,-19.69 C-10.44,-19.69 -9.5,-19.56 -9.5,-19.56 C-8.62,-19.12 -6.19,-17.44 -4.69,-16.69c "
+ android:valueTo="M-5.06 -18 C-5.06,-18 -5.06,-1.24 -5.06,-1.24 C-5.06,-1.24 -5.06,17.7 -5.06,17.7 C-5.06,19.36 -6.41,20.7 -8.06,20.7 C-8.06,20.7 -16,20.7 -16,20.7 C-17.66,20.7 -19,19.36 -19,17.7 C-19,17.7 -19,-18 -19,-18 C-19,-19.66 -17.66,-21 -16,-21 C-16,-21 -8.06,-21 -8.06,-21 C-6.41,-21 -5.06,-19.66 -5.06,-18c "
+ android:valueType="pathType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.433,0 0,1 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_0_G_D_0_P_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="333"
+ android:propertyName="pathData"
+ android:startOffset="0"
+ android:valueFrom="M-4.69 -16.69 C-4.69,-16.69 20.25,-1.25 20.31,0.25 C20.38,1.75 -4.88,16.89 -4.88,16.89 C-6.94,18.25 -8.56,19.4 -9.75,19.58 C-10.94,19.75 -12.19,18.94 -12.12,16.14 C-12.09,14.76 -12.12,15.92 -12.12,14.26 C-12.12,14.26 -11.94,-16.44 -11.94,-16.44 C-11.94,-18.09 -12.09,-19.69 -10.44,-19.69 C-10.44,-19.69 -9.5,-19.56 -9.5,-19.56 C-8.62,-19.12 -6.19,-17.44 -4.69,-16.69c "
+ android:valueTo="M-5.06 -18 C-5.06,-18 -5.06,-0.75 -5.06,-0.75 C-5.06,-0.75 -5.06,17.7 -5.06,17.7 C-5.06,19.36 -6.41,20.7 -8.06,20.7 C-8.06,20.7 -16,20.7 -16,20.7 C-17.66,20.7 -19,19.36 -19,17.7 C-19,17.7 -19,-18 -19,-18 C-19,-19.66 -17.66,-21 -16,-21 C-16,-21 -8.06,-21 -8.06,-21 C-6.41,-21 -5.06,-19.66 -5.06,-18c "
+ android:valueType="pathType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.433,0 0,1 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_0_G_T_1">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="333"
+ android:propertyName="translateX"
+ android:startOffset="0"
+ android:valueFrom="7.576"
+ android:valueTo="15.485"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.583,0 0.089,0.874 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="time_group">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="517"
+ android:propertyName="translateX"
+ android:startOffset="0"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <aapt:attr name="android:drawable">
+ <vector
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportHeight="24"
+ android:viewportWidth="24">
+ <group android:name="_R_G">
+ <group
+ android:name="_R_G_L_1_G"
+ android:pivotX="-12.031"
+ android:scaleX="0.33299999999999996"
+ android:scaleY="0.33299999999999996"
+ android:translateX="19.524"
+ android:translateY="12.084">
+ <path
+ android:name="_R_G_L_1_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#ffffff"
+ android:fillType="nonZero"
+ android:pathData=" M-4.69 -16.69 C-4.69,-16.69 20.25,-1.25 20.31,0.25 C20.38,1.75 -4.88,16.89 -4.88,16.89 C-6.94,18.25 -8.56,19.4 -9.75,19.58 C-10.94,19.75 -12.19,18.94 -12.12,16.14 C-12.09,14.76 -12.12,15.92 -12.12,14.26 C-12.12,14.26 -11.94,-16.44 -11.94,-16.44 C-11.94,-18.09 -12.09,-19.69 -10.44,-19.69 C-10.44,-19.69 -9.5,-19.56 -9.5,-19.56 C-8.62,-19.12 -6.19,-17.44 -4.69,-16.69c " />
+ </group>
+ <group
+ android:name="_R_G_L_0_G_T_1"
+ android:scaleX="0.33299999999999996"
+ android:scaleY="0.33299999999999996"
+ android:translateX="7.576"
+ android:translateY="12.084">
+ <group
+ android:name="_R_G_L_0_G"
+ android:translateX="12.031">
+ <path
+ android:name="_R_G_L_0_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#ffffff"
+ android:fillType="nonZero"
+ android:pathData=" M-4.69 -16.69 C-4.69,-16.69 20.25,-1.25 20.31,0.25 C20.38,1.75 -4.88,16.89 -4.88,16.89 C-6.94,18.25 -8.56,19.4 -9.75,19.58 C-10.94,19.75 -12.19,18.94 -12.12,16.14 C-12.09,14.76 -12.12,15.92 -12.12,14.26 C-12.12,14.26 -11.94,-16.44 -11.94,-16.44 C-11.94,-18.09 -12.09,-19.69 -10.44,-19.69 C-10.44,-19.69 -9.5,-19.56 -9.5,-19.56 C-8.62,-19.12 -6.19,-17.44 -4.69,-16.69c " />
+ </group>
+ </group>
+ </group>
+ <group android:name="time_group" />
+ </vector>
+ </aapt:attr>
+</animated-vector> \ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/ic_media_play_button_container.xml b/packages/SystemUI/res/drawable/ic_media_play_button_container.xml
new file mode 100644
index 000000000000..aa4e09fa4033
--- /dev/null
+++ b/packages/SystemUI/res/drawable/ic_media_play_button_container.xml
@@ -0,0 +1,135 @@
+<?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.
+ -->
+<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:aapt="http://schemas.android.com/aapt">
+ <aapt:attr name="android:drawable">
+ <vector
+ android:height="56dp"
+ android:width="88dp"
+ android:viewportHeight="56"
+ android:viewportWidth="88">
+ <group android:name="_R_G">
+ <group
+ android:name="_R_G_L_0_G"
+ android:translateX="43.528999999999996"
+ android:translateY="27.898"
+ android:pivotX="0.493"
+ android:pivotY="0.124"
+ android:scaleX="1.05905"
+ android:scaleY="1.0972">
+ <path
+ android:name="_R_G_L_0_G_D_0_P_0"
+ android:fillColor="#3d90ff"
+ android:fillAlpha="1"
+ android:fillType="nonZero"
+ android:pathData=" M34.47 0.63 C34.47,0.63 34.42,0.64 34.42,0.64 C33.93,12.88 24.69,21.84 13.06,21.97 C13.06,21.97 -12.54,21.97 -12.54,21.97 C-23.11,21.84 -33.38,13.11 -33.52,-0.27 C-33.52,-0.27 -33.52,-0.05 -33.52,-0.05 C-33.5,-13.21 -21.73,-21.76 -12.9,-21.76 C-12.9,-21.76 14.59,-21.76 14.59,-21.76 C24.81,-21.88 34.49,-10.58 34.47,0.63c "/>
+ </group>
+ </group>
+ <group android:name="time_group"/>
+ </vector>
+ </aapt:attr>
+ <target android:name="_R_G_L_0_G_D_0_P_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:propertyName="pathData"
+ android:duration="167"
+ android:startOffset="0"
+ android:valueFrom="M34.47 0.63 C34.47,0.63 34.42,0.64 34.42,0.64 C33.93,12.88 24.69,21.84 13.06,21.97 C13.06,21.97 -12.54,21.97 -12.54,21.97 C-23.11,21.84 -33.38,13.11 -33.52,-0.27 C-33.52,-0.27 -33.52,-0.05 -33.52,-0.05 C-33.5,-13.21 -21.73,-21.76 -12.9,-21.76 C-12.9,-21.76 14.59,-21.76 14.59,-21.76 C24.81,-21.88 34.49,-10.58 34.47,0.63c "
+ android:valueTo="M34.47 0.63 C34.47,0.63 34.42,0.64 34.42,0.64 C33.93,12.88 24.69,21.84 13.06,21.97 C13.06,21.97 -12.54,21.97 -12.54,21.97 C-23.11,21.84 -33.38,13.11 -33.52,-0.27 C-33.52,-0.27 -33.52,-0.05 -33.52,-0.05 C-33.5,-13.21 -21.73,-21.76 -12.9,-21.76 C-12.9,-21.76 14.59,-21.76 14.59,-21.76 C24.81,-21.88 34.49,-10.58 34.47,0.63c "
+ android:valueType="pathType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.493,0 0,1 1.0,1.0"/>
+ </aapt:attr>
+ </objectAnimator>
+ <objectAnimator
+ android:propertyName="pathData"
+ android:duration="333"
+ android:startOffset="167"
+ android:valueFrom="M34.47 0.63 C34.47,0.63 34.42,0.64 34.42,0.64 C33.93,12.88 24.69,21.84 13.06,21.97 C13.06,21.97 -12.54,21.97 -12.54,21.97 C-23.11,21.84 -33.38,13.11 -33.52,-0.27 C-33.52,-0.27 -33.52,-0.05 -33.52,-0.05 C-33.5,-13.21 -21.73,-21.76 -12.9,-21.76 C-12.9,-21.76 14.59,-21.76 14.59,-21.76 C24.81,-21.88 34.49,-10.58 34.47,0.63c "
+ android:valueTo="M34.49 -5.75 C34.49,-5.75 34.49,6 34.49,6 C34.49,14.84 27.32,22 18.49,22 C18.49,22 -17.5,22 -17.5,22 C-26.34,22 -33.5,14.84 -33.5,6 C-33.5,6 -33.5,-5.75 -33.5,-5.75 C-33.5,-14.59 -26.34,-21.75 -17.5,-21.75 C-17.5,-21.75 18.49,-21.75 18.49,-21.75 C27.32,-21.75 34.49,-14.59 34.49,-5.75c "
+ android:valueType="pathType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.493,0 0,1 1.0,1.0"/>
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_0_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:propertyName="scaleX"
+ android:duration="167"
+ android:startOffset="0"
+ android:valueFrom="1.05905"
+ android:valueTo="1.17758"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.226,0 0.667,1 1.0,1.0"/>
+ </aapt:attr>
+ </objectAnimator>
+ <objectAnimator
+ android:propertyName="scaleY"
+ android:duration="167"
+ android:startOffset="0"
+ android:valueFrom="1.0972"
+ android:valueTo="1.22"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.226,0 0.667,1 1.0,1.0"/>
+ </aapt:attr>
+ </objectAnimator>
+ <objectAnimator
+ android:propertyName="scaleX"
+ android:duration="333"
+ android:startOffset="167"
+ android:valueFrom="1.17758"
+ android:valueTo="1.05905"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.213,0 0.248,1 1.0,1.0"/>
+ </aapt:attr>
+ </objectAnimator>
+ <objectAnimator
+ android:propertyName="scaleY"
+ android:duration="333"
+ android:startOffset="167"
+ android:valueFrom="1.22"
+ android:valueTo="1.0972"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.213,0 0.248,1 1.0,1.0"/>
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="time_group">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:propertyName="translateX"
+ android:duration="517"
+ android:startOffset="0"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType"/>
+ </set>
+ </aapt:attr>
+ </target>
+</animated-vector> \ No newline at end of file
diff --git a/packages/SystemUI/res/layout/volume_dialog.xml b/packages/SystemUI/res/layout/volume_dialog.xml
index 58f2d3ccc6a8..67f620f6fc54 100644
--- a/packages/SystemUI/res/layout/volume_dialog.xml
+++ b/packages/SystemUI/res/layout/volume_dialog.xml
@@ -19,6 +19,7 @@
android:id="@+id/volume_dialog_root"
android:layout_width="match_parent"
android:layout_height="match_parent"
+ android:alpha="0"
android:clipChildren="false"
app:layoutDescription="@xml/volume_dialog_scene">
diff --git a/packages/SystemUI/res/layout/volume_ringer_button.xml b/packages/SystemUI/res/layout/volume_ringer_button.xml
index e65d0b938b65..6748cfa05c35 100644
--- a/packages/SystemUI/res/layout/volume_ringer_button.xml
+++ b/packages/SystemUI/res/layout/volume_ringer_button.xml
@@ -20,10 +20,9 @@
<ImageButton
android:id="@+id/volume_drawer_button"
- android:layout_width="@dimen/volume_dialog_ringer_drawer_button_size"
- android:layout_height="@dimen/volume_dialog_ringer_drawer_button_size"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
android:padding="@dimen/volume_dialog_ringer_drawer_button_icon_radius"
- android:layout_marginBottom="@dimen/volume_dialog_components_spacing"
android:contentDescription="@string/volume_ringer_mode"
android:gravity="center"
android:tint="@androidprv:color/materialColorOnSurface"
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 1f7889214bd5..2ffa3d19e161 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -1278,6 +1278,7 @@
<dimen name="qs_center_guideline_padding">10dp</dimen>
<dimen name="qs_media_action_spacing">4dp</dimen>
<dimen name="qs_media_action_margin">12dp</dimen>
+ <dimen name="qs_media_action_play_pause_width">72dp</dimen>
<dimen name="qs_seamless_height">24dp</dimen>
<dimen name="qs_seamless_icon_size">12dp</dimen>
<dimen name="qs_media_disabled_seekbar_height">1dp</dimen>
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index d445ed927a25..80fb8b9fcebc 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -2309,6 +2309,9 @@
<string name="group_system_lock_screen">Lock screen</string>
<!-- User visible title for the keyboard shortcut that pulls up Notes app for quick memo. [CHAR LIMIT=70] -->
<string name="group_system_quick_memo">Take a note</string>
+ <!-- TODO(b/383734125): make it translatable once string is finalized by UXW.-->
+ <!-- User visible title for the keyboard shortcut that toggles Voice Access. [CHAR LIMIT=70] -->
+ <string name="group_system_toggle_voice_access" translatable="false">Toggle Voice Access</string>
<!-- User visible title for the multitasking keyboard shortcuts list. [CHAR LIMIT=70] -->
<string name="keyboard_shortcut_group_system_multitasking">Multitasking</string>
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RecentsAnimationListener.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RecentsAnimationListener.java
index 51892aac606a..ff6bcdb150f8 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RecentsAnimationListener.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RecentsAnimationListener.java
@@ -19,6 +19,7 @@ package com.android.systemui.shared.system;
import android.graphics.Rect;
import android.os.Bundle;
import android.view.RemoteAnimationTarget;
+import android.window.TransitionInfo;
import com.android.systemui.shared.recents.model.ThumbnailData;
@@ -30,7 +31,7 @@ public interface RecentsAnimationListener {
*/
void onAnimationStart(RecentsAnimationControllerCompat controller,
RemoteAnimationTarget[] apps, RemoteAnimationTarget[] wallpapers,
- Rect homeContentInsets, Rect minimizedHomeBounds, Bundle extras);
+ Rect homeContentInsets, Rect minimizedHomeBounds, Bundle extras, TransitionInfo info);
/**
* Called when the animation into Recents was canceled. This call is made on the binder thread.
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardDisplayManager.java b/packages/SystemUI/src/com/android/keyguard/KeyguardDisplayManager.java
index acfa08643b63..c7ae02b61bff 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardDisplayManager.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardDisplayManager.java
@@ -142,33 +142,28 @@ public class KeyguardDisplayManager {
private boolean isKeyguardShowable(Display display) {
if (display == null) {
- if (DEBUG) Log.i(TAG, "Cannot show Keyguard on null display");
+ Log.i(TAG, "Cannot show Keyguard on null display");
return false;
}
if (ShadeWindowGoesAround.isEnabled()) {
int shadeDisplayId = mShadePositionRepositoryProvider.get().getDisplayId().getValue();
if (display.getDisplayId() == shadeDisplayId) {
- if (DEBUG) {
- Log.i(TAG,
- "Do not show KeyguardPresentation on the shade window display");
- }
+ Log.i(TAG, "Do not show KeyguardPresentation on the shade window display");
return false;
}
} else {
if (display.getDisplayId() == mDisplayTracker.getDefaultDisplayId()) {
- if (DEBUG) Log.i(TAG, "Do not show KeyguardPresentation on the default display");
+ Log.i(TAG, "Do not show KeyguardPresentation on the default display");
return false;
}
}
display.getDisplayInfo(mTmpDisplayInfo);
if ((mTmpDisplayInfo.flags & Display.FLAG_PRIVATE) != 0) {
- if (DEBUG) Log.i(TAG, "Do not show KeyguardPresentation on a private display");
+ Log.i(TAG, "Do not show KeyguardPresentation on a private display");
return false;
}
if ((mTmpDisplayInfo.flags & Display.FLAG_ALWAYS_UNLOCKED) != 0) {
- if (DEBUG) {
- Log.i(TAG, "Do not show KeyguardPresentation on an unlocked display");
- }
+ Log.i(TAG, "Do not show KeyguardPresentation on an unlocked display");
return false;
}
@@ -176,14 +171,11 @@ public class KeyguardDisplayManager {
mDeviceStateHelper.isConcurrentDisplayActive(display)
|| mDeviceStateHelper.isRearDisplayOuterDefaultActive(display);
if (mKeyguardStateController.isOccluded() && deviceStateOccludesKeyguard) {
- if (DEBUG) {
- // When activities with FLAG_SHOW_WHEN_LOCKED are shown on top of Keyguard, the
- // Keyguard state becomes "occluded". In this case, we should not show the
- // KeyguardPresentation, since the activity is presenting content onto the
- // non-default display.
- Log.i(TAG, "Do not show KeyguardPresentation when occluded and concurrent or rear"
- + " display is active");
- }
+ // When activities with FLAG_SHOW_WHEN_LOCKED are shown on top of Keyguard, the Keyguard
+ // state becomes "occluded". In this case, we should not show the KeyguardPresentation,
+ // since the activity is presenting content onto the non-default display.
+ Log.i(TAG, "Do not show KeyguardPresentation when occluded and concurrent or rear"
+ + " display is active");
return false;
}
@@ -197,7 +189,7 @@ public class KeyguardDisplayManager {
*/
private boolean showPresentation(Display display) {
if (!isKeyguardShowable(display)) return false;
- if (DEBUG) Log.i(TAG, "Keyguard enabled on display: " + display);
+ Log.i(TAG, "Keyguard enabled on display: " + display);
final int displayId = display.getDisplayId();
Presentation presentation = mPresentations.get(displayId);
if (presentation == null) {
@@ -239,7 +231,7 @@ public class KeyguardDisplayManager {
public void show() {
if (!mShowing) {
- if (DEBUG) Log.v(TAG, "show");
+ Log.v(TAG, "show");
if (mMediaRouter != null) {
mMediaRouter.addCallback(MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY,
mMediaRouterCallback, MediaRouter.CALLBACK_FLAG_PASSIVE_DISCOVERY);
@@ -253,7 +245,7 @@ public class KeyguardDisplayManager {
public void hide() {
if (mShowing) {
- if (DEBUG) Log.v(TAG, "hide");
+ Log.v(TAG, "hide");
if (mMediaRouter != null) {
mMediaRouter.removeCallback(mMediaRouterCallback);
}
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
index a703b027b691..7d291c311ca3 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
@@ -2591,6 +2591,15 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab
}
/**
+ * @return true if optical udfps HW is supported on this device. Can return true even if the
+ * user has not enrolled udfps. This may be false if called before
+ * onAllAuthenticatorsRegistered.
+ */
+ public boolean isOpticalUdfpsSupported() {
+ return mAuthController.isOpticalUdfpsSupported();
+ }
+
+ /**
* @return true if there's at least one sfps enrollment for the current user.
*/
public boolean isSfpsEnrolled() {
diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIInitializer.java b/packages/SystemUI/src/com/android/systemui/SystemUIInitializer.java
index f530522fb707..5f79c8cada45 100644
--- a/packages/SystemUI/src/com/android/systemui/SystemUIInitializer.java
+++ b/packages/SystemUI/src/com/android/systemui/SystemUIInitializer.java
@@ -100,7 +100,8 @@ public abstract class SystemUIInitializer {
.setDisplayAreaHelper(mWMComponent.getDisplayAreaHelper())
.setRecentTasks(mWMComponent.getRecentTasks())
.setBackAnimation(mWMComponent.getBackAnimation())
- .setDesktopMode(mWMComponent.getDesktopMode());
+ .setDesktopMode(mWMComponent.getDesktopMode())
+ .setAppZoomOut(mWMComponent.getAppZoomOut());
// Only initialize when not starting from tests since this currently initializes some
// components that shouldn't be run in the test environment
@@ -121,7 +122,8 @@ public abstract class SystemUIInitializer {
.setStartingSurface(Optional.ofNullable(null))
.setRecentTasks(Optional.ofNullable(null))
.setBackAnimation(Optional.ofNullable(null))
- .setDesktopMode(Optional.ofNullable(null));
+ .setDesktopMode(Optional.ofNullable(null))
+ .setAppZoomOut(Optional.ofNullable(null));
}
mSysUIComponent = builder.build();
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/FullscreenMagnificationController.java b/packages/SystemUI/src/com/android/systemui/accessibility/FullscreenMagnificationController.java
index 04afd8693e04..caf043a1b1be 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/FullscreenMagnificationController.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/FullscreenMagnificationController.java
@@ -22,6 +22,8 @@ import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
+import android.annotation.IntDef;
+import android.annotation.Nullable;
import android.annotation.UiContext;
import android.content.ComponentCallbacks;
import android.content.Context;
@@ -44,7 +46,8 @@ import android.view.SurfaceControlViewHost;
import android.view.View;
import android.view.WindowManager;
import android.view.accessibility.AccessibilityManager;
-import android.view.animation.AccelerateDecelerateInterpolator;
+import android.view.animation.AccelerateInterpolator;
+import android.view.animation.DecelerateInterpolator;
import android.view.animation.Interpolator;
import androidx.annotation.NonNull;
@@ -57,12 +60,16 @@ import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.res.R;
import com.android.systemui.util.leak.RotationUtils;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
import java.util.concurrent.Executor;
import java.util.function.Supplier;
public class FullscreenMagnificationController implements ComponentCallbacks {
- private static final String TAG = "FullscreenMagnificationController";
+ private static final String TAG = "FullscreenMagController";
+ private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+
private final Context mContext;
private final AccessibilityManager mAccessibilityManager;
private final WindowManager mWindowManager;
@@ -77,12 +84,14 @@ public class FullscreenMagnificationController implements ComponentCallbacks {
private int mBorderStoke;
private final int mDisplayId;
private static final Region sEmptyRegion = new Region();
- private ValueAnimator mShowHideBorderAnimator;
+ @VisibleForTesting
+ @Nullable
+ ValueAnimator mShowHideBorderAnimator;
private Handler mHandler;
private Executor mExecutor;
- private boolean mFullscreenMagnificationActivated = false;
private final Configuration mConfiguration;
- private final Runnable mShowBorderRunnable = this::showBorderWithNullCheck;
+ private final Runnable mHideBorderImmediatelyRunnable = this::hideBorderImmediately;
+ private final Runnable mShowBorderRunnable = this::showBorder;
private int mRotation;
private final IRotationWatcher mRotationWatcher = new IRotationWatcher.Stub() {
@Override
@@ -95,6 +104,21 @@ public class FullscreenMagnificationController implements ComponentCallbacks {
private final DisplayManager.DisplayListener mDisplayListener;
private String mCurrentDisplayUniqueId;
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ DISABLED,
+ DISABLING,
+ ENABLING,
+ ENABLED
+ })
+ @interface FullscreenMagnificationActivationState {}
+ private static final int DISABLED = 0;
+ private static final int DISABLING = 1;
+ private static final int ENABLING = 2;
+ private static final int ENABLED = 3;
+ @FullscreenMagnificationActivationState
+ private int mActivationState = DISABLED;
+
public FullscreenMagnificationController(
@UiContext Context context,
@Main Handler handler,
@@ -106,7 +130,7 @@ public class FullscreenMagnificationController implements ComponentCallbacks {
Supplier<SurfaceControlViewHost> scvhSupplier) {
this(context, handler, executor, displayManager, accessibilityManager,
windowManager, iWindowManager, scvhSupplier,
- new SurfaceControl.Transaction(), null);
+ new SurfaceControl.Transaction());
}
@VisibleForTesting
@@ -119,8 +143,7 @@ public class FullscreenMagnificationController implements ComponentCallbacks {
WindowManager windowManager,
IWindowManager iWindowManager,
Supplier<SurfaceControlViewHost> scvhSupplier,
- SurfaceControl.Transaction transaction,
- ValueAnimator valueAnimator) {
+ SurfaceControl.Transaction transaction) {
mContext = context;
mHandler = handler;
mExecutor = executor;
@@ -135,18 +158,6 @@ public class FullscreenMagnificationController implements ComponentCallbacks {
mConfiguration = new Configuration(context.getResources().getConfiguration());
mLongAnimationTimeMs = mContext.getResources().getInteger(
com.android.internal.R.integer.config_longAnimTime);
- mShowHideBorderAnimator = (valueAnimator == null)
- ? createNullTargetObjectAnimator() : valueAnimator;
- mShowHideBorderAnimator.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(@NonNull Animator animation, boolean isReverse) {
- if (isReverse) {
- // The animation was played in reverse, which means we are hiding the border.
- // We would like to perform clean up after the border is fully hidden.
- cleanUpBorder();
- }
- }
- });
mCurrentDisplayUniqueId = mContext.getDisplayNoVerify().getUniqueId();
mDisplayManager = displayManager;
mDisplayListener = new DisplayManager.DisplayListener() {
@@ -167,20 +178,51 @@ public class FullscreenMagnificationController implements ComponentCallbacks {
// Same unique ID means the physical display doesn't change. Early return.
return;
}
-
mCurrentDisplayUniqueId = uniqueId;
- applyCornerRadiusToBorder();
+ mHandler.post(FullscreenMagnificationController.this::applyCornerRadiusToBorder);
}
};
}
- private ValueAnimator createNullTargetObjectAnimator() {
+ @VisibleForTesting
+ @UiThread
+ ValueAnimator createShowTargetAnimator(@NonNull View target) {
+ if (mShowHideBorderAnimator != null) {
+ mShowHideBorderAnimator.cancel();
+ }
+
final ValueAnimator valueAnimator =
- ObjectAnimator.ofFloat(/* target= */ null, View.ALPHA, 0f, 1f);
- Interpolator interpolator = new AccelerateDecelerateInterpolator();
+ ObjectAnimator.ofFloat(target, View.ALPHA, 0f, 1f);
+ Interpolator interpolator = new AccelerateInterpolator();
valueAnimator.setInterpolator(interpolator);
valueAnimator.setDuration(mLongAnimationTimeMs);
+ valueAnimator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(@NonNull Animator animation) {
+ mHandler.post(() -> setState(ENABLED));
+ }});
+ return valueAnimator;
+ }
+
+ @VisibleForTesting
+ @UiThread
+ ValueAnimator createHideTargetAnimator(@NonNull View target) {
+ if (mShowHideBorderAnimator != null) {
+ mShowHideBorderAnimator.cancel();
+ }
+
+ final ValueAnimator valueAnimator =
+ ObjectAnimator.ofFloat(target, View.ALPHA, 1f, 0f);
+ Interpolator interpolator = new DecelerateInterpolator();
+
+ valueAnimator.setInterpolator(interpolator);
+ valueAnimator.setDuration(mLongAnimationTimeMs);
+ valueAnimator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(@NonNull Animator animation) {
+ mHandler.post(() -> cleanUpBorder());
+ }});
return valueAnimator;
}
@@ -190,14 +232,10 @@ public class FullscreenMagnificationController implements ComponentCallbacks {
*/
@UiThread
public void onFullscreenMagnificationActivationChanged(boolean activated) {
- final boolean changed = (mFullscreenMagnificationActivated != activated);
- if (changed) {
- mFullscreenMagnificationActivated = activated;
- if (activated) {
- createFullscreenMagnificationBorder();
- } else {
- removeFullscreenMagnificationBorder();
- }
+ if (activated) {
+ createFullscreenMagnificationBorder();
+ } else {
+ removeFullscreenMagnificationBorder();
}
}
@@ -207,16 +245,21 @@ public class FullscreenMagnificationController implements ComponentCallbacks {
*/
@UiThread
private void removeFullscreenMagnificationBorder() {
- if (mHandler.hasCallbacks(mShowBorderRunnable)) {
- mHandler.removeCallbacks(mShowBorderRunnable);
+ int state = getState();
+ if (state == DISABLING || state == DISABLED) {
+ // If there is an ongoing disable process or it is already disabled, return
+ return;
}
- mContext.unregisterComponentCallbacks(this);
-
-
- mShowHideBorderAnimator.reverse();
+ setState(DISABLING);
+ mShowHideBorderAnimator = createHideTargetAnimator(mFullscreenBorder);
+ mShowHideBorderAnimator.start();
}
- private void cleanUpBorder() {
+ @VisibleForTesting
+ @UiThread
+ void cleanUpBorder() {
+ mContext.unregisterComponentCallbacks(this);
+
if (Flags.updateCornerRadiusOnDisplayChanged()) {
mDisplayManager.unregisterDisplayListener(mDisplayListener);
}
@@ -227,6 +270,12 @@ public class FullscreenMagnificationController implements ComponentCallbacks {
}
if (mFullscreenBorder != null) {
+ if (mHandler.hasCallbacks(mHideBorderImmediatelyRunnable)) {
+ mHandler.removeCallbacks(mHideBorderImmediatelyRunnable);
+ }
+ if (mHandler.hasCallbacks(mShowBorderRunnable)) {
+ mHandler.removeCallbacks(mShowBorderRunnable);
+ }
mFullscreenBorder = null;
try {
mIWindowManager.removeRotationWatcher(mRotationWatcher);
@@ -234,6 +283,7 @@ public class FullscreenMagnificationController implements ComponentCallbacks {
Log.w(TAG, "Failed to remove rotation watcher", e);
}
}
+ setState(DISABLED);
}
/**
@@ -242,44 +292,47 @@ public class FullscreenMagnificationController implements ComponentCallbacks {
*/
@UiThread
private void createFullscreenMagnificationBorder() {
+ int state = getState();
+ if (state == ENABLING || state == ENABLED) {
+ // If there is an ongoing enable process or it is already enabled, return
+ return;
+ }
+ if (mShowHideBorderAnimator != null) {
+ mShowHideBorderAnimator.cancel();
+ }
+ setState(ENABLING);
+
onConfigurationChanged(mContext.getResources().getConfiguration());
mContext.registerComponentCallbacks(this);
if (mSurfaceControlViewHost == null) {
- // Create the view only if it does not exist yet. If we are trying to enable fullscreen
- // magnification before it was fully disabled, we use the previous view instead of
- // creating a new one.
+ // Create the view only if it does not exist yet. If we are trying to enable
+ // fullscreen magnification before it was fully disabled, we use the previous view
+ // instead of creating a new one.
mFullscreenBorder = LayoutInflater.from(mContext)
.inflate(R.layout.fullscreen_magnification_border, null);
- // Set the initial border view alpha manually so we won't show the border accidentally
- // after we apply show() to the SurfaceControl and before the animation starts to run.
+ // Set the initial border view alpha manually so we won't show the border
+ // accidentally after we apply show() to the SurfaceControl and before the
+ // animation starts to run.
mFullscreenBorder.setAlpha(0f);
- mShowHideBorderAnimator.setTarget(mFullscreenBorder);
mSurfaceControlViewHost = mScvhSupplier.get();
mSurfaceControlViewHost.setView(mFullscreenBorder, getBorderLayoutParams());
- mBorderSurfaceControl = mSurfaceControlViewHost.getSurfacePackage().getSurfaceControl();
+ mBorderSurfaceControl =
+ mSurfaceControlViewHost.getSurfacePackage().getSurfaceControl();
try {
mIWindowManager.watchRotation(mRotationWatcher, Display.DEFAULT_DISPLAY);
} catch (Exception e) {
Log.w(TAG, "Failed to register rotation watcher", e);
}
if (Flags.updateCornerRadiusOnDisplayChanged()) {
- mHandler.post(this::applyCornerRadiusToBorder);
+ applyCornerRadiusToBorder();
}
}
mTransaction
.addTransactionCommittedListener(
mExecutor,
- () -> {
- if (mShowHideBorderAnimator.isRunning()) {
- // Since the method is only called when there is an activation
- // status change, the running animator is hiding the border.
- mShowHideBorderAnimator.reverse();
- } else {
- mShowHideBorderAnimator.start();
- }
- })
+ this::showBorder)
.setPosition(mBorderSurfaceControl, -mBorderOffset, -mBorderOffset)
.setLayer(mBorderSurfaceControl, Integer.MAX_VALUE)
.show(mBorderSurfaceControl)
@@ -380,19 +433,25 @@ public class FullscreenMagnificationController implements ComponentCallbacks {
mHandler.removeCallbacks(mShowBorderRunnable);
}
- // We hide the border immediately as early as possible to beat the redrawing of window
- // in response to the orientation change so users won't see a weird shape border.
- mHandler.postAtFrontOfQueue(() -> {
- mFullscreenBorder.setAlpha(0f);
- });
-
+ // We hide the border immediately as early as possible to beat the redrawing of
+ // window in response to the orientation change so users won't see a weird shape
+ // border.
+ mHandler.postAtFrontOfQueue(mHideBorderImmediatelyRunnable);
mHandler.postDelayed(mShowBorderRunnable, mLongAnimationTimeMs);
}
- private void showBorderWithNullCheck() {
+ @UiThread
+ private void hideBorderImmediately() {
if (mShowHideBorderAnimator != null) {
- mShowHideBorderAnimator.start();
+ mShowHideBorderAnimator.cancel();
}
+ mFullscreenBorder.setAlpha(0f);
+ }
+
+ @UiThread
+ private void showBorder() {
+ mShowHideBorderAnimator = createShowTargetAnimator(mFullscreenBorder);
+ mShowHideBorderAnimator.start();
}
private void updateDimensions() {
@@ -404,7 +463,9 @@ public class FullscreenMagnificationController implements ComponentCallbacks {
R.dimen.magnifier_border_width_fullscreen_with_offset);
}
- private void applyCornerRadiusToBorder() {
+ @UiThread
+ @VisibleForTesting
+ void applyCornerRadiusToBorder() {
if (!isActivated()) {
return;
}
@@ -422,6 +483,20 @@ public class FullscreenMagnificationController implements ComponentCallbacks {
backgroundDrawable.setCornerRadius(cornerRadius);
}
+ @UiThread
+ private void setState(@FullscreenMagnificationActivationState int state) {
+ if (DEBUG) {
+ Log.d(TAG, "setState from " + mActivationState + " to " + state);
+ }
+ mActivationState = state;
+ }
+
+ @VisibleForTesting
+ @UiThread
+ int getState() {
+ return mActivationState;
+ }
+
@Override
public void onLowMemory() {
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuController.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuController.java
index 5f0acfa644dc..67aa4ff577b8 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuController.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuController.java
@@ -23,7 +23,6 @@ import android.annotation.Nullable;
import android.content.Context;
import android.hardware.display.DisplayManager;
import android.os.Handler;
-import android.os.UserHandle;
import android.text.TextUtils;
import android.view.Display;
import android.view.WindowManager;
@@ -58,7 +57,7 @@ public class AccessibilityFloatingMenuController implements
private final AccessibilityButtonTargetsObserver mAccessibilityButtonTargetsObserver;
private final KeyguardUpdateMonitor mKeyguardUpdateMonitor;
- private Context mContext;
+ private final Context mContext;
private final WindowManager mWindowManager;
private final ViewCaptureAwareWindowManager mViewCaptureAwareWindowManager;
private final DisplayManager mDisplayManager;
@@ -226,7 +225,6 @@ public class AccessibilityFloatingMenuController implements
@Override
public void onUserInitializationComplete(int userId) {
mIsUserInInitialization = false;
- mContext = mContext.createContextAsUser(UserHandle.of(userId), /* flags= */ 0);
mBtnMode = mAccessibilityButtonModeObserver.getCurrentAccessibilityButtonMode();
mBtnTargets =
mAccessibilityButtonTargetsObserver.getCurrentAccessibilityButtonTargets();
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuInfoRepository.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuInfoRepository.java
index 121b51f768e7..a1cb0367421b 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuInfoRepository.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuInfoRepository.java
@@ -79,6 +79,8 @@ class MenuInfoRepository {
private static final int DEFAULT_MIGRATION_TOOLTIP_VALUE_PROMPT = MigrationPrompt.DISABLED;
private final Context mContext;
+ // Pref always get the userId from the context to store SharedPreferences for the correct user
+ private final Context mCurrentUserContext;
private final Configuration mConfiguration;
private final AccessibilityManager mAccessibilityManager;
private final AccessibilityManager.AccessibilityServicesStateChangeListener
@@ -157,6 +159,9 @@ class MenuInfoRepository {
OnContentsChanged settingsContentsChanged, SecureSettings secureSettings,
@Nullable HearingAidDeviceManager hearingAidDeviceManager) {
mContext = context;
+ final int currentUserId = secureSettings.getRealUserHandle(UserHandle.USER_CURRENT);
+ mCurrentUserContext = context.createContextAsUser(
+ UserHandle.of(currentUserId), /* flags= */ 0);
mAccessibilityManager = accessibilityManager;
mConfiguration = new Configuration(context.getResources().getConfiguration());
mSettingsContentsCallback = settingsContentsChanged;
@@ -168,12 +173,13 @@ class MenuInfoRepository {
void loadMenuMoveToTucked(OnInfoReady<Boolean> callback) {
callback.onReady(
- Prefs.getBoolean(mContext, Prefs.Key.HAS_ACCESSIBILITY_FLOATING_MENU_TUCKED,
+ Prefs.getBoolean(
+ mCurrentUserContext, Prefs.Key.HAS_ACCESSIBILITY_FLOATING_MENU_TUCKED,
DEFAULT_MOVE_TO_TUCKED_VALUE));
}
void loadDockTooltipVisibility(OnInfoReady<Boolean> callback) {
- callback.onReady(Prefs.getBoolean(mContext,
+ callback.onReady(Prefs.getBoolean(mCurrentUserContext,
Prefs.Key.HAS_SEEN_ACCESSIBILITY_FLOATING_MENU_DOCK_TOOLTIP,
DEFAULT_HAS_SEEN_DOCK_TOOLTIP_VALUE));
}
@@ -215,19 +221,19 @@ class MenuInfoRepository {
}
void updateMoveToTucked(boolean isMoveToTucked) {
- Prefs.putBoolean(mContext, Prefs.Key.HAS_ACCESSIBILITY_FLOATING_MENU_TUCKED,
+ Prefs.putBoolean(mCurrentUserContext, Prefs.Key.HAS_ACCESSIBILITY_FLOATING_MENU_TUCKED,
isMoveToTucked);
}
void updateMenuSavingPosition(Position percentagePosition) {
mPercentagePosition = percentagePosition;
- Prefs.putString(mContext, Prefs.Key.ACCESSIBILITY_FLOATING_MENU_POSITION,
+ Prefs.putString(mCurrentUserContext, Prefs.Key.ACCESSIBILITY_FLOATING_MENU_POSITION,
percentagePosition.toString());
}
void updateDockTooltipVisibility(boolean hasSeen) {
- Prefs.putBoolean(mContext, Prefs.Key.HAS_SEEN_ACCESSIBILITY_FLOATING_MENU_DOCK_TOOLTIP,
- hasSeen);
+ Prefs.putBoolean(mCurrentUserContext,
+ Prefs.Key.HAS_SEEN_ACCESSIBILITY_FLOATING_MENU_DOCK_TOOLTIP, hasSeen);
}
void updateMigrationTooltipVisibility(boolean visible) {
@@ -243,7 +249,7 @@ class MenuInfoRepository {
}
private Position getStartPosition() {
- final String absolutePositionString = Prefs.getString(mContext,
+ final String absolutePositionString = Prefs.getString(mCurrentUserContext,
Prefs.Key.ACCESSIBILITY_FLOATING_MENU_POSITION, /* defaultValue= */ null);
final float defaultPositionXPercent =
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerController.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerController.java
index 184518ac35eb..e7470a34a065 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerController.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerController.java
@@ -17,6 +17,7 @@
package com.android.systemui.accessibility.floatingmenu;
import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_EXCLUDE_FROM_SCREEN_MAGNIFICATION;
+import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS;
import android.content.Context;
import android.graphics.PixelFormat;
@@ -90,7 +91,8 @@ class MenuViewLayerController implements IAccessibilityFloatingMenu {
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
PixelFormat.TRANSLUCENT);
params.receiveInsetsIgnoringZOrder = true;
- params.privateFlags |= PRIVATE_FLAG_EXCLUDE_FROM_SCREEN_MAGNIFICATION;
+ params.privateFlags |=
+ PRIVATE_FLAG_EXCLUDE_FROM_SCREEN_MAGNIFICATION | SYSTEM_FLAG_SHOW_FOR_ALL_USERS;
params.windowAnimations = android.R.style.Animation_Translucent;
// Insets are configured to allow the menu to display over navigation and system bars.
params.setFitInsetsTypes(0);
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
index d0cb507789a1..eee5f9e34317 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
@@ -1011,6 +1011,16 @@ public class AuthController implements
}
/**
+ * @return true if optical udfps HW is supported on this device. Can return true even if
+ * the user has not enrolled udfps. This may be false if called before
+ * onAllAuthenticatorsRegistered.
+ */
+ public boolean isOpticalUdfpsSupported() {
+ return getUdfpsProps() != null && !getUdfpsProps().isEmpty() && getUdfpsProps()
+ .get(0).isOpticalUdfps();
+ }
+
+ /**
* @return true if ultrasonic udfps HW is supported on this device. Can return true even if
* the user has not enrolled udfps. This may be false if called before
* onAllAuthenticatorsRegistered.
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorImpl.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorImpl.kt
index 4dc2a13480f5..0303048436c9 100644
--- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorImpl.kt
@@ -104,6 +104,31 @@ constructor(
}
}
+ override suspend fun onActionIconClick(deviceItem: DeviceItem, onIntent: (Intent) -> Unit) {
+ withContext(backgroundDispatcher) {
+ if (!audioSharingInteractor.audioSharingAvailable()) {
+ return@withContext deviceItemActionInteractorImpl.onActionIconClick(
+ deviceItem,
+ onIntent,
+ )
+ }
+
+ when (deviceItem.type) {
+ DeviceItemType.AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE -> {
+ uiEventLogger.log(BluetoothTileDialogUiEvent.CHECK_MARK_ACTION_BUTTON_CLICKED)
+ audioSharingInteractor.stopAudioSharing()
+ }
+ DeviceItemType.AVAILABLE_AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE -> {
+ uiEventLogger.log(BluetoothTileDialogUiEvent.PLUS_ACTION_BUTTON_CLICKED)
+ audioSharingInteractor.startAudioSharing()
+ }
+ else -> {
+ deviceItemActionInteractorImpl.onActionIconClick(deviceItem, onIntent)
+ }
+ }
+ }
+ }
+
private fun inSharingAndDeviceNoSource(
inAudioSharing: Boolean,
deviceItem: DeviceItem,
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractor.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractor.kt
index c4f26cd46bf8..116e76c82008 100644
--- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractor.kt
@@ -29,6 +29,7 @@ import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.flatMapLatest
@@ -54,6 +55,8 @@ interface AudioSharingInteractor {
suspend fun startAudioSharing()
+ suspend fun stopAudioSharing()
+
suspend fun audioSharingAvailable(): Boolean
suspend fun qsDialogImprovementAvailable(): Boolean
@@ -61,7 +64,7 @@ interface AudioSharingInteractor {
@SysUISingleton
@OptIn(ExperimentalCoroutinesApi::class)
-class AudioSharingInteractorImpl
+open class AudioSharingInteractorImpl
@Inject
constructor(
private val context: Context,
@@ -99,6 +102,9 @@ constructor(
if (audioSharingAvailable()) {
audioSharingRepository.leAudioBroadcastProfile?.let { profile ->
isAudioSharingOn
+ // Skip the default value, we only care about adding source for newly
+ // started audio sharing session
+ .drop(1)
.mapNotNull { audioSharingOn ->
if (audioSharingOn) {
// onBroadcastMetadataChanged could emit multiple times during one
@@ -145,6 +151,13 @@ constructor(
audioSharingRepository.startAudioSharing()
}
+ override suspend fun stopAudioSharing() {
+ if (!audioSharingAvailable()) {
+ return
+ }
+ audioSharingRepository.stopAudioSharing()
+ }
+
// TODO(b/367965193): Move this after flags rollout
override suspend fun audioSharingAvailable(): Boolean {
return audioSharingRepository.audioSharingAvailable()
@@ -181,6 +194,8 @@ class AudioSharingInteractorEmptyImpl @Inject constructor() : AudioSharingIntera
override suspend fun startAudioSharing() {}
+ override suspend fun stopAudioSharing() {}
+
override suspend fun audioSharingAvailable(): Boolean = false
override suspend fun qsDialogImprovementAvailable(): Boolean = false
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingRepository.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingRepository.kt
index b9b8d36d41e6..44f9769f5930 100644
--- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingRepository.kt
@@ -45,6 +45,8 @@ interface AudioSharingRepository {
suspend fun setActive(cachedBluetoothDevice: CachedBluetoothDevice)
suspend fun startAudioSharing()
+
+ suspend fun stopAudioSharing()
}
@SysUISingleton
@@ -100,6 +102,15 @@ class AudioSharingRepositoryImpl(
leAudioBroadcastProfile?.startPrivateBroadcast()
}
}
+
+ override suspend fun stopAudioSharing() {
+ withContext(backgroundDispatcher) {
+ if (!settingsLibAudioSharingRepository.audioSharingAvailable()) {
+ return@withContext
+ }
+ leAudioBroadcastProfile?.stopLatestBroadcast()
+ }
+ }
}
@SysUISingleton
@@ -117,4 +128,6 @@ class AudioSharingRepositoryEmptyImpl : AudioSharingRepository {
override suspend fun setActive(cachedBluetoothDevice: CachedBluetoothDevice) {}
override suspend fun startAudioSharing() {}
+
+ override suspend fun stopAudioSharing() {}
}
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegate.kt
index b294dd1b0b71..56caddfbd637 100644
--- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegate.kt
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegate.kt
@@ -56,6 +56,13 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.isActive
import kotlinx.coroutines.withContext
+data class DeviceItemClick(val deviceItem: DeviceItem, val clickedView: View, val target: Target) {
+ enum class Target {
+ ENTIRE_ROW,
+ ACTION_ICON,
+ }
+}
+
/** Dialog for showing active, connected and saved bluetooth devices. */
class BluetoothTileDialogDelegate
@AssistedInject
@@ -80,7 +87,7 @@ internal constructor(
internal val bluetoothAutoOnToggle
get() = mutableBluetoothAutoOnToggle.asStateFlow()
- private val mutableDeviceItemClick: MutableSharedFlow<DeviceItem> =
+ private val mutableDeviceItemClick: MutableSharedFlow<DeviceItemClick> =
MutableSharedFlow(extraBufferCapacity = 1)
internal val deviceItemClick
get() = mutableDeviceItemClick.asSharedFlow()
@@ -90,7 +97,7 @@ internal constructor(
internal val contentHeight
get() = mutableContentHeight.asSharedFlow()
- private val deviceItemAdapter: Adapter = Adapter(bluetoothTileDialogCallback)
+ private val deviceItemAdapter: Adapter = Adapter()
private var lastUiUpdateMs: Long = -1
@@ -334,8 +341,7 @@ internal constructor(
}
}
- internal inner class Adapter(private val onClickCallback: BluetoothTileDialogCallback) :
- RecyclerView.Adapter<Adapter.DeviceItemViewHolder>() {
+ internal inner class Adapter : RecyclerView.Adapter<Adapter.DeviceItemViewHolder>() {
private val diffUtilCallback =
object : DiffUtil.ItemCallback<DeviceItem>() {
@@ -376,7 +382,7 @@ internal constructor(
override fun onBindViewHolder(holder: DeviceItemViewHolder, position: Int) {
val item = getItem(position)
- holder.bind(item, onClickCallback)
+ holder.bind(item)
}
internal fun getItem(position: Int) = asyncListDiffer.currentList[position]
@@ -390,19 +396,18 @@ internal constructor(
private val nameView = view.requireViewById<TextView>(R.id.bluetooth_device_name)
private val summaryView = view.requireViewById<TextView>(R.id.bluetooth_device_summary)
private val iconView = view.requireViewById<ImageView>(R.id.bluetooth_device_icon)
- private val iconGear = view.requireViewById<ImageView>(R.id.gear_icon_image)
- private val gearView = view.requireViewById<View>(R.id.gear_icon)
+ private val actionIcon = view.requireViewById<ImageView>(R.id.gear_icon_image)
+ private val actionIconView = view.requireViewById<View>(R.id.gear_icon)
private val divider = view.requireViewById<View>(R.id.divider)
- internal fun bind(
- item: DeviceItem,
- deviceItemOnClickCallback: BluetoothTileDialogCallback,
- ) {
+ internal fun bind(item: DeviceItem) {
container.apply {
isEnabled = item.isEnabled
background = item.background?.let { context.getDrawable(it) }
setOnClickListener {
- mutableDeviceItemClick.tryEmit(item)
+ mutableDeviceItemClick.tryEmit(
+ DeviceItemClick(item, it, DeviceItemClick.Target.ENTIRE_ROW)
+ )
uiEventLogger.log(BluetoothTileDialogUiEvent.DEVICE_CLICKED)
}
@@ -421,7 +426,8 @@ internal constructor(
}
}
- iconGear.apply { drawable?.let { it.mutate()?.setTint(tintColor) } }
+ actionIcon.setImageResource(item.actionIconRes)
+ actionIcon.drawable?.setTint(tintColor)
divider.setBackgroundColor(tintColor)
@@ -454,8 +460,10 @@ internal constructor(
nameView.text = item.deviceName
summaryView.text = item.connectionSummary
- gearView.setOnClickListener {
- deviceItemOnClickCallback.onDeviceItemGearClicked(item, it)
+ actionIconView.setOnClickListener {
+ mutableDeviceItemClick.tryEmit(
+ DeviceItemClick(item, it, DeviceItemClick.Target.ACTION_ICON)
+ )
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogUiEvent.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogUiEvent.kt
index aad233fe40ca..7c66ec059e64 100644
--- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogUiEvent.kt
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogUiEvent.kt
@@ -49,7 +49,7 @@ enum class BluetoothTileDialogUiEvent(val metricId: Int) : UiEventLogger.UiEvent
LAUNCH_SETTINGS_NOT_SHARING_SAVED_LE_DEVICE_CLICKED(1719),
@Deprecated(
"Use case no longer needed",
- ReplaceWith("LAUNCH_SETTINGS_NOT_SHARING_ACTIVE_LE_DEVICE_CLICKED")
+ ReplaceWith("LAUNCH_SETTINGS_NOT_SHARING_ACTIVE_LE_DEVICE_CLICKED"),
)
@UiEvent(doc = "Not broadcasting, one of the two connected LE audio devices is clicked")
LAUNCH_SETTINGS_NOT_SHARING_CONNECTED_LE_DEVICE_CLICKED(1720),
@@ -59,7 +59,11 @@ enum class BluetoothTileDialogUiEvent(val metricId: Int) : UiEventLogger.UiEvent
@UiEvent(doc = "Clicked on switch active button on audio sharing dialog")
AUDIO_SHARING_DIALOG_SWITCH_ACTIVE_CLICKED(1890),
@UiEvent(doc = "Clicked on share audio button on audio sharing dialog")
- AUDIO_SHARING_DIALOG_SHARE_AUDIO_CLICKED(1891);
+ AUDIO_SHARING_DIALOG_SHARE_AUDIO_CLICKED(1891),
+ @UiEvent(doc = "Clicked on plus action button")
+ PLUS_ACTION_BUTTON_CLICKED(2061),
+ @UiEvent(doc = "Clicked on checkmark action button")
+ CHECK_MARK_ACTION_BUTTON_CLICKED(2062);
override fun getId() = metricId
}
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModel.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModel.kt
index 497d8cf2e159..9460e7c2c8d5 100644
--- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModel.kt
@@ -35,7 +35,6 @@ import com.android.systemui.animation.DialogCuj
import com.android.systemui.animation.DialogTransitionAnimator
import com.android.systemui.animation.Expandable
import com.android.systemui.bluetooth.qsdialog.BluetoothTileDialogDelegate.Companion.ACTION_AUDIO_SHARING
-import com.android.systemui.bluetooth.qsdialog.BluetoothTileDialogDelegate.Companion.ACTION_BLUETOOTH_DEVICE_DETAILS
import com.android.systemui.bluetooth.qsdialog.BluetoothTileDialogDelegate.Companion.ACTION_PAIR_NEW_DEVICE
import com.android.systemui.bluetooth.qsdialog.BluetoothTileDialogDelegate.Companion.ACTION_PREVIOUSLY_CONNECTED_DEVICE
import com.android.systemui.dagger.SysUISingleton
@@ -227,8 +226,22 @@ constructor(
// deviceItemClick is emitted when user clicked on a device item.
dialogDelegate.deviceItemClick
.onEach {
- deviceItemActionInteractor.onClick(it, dialog)
- logger.logDeviceClick(it.cachedBluetoothDevice.address, it.type)
+ when (it.target) {
+ DeviceItemClick.Target.ENTIRE_ROW -> {
+ deviceItemActionInteractor.onClick(it.deviceItem, dialog)
+ logger.logDeviceClick(
+ it.deviceItem.cachedBluetoothDevice.address,
+ it.deviceItem.type,
+ )
+ }
+
+ DeviceItemClick.Target.ACTION_ICON -> {
+ deviceItemActionInteractor.onActionIconClick(it.deviceItem) { intent
+ ->
+ startSettingsActivity(intent, it.clickedView)
+ }
+ }
+ }
}
.launchIn(this)
@@ -287,20 +300,6 @@ constructor(
)
}
- override fun onDeviceItemGearClicked(deviceItem: DeviceItem, view: View) {
- uiEventLogger.log(BluetoothTileDialogUiEvent.DEVICE_GEAR_CLICKED)
- val intent =
- Intent(ACTION_BLUETOOTH_DEVICE_DETAILS).apply {
- putExtra(
- EXTRA_SHOW_FRAGMENT_ARGUMENTS,
- Bundle().apply {
- putString("device_address", deviceItem.cachedBluetoothDevice.address)
- },
- )
- }
- startSettingsActivity(intent, view)
- }
-
override fun onSeeAllClicked(view: View) {
uiEventLogger.log(BluetoothTileDialogUiEvent.SEE_ALL_CLICKED)
startSettingsActivity(Intent(ACTION_PREVIOUSLY_CONNECTED_DEVICE), view)
@@ -382,8 +381,6 @@ constructor(
}
interface BluetoothTileDialogCallback {
- fun onDeviceItemGearClicked(deviceItem: DeviceItem, view: View)
-
fun onSeeAllClicked(view: View)
fun onPairNewDeviceClicked(view: View)
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItem.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItem.kt
index 2ba4c73a0293..f7af16d99fbf 100644
--- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItem.kt
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItem.kt
@@ -53,5 +53,6 @@ data class DeviceItem(
val background: Int? = null,
var isEnabled: Boolean = true,
var actionAccessibilityLabel: String = "",
- var isActive: Boolean = false
+ var isActive: Boolean = false,
+ val actionIconRes: Int = -1,
)
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractor.kt
index 2b55e1c51f5f..cb4ec37a1a66 100644
--- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractor.kt
@@ -16,6 +16,8 @@
package com.android.systemui.bluetooth.qsdialog
+import android.content.Intent
+import android.os.Bundle
import com.android.internal.logging.UiEventLogger
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
@@ -25,7 +27,9 @@ import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext
interface DeviceItemActionInteractor {
- suspend fun onClick(deviceItem: DeviceItem, dialog: SystemUIDialog) {}
+ suspend fun onClick(deviceItem: DeviceItem, dialog: SystemUIDialog)
+
+ suspend fun onActionIconClick(deviceItem: DeviceItem, onIntent: (Intent) -> Unit)
}
@SysUISingleton
@@ -67,4 +71,44 @@ constructor(
}
}
}
+
+ override suspend fun onActionIconClick(deviceItem: DeviceItem, onIntent: (Intent) -> Unit) {
+ withContext(backgroundDispatcher) {
+ deviceItem.cachedBluetoothDevice.apply {
+ when (deviceItem.type) {
+ DeviceItemType.ACTIVE_MEDIA_BLUETOOTH_DEVICE,
+ DeviceItemType.AVAILABLE_MEDIA_BLUETOOTH_DEVICE,
+ DeviceItemType.CONNECTED_BLUETOOTH_DEVICE,
+ DeviceItemType.SAVED_BLUETOOTH_DEVICE -> {
+ uiEventLogger.log(BluetoothTileDialogUiEvent.DEVICE_GEAR_CLICKED)
+ val intent =
+ Intent(ACTION_BLUETOOTH_DEVICE_DETAILS).apply {
+ putExtra(
+ EXTRA_SHOW_FRAGMENT_ARGUMENTS,
+ Bundle().apply {
+ putString(
+ "device_address",
+ deviceItem.cachedBluetoothDevice.address,
+ )
+ },
+ )
+ }
+ onIntent(intent)
+ }
+ DeviceItemType.AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE,
+ DeviceItemType.AVAILABLE_AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE -> {
+ throw IllegalArgumentException("Invalid device type: ${deviceItem.type}")
+ // Throw exception. Should already be handled in
+ // AudioSharingDeviceItemActionInteractor.
+ }
+ }
+ }
+ }
+ }
+
+ private companion object {
+ const val EXTRA_SHOW_FRAGMENT_ARGUMENTS = ":settings:show_fragment_args"
+ const val ACTION_BLUETOOTH_DEVICE_DETAILS =
+ "com.android.settings.BLUETOOTH_DEVICE_DETAIL_SETTINGS"
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactory.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactory.kt
index 92f05803f7cf..095e6e741584 100644
--- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactory.kt
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactory.kt
@@ -30,6 +30,8 @@ private val backgroundOff = R.drawable.bluetooth_tile_dialog_bg_off
private val backgroundOffBusy = R.drawable.bluetooth_tile_dialog_bg_off_busy
private val connected = R.string.quick_settings_bluetooth_device_connected
private val audioSharing = R.string.quick_settings_bluetooth_device_audio_sharing
+private val audioSharingAddIcon = R.drawable.ic_add
+private val audioSharingOnGoingIcon = R.drawable.ic_check
private val saved = R.string.quick_settings_bluetooth_device_saved
private val actionAccessibilityLabelActivate =
R.string.accessibility_quick_settings_bluetooth_device_tap_to_activate
@@ -63,6 +65,7 @@ abstract class DeviceItemFactory {
background: Int,
actionAccessibilityLabel: String,
isActive: Boolean,
+ actionIconRes: Int = R.drawable.ic_settings_24dp,
): DeviceItem {
return DeviceItem(
type = type,
@@ -75,6 +78,7 @@ abstract class DeviceItemFactory {
isEnabled = !cachedDevice.isBusy,
actionAccessibilityLabel = actionAccessibilityLabel,
isActive = isActive,
+ actionIconRes = actionIconRes,
)
}
}
@@ -125,6 +129,7 @@ internal class AudioSharingMediaDeviceItemFactory(
if (cachedDevice.isBusy) backgroundOffBusy else backgroundOn,
"",
isActive = !cachedDevice.isBusy,
+ actionIconRes = audioSharingOnGoingIcon,
)
}
}
@@ -156,6 +161,7 @@ internal class AvailableAudioSharingMediaDeviceItemFactory(
if (cachedDevice.isBusy) backgroundOffBusy else backgroundOff,
"",
isActive = false,
+ actionIconRes = audioSharingAddIcon,
)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalTransitionKeys.kt b/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalTransitionKeys.kt
index 78156dbc8964..ca49de3b1510 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalTransitionKeys.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalTransitionKeys.kt
@@ -25,6 +25,7 @@ import com.android.compose.animation.scene.TransitionKey
*/
object CommunalTransitionKeys {
/** Fades the glanceable hub without any translation */
+ @Deprecated("No longer supported as all hub transitions will be fades.")
val SimpleFade = TransitionKey("SimpleFade")
/** Transition from the glanceable hub before entering edit mode */
val ToEditMode = TransitionKey("ToEditMode")
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java b/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java
index 00eead6eb7fc..555fe6ef157d 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java
@@ -31,6 +31,7 @@ import com.android.systemui.statusbar.NotificationInsetsModule;
import com.android.systemui.statusbar.QsFrameTranslateModule;
import com.android.systemui.statusbar.phone.ConfigurationForwarder;
import com.android.systemui.statusbar.policy.ConfigurationController;
+import com.android.wm.shell.appzoomout.AppZoomOut;
import com.android.wm.shell.back.BackAnimation;
import com.android.wm.shell.bubbles.Bubbles;
import com.android.wm.shell.desktopmode.DesktopMode;
@@ -115,6 +116,9 @@ public interface SysUIComponent {
@BindsInstance
Builder setDesktopMode(Optional<DesktopMode> d);
+ @BindsInstance
+ Builder setAppZoomOut(Optional<AppZoomOut> a);
+
SysUIComponent build();
}
diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractor.kt
index 41a59a959771..ae6238724042 100644
--- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractor.kt
@@ -22,6 +22,7 @@ import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dump.DumpManager
import com.android.systemui.keyevent.domain.interactor.KeyEventInteractor
import com.android.systemui.keyguard.data.repository.BiometricSettingsRepository
+import com.android.systemui.keyguard.domain.interactor.KeyguardBypassInteractor
import com.android.systemui.power.domain.interactor.PowerInteractor
import com.android.systemui.power.shared.model.WakeSleepReason
import com.android.systemui.util.kotlin.FlowDumperImpl
@@ -50,6 +51,8 @@ class DeviceEntryHapticsInteractor
constructor(
biometricSettingsRepository: BiometricSettingsRepository,
deviceEntryBiometricAuthInteractor: DeviceEntryBiometricAuthInteractor,
+ deviceEntryFaceAuthInteractor: DeviceEntryFaceAuthInteractor,
+ keyguardBypassInteractor: KeyguardBypassInteractor,
deviceEntryFingerprintAuthInteractor: DeviceEntryFingerprintAuthInteractor,
deviceEntrySourceInteractor: DeviceEntrySourceInteractor,
fingerprintPropertyRepository: FingerprintPropertyRepository,
@@ -82,7 +85,7 @@ constructor(
emit(recentPowerButtonPressThresholdMs * -1L - 1L)
}
- val playSuccessHaptic: Flow<Unit> =
+ private val playHapticsOnDeviceEntry: Flow<Boolean> =
deviceEntrySourceInteractor.deviceEntryFromBiometricSource
.sample(
combine(
@@ -92,17 +95,29 @@ constructor(
::Triple,
)
)
- .filter { (sideFpsEnrolled, powerButtonDown, lastPowerButtonWakeup) ->
+ .map { (sideFpsEnrolled, powerButtonDown, lastPowerButtonWakeup) ->
val sideFpsAllowsHaptic =
!powerButtonDown &&
systemClock.uptimeMillis() - lastPowerButtonWakeup >
recentPowerButtonPressThresholdMs
val allowHaptic = !sideFpsEnrolled || sideFpsAllowsHaptic
if (!allowHaptic) {
- logger.d("Skip success haptic. Recent power button press or button is down.")
+ logger.d(
+ "Skip success entry haptic from power button. Recent power button press or button is down."
+ )
}
allowHaptic
}
+
+ private val playHapticsOnFaceAuthSuccessAndBypassDisabled: Flow<Boolean> =
+ deviceEntryFaceAuthInteractor.isAuthenticated
+ .filter { it }
+ .sample(keyguardBypassInteractor.isBypassAvailable)
+ .map { !it }
+
+ val playSuccessHaptic: Flow<Unit> =
+ merge(playHapticsOnDeviceEntry, playHapticsOnFaceAuthSuccessAndBypassDisabled)
+ .filter { it }
// map to Unit
.map {}
.dumpWhileCollecting("playSuccessHaptic")
diff --git a/packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractor.kt b/packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractor.kt
index 21002c676ec6..d7a4dba3188a 100644
--- a/packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractor.kt
@@ -278,11 +278,11 @@ constructor(
}
private suspend fun hasInitialDelayElapsed(deviceType: DeviceType): Boolean {
- val oobeLaunchTime =
- tutorialRepository.getScheduledTutorialLaunchTime(deviceType) ?: return false
- return clock
- .instant()
- .isAfter(oobeLaunchTime.plusSeconds(initialDelayDuration.inWholeSeconds))
+ val oobeTime =
+ tutorialRepository.getScheduledTutorialLaunchTime(deviceType)
+ ?: tutorialRepository.getNotifiedTime(deviceType)
+ ?: return false
+ return clock.instant().isAfter(oobeTime.plusSeconds(initialDelayDuration.inWholeSeconds))
}
private data class StatsUpdateRequest(
diff --git a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
index 129a6bb72996..2ed0671a570b 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
@@ -23,13 +23,7 @@ import com.android.server.notification.Flags.crossAppPoliteNotifications
import com.android.server.notification.Flags.politeNotifications
import com.android.server.notification.Flags.vibrateWhileUnlocked
import com.android.systemui.Flags.FLAG_COMMUNAL_HUB
-import com.android.systemui.Flags.FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON
-import com.android.systemui.Flags.FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS
-import com.android.systemui.Flags.FLAG_STATUS_BAR_USE_REPOS_FOR_CALL_CHIP
import com.android.systemui.Flags.communalHub
-import com.android.systemui.Flags.statusBarCallChipNotificationIcon
-import com.android.systemui.Flags.statusBarScreenSharingChips
-import com.android.systemui.Flags.statusBarUseReposForCallChip
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.scene.shared.flag.SceneContainerFlag
import com.android.systemui.shade.shared.flag.DualShade
@@ -63,10 +57,6 @@ class FlagDependencies @Inject constructor(featureFlags: FeatureFlagsClassic, ha
// DualShade dependencies
DualShade.token dependsOn SceneContainerFlag.getMainAconfigFlag()
-
- // Status bar chip dependencies
- statusBarCallChipNotificationIconToken dependsOn statusBarUseReposForCallChipToken
- statusBarCallChipNotificationIconToken dependsOn statusBarScreenSharingChipsToken
}
private inline val politeNotifications
@@ -86,17 +76,4 @@ class FlagDependencies @Inject constructor(featureFlags: FeatureFlagsClassic, ha
private inline val communalHub
get() = FlagToken(FLAG_COMMUNAL_HUB, communalHub())
-
- private inline val statusBarCallChipNotificationIconToken
- get() =
- FlagToken(
- FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON,
- statusBarCallChipNotificationIcon(),
- )
-
- private inline val statusBarScreenSharingChipsToken
- get() = FlagToken(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS, statusBarScreenSharingChips())
-
- private inline val statusBarUseReposForCallChipToken
- get() = FlagToken(FLAG_STATUS_BAR_USE_REPOS_FOR_CALL_CHIP, statusBarUseReposForCallChip())
}
diff --git a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java
index e1ebf7cdf472..cf5c3402792e 100644
--- a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java
+++ b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java
@@ -463,6 +463,9 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene
mTelephonyListenerManager.removeServiceStateListener(mPhoneStateListener);
mGlobalSettings.unregisterContentObserverSync(mAirplaneModeObserver);
mConfigurationController.removeCallback(this);
+ if (mShowSilentToggle) {
+ mRingerModeTracker.getRingerMode().removeObservers(this);
+ }
}
protected Context getContext() {
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/InputGestureMaps.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/InputGestureMaps.kt
index e255bdea6100..6a42cdc876ca 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/InputGestureMaps.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/InputGestureMaps.kt
@@ -41,6 +41,8 @@ import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVI
import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_TAKE_SCREENSHOT
import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_MAXIMIZE_FREEFORM_WINDOW
import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_NOTIFICATION_PANEL
+import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_VOICE_ACCESS
+import com.android.hardware.input.Flags.enableVoiceAccessKeyGestures
import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.AppCategories
import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.MultiTasking
import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.System
@@ -63,6 +65,7 @@ class InputGestureMaps @Inject constructor(private val context: Context) {
KEY_GESTURE_TYPE_LAUNCH_ASSISTANT to System,
KEY_GESTURE_TYPE_LAUNCH_VOICE_ASSISTANT to System,
KEY_GESTURE_TYPE_ALL_APPS to System,
+ KEY_GESTURE_TYPE_TOGGLE_VOICE_ACCESS to System,
// Multitasking Category
KEY_GESTURE_TYPE_RECENT_APPS_SWITCHER to MultiTasking,
@@ -100,6 +103,7 @@ class InputGestureMaps @Inject constructor(private val context: Context) {
KEY_GESTURE_TYPE_LAUNCH_ASSISTANT to R.string.shortcut_helper_category_system_apps,
KEY_GESTURE_TYPE_LAUNCH_VOICE_ASSISTANT to
R.string.shortcut_helper_category_system_apps,
+ KEY_GESTURE_TYPE_TOGGLE_VOICE_ACCESS to R.string.shortcut_helper_category_system_apps,
// Multitasking Category
KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_LEFT to
@@ -148,6 +152,7 @@ class InputGestureMaps @Inject constructor(private val context: Context) {
KEY_GESTURE_TYPE_LAUNCH_ASSISTANT to R.string.group_system_access_google_assistant,
KEY_GESTURE_TYPE_LAUNCH_VOICE_ASSISTANT to
R.string.group_system_access_google_assistant,
+ KEY_GESTURE_TYPE_TOGGLE_VOICE_ACCESS to R.string.group_system_toggle_voice_access,
// Multitasking Category
KEY_GESTURE_TYPE_RECENT_APPS_SWITCHER to R.string.group_system_cycle_forward,
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/source/SystemShortcutsSource.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/source/SystemShortcutsSource.kt
index 5060abdda247..c3c9df97a682 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/source/SystemShortcutsSource.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/source/SystemShortcutsSource.kt
@@ -32,12 +32,14 @@ import android.view.KeyEvent.KEYCODE_RECENT_APPS
import android.view.KeyEvent.KEYCODE_S
import android.view.KeyEvent.KEYCODE_SLASH
import android.view.KeyEvent.KEYCODE_TAB
+import android.view.KeyEvent.KEYCODE_V
import android.view.KeyEvent.META_ALT_ON
import android.view.KeyEvent.META_CTRL_ON
import android.view.KeyEvent.META_META_ON
import android.view.KeyEvent.META_SHIFT_ON
import android.view.KeyboardShortcutGroup
import android.view.KeyboardShortcutInfo
+import com.android.hardware.input.Flags.enableVoiceAccessKeyGestures
import com.android.systemui.Flags.shortcutHelperKeyGlyph
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.keyboard.shortcut.data.model.shortcutInfo
@@ -118,8 +120,8 @@ constructor(@Main private val resources: Resources, private val inputManager: In
return shortcuts
}
- private fun systemControlsShortcuts() =
- listOf(
+ private fun systemControlsShortcuts(): List<KeyboardShortcutInfo> {
+ val shortcuts = mutableListOf(
// Access list of all apps and search (i.e. Search/Launcher):
// - Meta
shortcutInfo(resources.getString(R.string.group_system_access_all_apps_search)) {
@@ -176,6 +178,19 @@ constructor(@Main private val resources: Resources, private val inputManager: In
},
)
+ if (enableVoiceAccessKeyGestures()) {
+ shortcuts.add(
+ // Toggle voice access:
+ // - Meta + Alt + V
+ shortcutInfo(resources.getString(R.string.group_system_toggle_voice_access)) {
+ command(META_META_ON or META_ALT_ON, KEYCODE_V)
+ }
+ )
+ }
+
+ return shortcuts
+ }
+
private fun systemAppsShortcuts() =
listOf(
// Pull up Notes app for quick memo:
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/ShortcutCustomizationDialogStarter.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/ShortcutCustomizationDialogStarter.kt
index 274fa59045d7..a16b4a6892b4 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/ShortcutCustomizationDialogStarter.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/ShortcutCustomizationDialogStarter.kt
@@ -53,11 +53,11 @@ constructor(
override suspend fun onActivated(): Nothing {
viewModel.shortcutCustomizationUiState.collect { uiState ->
- when(uiState){
+ when (uiState) {
is AddShortcutDialog,
is DeleteShortcutDialog,
is ResetShortcutDialog -> {
- if (dialog == null){
+ if (dialog == null) {
dialog = createDialog().also { it.show() }
}
}
@@ -85,7 +85,9 @@ constructor(
ShortcutCustomizationDialog(
uiState = uiState,
modifier = Modifier.width(364.dp).wrapContentHeight().padding(vertical = 24.dp),
- onKeyPress = { viewModel.onKeyPressed(it) },
+ onShortcutKeyCombinationSelected = {
+ viewModel.onShortcutKeyCombinationSelected(it)
+ },
onCancel = { dialog.dismiss() },
onConfirmSetShortcut = { coroutineScope.launch { viewModel.onSetShortcut() } },
onConfirmDeleteShortcut = {
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutCustomizer.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutCustomizer.kt
index 3819f6d41856..d9e55f89cda5 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutCustomizer.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutCustomizer.kt
@@ -49,8 +49,12 @@ import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusProperties
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.KeyEvent
-import androidx.compose.ui.input.key.onKeyEvent
+import androidx.compose.ui.input.key.KeyEventType
+import androidx.compose.ui.input.key.key
+import androidx.compose.ui.input.key.onPreviewKeyEvent
+import androidx.compose.ui.input.key.type
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
@@ -65,7 +69,7 @@ import com.android.systemui.res.R
fun ShortcutCustomizationDialog(
uiState: ShortcutCustomizationUiState,
modifier: Modifier = Modifier,
- onKeyPress: (KeyEvent) -> Boolean,
+ onShortcutKeyCombinationSelected: (KeyEvent) -> Boolean,
onCancel: () -> Unit,
onConfirmSetShortcut: () -> Unit,
onConfirmDeleteShortcut: () -> Unit,
@@ -73,7 +77,13 @@ fun ShortcutCustomizationDialog(
) {
when (uiState) {
is ShortcutCustomizationUiState.AddShortcutDialog -> {
- AddShortcutDialog(modifier, uiState, onKeyPress, onCancel, onConfirmSetShortcut)
+ AddShortcutDialog(
+ modifier,
+ uiState,
+ onShortcutKeyCombinationSelected,
+ onCancel,
+ onConfirmSetShortcut,
+ )
}
is ShortcutCustomizationUiState.DeleteShortcutDialog -> {
DeleteShortcutDialog(modifier, onCancel, onConfirmDeleteShortcut)
@@ -91,29 +101,27 @@ fun ShortcutCustomizationDialog(
private fun AddShortcutDialog(
modifier: Modifier,
uiState: ShortcutCustomizationUiState.AddShortcutDialog,
- onKeyPress: (KeyEvent) -> Boolean,
+ onShortcutKeyCombinationSelected: (KeyEvent) -> Boolean,
onCancel: () -> Unit,
- onConfirmSetShortcut: () -> Unit
-){
+ onConfirmSetShortcut: () -> Unit,
+) {
Column(modifier = modifier) {
Title(uiState.shortcutLabel)
Description(
- text =
- stringResource(
- id = R.string.shortcut_customize_mode_add_shortcut_description
- )
+ text = stringResource(id = R.string.shortcut_customize_mode_add_shortcut_description)
)
PromptShortcutModifier(
modifier =
- Modifier.padding(top = 24.dp, start = 116.5.dp, end = 116.5.dp)
- .width(131.dp)
- .height(48.dp),
+ Modifier.padding(top = 24.dp, start = 116.5.dp, end = 116.5.dp)
+ .width(131.dp)
+ .height(48.dp),
defaultModifierKey = uiState.defaultCustomShortcutModifierKey,
)
SelectedKeyCombinationContainer(
shouldShowError = uiState.errorMessage.isNotEmpty(),
- onKeyPress = onKeyPress,
+ onShortcutKeyCombinationSelected = onShortcutKeyCombinationSelected,
pressedKeys = uiState.pressedKeys,
+ onConfirmSetShortcut = onConfirmSetShortcut,
)
ErrorMessageContainer(uiState.errorMessage)
DialogButtons(
@@ -121,9 +129,7 @@ private fun AddShortcutDialog(
isConfirmButtonEnabled = uiState.pressedKeys.isNotEmpty(),
onConfirm = onConfirmSetShortcut,
confirmButtonText =
- stringResource(
- R.string.shortcut_helper_customize_dialog_set_shortcut_button_label
- ),
+ stringResource(R.string.shortcut_helper_customize_dialog_set_shortcut_button_label),
)
}
}
@@ -132,20 +138,15 @@ private fun AddShortcutDialog(
private fun DeleteShortcutDialog(
modifier: Modifier,
onCancel: () -> Unit,
- onConfirmDeleteShortcut: () -> Unit
-){
+ onConfirmDeleteShortcut: () -> Unit,
+) {
ConfirmationDialog(
modifier = modifier,
- title =
- stringResource(
- id = R.string.shortcut_customize_mode_remove_shortcut_dialog_title
- ),
+ title = stringResource(id = R.string.shortcut_customize_mode_remove_shortcut_dialog_title),
description =
- stringResource(
- id = R.string.shortcut_customize_mode_remove_shortcut_description
- ),
+ stringResource(id = R.string.shortcut_customize_mode_remove_shortcut_description),
confirmButtonText =
- stringResource(R.string.shortcut_helper_customize_dialog_remove_button_label),
+ stringResource(R.string.shortcut_helper_customize_dialog_remove_button_label),
onCancel = onCancel,
onConfirm = onConfirmDeleteShortcut,
)
@@ -155,20 +156,15 @@ private fun DeleteShortcutDialog(
private fun ResetShortcutDialog(
modifier: Modifier,
onCancel: () -> Unit,
- onConfirmResetShortcut: () -> Unit
-){
+ onConfirmResetShortcut: () -> Unit,
+) {
ConfirmationDialog(
modifier = modifier,
- title =
- stringResource(
- id = R.string.shortcut_customize_mode_reset_shortcut_dialog_title
- ),
+ title = stringResource(id = R.string.shortcut_customize_mode_reset_shortcut_dialog_title),
description =
- stringResource(
- id = R.string.shortcut_customize_mode_reset_shortcut_description
- ),
+ stringResource(id = R.string.shortcut_customize_mode_reset_shortcut_description),
confirmButtonText =
- stringResource(R.string.shortcut_helper_customize_dialog_reset_button_label),
+ stringResource(R.string.shortcut_helper_customize_dialog_reset_button_label),
onCancel = onCancel,
onConfirm = onConfirmResetShortcut,
)
@@ -201,6 +197,9 @@ private fun DialogButtons(
onConfirm: () -> Unit,
confirmButtonText: String,
) {
+ val focusRequester = remember { FocusRequester() }
+ LaunchedEffect(Unit) { focusRequester.requestFocus() }
+
Row(
modifier =
Modifier.padding(top = 24.dp, start = 24.dp, end = 24.dp)
@@ -218,6 +217,10 @@ private fun DialogButtons(
)
Spacer(modifier = Modifier.width(8.dp))
ShortcutHelperButton(
+ modifier =
+ Modifier.focusRequester(focusRequester).focusProperties {
+ canFocus = true
+ }, // enable focus on touch/click mode
onClick = onConfirm,
color = MaterialTheme.colorScheme.primary,
width = 116.dp,
@@ -248,8 +251,9 @@ private fun ErrorMessageContainer(errorMessage: String) {
@Composable
private fun SelectedKeyCombinationContainer(
shouldShowError: Boolean,
- onKeyPress: (KeyEvent) -> Boolean,
+ onShortcutKeyCombinationSelected: (KeyEvent) -> Boolean,
pressedKeys: List<ShortcutKey>,
+ onConfirmSetShortcut: () -> Unit,
) {
val interactionSource = remember { MutableInteractionSource() }
val isFocused by interactionSource.collectIsFocusedAsState()
@@ -269,7 +273,17 @@ private fun SelectedKeyCombinationContainer(
Modifier.padding(all = 16.dp)
.sizeIn(minWidth = 332.dp, minHeight = 56.dp)
.border(width = 2.dp, color = outlineColor, shape = RoundedCornerShape(50.dp))
- .onKeyEvent { onKeyPress(it) }
+ .onPreviewKeyEvent { keyEvent ->
+ val keyEventProcessed = onShortcutKeyCombinationSelected(keyEvent)
+ if (
+ !keyEventProcessed &&
+ keyEvent.key == Key.Enter &&
+ keyEvent.type == KeyEventType.KeyUp
+ ) {
+ onConfirmSetShortcut()
+ true
+ } else keyEventProcessed
+ }
.focusProperties { canFocus = true } // enables keyboard focus when in touch mode
.focusRequester(focusRequester),
interactionSource = interactionSource,
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutCustomizationViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutCustomizationViewModel.kt
index 373eb250d61d..915a66c43a12 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutCustomizationViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutCustomizationViewModel.kt
@@ -46,6 +46,7 @@ constructor(
private val context: Context,
private val shortcutCustomizationInteractor: ShortcutCustomizationInteractor,
) {
+ private var keyDownEventCache: KeyEvent? = null
private val _shortcutCustomizationUiState =
MutableStateFlow<ShortcutCustomizationUiState>(ShortcutCustomizationUiState.Inactive)
@@ -94,9 +95,16 @@ constructor(
shortcutCustomizationInteractor.updateUserSelectedKeyCombination(null)
}
- fun onKeyPressed(keyEvent: KeyEvent): Boolean {
- if ((keyEvent.isMetaPressed && keyEvent.type == KeyEventType.KeyDown)) {
- updatePressedKeys(keyEvent)
+ fun onShortcutKeyCombinationSelected(keyEvent: KeyEvent): Boolean {
+ if (isModifier(keyEvent)) {
+ return false
+ }
+ if (keyEvent.isMetaPressed && keyEvent.type == KeyEventType.KeyDown) {
+ keyDownEventCache = keyEvent
+ return true
+ } else if (keyEvent.type == KeyEventType.KeyUp && keyEvent.key == keyDownEventCache?.key) {
+ updatePressedKeys(keyDownEventCache!!)
+ clearKeyDownEventCache()
return true
}
return false
@@ -157,16 +165,21 @@ constructor(
return (uiState as? AddShortcutDialog)?.copy(errorMessage = errorMessage) ?: uiState
}
+ private fun isModifier(keyEvent: KeyEvent) = SUPPORTED_MODIFIERS.contains(keyEvent.key)
+
private fun updatePressedKeys(keyEvent: KeyEvent) {
- val isModifier = SUPPORTED_MODIFIERS.contains(keyEvent.key)
val keyCombination =
KeyCombination(
modifiers = keyEvent.nativeKeyEvent.modifiers,
- keyCode = if (!isModifier) keyEvent.key.nativeKeyCode else null,
+ keyCode = if (!isModifier(keyEvent)) keyEvent.key.nativeKeyCode else null,
)
shortcutCustomizationInteractor.updateUserSelectedKeyCombination(keyCombination)
}
+ private fun clearKeyDownEventCache() {
+ keyDownEventCache = null
+ }
+
@AssistedFactory
interface Factory {
fun create(): ShortcutCustomizationViewModel
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
index fbe31bbf36e6..8f7f2a0a8cbb 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
@@ -44,7 +44,6 @@ import com.android.systemui.keyguard.shared.model.KeyguardState.GONE
import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN
import com.android.systemui.keyguard.shared.model.KeyguardState.OCCLUDED
import com.android.systemui.keyguard.shared.model.StatusBarState
-import com.android.systemui.power.domain.interactor.PowerInteractor
import com.android.systemui.res.R
import com.android.systemui.scene.domain.interactor.SceneInteractor
import com.android.systemui.scene.shared.flag.SceneContainerFlag
@@ -84,7 +83,6 @@ class KeyguardInteractor
@Inject
constructor(
private val repository: KeyguardRepository,
- powerInteractor: PowerInteractor,
bouncerRepository: KeyguardBouncerRepository,
@ShadeDisplayAware configurationInteractor: ConfigurationInteractor,
shadeRepository: ShadeRepository,
@@ -216,11 +214,7 @@ constructor(
// should actually be quite strange to leave AOD and then go straight to
// DREAMING so this should be fine.
delay(IS_ABLE_TO_DREAM_DELAY_MS)
- isDreaming
- .sample(powerInteractor.isAwake) { isDreaming, isAwake ->
- isDreaming && isAwake
- }
- .debounce(50L)
+ isDreaming.debounce(50L)
} else {
flowOf(false)
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/BlurConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/BlurConfig.kt
index 542fb9b46bef..3eb8522e0338 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/BlurConfig.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/BlurConfig.kt
@@ -23,4 +23,10 @@ data class BlurConfig(val minBlurRadiusPx: Float, val maxBlurRadiusPx: Float) {
// No-op config that will be used by dagger of other SysUI variants which don't blur the
// background surface.
@Inject constructor() : this(0.0f, 0.0f)
+
+ companion object {
+ // Blur the shade much lesser than the background surface so that the surface is
+ // distinguishable from the background.
+ @JvmStatic fun Float.maxBlurRadiusToNotificationPanelBlurRadius(): Float = this / 3.0f
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/PrimaryBouncerTransition.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/PrimaryBouncerTransition.kt
index e77e9dd9e9ed..eb1afb406d2b 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/PrimaryBouncerTransition.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/PrimaryBouncerTransition.kt
@@ -30,6 +30,9 @@ interface PrimaryBouncerTransition {
/** Radius of blur applied to the window's root view. */
val windowBlurRadius: Flow<Float>
+ /** Radius of blur applied to the notifications on expanded shade */
+ val notificationBlurRadius: Flow<Float>
+
fun transitionProgressToBlurRadius(
starBlurRadius: Float,
endBlurRadius: Float,
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToPrimaryBouncerTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToPrimaryBouncerTransitionViewModel.kt
index f17455788d6e..92bb5e6029cb 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToPrimaryBouncerTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToPrimaryBouncerTransitionViewModel.kt
@@ -16,6 +16,7 @@
package com.android.systemui.keyguard.ui.viewmodel
+import com.android.systemui.Flags
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.keyguard.domain.interactor.FromAlternateBouncerTransitionInteractor
import com.android.systemui.keyguard.shared.model.Edge
@@ -23,6 +24,7 @@ import com.android.systemui.keyguard.shared.model.KeyguardState.ALTERNATE_BOUNCE
import com.android.systemui.keyguard.shared.model.KeyguardState.PRIMARY_BOUNCER
import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
import com.android.systemui.keyguard.ui.transitions.BlurConfig
+import com.android.systemui.keyguard.ui.transitions.BlurConfig.Companion.maxBlurRadiusToNotificationPanelBlurRadius
import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition
import com.android.systemui.keyguard.ui.transitions.PrimaryBouncerTransition
import com.android.systemui.scene.shared.flag.SceneContainerFlag
@@ -73,7 +75,28 @@ constructor(
val lockscreenAlpha: Flow<Float> = if (WindowBlurFlag.isEnabled) alphaFlow else emptyFlow()
- val notificationAlpha: Flow<Float> = alphaFlow
+ val notificationAlpha: Flow<Float> =
+ if (Flags.bouncerUiRevamp()) {
+ shadeDependentFlows.transitionFlow(
+ flowWhenShadeIsNotExpanded = lockscreenAlpha,
+ flowWhenShadeIsExpanded = transitionAnimation.immediatelyTransitionTo(1f),
+ )
+ } else {
+ alphaFlow
+ }
+
+ override val notificationBlurRadius: Flow<Float> =
+ if (Flags.bouncerUiRevamp()) {
+ shadeDependentFlows.transitionFlow(
+ flowWhenShadeIsNotExpanded = emptyFlow(),
+ flowWhenShadeIsExpanded =
+ transitionAnimation.immediatelyTransitionTo(
+ blurConfig.maxBlurRadiusPx.maxBlurRadiusToNotificationPanelBlurRadius()
+ ),
+ )
+ } else {
+ emptyFlow<Float>()
+ }
override val deviceEntryParentViewAlpha: Flow<Float> =
transitionAnimation.immediatelyTransitionTo(0f)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToPrimaryBouncerTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToPrimaryBouncerTransitionViewModel.kt
index dbb6a49e7844..e3b55874de6f 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToPrimaryBouncerTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToPrimaryBouncerTransitionViewModel.kt
@@ -53,4 +53,7 @@ constructor(blurConfig: BlurConfig, animationFlow: KeyguardTransitionAnimationFl
override val windowBlurRadius: Flow<Float> =
transitionAnimation.immediatelyTransitionTo(blurConfig.maxBlurRadiusPx)
+
+ override val notificationBlurRadius: Flow<Float> =
+ transitionAnimation.immediatelyTransitionTo(0.0f)
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DozingToPrimaryBouncerTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DozingToPrimaryBouncerTransitionViewModel.kt
index d8b617a60129..c937d5c6453d 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DozingToPrimaryBouncerTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DozingToPrimaryBouncerTransitionViewModel.kt
@@ -64,4 +64,6 @@ constructor(private val blurConfig: BlurConfig, animationFlow: KeyguardTransitio
},
onFinish = { blurConfig.maxBlurRadiusPx },
)
+ override val notificationBlurRadius: Flow<Float> =
+ transitionAnimation.immediatelyTransitionTo(0f)
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToPrimaryBouncerTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToPrimaryBouncerTransitionViewModel.kt
index 597df15a2b55..5ab458334a25 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToPrimaryBouncerTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToPrimaryBouncerTransitionViewModel.kt
@@ -42,4 +42,7 @@ constructor(private val blurConfig: BlurConfig, animationFlow: KeyguardTransitio
override val windowBlurRadius: Flow<Float> =
transitionAnimation.immediatelyTransitionTo(blurConfig.maxBlurRadiusPx)
+
+ override val notificationBlurRadius: Flow<Float> =
+ transitionAnimation.immediatelyTransitionTo(0.0f)
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModel.kt
index c373fd01ba20..44c4c8723dcb 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModel.kt
@@ -16,6 +16,7 @@
package com.android.systemui.keyguard.ui.viewmodel
+import com.android.systemui.Flags
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.keyguard.domain.interactor.FromLockscreenTransitionInteractor
import com.android.systemui.keyguard.shared.model.Edge
@@ -23,6 +24,7 @@ import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN
import com.android.systemui.keyguard.shared.model.KeyguardState.PRIMARY_BOUNCER
import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
import com.android.systemui.keyguard.ui.transitions.BlurConfig
+import com.android.systemui.keyguard.ui.transitions.BlurConfig.Companion.maxBlurRadiusToNotificationPanelBlurRadius
import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition
import com.android.systemui.keyguard.ui.transitions.PrimaryBouncerTransition
import com.android.systemui.scene.shared.flag.SceneContainerFlag
@@ -32,6 +34,7 @@ import javax.inject.Inject
import kotlin.time.Duration.Companion.milliseconds
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.emptyFlow
/**
* Breaks down LOCKSCREEN->PRIMARY BOUNCER transition into discrete steps for corresponding views to
@@ -70,6 +73,29 @@ constructor(
val lockscreenAlpha: Flow<Float> = shortcutsAlpha
+ val notificationAlpha: Flow<Float> =
+ if (Flags.bouncerUiRevamp()) {
+ shadeDependentFlows.transitionFlow(
+ flowWhenShadeIsNotExpanded = lockscreenAlpha,
+ flowWhenShadeIsExpanded = transitionAnimation.immediatelyTransitionTo(1f),
+ )
+ } else {
+ lockscreenAlpha
+ }
+
+ override val notificationBlurRadius: Flow<Float> =
+ if (Flags.bouncerUiRevamp()) {
+ shadeDependentFlows.transitionFlow(
+ flowWhenShadeIsNotExpanded = emptyFlow(),
+ flowWhenShadeIsExpanded =
+ transitionAnimation.immediatelyTransitionTo(
+ blurConfig.maxBlurRadiusPx.maxBlurRadiusToNotificationPanelBlurRadius()
+ ),
+ )
+ } else {
+ emptyFlow()
+ }
+
override val deviceEntryParentViewAlpha: Flow<Float> =
shadeDependentFlows.transitionFlow(
flowWhenShadeIsNotExpanded =
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToPrimaryBouncerTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToPrimaryBouncerTransitionViewModel.kt
index 44598107fa4b..4d3e27265cea 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToPrimaryBouncerTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToPrimaryBouncerTransitionViewModel.kt
@@ -42,4 +42,7 @@ constructor(blurConfig: BlurConfig, animationFlow: KeyguardTransitionAnimationFl
override val windowBlurRadius: Flow<Float> =
transitionAnimation.immediatelyTransitionTo(blurConfig.maxBlurRadiusPx)
+
+ override val notificationBlurRadius: Flow<Float> =
+ transitionAnimation.immediatelyTransitionTo(0.0f)
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToAodTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToAodTransitionViewModel.kt
index fab8008cbfa7..224191b64f5f 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToAodTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToAodTransitionViewModel.kt
@@ -91,4 +91,7 @@ constructor(
},
onFinish = { blurConfig.minBlurRadiusPx },
)
+
+ override val notificationBlurRadius: Flow<Float> =
+ transitionAnimation.immediatelyTransitionTo(0.0f)
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToDozingTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToDozingTransitionViewModel.kt
index eebdf2ef418e..0f8495f34d22 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToDozingTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToDozingTransitionViewModel.kt
@@ -80,4 +80,6 @@ constructor(
},
onFinish = { blurConfig.minBlurRadiusPx },
)
+ override val notificationBlurRadius: Flow<Float> =
+ transitionAnimation.immediatelyTransitionTo(0.0f)
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGlanceableHubTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGlanceableHubTransitionViewModel.kt
index 3636b747d5c9..a13eef2388f7 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGlanceableHubTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGlanceableHubTransitionViewModel.kt
@@ -43,4 +43,7 @@ constructor(private val blurConfig: BlurConfig, animationFlow: KeyguardTransitio
override val windowBlurRadius: Flow<Float> =
transitionAnimation.immediatelyTransitionTo(blurConfig.minBlurRadiusPx)
+
+ override val notificationBlurRadius: Flow<Float> =
+ transitionAnimation.immediatelyTransitionTo(0.0f)
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModel.kt
index 4ed3e6cde230..d1233f220f47 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModel.kt
@@ -166,6 +166,9 @@ constructor(
createBouncerWindowBlurFlow(primaryBouncerInteractor::willRunDismissFromKeyguard)
}
+ override val notificationBlurRadius: Flow<Float> =
+ transitionAnimation.immediatelyTransitionTo(0.0f)
+
val scrimAlpha: Flow<ScrimAlpha> =
bouncerToGoneFlows.scrimAlpha(TO_GONE_DURATION, PRIMARY_BOUNCER)
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModel.kt
index 2edc93cb5617..c53a408a88e1 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModel.kt
@@ -91,4 +91,7 @@ constructor(
},
),
)
+
+ override val notificationBlurRadius: Flow<Float> =
+ transitionAnimation.immediatelyTransitionTo(0.0f)
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToOccludedTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToOccludedTransitionViewModel.kt
index 3a54a26858d4..fe1708efea2f 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToOccludedTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToOccludedTransitionViewModel.kt
@@ -42,4 +42,7 @@ constructor(private val blurConfig: BlurConfig, animationFlow: KeyguardTransitio
override val windowBlurRadius: Flow<Float> =
transitionAnimation.immediatelyTransitionTo(blurConfig.minBlurRadiusPx)
+
+ override val notificationBlurRadius: Flow<Float> =
+ transitionAnimation.immediatelyTransitionTo(0.0f)
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/Media3ActionFactory.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/Media3ActionFactory.kt
index 09544827a51a..a6b9442b1270 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/Media3ActionFactory.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/Media3ActionFactory.kt
@@ -31,6 +31,7 @@ import androidx.media3.session.CommandButton
import androidx.media3.session.MediaController as Media3Controller
import androidx.media3.session.SessionCommand
import androidx.media3.session.SessionToken
+import com.android.systemui.Flags
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
@@ -128,7 +129,11 @@ constructor(
drawable,
null, // no action to perform when clicked
context.getString(R.string.controls_media_button_connecting),
- context.getDrawable(R.drawable.ic_media_connecting_container),
+ if (Flags.mediaControlsUiUpdate()) {
+ context.getDrawable(R.drawable.ic_media_connecting_status_container)
+ } else {
+ context.getDrawable(R.drawable.ic_media_connecting_container)
+ },
// Specify a rebind id to prevent the spinner from restarting on later binds.
com.android.internal.R.drawable.progress_small_material,
)
@@ -230,17 +235,33 @@ constructor(
Player.COMMAND_PLAY_PAUSE -> {
if (!controller.isPlaying) {
MediaAction(
- context.getDrawable(R.drawable.ic_media_play),
+ if (Flags.mediaControlsUiUpdate()) {
+ context.getDrawable(R.drawable.ic_media_play_button)
+ } else {
+ context.getDrawable(R.drawable.ic_media_play)
+ },
{ executeAction(packageName, token, Player.COMMAND_PLAY_PAUSE) },
context.getString(R.string.controls_media_button_play),
- context.getDrawable(R.drawable.ic_media_play_container),
+ if (Flags.mediaControlsUiUpdate()) {
+ context.getDrawable(R.drawable.ic_media_play_button_container)
+ } else {
+ context.getDrawable(R.drawable.ic_media_play_container)
+ },
)
} else {
MediaAction(
- context.getDrawable(R.drawable.ic_media_pause),
+ if (Flags.mediaControlsUiUpdate()) {
+ context.getDrawable(R.drawable.ic_media_pause_button)
+ } else {
+ context.getDrawable(R.drawable.ic_media_pause)
+ },
{ executeAction(packageName, token, Player.COMMAND_PLAY_PAUSE) },
context.getString(R.string.controls_media_button_pause),
- context.getDrawable(R.drawable.ic_media_pause_container),
+ if (Flags.mediaControlsUiUpdate()) {
+ context.getDrawable(R.drawable.ic_media_pause_button_container)
+ } else {
+ context.getDrawable(R.drawable.ic_media_pause_container)
+ },
)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaActions.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaActions.kt
index 4f9791353b8a..9bf556cf07c2 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaActions.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaActions.kt
@@ -29,6 +29,7 @@ import android.media.session.PlaybackState
import android.service.notification.StatusBarNotification
import android.util.Log
import androidx.media.utils.MediaConstants
+import com.android.systemui.Flags
import com.android.systemui.media.controls.domain.pipeline.LegacyMediaDataManagerImpl.Companion.MAX_COMPACT_ACTIONS
import com.android.systemui.media.controls.domain.pipeline.LegacyMediaDataManagerImpl.Companion.MAX_NOTIFICATION_ACTIONS
import com.android.systemui.media.controls.shared.MediaControlDrawables
@@ -69,7 +70,11 @@ fun createActionsFromState(
drawable,
null, // no action to perform when clicked
context.getString(R.string.controls_media_button_connecting),
- context.getDrawable(R.drawable.ic_media_connecting_container),
+ if (Flags.mediaControlsUiUpdate()) {
+ context.getDrawable(R.drawable.ic_media_connecting_status_container)
+ } else {
+ context.getDrawable(R.drawable.ic_media_connecting_container)
+ },
// Specify a rebind id to prevent the spinner from restarting on later binds.
com.android.internal.R.drawable.progress_small_material,
)
@@ -157,18 +162,34 @@ private fun getStandardAction(
return when (action) {
PlaybackState.ACTION_PLAY -> {
MediaAction(
- context.getDrawable(R.drawable.ic_media_play),
+ if (Flags.mediaControlsUiUpdate()) {
+ context.getDrawable(R.drawable.ic_media_play_button)
+ } else {
+ context.getDrawable(R.drawable.ic_media_play)
+ },
{ controller.transportControls.play() },
context.getString(R.string.controls_media_button_play),
- context.getDrawable(R.drawable.ic_media_play_container),
+ if (Flags.mediaControlsUiUpdate()) {
+ context.getDrawable(R.drawable.ic_media_play_button_container)
+ } else {
+ context.getDrawable(R.drawable.ic_media_play_container)
+ },
)
}
PlaybackState.ACTION_PAUSE -> {
MediaAction(
- context.getDrawable(R.drawable.ic_media_pause),
+ if (Flags.mediaControlsUiUpdate()) {
+ context.getDrawable(R.drawable.ic_media_pause_button)
+ } else {
+ context.getDrawable(R.drawable.ic_media_pause)
+ },
{ controller.transportControls.pause() },
context.getString(R.string.controls_media_button_pause),
- context.getDrawable(R.drawable.ic_media_pause_container),
+ if (Flags.mediaControlsUiUpdate()) {
+ context.getDrawable(R.drawable.ic_media_pause_button_container)
+ } else {
+ context.getDrawable(R.drawable.ic_media_pause_container)
+ },
)
}
PlaybackState.ACTION_SKIP_TO_PREVIOUS -> {
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaViewController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaViewController.kt
index 3928a711f840..a2ddc20844e7 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaViewController.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaViewController.kt
@@ -1016,9 +1016,24 @@ constructor(
expandedLayout.load(context, R.xml.media_recommendations_expanded)
}
}
+ readjustPlayPauseWidth()
refreshState()
}
+ private fun readjustPlayPauseWidth() {
+ // TODO: move to xml file when flag is removed.
+ if (Flags.mediaControlsUiUpdate()) {
+ collapsedLayout.constrainWidth(
+ R.id.actionPlayPause,
+ context.resources.getDimensionPixelSize(R.dimen.qs_media_action_play_pause_width),
+ )
+ expandedLayout.constrainWidth(
+ R.id.actionPlayPause,
+ context.resources.getDimensionPixelSize(R.dimen.qs_media_action_play_pause_width),
+ )
+ }
+ }
+
/** Get a view state based on the width and height set by the scene */
private fun obtainSceneContainerViewState(state: MediaHostState?): TransitionViewState? {
logger.logMediaSize("scene container", widthInSceneContainerPx, heightInSceneContainerPx)
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/toolbar/EditModeButtonViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/toolbar/EditModeButtonViewModel.kt
index f60621882ac0..59990ea22c2f 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/toolbar/EditModeButtonViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/toolbar/EditModeButtonViewModel.kt
@@ -17,6 +17,7 @@
package com.android.systemui.qs.panels.ui.viewmodel.toolbar
import com.android.systemui.classifier.domain.interactor.FalsingInteractor
+import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.plugins.FalsingManager
import com.android.systemui.qs.panels.ui.viewmodel.EditModeViewModel
import dagger.assisted.AssistedFactory
@@ -27,11 +28,12 @@ class EditModeButtonViewModel
constructor(
private val editModeViewModel: EditModeViewModel,
private val falsingInteractor: FalsingInteractor,
+ private val activityStarter: ActivityStarter,
) {
fun onButtonClick() {
if (!falsingInteractor.isFalseTap(FalsingManager.LOW_PENALTY)) {
- editModeViewModel.startEditing()
+ activityStarter.postQSRunnableDismissingKeyguard { editModeViewModel.startEditing() }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenRecordTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenRecordTile.java
index ec8d30b01eab..e93cec875429 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenRecordTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenRecordTile.java
@@ -41,12 +41,14 @@ import com.android.systemui.mediaprojection.MediaProjectionMetricsLogger;
import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.plugins.FalsingManager;
import com.android.systemui.plugins.qs.QSTile;
+import com.android.systemui.plugins.qs.TileDetailsViewModel;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.qs.QSHost;
import com.android.systemui.qs.QsEventLogger;
import com.android.systemui.qs.logging.QSLogger;
import com.android.systemui.qs.pipeline.domain.interactor.PanelInteractor;
import com.android.systemui.qs.tileimpl.QSTileImpl;
+import com.android.systemui.qs.tiles.dialog.ScreenRecordDetailsViewModel;
import com.android.systemui.res.R;
import com.android.systemui.screenrecord.RecordingController;
import com.android.systemui.screenrecord.data.model.ScreenRecordModel;
@@ -54,6 +56,8 @@ import com.android.systemui.settings.UserContextProvider;
import com.android.systemui.statusbar.phone.KeyguardDismissUtil;
import com.android.systemui.statusbar.policy.KeyguardStateController;
+import java.util.function.Consumer;
+
import javax.inject.Inject;
/**
@@ -122,17 +126,78 @@ public class ScreenRecordTile extends QSTileImpl<QSTile.BooleanState>
@Override
protected void handleClick(@Nullable Expandable expandable) {
+ handleClick(() -> showDialog(expandable));
+ }
+
+ private void showDialog(@Nullable Expandable expandable) {
+ final Dialog dialog = mController.createScreenRecordDialog(
+ this::onStartRecordingClicked);
+
+ executeWhenUnlockedKeyguard(() -> {
+ // We animate from the touched view only if we are not on the keyguard, given that if we
+ // are we will dismiss it which will also collapse the shade.
+ boolean shouldAnimateFromExpandable =
+ expandable != null && !mKeyguardStateController.isShowing();
+
+ if (shouldAnimateFromExpandable) {
+ DialogTransitionAnimator.Controller controller =
+ expandable.dialogTransitionController(new DialogCuj(
+ InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN,
+ INTERACTION_JANK_TAG));
+ if (controller != null) {
+ mDialogTransitionAnimator.show(dialog,
+ controller, /* animateBackgroundBoundsChange= */ true);
+ } else {
+ dialog.show();
+ }
+ } else {
+ dialog.show();
+ }
+ });
+ }
+
+ private void onStartRecordingClicked() {
+ // We dismiss the shade. Since starting the recording will also dismiss the dialog (if
+ // there is one showing), we disable the exit animation which looks weird when it happens
+ // at the same time as the shade collapsing.
+ mDialogTransitionAnimator.disableAllCurrentDialogsExitAnimations();
+ mPanelInteractor.collapsePanels();
+ }
+
+ private void executeWhenUnlockedKeyguard(Runnable dismissActionCallback) {
+ ActivityStarter.OnDismissAction dismissAction = () -> {
+ dismissActionCallback.run();
+
+ int uid = mUserContextProvider.getUserContext().getUserId();
+ mMediaProjectionMetricsLogger.notifyPermissionRequestDisplayed(uid);
+
+ return false;
+ };
+
+ mKeyguardDismissUtil.executeWhenUnlocked(dismissAction, false /* requiresShadeOpen */,
+ true /* afterKeyguardDone */);
+ }
+
+ private void handleClick(Runnable showPromptCallback) {
if (mController.isStarting()) {
cancelCountdown();
} else if (mController.isRecording()) {
stopRecording();
} else {
- mUiHandler.post(() -> showPrompt(expandable));
+ mUiHandler.post(showPromptCallback);
}
refreshState();
}
@Override
+ public boolean getDetailsViewModel(Consumer<TileDetailsViewModel> callback) {
+ handleClick(() ->
+ callback.accept(new ScreenRecordDetailsViewModel())
+ );
+ return true;
+ }
+
+ @Override
protected void handleUpdateState(BooleanState state, Object arg) {
boolean isStarting = mController.isStarting();
boolean isRecording = mController.isRecording();
@@ -178,49 +243,6 @@ public class ScreenRecordTile extends QSTileImpl<QSTile.BooleanState>
return mContext.getString(R.string.quick_settings_screen_record_label);
}
- private void showPrompt(@Nullable Expandable expandable) {
- // We animate from the touched view only if we are not on the keyguard, given that if we
- // are we will dismiss it which will also collapse the shade.
- boolean shouldAnimateFromExpandable =
- expandable != null && !mKeyguardStateController.isShowing();
-
- // Create the recording dialog that will collapse the shade only if we start the recording.
- Runnable onStartRecordingClicked = () -> {
- // We dismiss the shade. Since starting the recording will also dismiss the dialog, we
- // disable the exit animation which looks weird when it happens at the same time as the
- // shade collapsing.
- mDialogTransitionAnimator.disableAllCurrentDialogsExitAnimations();
- mPanelInteractor.collapsePanels();
- };
-
- final Dialog dialog = mController.createScreenRecordDialog(onStartRecordingClicked);
-
- ActivityStarter.OnDismissAction dismissAction = () -> {
- if (shouldAnimateFromExpandable) {
- DialogTransitionAnimator.Controller controller =
- expandable.dialogTransitionController(new DialogCuj(
- InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN,
- INTERACTION_JANK_TAG));
- if (controller != null) {
- mDialogTransitionAnimator.show(dialog,
- controller, /* animateBackgroundBoundsChange= */ true);
- } else {
- dialog.show();
- }
- } else {
- dialog.show();
- }
-
- int uid = mUserContextProvider.getUserContext().getUserId();
- mMediaProjectionMetricsLogger.notifyPermissionRequestDisplayed(uid);
-
- return false;
- };
-
- mKeyguardDismissUtil.executeWhenUnlocked(dismissAction, false /* requiresShadeOpen */,
- true /* afterKeyguardDone */);
- }
-
private void cancelCountdown() {
Log.d(TAG, "Cancelling countdown");
mController.cancelCountdown();
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/ScreenRecordDetailsViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/ScreenRecordDetailsViewModel.kt
new file mode 100644
index 000000000000..42cb1248ccff
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/ScreenRecordDetailsViewModel.kt
@@ -0,0 +1,61 @@
+/*
+ * 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.tiles.dialog
+
+import android.view.LayoutInflater
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.heightIn
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.viewinterop.AndroidView
+import com.android.systemui.plugins.qs.TileDetailsViewModel
+import com.android.systemui.res.R
+
+/** The view model used for the screen record details view in the Quick Settings */
+class ScreenRecordDetailsViewModel() : TileDetailsViewModel() {
+ @Composable
+ override fun GetContentView() {
+ // TODO(b/378514312): Finish implementing this function.
+ AndroidView(
+ modifier = Modifier.fillMaxWidth().heightIn(max = VIEW_MAX_HEIGHT),
+ factory = { context ->
+ // Inflate with the existing dialog xml layout
+ LayoutInflater.from(context).inflate(R.layout.screen_share_dialog, null)
+ },
+ )
+ }
+
+ override fun clickOnSettingsButton() {
+ // No settings button in this tile.
+ }
+
+ override fun getTitle(): String {
+ // TODO(b/388321032): Replace this string with a string in a translatable xml file,
+ return "Screen recording"
+ }
+
+ override fun getSubTitle(): String {
+ // No sub-title in this tile.
+ return ""
+ }
+
+ companion object {
+ private val VIEW_MAX_HEIGHT: Dp = 320.dp
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt
index c34edc81bfe7..30d1f05771d7 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt
@@ -118,7 +118,8 @@ constructor(
override fun addCallback(callback: QSTile.Callback?) {
callback ?: return
callbacks.add(callback)
- state?.let(callback::onStateChanged)
+ state.copyTo(cachedState)
+ state.let(callback::onStateChanged)
}
override fun removeCallback(callback: QSTile.Callback?) {
@@ -212,9 +213,9 @@ constructor(
qsTileViewModel.destroy()
}
- override fun getState(): QSTile.State =
+ override fun getState(): QSTile.AdapterState =
qsTileViewModel.currentState?.let { mapState(context, it, qsTileViewModel.config) }
- ?: QSTile.State()
+ ?: QSTile.AdapterState()
override fun getInstanceId(): InstanceId = qsTileViewModel.config.instanceId
@@ -241,7 +242,7 @@ constructor(
context: Context,
viewModelState: QSTileState,
config: QSTileConfig,
- ): QSTile.State =
+ ): QSTile.AdapterState =
// we have to use QSTile.BooleanState to support different side icons
// which are bound to instanceof QSTile.BooleanState in QSTileView.
QSTile.AdapterState().apply {
diff --git a/packages/SystemUI/src/com/android/systemui/scene/data/repository/SceneContainerRepository.kt b/packages/SystemUI/src/com/android/systemui/scene/data/repository/SceneContainerRepository.kt
index d60f05e685bb..0488962fd076 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/data/repository/SceneContainerRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/data/repository/SceneContainerRepository.kt
@@ -90,22 +90,15 @@ constructor(
initialValue = defaultTransitionState,
)
- fun changeScene(
- toScene: SceneKey,
- transitionKey: TransitionKey? = null,
- ) {
- dataSource.changeScene(
- toScene = toScene,
- transitionKey = transitionKey,
- )
+ /** Number of currently active transition animations. */
+ val activeTransitionAnimationCount = MutableStateFlow(0)
+
+ fun changeScene(toScene: SceneKey, transitionKey: TransitionKey? = null) {
+ dataSource.changeScene(toScene = toScene, transitionKey = transitionKey)
}
- fun snapToScene(
- toScene: SceneKey,
- ) {
- dataSource.snapToScene(
- toScene = toScene,
- )
+ fun snapToScene(toScene: SceneKey) {
+ dataSource.snapToScene(toScene = toScene)
}
/**
@@ -116,10 +109,7 @@ constructor(
* [overlay] is already shown.
*/
fun showOverlay(overlay: OverlayKey, transitionKey: TransitionKey? = null) {
- dataSource.showOverlay(
- overlay = overlay,
- transitionKey = transitionKey,
- )
+ dataSource.showOverlay(overlay = overlay, transitionKey = transitionKey)
}
/**
@@ -130,10 +120,7 @@ constructor(
* if [overlay] is already hidden.
*/
fun hideOverlay(overlay: OverlayKey, transitionKey: TransitionKey? = null) {
- dataSource.hideOverlay(
- overlay = overlay,
- transitionKey = transitionKey,
- )
+ dataSource.hideOverlay(overlay = overlay, transitionKey = transitionKey)
}
/**
@@ -143,11 +130,7 @@ constructor(
* This throws if [from] is not currently shown or if [to] is already shown.
*/
fun replaceOverlay(from: OverlayKey, to: OverlayKey, transitionKey: TransitionKey? = null) {
- dataSource.replaceOverlay(
- from = from,
- to = to,
- transitionKey = transitionKey,
- )
+ dataSource.replaceOverlay(from = from, to = to, transitionKey = transitionKey)
}
/** Sets whether the container is visible. */
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 0e6fc36fb96a..ba9dc7625769 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
@@ -47,6 +47,7 @@ import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.flow.update
/**
* Generic business logic and app state accessors for the scene framework.
@@ -165,12 +166,15 @@ constructor(
/** Whether the scene container is visible. */
val isVisible: StateFlow<Boolean> =
- combine(repository.isVisible, repository.isRemoteUserInputOngoing) {
- isVisible,
- isRemoteUserInteractionOngoing ->
+ combine(
+ repository.isVisible,
+ repository.isRemoteUserInputOngoing,
+ repository.activeTransitionAnimationCount,
+ ) { isVisible, isRemoteUserInteractionOngoing, activeTransitionAnimationCount ->
isVisibleInternal(
raw = isVisible,
isRemoteUserInputOngoing = isRemoteUserInteractionOngoing,
+ activeTransitionAnimationCount = activeTransitionAnimationCount,
)
}
.stateIn(
@@ -436,8 +440,9 @@ constructor(
private fun isVisibleInternal(
raw: Boolean = repository.isVisible.value,
isRemoteUserInputOngoing: Boolean = repository.isRemoteUserInputOngoing.value,
+ activeTransitionAnimationCount: Int = repository.activeTransitionAnimationCount.value,
): Boolean {
- return raw || isRemoteUserInputOngoing
+ return raw || isRemoteUserInputOngoing || activeTransitionAnimationCount > 0
}
/**
@@ -525,4 +530,50 @@ constructor(
): Flow<Map<UserAction, UserActionResult>> {
return disabledContentInteractor.filteredUserActions(unfiltered)
}
+
+ /**
+ * Notifies that a transition animation has started.
+ *
+ * The scene container will remain visible while any transition animation is running within it.
+ */
+ fun onTransitionAnimationStart() {
+ repository.activeTransitionAnimationCount.update { current ->
+ (current + 1).also {
+ check(it < 10) {
+ "Number of active transition animations is too high. Something must be" +
+ " calling onTransitionAnimationStart too many times!"
+ }
+ }
+ }
+ }
+
+ /**
+ * Notifies that a transition animation has ended.
+ *
+ * The scene container will remain visible while any transition animation is running within it.
+ */
+ fun onTransitionAnimationEnd() {
+ decrementActiveTransitionAnimationCount()
+ }
+
+ /**
+ * Notifies that a transition animation has been canceled.
+ *
+ * The scene container will remain visible while any transition animation is running within it.
+ */
+ fun onTransitionAnimationCancelled() {
+ decrementActiveTransitionAnimationCount()
+ }
+
+ private fun decrementActiveTransitionAnimationCount() {
+ repository.activeTransitionAnimationCount.update { current ->
+ (current - 1).also {
+ check(it >= 0) {
+ "Number of active transition animations is negative. Something must be" +
+ " calling onTransitionAnimationEnd or onTransitionAnimationCancelled too" +
+ " many times!"
+ }
+ }
+ }
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt
index 8d8c24eae9e2..3a23a71cf7bf 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt
@@ -25,6 +25,7 @@ import com.android.internal.logging.UiEventLogger
import com.android.keyguard.AuthInteractionProperties
import com.android.systemui.CoreStartable
import com.android.systemui.Flags
+import com.android.systemui.animation.ActivityTransitionAnimator
import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor
import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor
@@ -146,6 +147,7 @@ constructor(
private val vibratorHelper: VibratorHelper,
private val msdlPlayer: MSDLPlayer,
private val disabledContentInteractor: DisabledContentInteractor,
+ private val activityTransitionAnimator: ActivityTransitionAnimator,
) : CoreStartable {
private val centralSurfaces: CentralSurfaces?
get() = centralSurfacesOptLazy.get().getOrNull()
@@ -169,6 +171,7 @@ constructor(
handleKeyguardEnabledness()
notifyKeyguardDismissCancelledCallbacks()
refreshLockscreenEnabled()
+ hydrateActivityTransitionAnimationState()
} else {
sceneLogger.logFrameworkEnabled(
isEnabled = false,
@@ -929,6 +932,35 @@ constructor(
}
}
+ /**
+ * Wires the scene framework to activity transition animations that originate from anywhere. A
+ * subset of these may actually originate from UI inside one of the scenes in the framework.
+ *
+ * Telling the scene framework about ongoing activity transition animations is critical so the
+ * scene framework doesn't make its scene container invisible during a transition.
+ *
+ * As it turns out, making the scene container view invisible during a transition animation
+ * disrupts the animation and causes interaction jank CUJ tracking to ignore reports of the CUJ
+ * ending or being canceled.
+ */
+ private fun hydrateActivityTransitionAnimationState() {
+ activityTransitionAnimator.addListener(
+ object : ActivityTransitionAnimator.Listener {
+ override fun onTransitionAnimationStart() {
+ sceneInteractor.onTransitionAnimationStart()
+ }
+
+ override fun onTransitionAnimationEnd() {
+ sceneInteractor.onTransitionAnimationEnd()
+ }
+
+ override fun onTransitionAnimationCancelled() {
+ sceneInteractor.onTransitionAnimationCancelled()
+ }
+ }
+ )
+ }
+
companion object {
private const val TAG = "SceneContainerStartable"
}
diff --git a/packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt b/packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt
index 42d83637ec1a..a48d4d4d3b5f 100644
--- a/packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt
@@ -316,7 +316,7 @@ internal constructor(
val callback = it.callback.get()
if (callback != null) {
it.executor.execute {
- traceSection({ "$callback" }) { action(callback) { latch.countDown() } }
+ traceSection({ "UserTrackerImpl::$callback" }) { action(callback) { latch.countDown() } }
}
} else {
latch.countDown()
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index 19152170757c..19bf4c0bab81 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
@@ -109,6 +109,7 @@ import com.android.systemui.keyguard.shared.model.Edge;
import com.android.systemui.keyguard.shared.model.TransitionState;
import com.android.systemui.keyguard.shared.model.TransitionStep;
import com.android.systemui.keyguard.ui.binder.KeyguardLongPressViewBinder;
+import com.android.systemui.keyguard.ui.transitions.BlurConfig;
import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel;
import com.android.systemui.keyguard.ui.viewmodel.KeyguardTouchHandlingViewModel;
import com.android.systemui.media.controls.domain.pipeline.MediaDataManager;
@@ -914,13 +915,12 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump
if (!com.android.systemui.Flags.bouncerUiRevamp()) return;
if (isBouncerShowing && isExpanded()) {
- // Blur the shade much lesser than the background surface so that the surface is
- // distinguishable from the background.
- float shadeBlurEffect = mDepthController.getMaxBlurRadiusPx() / 3;
+ float shadeBlurEffect = BlurConfig.maxBlurRadiusToNotificationPanelBlurRadius(
+ mDepthController.getMaxBlurRadiusPx());
mView.setRenderEffect(RenderEffect.createBlurEffect(
shadeBlurEffect,
shadeBlurEffect,
- Shader.TileMode.MIRROR));
+ Shader.TileMode.CLAMP));
} else {
mView.setRenderEffect(null);
}
@@ -3959,7 +3959,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump
}
final boolean isTrackpadThreeFingerSwipe = isTrackpadThreeFingerSwipe(event);
- if (com.android.systemui.Flags.disableShadeExpandsOnTrackpadTwoFingerSwipe()
+ if (com.android.systemui.Flags.disableShadeTrackpadTwoFingerSwipe()
&& !isTrackpadThreeFingerSwipe && isTwoFingerSwipeTrackpadEvent(event)
&& !isPanelExpanded()) {
if (isDown) {
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayAwareModule.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayAwareModule.kt
index 63e8ba8f65cd..747642097327 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayAwareModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayAwareModule.kt
@@ -37,12 +37,13 @@ import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.res.R
import com.android.systemui.scene.ui.view.WindowRootView
import com.android.systemui.shade.data.repository.MutableShadeDisplaysRepository
-import com.android.systemui.shade.domain.interactor.ShadeDialogContextInteractor
-import com.android.systemui.shade.domain.interactor.ShadeDialogContextInteractorImpl
import com.android.systemui.shade.data.repository.ShadeDisplaysRepository
import com.android.systemui.shade.data.repository.ShadeDisplaysRepositoryImpl
import com.android.systemui.shade.display.ShadeDisplayPolicyModule
+import com.android.systemui.shade.domain.interactor.ShadeDialogContextInteractor
+import com.android.systemui.shade.domain.interactor.ShadeDialogContextInteractorImpl
import com.android.systemui.shade.domain.interactor.ShadeDisplaysInteractor
+import com.android.systemui.shade.domain.interactor.ShadeExpandedStateInteractor
import com.android.systemui.shade.shared.flag.ShadeWindowGoesAround
import com.android.systemui.statusbar.phone.ConfigurationControllerImpl
import com.android.systemui.statusbar.phone.ConfigurationForwarder
@@ -276,6 +277,8 @@ object ShadeDisplayAwareModule {
@Module
internal interface OptionalShadeDisplayAwareBindings {
@BindsOptionalOf fun bindOptionalOfWindowRootView(): WindowRootView
+
+ @BindsOptionalOf fun bindOptionalOShadeExpandedStateInteractor(): ShadeExpandedStateInteractor
}
/**
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayChangeLatencyTracker.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayChangeLatencyTracker.kt
index 4d35d0eba178..e358dcec8b10 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayChangeLatencyTracker.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayChangeLatencyTracker.kt
@@ -24,7 +24,6 @@ import com.android.systemui.common.ui.view.ChoreographerUtils
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.scene.ui.view.WindowRootView
-import com.android.systemui.shade.ShadeDisplayChangeLatencyTracker.Companion.TIMEOUT
import com.android.systemui.shade.data.repository.ShadeDisplaysRepository
import com.android.systemui.util.kotlin.getOrNull
import java.util.Optional
@@ -33,7 +32,6 @@ import javax.inject.Inject
import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
-import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.filter
@@ -135,7 +133,7 @@ constructor(
private companion object {
const val TAG = "ShadeDisplayLatency"
- val t = TrackTracer(trackName = TAG)
+ val t = TrackTracer(trackName = TAG, trackGroup = "shade")
val TIMEOUT = 3.seconds
const val SHADE_MOVE_ACTION = LatencyTracker.ACTION_SHADE_WINDOW_DISPLAY_CHANGE
}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeExpansionStateManager.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeExpansionStateManager.kt
index 359ddd86f115..5fab889735a6 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeExpansionStateManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeExpansionStateManager.kt
@@ -18,13 +18,16 @@ package com.android.systemui.shade
import android.annotation.IntDef
import android.os.Trace
+import android.os.Trace.TRACE_TAG_APP as TRACE_TAG
import android.util.Log
import androidx.annotation.FloatRange
+import com.android.app.tracing.TraceStateLogger
+import com.android.app.tracing.TrackGroupUtils.trackGroup
+import com.android.app.tracing.coroutines.TrackTracer
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.util.Compile
import java.util.concurrent.CopyOnWriteArrayList
import javax.inject.Inject
-import android.os.Trace.TRACE_TAG_APP as TRACE_TAG
/**
* A class responsible for managing the notification panel's current state.
@@ -38,6 +41,8 @@ class ShadeExpansionStateManager @Inject constructor() {
private val expansionListeners = CopyOnWriteArrayList<ShadeExpansionListener>()
private val stateListeners = CopyOnWriteArrayList<ShadeStateListener>()
+ private val stateLogger = TraceStateLogger(trackGroup("shade", TRACK_NAME))
+
@PanelState private var state: Int = STATE_CLOSED
@FloatRange(from = 0.0, to = 1.0) private var fraction: Float = 0f
private var expanded: Boolean = false
@@ -75,7 +80,7 @@ class ShadeExpansionStateManager @Inject constructor() {
fun onPanelExpansionChanged(
@FloatRange(from = 0.0, to = 1.0) fraction: Float,
expanded: Boolean,
- tracking: Boolean
+ tracking: Boolean,
) {
require(!fraction.isNaN()) { "fraction cannot be NaN" }
val oldState = state
@@ -113,11 +118,8 @@ class ShadeExpansionStateManager @Inject constructor() {
)
if (Trace.isTagEnabled(TRACE_TAG)) {
- Trace.traceCounter(TRACE_TAG, "panel_expansion", (fraction * 100).toInt())
- if (state != oldState) {
- Trace.asyncTraceForTrackEnd(TRACE_TAG, TRACK_NAME, 0)
- Trace.asyncTraceForTrackBegin(TRACE_TAG, TRACK_NAME, state.panelStateToString(), 0)
- }
+ TrackTracer.instantForGroup("shade", "panel_expansion", fraction)
+ stateLogger.log(state.panelStateToString())
}
val expansionChangeEvent = ShadeExpansionChangeEvent(fraction, expanded, tracking)
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt
index 2348a110eb3a..b9df9f868dc3 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt
@@ -35,6 +35,8 @@ import com.android.systemui.shade.domain.interactor.ShadeAnimationInteractorLega
import com.android.systemui.shade.domain.interactor.ShadeAnimationInteractorSceneContainerImpl
import com.android.systemui.shade.domain.interactor.ShadeBackActionInteractor
import com.android.systemui.shade.domain.interactor.ShadeBackActionInteractorImpl
+import com.android.systemui.shade.domain.interactor.ShadeExpandedStateInteractor
+import com.android.systemui.shade.domain.interactor.ShadeExpandedStateInteractorImpl
import com.android.systemui.shade.domain.interactor.ShadeInteractor
import com.android.systemui.shade.domain.interactor.ShadeInteractorImpl
import com.android.systemui.shade.domain.interactor.ShadeInteractorLegacyImpl
@@ -176,4 +178,10 @@ abstract class ShadeModule {
@Binds
@SysUISingleton
abstract fun bindShadeModeInteractor(impl: ShadeModeInteractorImpl): ShadeModeInteractor
+
+ @Binds
+ @SysUISingleton
+ abstract fun bindShadeExpandedStateInteractor(
+ impl: ShadeExpandedStateInteractorImpl
+ ): ShadeExpandedStateInteractor
}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeStateTraceLogger.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeStateTraceLogger.kt
new file mode 100644
index 000000000000..2705cdafb4de
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeStateTraceLogger.kt
@@ -0,0 +1,70 @@
+/*
+ * 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.shade
+
+import com.android.app.tracing.TraceStateLogger
+import com.android.app.tracing.TrackGroupUtils.trackGroup
+import com.android.app.tracing.coroutines.TrackTracer.Companion.instantForGroup
+import com.android.app.tracing.coroutines.launchTraced
+import com.android.systemui.CoreStartable
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.shade.data.repository.ShadeDisplaysRepository
+import com.android.systemui.shade.domain.interactor.ShadeInteractor
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+
+@SysUISingleton
+class ShadeStateTraceLogger
+@Inject
+constructor(
+ private val shadeInteractor: ShadeInteractor,
+ private val shadeDisplaysRepository: ShadeDisplaysRepository,
+ @Application private val scope: CoroutineScope,
+) : CoreStartable {
+ override fun start() {
+ scope.launchTraced("ShadeStateTraceLogger") {
+ launch {
+ val stateLogger = createTraceStateLogger("isShadeLayoutWide")
+ shadeInteractor.isShadeLayoutWide.collect { stateLogger.log(it.toString()) }
+ }
+ launch {
+ val stateLogger = createTraceStateLogger("shadeMode")
+ shadeInteractor.shadeMode.collect { stateLogger.log(it.toString()) }
+ }
+ launch {
+ shadeInteractor.shadeExpansion.collect {
+ instantForGroup(TRACK_GROUP_NAME, "shadeExpansion", it)
+ }
+ }
+ launch {
+ shadeDisplaysRepository.displayId.collect {
+ instantForGroup(TRACK_GROUP_NAME, "displayId", it)
+ }
+ }
+ }
+ }
+
+ private fun createTraceStateLogger(trackName: String): TraceStateLogger {
+ return TraceStateLogger(trackGroup(TRACK_GROUP_NAME, trackName))
+ }
+
+ private companion object {
+ const val TRACK_GROUP_NAME = "shade"
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeTraceLogger.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeTraceLogger.kt
index a36c56eafbfc..9a9fc467c53f 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeTraceLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeTraceLogger.kt
@@ -27,7 +27,7 @@ import com.android.app.tracing.coroutines.TrackTracer
* them across various threads' logs.
*/
object ShadeTraceLogger {
- private val t = TrackTracer(trackName = "ShadeTraceLogger")
+ val t = TrackTracer(trackName = "ShadeTraceLogger", trackGroup = "shade")
@JvmStatic
fun logOnMovedToDisplay(displayId: Int, config: Configuration) {
@@ -44,8 +44,11 @@ object ShadeTraceLogger {
t.instant { "moveShadeWindowTo(displayId=$displayId)" }
}
- @JvmStatic
- fun traceReparenting(r: () -> Unit) {
+ suspend fun traceReparenting(r: suspend () -> Unit) {
t.traceAsync({ "reparenting" }) { r() }
}
+
+ inline fun traceWaitForExpansion(expansion: Float, r: () -> Unit) {
+ t.traceAsync({ "waiting for shade expansion to match $expansion" }) { r() }
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/StartShadeModule.kt b/packages/SystemUI/src/com/android/systemui/shade/StartShadeModule.kt
index c4de78b8a28e..570a7853c394 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/StartShadeModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/StartShadeModule.kt
@@ -40,4 +40,9 @@ internal abstract class StartShadeModule {
@IntoMap
@ClassKey(ShadeStartable::class)
abstract fun provideShadeStartable(startable: ShadeStartable): CoreStartable
+
+ @Binds
+ @IntoMap
+ @ClassKey(ShadeStateTraceLogger::class)
+ abstract fun provideShadeStateTraceLogger(startable: ShadeStateTraceLogger): CoreStartable
}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/FakeShadeExpandedStateInteractor.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/FakeShadeExpandedStateInteractor.kt
new file mode 100644
index 000000000000..eab00166c8ef
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/FakeShadeExpandedStateInteractor.kt
@@ -0,0 +1,33 @@
+/*
+ * 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.shade.domain.interactor
+
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+
+/** Fake [ShadeExpandedStateInteractor] for tests. */
+class FakeShadeExpandedStateInteractor : ShadeExpandedStateInteractor {
+
+ private val mutableExpandedElement =
+ MutableStateFlow<ShadeExpandedStateInteractor.ShadeElement?>(null)
+ override val currentlyExpandedElement: StateFlow<ShadeExpandedStateInteractor.ShadeElement?>
+ get() = mutableExpandedElement
+
+ fun setState(state: ShadeExpandedStateInteractor.ShadeElement?) {
+ mutableExpandedElement.value = state
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractor.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractor.kt
index be561b178136..691a383cb338 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractor.kt
@@ -32,6 +32,7 @@ import com.android.systemui.shade.ShadeTraceLogger.traceReparenting
import com.android.systemui.shade.data.repository.ShadeDisplaysRepository
import com.android.systemui.shade.shared.flag.ShadeWindowGoesAround
import com.android.window.flags.Flags
+import java.util.Optional
import javax.inject.Inject
import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.CoroutineScope
@@ -47,8 +48,19 @@ constructor(
@Background private val bgScope: CoroutineScope,
@Main private val mainThreadContext: CoroutineContext,
private val shadeDisplayChangeLatencyTracker: ShadeDisplayChangeLatencyTracker,
+ shadeExpandedInteractor: Optional<ShadeExpandedStateInteractor>,
) : CoreStartable {
+ private val shadeExpandedInteractor =
+ shadeExpandedInteractor.orElse(null)
+ ?: error(
+ """
+ ShadeExpandedStateInteractor must be provided for ShadeDisplaysInteractor to work.
+ If it is not, it means this is being instantiated in a SystemUI variant that shouldn't.
+ """
+ .trimIndent()
+ )
+
override fun start() {
ShadeWindowGoesAround.isUnexpectedlyInLegacyMode()
bgScope.launchTraced(TAG) {
@@ -78,9 +90,12 @@ constructor(
withContext(mainThreadContext) {
traceReparenting {
shadeDisplayChangeLatencyTracker.onShadeDisplayChanging(destinationId)
+ val expandedElement = shadeExpandedInteractor.currentlyExpandedElement.value
+ expandedElement?.collapse(reason = "Shade window move")
reparentToDisplayId(id = destinationId)
+ expandedElement?.expand(reason = "Shade window move")
+ checkContextDisplayMatchesExpected(destinationId)
}
- checkContextDisplayMatchesExpected(destinationId)
}
} catch (e: IllegalStateException) {
Log.e(
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeExpandedStateInteractor.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeExpandedStateInteractor.kt
new file mode 100644
index 000000000000..dd3abeec5a72
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeExpandedStateInteractor.kt
@@ -0,0 +1,118 @@
+/*
+ * 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.shade.domain.interactor
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.scene.shared.flag.SceneContainerFlag
+import com.android.systemui.shade.ShadeTraceLogger.traceWaitForExpansion
+import com.android.systemui.shade.domain.interactor.ShadeExpandedStateInteractor.ShadeElement
+import com.android.systemui.shade.shared.flag.DualShade
+import com.android.systemui.util.kotlin.Utils.Companion.combineState
+import javax.inject.Inject
+import kotlin.time.Duration.Companion.seconds
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.withContext
+import kotlinx.coroutines.withTimeout
+
+/**
+ * Wrapper around [ShadeInteractor] to facilitate expansion and collapse of Notifications and quick
+ * settings.
+ *
+ * Specifially created to simplify [ShadeDisplaysInteractor] logic.
+ *
+ * NOTE: with [SceneContainerFlag] or [DualShade] disabled, [currentlyExpandedElement] will always
+ * return null!
+ */
+interface ShadeExpandedStateInteractor {
+ /** Returns the expanded [ShadeElement]. If none is, returns null. */
+ val currentlyExpandedElement: StateFlow<ShadeElement?>
+
+ /** An element from the shade window that can be expanded or collapsed. */
+ abstract class ShadeElement {
+ /** Expands the shade element, returning when the expansion is done */
+ abstract suspend fun expand(reason: String)
+
+ /** Collapses the shade element, returning when the collapse is done. */
+ abstract suspend fun collapse(reason: String)
+ }
+}
+
+@SysUISingleton
+class ShadeExpandedStateInteractorImpl
+@Inject
+constructor(
+ private val shadeInteractor: ShadeInteractor,
+ @Background private val bgScope: CoroutineScope,
+) : ShadeExpandedStateInteractor {
+
+ private val notificationElement = NotificationElement()
+ private val qsElement = QSElement()
+
+ override val currentlyExpandedElement: StateFlow<ShadeElement?> =
+ if (SceneContainerFlag.isEnabled) {
+ combineState(
+ shadeInteractor.isShadeAnyExpanded,
+ shadeInteractor.isQsExpanded,
+ bgScope,
+ SharingStarted.Eagerly,
+ ) { isShadeAnyExpanded, isQsExpanded ->
+ when {
+ isShadeAnyExpanded -> notificationElement
+ isQsExpanded -> qsElement
+ else -> null
+ }
+ }
+ } else {
+ MutableStateFlow(null)
+ }
+
+ inner class NotificationElement : ShadeElement() {
+ override suspend fun expand(reason: String) {
+ shadeInteractor.expandNotificationsShade(reason)
+ shadeInteractor.shadeExpansion.waitUntil(1f)
+ }
+
+ override suspend fun collapse(reason: String) {
+ shadeInteractor.collapseNotificationsShade(reason)
+ shadeInteractor.shadeExpansion.waitUntil(0f)
+ }
+ }
+
+ inner class QSElement : ShadeElement() {
+ override suspend fun expand(reason: String) {
+ shadeInteractor.expandQuickSettingsShade(reason)
+ shadeInteractor.qsExpansion.waitUntil(1f)
+ }
+
+ override suspend fun collapse(reason: String) {
+ shadeInteractor.collapseQuickSettingsShade(reason)
+ shadeInteractor.qsExpansion.waitUntil(0f)
+ }
+ }
+
+ private suspend fun StateFlow<Float>.waitUntil(f: Float) {
+ // it's important to not do this in the main thread otherwise it will block any rendering.
+ withContext(bgScope.coroutineContext) {
+ withTimeout(1.seconds) { traceWaitForExpansion(expansion = f) { first { it == f } } }
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/LightRevealScrim.kt b/packages/SystemUI/src/com/android/systemui/statusbar/LightRevealScrim.kt
index 37989f56d559..2885ce80bda9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/LightRevealScrim.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/LightRevealScrim.kt
@@ -11,13 +11,13 @@ import android.graphics.PorterDuffColorFilter
import android.graphics.PorterDuffXfermode
import android.graphics.RadialGradient
import android.graphics.Shader
-import android.os.Trace
import android.util.AttributeSet
import android.util.MathUtils.lerp
import android.view.MotionEvent
import android.view.View
import android.view.animation.PathInterpolator
import com.android.app.animation.Interpolators
+import com.android.app.tracing.coroutines.TrackTracer
import com.android.keyguard.logging.ScrimLogger
import com.android.systemui.shade.TouchLogger
import com.android.systemui.statusbar.LightRevealEffect.Companion.getPercentPastThreshold
@@ -321,9 +321,8 @@ constructor(
}
revealEffect.setRevealAmountOnScrim(value, this)
updateScrimOpaque()
- Trace.traceCounter(
- Trace.TRACE_TAG_APP,
- "light_reveal_amount $logString",
+ TrackTracer.instantForGroup(
+ "scrim", { "light_reveal_amount $logString" },
(field * 100).toInt()
)
invalidate()
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java
index 2bcd3fcfed17..10b726b90894 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java
@@ -92,6 +92,7 @@ import java.util.List;
import java.util.Objects;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
import javax.inject.Inject;
@@ -297,6 +298,9 @@ public class NotificationLockscreenUserManagerImpl implements
// The last lock time. Uses currentTimeMillis
@VisibleForTesting
protected final AtomicLong mLastLockTime = new AtomicLong(-1);
+ // Whether or not the device is locked
+ @VisibleForTesting
+ protected final AtomicBoolean mLocked = new AtomicBoolean(true);
protected int mCurrentUserId = 0;
@@ -369,6 +373,7 @@ public class NotificationLockscreenUserManagerImpl implements
if (!unlocked) {
mLastLockTime.set(System.currentTimeMillis());
}
+ mLocked.set(!unlocked);
}));
}
}
@@ -737,7 +742,7 @@ public class NotificationLockscreenUserManagerImpl implements
return false;
}
- if (!mKeyguardManager.isDeviceLocked()) {
+ if (!mLocked.get()) {
return false;
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt
index e83cded4e2ce..38f7c39203f0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt
@@ -22,7 +22,6 @@ import android.animation.ValueAnimator
import android.content.Context
import android.content.res.Configuration
import android.os.SystemClock
-import android.os.Trace
import android.util.IndentingPrintWriter
import android.util.Log
import android.util.MathUtils
@@ -33,7 +32,9 @@ import androidx.dynamicanimation.animation.FloatPropertyCompat
import androidx.dynamicanimation.animation.SpringAnimation
import androidx.dynamicanimation.animation.SpringForce
import com.android.app.animation.Interpolators
+import com.android.app.tracing.coroutines.TrackTracer
import com.android.systemui.Dumpable
+import com.android.systemui.Flags.spatialModelAppPushback
import com.android.systemui.animation.ShadeInterpolation
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
@@ -52,7 +53,9 @@ import com.android.systemui.statusbar.policy.SplitShadeStateController
import com.android.systemui.util.WallpaperController
import com.android.systemui.window.domain.interactor.WindowRootViewBlurInteractor
import com.android.systemui.window.flag.WindowBlurFlag
+import com.android.wm.shell.appzoomout.AppZoomOut
import java.io.PrintWriter
+import java.util.Optional
import javax.inject.Inject
import kotlin.math.max
import kotlin.math.sign
@@ -79,6 +82,7 @@ constructor(
private val splitShadeStateController: SplitShadeStateController,
private val windowRootViewBlurInteractor: WindowRootViewBlurInteractor,
@Application private val applicationScope: CoroutineScope,
+ private val appZoomOutOptional: Optional<AppZoomOut>,
dumpManager: DumpManager,
configurationController: ConfigurationController,
) : ShadeExpansionListener, Dumpable {
@@ -263,7 +267,7 @@ constructor(
updateScheduled = false
val (blur, zoomOutFromShadeRadius) = computeBlurAndZoomOut()
val opaque = shouldBlurBeOpaque
- Trace.traceCounter(Trace.TRACE_TAG_APP, "shade_blur_radius", blur)
+ TrackTracer.instantForGroup("shade", "shade_blur_radius", blur)
blurUtils.applyBlur(root.viewRootImpl, blur, opaque)
onBlurApplied(blur, zoomOutFromShadeRadius)
}
@@ -271,6 +275,13 @@ constructor(
private fun onBlurApplied(appliedBlurRadius: Int, zoomOutFromShadeRadius: Float) {
lastAppliedBlur = appliedBlurRadius
wallpaperController.setNotificationShadeZoom(zoomOutFromShadeRadius)
+ if (spatialModelAppPushback()) {
+ appZoomOutOptional.ifPresent { appZoomOut ->
+ appZoomOut.setProgress(
+ zoomOutFromShadeRadius
+ )
+ }
+ }
listeners.forEach {
it.onWallpaperZoomOutChanged(zoomOutFromShadeRadius)
it.onBlurRadiusChanged(appliedBlurRadius)
@@ -384,7 +395,7 @@ constructor(
windowRootViewBlurInteractor.onBlurAppliedEvent.collect { appliedBlurRadius ->
if (updateScheduled) {
// Process the blur applied event only if we scheduled the update
- Trace.traceCounter(Trace.TRACE_TAG_APP, "shade_blur_radius", appliedBlurRadius)
+ TrackTracer.instantForGroup("shade", "shade_blur_radius", appliedBlurRadius)
updateScheduled = false
onBlurApplied(appliedBlurRadius, zoomOutCalculatedFromShadeRadius)
} else {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java
index 48cf7a83c324..155049f512d8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java
@@ -23,6 +23,7 @@ import android.content.Context;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Rect;
+import android.os.Bundle;
import android.util.AttributeSet;
import android.util.IndentingPrintWriter;
import android.util.MathUtils;
@@ -30,6 +31,7 @@ import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
import android.view.animation.Interpolator;
import android.view.animation.PathInterpolator;
@@ -1014,12 +1016,24 @@ public class NotificationShelf extends ActivatableNotificationView {
public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
super.onInitializeAccessibilityNodeInfo(info);
if (mInteractive) {
+ // Add two accessibility actions that both performs expanding the notification shade
info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_EXPAND);
- AccessibilityNodeInfo.AccessibilityAction unlock
- = new AccessibilityNodeInfo.AccessibilityAction(
+
+ AccessibilityAction seeAll = new AccessibilityAction(
AccessibilityNodeInfo.ACTION_CLICK,
- getContext().getString(R.string.accessibility_overflow_action));
- info.addAction(unlock);
+ getContext().getString(R.string.accessibility_overflow_action)
+ );
+ info.addAction(seeAll);
+ }
+ }
+
+ @Override
+ public boolean performAccessibilityAction(int action, Bundle args) {
+ // override ACTION_EXPAND with ACTION_CLICK
+ if (action == AccessibilityNodeInfo.ACTION_EXPAND) {
+ return super.performAccessibilityAction(AccessibilityNodeInfo.ACTION_CLICK, args);
+ } else {
+ return super.performAccessibilityAction(action, args);
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java
index a7ad46296e08..ead8f6a1123e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java
@@ -37,6 +37,7 @@ import android.view.animation.Interpolator;
import androidx.annotation.NonNull;
import com.android.app.animation.Interpolators;
+import com.android.app.tracing.coroutines.TrackTracer;
import com.android.compose.animation.scene.OverlayKey;
import com.android.compose.animation.scene.SceneKey;
import com.android.internal.annotations.GuardedBy;
@@ -671,7 +672,7 @@ public class StatusBarStateControllerImpl implements
}
private void recordHistoricalState(int newState, int lastState, boolean upcoming) {
- Trace.traceCounter(Trace.TRACE_TAG_APP, "statusBarState", newState);
+ TrackTracer.instantForGroup("statusBar", "state", newState);
mHistoryIndex = (mHistoryIndex + 1) % HISTORY_SIZE;
HistoricalState state = mHistoricalRecords[mHistoryIndex];
state.mNewState = newState;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModel.kt
index de08e3891902..86954d569199 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModel.kt
@@ -18,7 +18,6 @@ package com.android.systemui.statusbar.chips.call.ui.viewmodel
import android.view.View
import com.android.internal.jank.InteractionJankMonitor
-import com.android.systemui.Flags
import com.android.systemui.animation.ActivityTransitionAnimator
import com.android.systemui.common.shared.model.ContentDescription
import com.android.systemui.common.shared.model.Icon
@@ -64,18 +63,12 @@ constructor(
is OngoingCallModel.InCallWithVisibleApp -> OngoingActivityChipModel.Hidden()
is OngoingCallModel.InCall -> {
val icon =
- if (
- Flags.statusBarCallChipNotificationIcon() &&
- state.notificationIconView != null
- ) {
+ if (state.notificationIconView != null) {
StatusBarConnectedDisplays.assertInLegacyMode()
OngoingActivityChipModel.ChipIcon.StatusBarView(
state.notificationIconView
)
- } else if (
- StatusBarConnectedDisplays.isEnabled &&
- Flags.statusBarCallChipNotificationIcon()
- ) {
+ } else if (StatusBarConnectedDisplays.isEnabled) {
OngoingActivityChipModel.ChipIcon.StatusBarNotificationIcon(
state.notificationKey
)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModel.kt
index 2f6431b05c8b..ec3a5b271e35 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModel.kt
@@ -27,6 +27,7 @@ import com.android.systemui.statusbar.chips.ui.model.ColorsModel
import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel
import com.android.systemui.statusbar.core.StatusBarConnectedDisplays
import com.android.systemui.statusbar.notification.domain.interactor.HeadsUpNotificationInteractor
+import com.android.systemui.statusbar.notification.domain.model.TopPinnedState
import com.android.systemui.statusbar.notification.headsup.PinnedStatus
import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel
import javax.inject.Inject
@@ -60,7 +61,7 @@ constructor(
/** Converts the notification to the [OngoingActivityChipModel] object. */
private fun NotificationChipModel.toActivityChipModel(
- headsUpState: PinnedStatus
+ headsUpState: TopPinnedState
): OngoingActivityChipModel.Shown {
StatusBarNotifChips.assertInNewMode()
val icon =
@@ -87,8 +88,12 @@ constructor(
}
}
- if (headsUpState == PinnedStatus.PinnedByUser) {
- // If the user tapped the chip to show the HUN, we want to just show the icon because
+ val isShowingHeadsUpFromChipTap =
+ headsUpState is TopPinnedState.Pinned &&
+ headsUpState.status == PinnedStatus.PinnedByUser &&
+ headsUpState.key == this.key
+ if (isShowingHeadsUpFromChipTap) {
+ // If the user tapped this chip to show the HUN, we want to just show the icon because
// the HUN will show the rest of the information.
return OngoingActivityChipModel.Shown.IconOnly(icon, colors, onClickListener)
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/binder/OngoingActivityChipBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/binder/OngoingActivityChipBinder.kt
index 69ef09d8bf5e..b0fa9d842480 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/binder/OngoingActivityChipBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/binder/OngoingActivityChipBinder.kt
@@ -25,6 +25,7 @@ import android.widget.DateTimeView
import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.TextView
+import androidx.annotation.UiThread
import com.android.systemui.common.ui.binder.IconViewBinder
import com.android.systemui.res.R
import com.android.systemui.statusbar.StatusBarIconView
@@ -38,24 +39,24 @@ import com.android.systemui.statusbar.notification.icon.ui.viewbinder.Notificati
/** Binder for ongoing activity chip views. */
object OngoingActivityChipBinder {
/** Binds the given [chipModel] data to the given [chipView]. */
- fun bind(chipModel: OngoingActivityChipModel, chipView: View, iconViewStore: IconViewStore?) {
- val chipContext = chipView.context
- val chipDefaultIconView: ImageView =
- chipView.requireViewById(R.id.ongoing_activity_chip_icon)
- val chipTimeView: ChipChronometer =
- chipView.requireViewById(R.id.ongoing_activity_chip_time)
- val chipTextView: TextView = chipView.requireViewById(R.id.ongoing_activity_chip_text)
- val chipShortTimeDeltaView: DateTimeView =
- chipView.requireViewById(R.id.ongoing_activity_chip_short_time_delta)
- val chipBackgroundView: ChipBackgroundContainer =
- chipView.requireViewById(R.id.ongoing_activity_chip_background)
+ fun bind(
+ chipModel: OngoingActivityChipModel,
+ viewBinding: OngoingActivityChipViewBinding,
+ iconViewStore: IconViewStore?,
+ ) {
+ val chipContext = viewBinding.rootView.context
+ val chipDefaultIconView = viewBinding.defaultIconView
+ val chipTimeView = viewBinding.timeView
+ val chipTextView = viewBinding.textView
+ val chipShortTimeDeltaView = viewBinding.shortTimeDeltaView
+ val chipBackgroundView = viewBinding.backgroundView
when (chipModel) {
is OngoingActivityChipModel.Shown -> {
// Data
setChipIcon(chipModel, chipBackgroundView, chipDefaultIconView, iconViewStore)
setChipMainContent(chipModel, chipTextView, chipTimeView, chipShortTimeDeltaView)
- chipView.setOnClickListener(chipModel.onClickListener)
+ viewBinding.rootView.setOnClickListener(chipModel.onClickListener)
updateChipPadding(
chipModel,
chipBackgroundView,
@@ -65,7 +66,7 @@ object OngoingActivityChipBinder {
)
// Accessibility
- setChipAccessibility(chipModel, chipView, chipBackgroundView)
+ setChipAccessibility(chipModel, viewBinding.rootView, chipBackgroundView)
// Colors
val textColor = chipModel.colors.text(chipContext)
@@ -83,6 +84,85 @@ object OngoingActivityChipBinder {
}
}
+ /** Stores [rootView] and relevant child views in an object for easy reference. */
+ fun createBinding(rootView: View): OngoingActivityChipViewBinding {
+ return OngoingActivityChipViewBinding(
+ rootView = rootView,
+ timeView = rootView.requireViewById(R.id.ongoing_activity_chip_time),
+ textView = rootView.requireViewById(R.id.ongoing_activity_chip_text),
+ shortTimeDeltaView =
+ rootView.requireViewById(R.id.ongoing_activity_chip_short_time_delta),
+ defaultIconView = rootView.requireViewById(R.id.ongoing_activity_chip_icon),
+ backgroundView = rootView.requireViewById(R.id.ongoing_activity_chip_background),
+ )
+ }
+
+ /**
+ * Resets any width restrictions that were placed on the primary chip's contents.
+ *
+ * Should be used when the user's screen bounds changed because there may now be more room in
+ * the status bar to show additional content.
+ */
+ fun resetPrimaryChipWidthRestrictions(
+ primaryChipViewBinding: OngoingActivityChipViewBinding,
+ currentPrimaryChipViewModel: OngoingActivityChipModel,
+ ) {
+ if (currentPrimaryChipViewModel is OngoingActivityChipModel.Hidden) {
+ return
+ }
+ resetChipMainContentWidthRestrictions(
+ primaryChipViewBinding,
+ currentPrimaryChipViewModel as OngoingActivityChipModel.Shown,
+ )
+ }
+
+ /**
+ * Resets any width restrictions that were placed on the secondary chip and its contents.
+ *
+ * Should be used when the user's screen bounds changed because there may now be more room in
+ * the status bar to show additional content.
+ */
+ fun resetSecondaryChipWidthRestrictions(
+ secondaryChipViewBinding: OngoingActivityChipViewBinding,
+ currentSecondaryChipModel: OngoingActivityChipModel,
+ ) {
+ if (currentSecondaryChipModel is OngoingActivityChipModel.Hidden) {
+ return
+ }
+ secondaryChipViewBinding.rootView.resetWidthRestriction()
+ resetChipMainContentWidthRestrictions(
+ secondaryChipViewBinding,
+ currentSecondaryChipModel as OngoingActivityChipModel.Shown,
+ )
+ }
+
+ private fun resetChipMainContentWidthRestrictions(
+ viewBinding: OngoingActivityChipViewBinding,
+ model: OngoingActivityChipModel.Shown,
+ ) {
+ when (model) {
+ is OngoingActivityChipModel.Shown.Text -> viewBinding.textView.resetWidthRestriction()
+ is OngoingActivityChipModel.Shown.Timer -> viewBinding.timeView.resetWidthRestriction()
+ is OngoingActivityChipModel.Shown.ShortTimeDelta ->
+ viewBinding.shortTimeDeltaView.resetWidthRestriction()
+ is OngoingActivityChipModel.Shown.IconOnly,
+ is OngoingActivityChipModel.Shown.Countdown -> {}
+ }
+ }
+
+ /**
+ * Resets any width restrictions that were placed on the given view.
+ *
+ * Should be used when the user's screen bounds changed because there may now be more room in
+ * the status bar to show additional content.
+ */
+ @UiThread
+ fun View.resetWidthRestriction() {
+ // View needs to be visible in order to be re-measured
+ visibility = View.VISIBLE
+ forceLayout()
+ }
+
private fun setChipIcon(
chipModel: OngoingActivityChipModel.Shown,
backgroundView: ChipBackgroundContainer,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/binder/OngoingActivityChipViewBinding.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/binder/OngoingActivityChipViewBinding.kt
new file mode 100644
index 000000000000..1814b7430330
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/binder/OngoingActivityChipViewBinding.kt
@@ -0,0 +1,34 @@
+/*
+ * 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.chips.ui.binder
+
+import android.view.View
+import android.widget.ImageView
+import com.android.systemui.statusbar.chips.ui.view.ChipBackgroundContainer
+import com.android.systemui.statusbar.chips.ui.view.ChipChronometer
+import com.android.systemui.statusbar.chips.ui.view.ChipDateTimeView
+import com.android.systemui.statusbar.chips.ui.view.ChipTextView
+
+/** Stores bound views for a given chip. */
+data class OngoingActivityChipViewBinding(
+ val rootView: View,
+ val timeView: ChipChronometer,
+ val textView: ChipTextView,
+ val shortTimeDeltaView: ChipDateTimeView,
+ val defaultIconView: ImageView,
+ val backgroundView: ChipBackgroundContainer,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChip.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChip.kt
new file mode 100644
index 000000000000..1be5842bceeb
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChip.kt
@@ -0,0 +1,208 @@
+/*
+ * 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.chips.ui.compose
+
+import android.content.res.ColorStateList
+import android.view.ViewGroup
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.widthIn
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.dimensionResource
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.viewinterop.AndroidView
+import com.android.systemui.common.ui.compose.Icon
+import com.android.systemui.res.R
+import com.android.systemui.statusbar.chips.ui.compose.modifiers.neverDecreaseWidth
+import com.android.systemui.statusbar.chips.ui.model.ColorsModel
+import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel
+
+@Composable
+fun OngoingActivityChip(model: OngoingActivityChipModel.Shown, modifier: Modifier = Modifier) {
+ val context = LocalContext.current
+ val isClickable = model.onClickListener != null
+ val hasEmbeddedIcon = model.icon is OngoingActivityChipModel.ChipIcon.StatusBarView
+
+ // Use a Box with `fillMaxHeight` to create a larger click surface for the chip. The visible
+ // height of the chip is determined by the height of the background of the Row below.
+ Box(
+ contentAlignment = Alignment.Center,
+ modifier =
+ modifier
+ .fillMaxHeight()
+ .clickable(
+ enabled = isClickable,
+ onClick = {
+ // TODO(b/372657935): Implement click actions.
+ },
+ ),
+ ) {
+ Row(
+ horizontalArrangement = Arrangement.Center,
+ verticalAlignment = Alignment.CenterVertically,
+ modifier =
+ Modifier.clip(
+ RoundedCornerShape(
+ dimensionResource(id = R.dimen.ongoing_activity_chip_corner_radius)
+ )
+ )
+ .height(dimensionResource(R.dimen.ongoing_appops_chip_height))
+ .widthIn(
+ min =
+ if (isClickable) {
+ dimensionResource(id = R.dimen.min_clickable_item_size)
+ } else {
+ 0.dp
+ }
+ )
+ .background(Color(model.colors.background(context).defaultColor))
+ .padding(
+ horizontal =
+ if (hasEmbeddedIcon) {
+ 0.dp
+ } else {
+ dimensionResource(id = R.dimen.ongoing_activity_chip_side_padding)
+ }
+ ),
+ ) {
+ model.icon?.let { ChipIcon(viewModel = it, colors = model.colors) }
+
+ val isIconOnly = model is OngoingActivityChipModel.Shown.IconOnly
+ val isTextOnly = model.icon == null
+ if (!isIconOnly) {
+ ChipContent(
+ viewModel = model,
+ modifier =
+ Modifier.padding(
+ start =
+ if (isTextOnly || hasEmbeddedIcon) {
+ 0.dp
+ } else {
+ dimensionResource(
+ id = R.dimen.ongoing_activity_chip_icon_text_padding
+ )
+ },
+ end =
+ if (hasEmbeddedIcon) {
+ dimensionResource(
+ id =
+ R.dimen
+ .ongoing_activity_chip_text_end_padding_for_embedded_padding_icon
+ )
+ } else {
+ 0.dp
+ },
+ ),
+ )
+ }
+ }
+ }
+}
+
+@Composable
+private fun ChipIcon(
+ viewModel: OngoingActivityChipModel.ChipIcon,
+ colors: ColorsModel,
+ modifier: Modifier = Modifier,
+) {
+ val context = LocalContext.current
+
+ when (viewModel) {
+ is OngoingActivityChipModel.ChipIcon.StatusBarView -> {
+ val originalIcon = viewModel.impl
+ val iconSizePx =
+ context.resources.getDimensionPixelSize(
+ R.dimen.ongoing_activity_chip_embedded_padding_icon_size
+ )
+ AndroidView(
+ modifier = modifier,
+ factory = { _ ->
+ originalIcon.apply {
+ layoutParams = ViewGroup.LayoutParams(iconSizePx, iconSizePx)
+ imageTintList = ColorStateList.valueOf(colors.text(context))
+ }
+ },
+ )
+ }
+
+ is OngoingActivityChipModel.ChipIcon.SingleColorIcon -> {
+ Icon(
+ icon = viewModel.impl,
+ tint = Color(colors.text(context)),
+ modifier =
+ modifier.size(dimensionResource(id = R.dimen.ongoing_activity_chip_icon_size)),
+ )
+ }
+
+ // TODO(b/372657935): Add recommended architecture implementation for
+ // StatusBarNotificationIcons
+ is OngoingActivityChipModel.ChipIcon.StatusBarNotificationIcon -> {}
+ }
+}
+
+@Composable
+private fun ChipContent(viewModel: OngoingActivityChipModel.Shown, modifier: Modifier = Modifier) {
+ val context = LocalContext.current
+ when (viewModel) {
+ is OngoingActivityChipModel.Shown.Timer -> {
+ ChronometerText(
+ startTimeMillis = viewModel.startTimeMs,
+ style = MaterialTheme.typography.labelLarge,
+ color = Color(viewModel.colors.text(context)),
+ modifier = modifier,
+ )
+ }
+
+ is OngoingActivityChipModel.Shown.Countdown -> {
+ ChipText(
+ text = viewModel.secondsUntilStarted.toString(),
+ color = Color(viewModel.colors.text(context)),
+ style = MaterialTheme.typography.labelLarge,
+ modifier = modifier.neverDecreaseWidth(),
+ backgroundColor = Color(viewModel.colors.background(context).defaultColor),
+ )
+ }
+
+ is OngoingActivityChipModel.Shown.Text -> {
+ ChipText(
+ text = viewModel.text,
+ color = Color(viewModel.colors.text(context)),
+ style = MaterialTheme.typography.labelLarge,
+ modifier = modifier,
+ backgroundColor = Color(viewModel.colors.background(context).defaultColor),
+ )
+ }
+
+ is OngoingActivityChipModel.Shown.ShortTimeDelta -> {
+ // TODO(b/372657935): Implement ShortTimeDelta content in compose.
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChips.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChips.kt
new file mode 100644
index 000000000000..85ea087f531b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChips.kt
@@ -0,0 +1,47 @@
+/*
+ * 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.chips.ui.compose
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.padding
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import com.android.systemui.statusbar.chips.ui.model.MultipleOngoingActivityChipsModel
+import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel
+
+@Composable
+fun OngoingActivityChips(chips: MultipleOngoingActivityChipsModel, modifier: Modifier = Modifier) {
+ Row(
+ // TODO(b/372657935): Remove magic numbers for padding and spacing.
+ modifier = modifier.fillMaxHeight().padding(horizontal = 6.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(8.dp),
+ ) {
+ // TODO(b/372657935): Make sure chips are only shown when there is enough horizontal
+ // space.
+ if (chips.primary is OngoingActivityChipModel.Shown) {
+ OngoingActivityChip(model = chips.primary)
+ }
+ if (chips.secondary is OngoingActivityChipModel.Shown) {
+ OngoingActivityChip(model = chips.secondary)
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/OngoingActivityChipModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/OngoingActivityChipModel.kt
index c81e8e211507..956d99e46766 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/OngoingActivityChipModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/OngoingActivityChipModel.kt
@@ -17,7 +17,6 @@
package com.android.systemui.statusbar.chips.ui.model
import android.view.View
-import com.android.systemui.Flags
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.statusbar.StatusBarIconView
import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips
@@ -130,10 +129,6 @@ sealed class OngoingActivityChipModel {
*/
data class StatusBarView(val impl: StatusBarIconView) : ChipIcon {
init {
- check(Flags.statusBarCallChipNotificationIcon()) {
- "OngoingActivityChipModel.ChipIcon.StatusBarView created even though " +
- "Flags.statusBarCallChipNotificationIcon is not enabled"
- }
StatusBarConnectedDisplays.assertInLegacyMode()
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/view/ChipChronometer.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/view/ChipChronometer.kt
index ff3061e850d9..7b4b79d7c852 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/view/ChipChronometer.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/view/ChipChronometer.kt
@@ -33,10 +33,8 @@ import androidx.annotation.UiThread
* that wide. This means the chip may get larger over time (e.g. in the transition from 59:59 to
* 1:00:00), but never smaller.
* 2) Hiding the text if the time gets too long for the space available. Once the text has been
- * hidden, it remains hidden for the duration of the activity.
- *
- * Note that if the text was too big in portrait mode, resulting in the text being hidden, then the
- * text will also be hidden in landscape (even if there is enough space for it in landscape).
+ * hidden, it remains hidden for the duration of the activity (or until [resetWidthRestriction]
+ * is called).
*/
class ChipChronometer
@JvmOverloads
@@ -51,12 +49,23 @@ constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0) :
private var shouldHideText: Boolean = false
override fun setBase(base: Long) {
- // These variables may have changed during the previous activity, so re-set them before the
- // new activity starts.
+ resetWidthRestriction()
+ super.setBase(base)
+ }
+
+ /**
+ * Resets any width restrictions that were placed on the chronometer.
+ *
+ * Should be used when the user's screen bounds changed because there may now be more room in
+ * the status bar to show additional content.
+ */
+ @UiThread
+ fun resetWidthRestriction() {
minimumTextWidth = 0
shouldHideText = false
+ // View needs to be visible in order to be re-measured
visibility = VISIBLE
- super.setBase(base)
+ forceLayout()
}
/** Sets whether this view should hide its text or not. */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarModule.kt
index eff959d0f83b..351cdc8e7f36 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarModule.kt
@@ -29,6 +29,7 @@ import com.android.systemui.statusbar.data.StatusBarDataLayerModule
import com.android.systemui.statusbar.data.repository.LightBarControllerStore
import com.android.systemui.statusbar.layout.StatusBarContentInsetsProvider
import com.android.systemui.statusbar.layout.StatusBarContentInsetsProviderImpl
+import com.android.systemui.statusbar.layout.ui.viewmodel.StatusBarContentInsetsViewModel
import com.android.systemui.statusbar.phone.AutoHideController
import com.android.systemui.statusbar.phone.AutoHideControllerImpl
import com.android.systemui.statusbar.phone.LightBarController
@@ -39,6 +40,7 @@ import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallLog
import com.android.systemui.statusbar.phone.ongoingcall.StatusBarChipsModernization
import com.android.systemui.statusbar.phone.ongoingcall.domain.interactor.OngoingCallInteractor
import com.android.systemui.statusbar.policy.ConfigurationController
+import com.android.systemui.statusbar.ui.StatusBarUiLayerModule
import com.android.systemui.statusbar.ui.SystemBarUtilsProxyImpl
import com.android.systemui.statusbar.window.MultiDisplayStatusBarWindowControllerStore
import com.android.systemui.statusbar.window.SingleDisplayStatusBarWindowControllerStore
@@ -60,7 +62,14 @@ import dagger.multibindings.IntoMap
* ([com.android.systemui.statusbar.pipeline.dagger.StatusBarPipelineModule],
* [com.android.systemui.statusbar.policy.dagger.StatusBarPolicyModule], etc.).
*/
-@Module(includes = [StatusBarDataLayerModule::class, SystemBarUtilsProxyImpl.Module::class])
+@Module(
+ includes =
+ [
+ StatusBarDataLayerModule::class,
+ StatusBarUiLayerModule::class,
+ SystemBarUtilsProxyImpl.Module::class,
+ ]
+)
interface StatusBarModule {
@Binds
@@ -169,5 +178,13 @@ interface StatusBarModule {
): StatusBarContentInsetsProvider {
return factory.create(context, configurationController, sysUICutoutProvider)
}
+
+ @Provides
+ @SysUISingleton
+ fun contentInsetsViewModel(
+ insetsProvider: StatusBarContentInsetsProvider
+ ): StatusBarContentInsetsViewModel {
+ return StatusBarContentInsetsViewModel(insetsProvider)
+ }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/layout/ui/viewmodel/StatusBarContentInsetsViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/layout/ui/viewmodel/StatusBarContentInsetsViewModel.kt
new file mode 100644
index 000000000000..03c07480ecea
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/layout/ui/viewmodel/StatusBarContentInsetsViewModel.kt
@@ -0,0 +1,51 @@
+/*
+ * 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.layout.ui.viewmodel
+
+import android.graphics.Rect
+import com.android.systemui.statusbar.layout.StatusBarContentInsetsChangedListener
+import com.android.systemui.statusbar.layout.StatusBarContentInsetsProvider
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.onStart
+
+/** A recommended architecture version of [StatusBarContentInsetsProvider]. */
+class StatusBarContentInsetsViewModel(
+ private val statusBarContentInsetsProvider: StatusBarContentInsetsProvider
+) {
+ /** Emits the status bar content area for the given rotation in absolute bounds. */
+ val contentArea: Flow<Rect> =
+ conflatedCallbackFlow {
+ val listener =
+ object : StatusBarContentInsetsChangedListener {
+ override fun onStatusBarContentInsetsChanged() {
+ trySend(
+ statusBarContentInsetsProvider
+ .getStatusBarContentAreaForCurrentRotation()
+ )
+ }
+ }
+ statusBarContentInsetsProvider.addCallback(listener)
+ awaitClose { statusBarContentInsetsProvider.removeCallback(listener) }
+ }
+ .onStart {
+ emit(statusBarContentInsetsProvider.getStatusBarContentAreaForCurrentRotation())
+ }
+ .distinctUntilChanged()
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/layout/ui/viewmodel/StatusBarContentInsetsViewModelStore.kt b/packages/SystemUI/src/com/android/systemui/statusbar/layout/ui/viewmodel/StatusBarContentInsetsViewModelStore.kt
new file mode 100644
index 000000000000..d2dccc49ffd7
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/layout/ui/viewmodel/StatusBarContentInsetsViewModelStore.kt
@@ -0,0 +1,99 @@
+/*
+ * 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.layout.ui.viewmodel
+
+import com.android.systemui.CoreStartable
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.display.data.repository.DisplayRepository
+import com.android.systemui.display.data.repository.PerDisplayStore
+import com.android.systemui.display.data.repository.PerDisplayStoreImpl
+import com.android.systemui.display.data.repository.SingleDisplayStore
+import com.android.systemui.statusbar.core.StatusBarConnectedDisplays
+import com.android.systemui.statusbar.data.repository.StatusBarContentInsetsProviderStore
+import dagger.Lazy
+import dagger.Module
+import dagger.Provides
+import dagger.multibindings.ClassKey
+import dagger.multibindings.IntoMap
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+
+/** Provides per-display instances of [StatusBarContentInsetsViewModel]. */
+interface StatusBarContentInsetsViewModelStore : PerDisplayStore<StatusBarContentInsetsViewModel>
+
+@SysUISingleton
+class MultiDisplayStatusBarContentInsetsViewModelStore
+@Inject
+constructor(
+ @Background backgroundApplicationScope: CoroutineScope,
+ displayRepository: DisplayRepository,
+ private val statusBarContentInsetsProviderStore: StatusBarContentInsetsProviderStore,
+) :
+ StatusBarContentInsetsViewModelStore,
+ PerDisplayStoreImpl<StatusBarContentInsetsViewModel>(
+ backgroundApplicationScope,
+ displayRepository,
+ ) {
+
+ override fun createInstanceForDisplay(displayId: Int): StatusBarContentInsetsViewModel? {
+ val insetsProvider =
+ statusBarContentInsetsProviderStore.forDisplay(displayId) ?: return null
+ return StatusBarContentInsetsViewModel(insetsProvider)
+ }
+
+ override val instanceClass = StatusBarContentInsetsViewModel::class.java
+}
+
+@SysUISingleton
+class SingleDisplayStatusBarContentInsetsViewModelStore
+@Inject
+constructor(statusBarContentInsetsViewModel: StatusBarContentInsetsViewModel) :
+ StatusBarContentInsetsViewModelStore,
+ PerDisplayStore<StatusBarContentInsetsViewModel> by SingleDisplayStore(
+ defaultInstance = statusBarContentInsetsViewModel
+ )
+
+@Module
+object StatusBarContentInsetsViewModelStoreModule {
+ @Provides
+ @SysUISingleton
+ @IntoMap
+ @ClassKey(StatusBarContentInsetsViewModelStore::class)
+ fun storeAsCoreStartable(
+ multiDisplayLazy: Lazy<MultiDisplayStatusBarContentInsetsViewModelStore>
+ ): CoreStartable {
+ return if (StatusBarConnectedDisplays.isEnabled) {
+ return multiDisplayLazy.get()
+ } else {
+ CoreStartable.NOP
+ }
+ }
+
+ @Provides
+ @SysUISingleton
+ fun store(
+ singleDisplayLazy: Lazy<SingleDisplayStatusBarContentInsetsViewModelStore>,
+ multiDisplayLazy: Lazy<MultiDisplayStatusBarContentInsetsViewModelStore>,
+ ): StatusBarContentInsetsViewModelStore {
+ return if (StatusBarConnectedDisplays.isEnabled) {
+ multiDisplayLazy.get()
+ } else {
+ singleDisplayLazy.get()
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt
index c7535ec14d5d..eb5a3703bcfb 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt
@@ -112,20 +112,20 @@ constructor(
if (StatusBarNotifChips.isEnabled) {
applicationScope.launch {
statusBarNotificationChipsInteractor.promotedNotificationChipTapEvent.collect {
- showPromotedNotificationHeadsUp(it)
+ onPromotedNotificationChipTapEvent(it)
}
}
}
}
/**
- * Shows the promoted notification with the given [key] as heads-up.
+ * Updates the heads-up state based on which promoted notification with the given [key] was
+ * tapped.
*
* Must be run on the main thread.
*/
- private fun showPromotedNotificationHeadsUp(key: String) {
+ private fun onPromotedNotificationChipTapEvent(key: String) {
StatusBarNotifChips.assertInNewMode()
- mLogger.logShowPromotedNotificationHeadsUp(key)
val entry = notifCollection.getEntry(key)
if (entry == null) {
@@ -135,22 +135,29 @@ constructor(
// TODO(b/364653005): Validate that the given key indeed matches a promoted notification,
// not just any notification.
+ val isCurrentlyHeadsUp = mHeadsUpManager.isHeadsUpEntry(entry.key)
val posted =
PostedEntry(
entry,
wasAdded = false,
wasUpdated = false,
- // Force-set this notification to show heads-up.
- shouldHeadsUpEver = true,
- shouldHeadsUpAgain = true,
+ // We want the chip to act as a toggle, so if the chip's notification is currently
+ // showing as heads up, then we should stop showing it.
+ shouldHeadsUpEver = !isCurrentlyHeadsUp,
+ shouldHeadsUpAgain = !isCurrentlyHeadsUp,
isPinnedByUser = true,
- isHeadsUpEntry = mHeadsUpManager.isHeadsUpEntry(entry.key),
+ isHeadsUpEntry = isCurrentlyHeadsUp,
isBinding = isEntryBinding(entry),
)
+ if (isCurrentlyHeadsUp) {
+ mLogger.logHidePromotedNotificationHeadsUp(key)
+ } else {
+ mLogger.logShowPromotedNotificationHeadsUp(key)
+ }
mExecutor.execute {
mPostedEntries[entry.key] = posted
- mNotifPromoter.invalidateList("showPromotedNotificationHeadsUp: ${entry.logKey}")
+ mNotifPromoter.invalidateList("onPromotedNotificationChipTapEvent: ${entry.logKey}")
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorLogger.kt
index e443a0418ffd..5141aa35b041 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorLogger.kt
@@ -148,6 +148,15 @@ class HeadsUpCoordinatorLogger(private val buffer: LogBuffer, private val verbos
)
}
+ fun logHidePromotedNotificationHeadsUp(key: String) {
+ buffer.log(
+ TAG,
+ LogLevel.DEBUG,
+ { str1 = key },
+ { "requesting promoted entry to hide heads up: $str1" },
+ )
+ }
+
fun logPromotedNotificationForHeadsUpNotFound(key: String) {
buffer.log(
TAG,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractor.kt
index 75c7d2d5be98..6140c92369b3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractor.kt
@@ -25,6 +25,7 @@ import com.android.systemui.scene.shared.flag.SceneContainerFlag
import com.android.systemui.shade.domain.interactor.ShadeInteractor
import com.android.systemui.statusbar.notification.data.repository.HeadsUpRepository
import com.android.systemui.statusbar.notification.data.repository.HeadsUpRowRepository
+import com.android.systemui.statusbar.notification.domain.model.TopPinnedState
import com.android.systemui.statusbar.notification.headsup.PinnedStatus
import com.android.systemui.statusbar.notification.shared.HeadsUpRowKey
import javax.inject.Inject
@@ -98,21 +99,39 @@ constructor(
}
}
- /** What [PinnedStatus] does the top row have? */
- private val topPinnedStatus: Flow<PinnedStatus> =
+ /** What [PinnedStatus] and key does the top row have? */
+ private val topPinnedState: Flow<TopPinnedState> =
headsUpRepository.activeHeadsUpRows.flatMapLatest { rows ->
if (rows.isNotEmpty()) {
- combine(rows.map { it.pinnedStatus }) { pinnedStatus ->
- pinnedStatus.firstOrNull { it.isPinned } ?: PinnedStatus.NotPinned
+ // For each row, emits a (key, pinnedStatus) pair each time any row's
+ // `pinnedStatus` changes
+ val toCombine: List<Flow<Pair<String, PinnedStatus>>> =
+ rows.map { row -> row.pinnedStatus.map { status -> row.key to status } }
+ combine(toCombine) { pairs ->
+ val topPinnedRow: Pair<String, PinnedStatus>? =
+ pairs.firstOrNull { it.second.isPinned }
+ if (topPinnedRow != null) {
+ TopPinnedState.Pinned(
+ key = topPinnedRow.first,
+ status = topPinnedRow.second,
+ )
+ } else {
+ TopPinnedState.NothingPinned
+ }
}
} else {
- // if the set is empty, there are no flows to combine
- flowOf(PinnedStatus.NotPinned)
+ flowOf(TopPinnedState.NothingPinned)
}
}
/** Are there any pinned heads up rows to display? */
- val hasPinnedRows: Flow<Boolean> = topPinnedStatus.map { it.isPinned }
+ val hasPinnedRows: Flow<Boolean> =
+ topPinnedState.map {
+ when (it) {
+ is TopPinnedState.Pinned -> it.status.isPinned
+ is TopPinnedState.NothingPinned -> false
+ }
+ }
val isHeadsUpOrAnimatingAway: Flow<Boolean> by lazy {
if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) {
@@ -142,13 +161,25 @@ constructor(
}
}
- /** Emits the pinned notification state as it relates to the status bar. */
- val statusBarHeadsUpState: Flow<PinnedStatus> =
- combine(topPinnedStatus, canShowHeadsUp) { topPinnedStatus, canShowHeadsUp ->
+ /**
+ * Emits the pinned notification state as it relates to the status bar. Includes both the pinned
+ * status and key of the notification that's pinned (if there is a pinned notification).
+ */
+ val statusBarHeadsUpState: Flow<TopPinnedState> =
+ combine(topPinnedState, canShowHeadsUp) { topPinnedState, canShowHeadsUp ->
if (canShowHeadsUp) {
- topPinnedStatus
+ topPinnedState
} else {
- PinnedStatus.NotPinned
+ TopPinnedState.NothingPinned
+ }
+ }
+
+ /** Emits the pinned notification status as it relates to the status bar. */
+ val statusBarHeadsUpStatus: Flow<PinnedStatus> =
+ statusBarHeadsUpState.map {
+ when (it) {
+ is TopPinnedState.Pinned -> it.status
+ is TopPinnedState.NothingPinned -> PinnedStatus.NotPinned
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationListInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationListInteractor.kt
index 042389f7fde7..fd5973e0ab3b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationListInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationListInteractor.kt
@@ -25,7 +25,6 @@ import android.graphics.drawable.Icon
import android.service.notification.StatusBarNotification
import android.util.ArrayMap
import com.android.app.tracing.traceSection
-import com.android.systemui.Flags
import com.android.systemui.statusbar.StatusBarIconView
import com.android.systemui.statusbar.notification.collection.GroupEntry
import com.android.systemui.statusbar.notification.collection.ListEntry
@@ -132,12 +131,6 @@ private class ActiveNotificationsStoreBuilder(
}
private fun NotificationEntry.toModel(): ActiveNotificationModel {
- val statusBarChipIcon =
- if (Flags.statusBarCallChipNotificationIcon()) {
- icons.statusBarChipIcon
- } else {
- null
- }
val promotedContent =
if (PromotedNotificationContentModel.featureFlagEnabled()) {
promotedNotificationContentModel
@@ -158,7 +151,7 @@ private class ActiveNotificationsStoreBuilder(
aodIcon = icons.aodIcon?.sourceIcon,
shelfIcon = icons.shelfIcon?.sourceIcon,
statusBarIcon = icons.statusBarIcon?.sourceIcon,
- statusBarChipIconView = statusBarChipIcon,
+ statusBarChipIconView = icons.statusBarChipIcon,
uid = sbn.uid,
packageName = sbn.packageName,
contentIntent = sbn.notification.contentIntent,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/model/TopPinnedState.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/model/TopPinnedState.kt
new file mode 100644
index 000000000000..51c448adf998
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/model/TopPinnedState.kt
@@ -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.domain.model
+
+import com.android.systemui.statusbar.notification.headsup.PinnedStatus
+
+/** A class representing the state of the top pinned row. */
+sealed interface TopPinnedState {
+ /** Nothing is pinned. */
+ data object NothingPinned : TopPinnedState
+
+ /**
+ * The top pinned row is a notification with the given key and status.
+ *
+ * @property status must have [PinnedStatus.isPinned] as true.
+ */
+ data class Pinned(val key: String, val status: PinnedStatus) : TopPinnedState {
+ init {
+ check(status.isPinned)
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconManager.kt
index b56a838a80a5..31375cc4a03a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconManager.kt
@@ -162,9 +162,7 @@ constructor(
val sbIcon = iconBuilder.createIconView(entry)
sbIcon.scaleType = ImageView.ScaleType.CENTER_INSIDE
val sbChipIcon: StatusBarIconView?
- if (
- Flags.statusBarCallChipNotificationIcon() && !StatusBarConnectedDisplays.isEnabled
- ) {
+ if (!StatusBarConnectedDisplays.isEnabled) {
sbChipIcon = iconBuilder.createIconView(entry)
sbChipIcon.scaleType = ImageView.ScaleType.CENTER_INSIDE
} else {
@@ -186,7 +184,7 @@ constructor(
try {
setIcon(entry, normalIconDescriptor, sbIcon)
- if (Flags.statusBarCallChipNotificationIcon() && sbChipIcon != null) {
+ if (sbChipIcon != null) {
setIcon(entry, normalIconDescriptor, sbChipIcon)
}
setIcon(entry, sensitiveIconDescriptor, shelfIcon)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationLogger.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationLogger.java
index c6832bc20e6d..cc4be57168cc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationLogger.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationLogger.java
@@ -20,7 +20,6 @@ import android.os.Handler;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.SystemClock;
-import android.os.Trace;
import android.service.notification.NotificationListenerService;
import android.util.ArrayMap;
import android.util.ArraySet;
@@ -29,6 +28,7 @@ import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import com.android.app.tracing.coroutines.TrackTracer;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.statusbar.IStatusBarService;
@@ -152,8 +152,8 @@ public class NotificationLogger implements StateListener, CoreStartable,
mExpansionStateLogger.onVisibilityChanged(
mTmpCurrentlyVisibleNotifications, mTmpCurrentlyVisibleNotifications);
- Trace.traceCounter(Trace.TRACE_TAG_APP, "Notifications [Active]", N);
- Trace.traceCounter(Trace.TRACE_TAG_APP, "Notifications [Visible]",
+ TrackTracer.instantForGroup("Notifications", "Active", N);
+ TrackTracer.instantForGroup("Notifications", "Visible",
mCurrentlyVisibleNotifications.size());
recycleAllVisibilityObjects(mTmpNoLongerVisibleNotifications);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ChannelEditorListView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ChannelEditorListView.kt
index 10e67a40ebc9..640d364895ae 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ChannelEditorListView.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ChannelEditorListView.kt
@@ -25,7 +25,7 @@ import android.app.NotificationManager.IMPORTANCE_NONE
import android.app.NotificationManager.IMPORTANCE_UNSPECIFIED
import android.content.Context
import android.graphics.drawable.Drawable
-import android.text.TextUtils
+import android.text.TextUtils.isEmpty
import android.transition.AutoTransition
import android.transition.Transition
import android.transition.TransitionManager
@@ -37,13 +37,10 @@ import android.widget.LinearLayout
import android.widget.Switch
import android.widget.TextView
import com.android.settingslib.Utils
-
import com.android.systemui.res.R
import com.android.systemui.util.Assert
-/**
- * Half-shelf for notification channel controls
- */
+/** Half-shelf for notification channel controls */
class ChannelEditorListView(c: Context, attrs: AttributeSet) : LinearLayout(c, attrs) {
lateinit var controller: ChannelEditorDialogController
var appIcon: Drawable? = null
@@ -84,23 +81,21 @@ class ChannelEditorListView(c: Context, attrs: AttributeSet) : LinearLayout(c, a
val transition = AutoTransition()
transition.duration = 200
- transition.addListener(object : Transition.TransitionListener {
- override fun onTransitionEnd(p0: Transition?) {
- notifySubtreeAccessibilityStateChangedIfNeeded()
- }
+ transition.addListener(
+ object : Transition.TransitionListener {
+ override fun onTransitionEnd(p0: Transition?) {
+ notifySubtreeAccessibilityStateChangedIfNeeded()
+ }
- override fun onTransitionResume(p0: Transition?) {
- }
+ override fun onTransitionResume(p0: Transition?) {}
- override fun onTransitionPause(p0: Transition?) {
- }
+ override fun onTransitionPause(p0: Transition?) {}
- override fun onTransitionCancel(p0: Transition?) {
- }
+ override fun onTransitionCancel(p0: Transition?) {}
- override fun onTransitionStart(p0: Transition?) {
+ override fun onTransitionStart(p0: Transition?) {}
}
- })
+ )
TransitionManager.beginDelayedTransition(this, transition)
// Remove any rows
@@ -130,8 +125,9 @@ class ChannelEditorListView(c: Context, attrs: AttributeSet) : LinearLayout(c, a
private fun updateAppControlRow(enabled: Boolean) {
appControlRow.iconView.setImageDrawable(appIcon)
- appControlRow.channelName.text = context.resources
- .getString(R.string.notification_channel_dialog_title, appName)
+ val title = context.resources.getString(R.string.notification_channel_dialog_title, appName)
+ appControlRow.channelName.text = title
+ appControlRow.switch.contentDescription = title
appControlRow.switch.isChecked = enabled
appControlRow.switch.setOnCheckedChangeListener { _, b ->
controller.proposeSetAppNotificationsEnabled(b)
@@ -164,8 +160,8 @@ class ChannelRow(c: Context, attrs: AttributeSet) : LinearLayout(c, attrs) {
var gentle = false
init {
- highlightColor = Utils.getColorAttrDefaultColor(
- context, android.R.attr.colorControlHighlight)
+ highlightColor =
+ Utils.getColorAttrDefaultColor(context, android.R.attr.colorControlHighlight)
}
var channel: NotificationChannel? = null
@@ -182,17 +178,16 @@ class ChannelRow(c: Context, attrs: AttributeSet) : LinearLayout(c, attrs) {
switch = requireViewById(R.id.toggle)
switch.setOnCheckedChangeListener { _, b ->
channel?.let {
- controller.proposeEditForChannel(it,
- if (b) it.originalImportance.coerceAtLeast(IMPORTANCE_LOW)
- else IMPORTANCE_NONE)
+ controller.proposeEditForChannel(
+ it,
+ if (b) it.originalImportance.coerceAtLeast(IMPORTANCE_LOW) else IMPORTANCE_NONE,
+ )
}
}
setOnClickListener { switch.toggle() }
}
- /**
- * Play an animation that highlights this row
- */
+ /** Play an animation that highlights this row */
fun playHighlight() {
// Use 0 for the start value because our background is given to us by our parent
val fadeInLoop = ValueAnimator.ofObject(ArgbEvaluator(), 0, highlightColor)
@@ -211,17 +206,21 @@ class ChannelRow(c: Context, attrs: AttributeSet) : LinearLayout(c, attrs) {
channelName.text = nc.name ?: ""
- nc.group?.let { groupId ->
- channelDescription.text = controller.groupNameForId(groupId)
- }
+ nc.group?.let { groupId -> channelDescription.text = controller.groupNameForId(groupId) }
- if (nc.group == null || TextUtils.isEmpty(channelDescription.text)) {
+ if (nc.group == null || isEmpty(channelDescription.text)) {
channelDescription.visibility = View.GONE
} else {
channelDescription.visibility = View.VISIBLE
}
switch.isChecked = nc.importance != IMPORTANCE_NONE
+ switch.contentDescription =
+ if (isEmpty(channelDescription.text)) {
+ channelName.text
+ } else {
+ "${channelName.text} ${channelDescription.text}"
+ }
}
private fun updateImportance() {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
index 7e3d0043b91a..95604c113a15 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
@@ -1267,6 +1267,9 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
}
if (mExpandedWhenPinned) {
return Math.max(getMaxExpandHeight(), getHeadsUpHeight());
+ } else if (android.app.Flags.compactHeadsUpNotification()
+ && getShowingLayout().isHUNCompact()) {
+ return getHeadsUpHeight();
} else if (atLeastMinHeight) {
return Math.max(getCollapsedHeight(), getHeadsUpHeight());
} else {
@@ -3680,6 +3683,10 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
return super.disallowSingleClick(event);
}
+ // TODO: b/388470175 - Although this does get triggered when a notification
+ // is expanded by the system (e.g. the first notication in the shade), it
+ // will not be when a notification is collapsed by the system (such as when
+ // the shade is closed).
private void onExpansionChanged(boolean userAction, boolean wasExpanded) {
boolean nowExpanded = isExpanded();
if (mIsSummaryWithChildren && (!mIsMinimized || wasExpanded)) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/MagicActionBackgroundDrawable.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/MagicActionBackgroundDrawable.kt
new file mode 100644
index 000000000000..e27ff7d6746b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/MagicActionBackgroundDrawable.kt
@@ -0,0 +1,55 @@
+/*
+ * 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.content.Context
+import android.graphics.Canvas
+import android.graphics.ColorFilter
+import android.graphics.Paint
+import android.graphics.PixelFormat
+import android.graphics.drawable.Drawable
+
+/**
+ * A background style for smarter-smart-actions.
+ *
+ * TODO(b/383567383) implement final UX
+ */
+class MagicActionBackgroundDrawable(context: Context) : Drawable() {
+
+ private var _alpha: Int = 255
+ private var _colorFilter: ColorFilter? = null
+ private val paint =
+ Paint().apply {
+ color = context.getColor(com.android.internal.R.color.materialColorPrimaryContainer)
+ }
+
+ override fun draw(canvas: Canvas) {
+ canvas.drawRect(bounds, paint)
+ }
+
+ override fun setAlpha(alpha: Int) {
+ _alpha = alpha
+ invalidateSelf()
+ }
+
+ override fun setColorFilter(colorFilter: ColorFilter?) {
+ _colorFilter = colorFilter
+ invalidateSelf()
+ }
+
+ override fun getOpacity(): Int = PixelFormat.TRANSLUCENT
+}
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 7c44eae6c0b8..70e27a981b49 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
@@ -16,8 +16,6 @@
package com.android.systemui.statusbar.notification.row;
-import static android.app.Flags.notificationsRedesignTemplates;
-
import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE;
import static com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_SENSITIVE_CONTENT;
import static com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_CONTRACTED;
@@ -481,16 +479,15 @@ public class NotificationContentInflater implements NotificationRowContentBinder
logger.logAsyncTaskProgress(entryForLogging,
"creating low-priority group summary remote view");
result.mNewMinimizedGroupHeaderView =
- builder.makeLowPriorityContentView(/* useRegularSubtext = */ true,
- /* highlightExpander = */ notificationsRedesignTemplates());
+ builder.makeLowPriorityContentView(true /* useRegularSubtext */);
}
}
setNotifsViewsInflaterFactory(result, row, notifLayoutInflaterFactoryProvider);
result.packageContext = packageContext;
result.headsUpStatusBarText = builder.getHeadsUpStatusBarText(
- /* showingPublic = */ false);
+ false /* showingPublic */);
result.headsUpStatusBarTextPublic = builder.getHeadsUpStatusBarText(
- /* showingPublic = */ true);
+ true /* showingPublic */);
return result;
});
@@ -1139,8 +1136,7 @@ public class NotificationContentInflater implements NotificationRowContentBinder
private static RemoteViews createContentView(Notification.Builder builder,
boolean isMinimized, boolean useLarge) {
if (isMinimized) {
- return builder.makeLowPriorityContentView(/* useRegularSubtext = */ false,
- /* highlightExpander = */ false);
+ return builder.makeLowPriorityContentView(false /* useRegularSubtext */);
}
return builder.createContentView(useLarge);
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java
index 786d7d9ea0f3..0d2998174121 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java
@@ -207,6 +207,8 @@ public class NotificationContentView extends FrameLayout implements Notification
private boolean mContentAnimating;
private UiEventLogger mUiEventLogger;
+ private boolean mIsHUNCompact;
+
public NotificationContentView(Context context, AttributeSet attrs) {
super(context, attrs);
mHybridGroupManager = new HybridGroupManager(getContext());
@@ -543,6 +545,7 @@ public class NotificationContentView extends FrameLayout implements Notification
if (child == null) {
mHeadsUpChild = null;
mHeadsUpWrapper = null;
+ mIsHUNCompact = false;
if (mTransformationStartVisibleType == VISIBLE_TYPE_HEADSUP) {
mTransformationStartVisibleType = VISIBLE_TYPE_NONE;
}
@@ -556,8 +559,9 @@ public class NotificationContentView extends FrameLayout implements Notification
mHeadsUpWrapper = NotificationViewWrapper.wrap(getContext(), child,
mContainingNotification);
- if (Flags.compactHeadsUpNotification()
- && mHeadsUpWrapper instanceof NotificationCompactHeadsUpTemplateViewWrapper) {
+ mIsHUNCompact = Flags.compactHeadsUpNotification()
+ && mHeadsUpWrapper instanceof NotificationCompactHeadsUpTemplateViewWrapper;
+ if (mIsHUNCompact) {
logCompactHUNShownEvent();
}
@@ -902,6 +906,10 @@ public class NotificationContentView extends FrameLayout implements Notification
}
}
+ public boolean isHUNCompact() {
+ return mIsHUNCompact;
+ }
+
private boolean isGroupExpanded() {
return mContainingNotification.isGroupExpanded();
}
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 ae9b69c8f6bf..c619b17f1ad8 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
@@ -16,7 +16,6 @@
package com.android.systemui.statusbar.notification.row
import android.annotation.SuppressLint
-import android.app.Flags.notificationsRedesignTemplates
import android.app.Notification
import android.app.Notification.MessagingStyle
import android.content.Context
@@ -888,10 +887,7 @@ constructor(
entryForLogging,
"creating low-priority group summary remote view",
)
- builder.makeLowPriorityContentView(
- /* useRegularSubtext = */ true,
- /* highlightExpander = */ notificationsRedesignTemplates(),
- )
+ builder.makeLowPriorityContentView(true /* useRegularSubtext */)
} else null
NewRemoteViews(
contracted = contracted,
@@ -1661,10 +1657,7 @@ constructor(
useLarge: Boolean,
): RemoteViews {
return if (isMinimized) {
- builder.makeLowPriorityContentView(
- /* useRegularSubtext = */ false,
- /* highlightExpander = */ false,
- )
+ builder.makeLowPriorityContentView(false /* useRegularSubtext */)
} else builder.createContentView(useLarge)
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java
index e477c7430262..8e48065d9d1d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java
@@ -568,8 +568,7 @@ public class NotificationChildrenContainer extends ViewGroup
builder = Notification.Builder.recoverBuilder(getContext(),
notification.getNotification());
}
- header = builder.makeLowPriorityContentView(true /* useRegularSubtext */,
- notificationsRedesignTemplates() /* highlightExpander */);
+ header = builder.makeLowPriorityContentView(true /* useRegularSubtext */);
if (mMinimizedGroupHeader == null) {
mMinimizedGroupHeader = (NotificationHeaderView) header.apply(getContext(),
this);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
index e752e6581421..c717e3b229be 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
@@ -35,6 +35,8 @@ import static com.android.systemui.statusbar.notification.stack.StackStateAnimat
import android.animation.ObjectAnimator;
import android.content.res.Configuration;
import android.graphics.Point;
+import android.graphics.RenderEffect;
+import android.graphics.Shader;
import android.os.Trace;
import android.os.UserHandle;
import android.provider.Settings;
@@ -1237,6 +1239,22 @@ public class NotificationStackScrollLayoutController implements Dumpable {
updateAlpha();
}
+ /**
+ * Applies a blur effect to the view.
+ *
+ * @param blurRadius Radius of blur
+ */
+ public void setBlurRadius(float blurRadius) {
+ if (blurRadius > 0.0f) {
+ mView.setRenderEffect(RenderEffect.createBlurEffect(
+ blurRadius,
+ blurRadius,
+ Shader.TileMode.CLAMP));
+ } else {
+ mView.setRenderEffect(null);
+ }
+ }
+
private void updateAlpha() {
if (mView != null) {
mView.setAlpha(Math.min(Math.min(mMaxAlphaFromView, mMaxAlphaForKeyguard),
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationStatsLoggerImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationStatsLoggerImpl.kt
index 53749ff24394..c8c798d00a06 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationStatsLoggerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationStatsLoggerImpl.kt
@@ -16,9 +16,9 @@
package com.android.systemui.statusbar.notification.stack.ui.view
-import android.os.Trace
import android.service.notification.NotificationListenerService
import androidx.annotation.VisibleForTesting
+import com.android.app.tracing.coroutines.TrackTracer
import com.android.internal.statusbar.IStatusBarService
import com.android.internal.statusbar.NotificationVisibility
import com.android.systemui.dagger.SysUISingleton
@@ -183,8 +183,8 @@ constructor(
maybeLogVisibilityChanges(newlyVisible, noLongerVisible, activeNotifCount)
updateExpansionStates(newlyVisible, noLongerVisible)
- Trace.traceCounter(Trace.TRACE_TAG_APP, "Notifications [Active]", activeNotifCount)
- Trace.traceCounter(Trace.TRACE_TAG_APP, "Notifications [Visible]", newVisibilities.size)
+ TrackTracer.instantForGroup("Notifications", "Active", activeNotifCount)
+ TrackTracer.instantForGroup("Notifications", "Visible", newVisibilities.size)
lastLoggedVisibilities.clear()
lastLoggedVisibilities.putAll(newVisibilities)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt
index 0b2b84e60f4b..3ea4d488357d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt
@@ -179,6 +179,10 @@ constructor(
}
}
+ if (Flags.bouncerUiRevamp()) {
+ launch { viewModel.blurRadius.collect { controller.setBlurRadius(it) } }
+ }
+
if (communalSettingsInteractor.isCommunalFlagEnabled()) {
launch {
viewModel.glanceableHubAlpha.collect {
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 fc8c70fb8e9a..f0455fc3a22b 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
@@ -42,6 +42,7 @@ import com.android.systemui.keyguard.shared.model.KeyguardState.OCCLUDED
import com.android.systemui.keyguard.shared.model.KeyguardState.PRIMARY_BOUNCER
import com.android.systemui.keyguard.shared.model.StatusBarState.SHADE
import com.android.systemui.keyguard.shared.model.StatusBarState.SHADE_LOCKED
+import com.android.systemui.keyguard.ui.transitions.PrimaryBouncerTransition
import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerToGoneTransitionViewModel
import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerToPrimaryBouncerTransitionViewModel
import com.android.systemui.keyguard.ui.viewmodel.AodBurnInViewModel
@@ -154,6 +155,7 @@ constructor(
private val primaryBouncerToGoneTransitionViewModel: PrimaryBouncerToGoneTransitionViewModel,
private val primaryBouncerToLockscreenTransitionViewModel:
PrimaryBouncerToLockscreenTransitionViewModel,
+ private val primaryBouncerTransitions: Set<@JvmSuppressWildcards PrimaryBouncerTransition>,
aodBurnInViewModel: AodBurnInViewModel,
private val communalSceneInteractor: CommunalSceneInteractor,
// Lazy because it's only used in the SceneContainer + Dual Shade configuration.
@@ -562,7 +564,7 @@ constructor(
lockscreenToDreamingTransitionViewModel.lockscreenAlpha,
lockscreenToGoneTransitionViewModel.notificationAlpha(viewState),
lockscreenToOccludedTransitionViewModel.lockscreenAlpha,
- lockscreenToPrimaryBouncerTransitionViewModel.lockscreenAlpha,
+ lockscreenToPrimaryBouncerTransitionViewModel.notificationAlpha,
alternateBouncerToPrimaryBouncerTransitionViewModel.notificationAlpha,
occludedToAodTransitionViewModel.lockscreenAlpha,
occludedToGoneTransitionViewModel.notificationAlpha(viewState),
@@ -626,6 +628,12 @@ constructor(
.dumpWhileCollecting("keyguardAlpha")
}
+ val blurRadius =
+ primaryBouncerTransitions
+ .map { transition -> transition.notificationBlurRadius }
+ .merge()
+ .dumpWhileCollecting("blurRadius")
+
/**
* Returns a flow of the expected alpha while running a LOCKSCREEN<->GLANCEABLE_HUB or
* DREAMING<->GLANCEABLE_HUB transition or idle on the hub.
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java
index 6f29f618ee0d..afc5bc67c0a1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java
@@ -750,7 +750,7 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp
BiometricUnlockSource.Companion.fromBiometricSourceType(biometricSourceType)
);
} else if (biometricSourceType == BiometricSourceType.FINGERPRINT
- && mUpdateMonitor.isUdfpsSupported()) {
+ && mUpdateMonitor.isOpticalUdfpsSupported()) {
long currUptimeMillis = mSystemClock.uptimeMillis();
if (currUptimeMillis - mLastFpFailureUptimeMillis < mConsecutiveFpFailureThreshold) {
mNumConsecutiveFpFailures += 1;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
index 324db79a4078..d43fed0cbf59 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
@@ -43,6 +43,7 @@ import android.view.animation.Interpolator;
import androidx.annotation.FloatRange;
import androidx.annotation.Nullable;
+import com.android.app.tracing.coroutines.TrackTracer;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.colorextraction.ColorExtractor.GradientColors;
import com.android.internal.graphics.ColorUtils;
@@ -554,7 +555,7 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump
final ScrimState oldState = mState;
mState = state;
- Trace.traceCounter(Trace.TRACE_TAG_APP, "scrim_state", mState.ordinal());
+ TrackTracer.instantForGroup("scrim", "state", mState.ordinal());
if (mCallback != null) {
mCallback.onCancelled();
@@ -1279,10 +1280,9 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump
tint = getDebugScrimTint(scrimView);
}
- Trace.traceCounter(Trace.TRACE_TAG_APP, getScrimName(scrimView) + "_alpha",
+ TrackTracer.instantForGroup("scrim", getScrimName(scrimView) + "_alpha",
(int) (alpha * 255));
-
- Trace.traceCounter(Trace.TRACE_TAG_APP, getScrimName(scrimView) + "_tint",
+ TrackTracer.instantForGroup("scrim", getScrimName(scrimView) + "_tint",
Color.alpha(tint));
scrimView.setTint(tint);
if (!mIsBouncerToGoneTransitionRunning) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimState.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimState.java
index 198859a9013d..8dcb66312558 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimState.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimState.java
@@ -17,8 +17,8 @@
package com.android.systemui.statusbar.phone;
import android.graphics.Color;
-import android.os.Trace;
+import com.android.app.tracing.coroutines.TrackTracer;
import com.android.systemui.dock.DockManager;
import com.android.systemui.res.R;
import com.android.systemui.scrim.ScrimView;
@@ -425,11 +425,11 @@ public enum ScrimState {
tint = scrim == mScrimInFront ? ScrimController.DEBUG_FRONT_TINT
: ScrimController.DEBUG_BEHIND_TINT;
}
- Trace.traceCounter(Trace.TRACE_TAG_APP,
+ TrackTracer.instantForGroup("scrim",
scrim == mScrimInFront ? "front_scrim_alpha" : "back_scrim_alpha",
(int) (alpha * 255));
- Trace.traceCounter(Trace.TRACE_TAG_APP,
+ TrackTracer.instantForGroup("scrim",
scrim == mScrimInFront ? "front_scrim_tint" : "back_scrim_tint",
Color.alpha(tint));
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java
index c31e34c50b06..e622d8f52894 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java
@@ -81,6 +81,7 @@ import com.android.systemui.statusbar.phone.ui.StatusBarIconController;
import com.android.systemui.statusbar.pipeline.shared.ui.binder.HomeStatusBarViewBinder;
import com.android.systemui.statusbar.pipeline.shared.ui.binder.StatusBarVisibilityChangeListener;
import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.HomeStatusBarViewModel;
+import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.HomeStatusBarViewModel.HomeStatusBarViewModelFactory;
import com.android.systemui.statusbar.policy.KeyguardStateController;
import com.android.systemui.statusbar.window.StatusBarWindowController;
import com.android.systemui.statusbar.window.StatusBarWindowControllerStore;
@@ -142,6 +143,8 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue
private StatusBarVisibilityModel mLastModifiedVisibility =
StatusBarVisibilityModel.createDefaultModel();
private DarkIconManager mDarkIconManager;
+ private HomeStatusBarViewModel mHomeStatusBarViewModel;
+
private final HomeStatusBarComponent.Factory mHomeStatusBarComponentFactory;
private final CommandQueue mCommandQueue;
private final CollapsedStatusBarFragmentLogger mCollapsedStatusBarFragmentLogger;
@@ -151,8 +154,8 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue
private final ShadeExpansionStateManager mShadeExpansionStateManager;
private final StatusBarIconController mStatusBarIconController;
private final CarrierConfigTracker mCarrierConfigTracker;
- private final HomeStatusBarViewModel mHomeStatusBarViewModel;
private final HomeStatusBarViewBinder mHomeStatusBarViewBinder;
+ private final HomeStatusBarViewModelFactory mHomeStatusBarViewModelFactory;
private final StatusBarHideIconsForBouncerManager mStatusBarHideIconsForBouncerManager;
private final DarkIconManager.Factory mDarkIconManagerFactory;
private final SecureSettings mSecureSettings;
@@ -256,7 +259,7 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue
ShadeExpansionStateManager shadeExpansionStateManager,
StatusBarIconController statusBarIconController,
DarkIconManager.Factory darkIconManagerFactory,
- HomeStatusBarViewModel homeStatusBarViewModel,
+ HomeStatusBarViewModelFactory homeStatusBarViewModelFactory,
HomeStatusBarViewBinder homeStatusBarViewBinder,
StatusBarHideIconsForBouncerManager statusBarHideIconsForBouncerManager,
KeyguardStateController keyguardStateController,
@@ -281,7 +284,7 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue
mAnimationScheduler = animationScheduler;
mShadeExpansionStateManager = shadeExpansionStateManager;
mStatusBarIconController = statusBarIconController;
- mHomeStatusBarViewModel = homeStatusBarViewModel;
+ mHomeStatusBarViewModelFactory = homeStatusBarViewModelFactory;
mHomeStatusBarViewBinder = homeStatusBarViewBinder;
mStatusBarHideIconsForBouncerManager = statusBarHideIconsForBouncerManager;
mDarkIconManagerFactory = darkIconManagerFactory;
@@ -410,6 +413,7 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue
mCarrierConfigTracker.addCallback(mCarrierConfigCallback);
mCarrierConfigTracker.addDefaultDataSubscriptionChangedListener(mDefaultDataListener);
+ mHomeStatusBarViewModel = mHomeStatusBarViewModelFactory.create(displayId);
mHomeStatusBarViewBinder.bind(
view.getContext().getDisplayId(),
mStatusBar,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt
index c57cede754d3..f56c2d5dc5e8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt
@@ -18,8 +18,6 @@ package com.android.systemui.statusbar.phone.ongoingcall
import android.app.ActivityManager
import android.app.IActivityManager
-import android.app.Notification
-import android.app.Notification.CallStyle.CALL_TYPE_ONGOING
import android.app.PendingIntent
import android.app.UidObserver
import android.content.Context
@@ -44,9 +42,6 @@ import com.android.systemui.statusbar.chips.ui.view.ChipBackgroundContainer
import com.android.systemui.statusbar.chips.ui.view.ChipChronometer
import com.android.systemui.statusbar.data.repository.StatusBarModeRepositoryStore
import com.android.systemui.statusbar.gesture.SwipeStatusBarAwayGestureHandler
-import com.android.systemui.statusbar.notification.collection.NotificationEntry
-import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection
-import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener
import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor
import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel
import com.android.systemui.statusbar.notification.shared.CallType
@@ -60,7 +55,9 @@ import java.util.concurrent.Executor
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
-/** A controller to handle the ongoing call chip in the collapsed status bar.
+/**
+ * A controller to handle the ongoing call chip in the collapsed status bar.
+ *
* @deprecated Use [OngoingCallInteractor] instead, which follows recommended architecture patterns
*/
@Deprecated("Use OngoingCallInteractor instead")
@@ -71,7 +68,6 @@ constructor(
@Application private val scope: CoroutineScope,
private val context: Context,
private val ongoingCallRepository: OngoingCallRepository,
- private val notifCollection: CommonNotifCollection,
private val activeNotificationsInteractor: ActiveNotificationsInteractor,
private val systemClock: SystemClock,
private val activityStarter: ActivityStarter,
@@ -90,105 +86,24 @@ constructor(
private val mListeners: MutableList<OngoingCallListener> = mutableListOf()
private val uidObserver = CallAppUidObserver()
- private val notifListener =
- object : NotifCollectionListener {
- // Temporary workaround for b/178406514 for testing purposes.
- //
- // b/178406514 means that posting an incoming call notif then updating it to an ongoing
- // call notif does not work (SysUI never receives the update). This workaround allows us
- // to trigger the ongoing call chip when an ongoing call notif is *added* rather than
- // *updated*, allowing us to test the chip.
- //
- // TODO(b/183229367): Remove this function override when b/178406514 is fixed.
- override fun onEntryAdded(entry: NotificationEntry) {
- onEntryUpdated(entry, true)
- }
-
- override fun onEntryUpdated(entry: NotificationEntry) {
- StatusBarUseReposForCallChip.assertInLegacyMode()
- // We have a new call notification or our existing call notification has been
- // updated.
- // TODO(b/183229367): This likely won't work if you take a call from one app then
- // switch to a call from another app.
- if (
- callNotificationInfo == null && isCallNotification(entry) ||
- (entry.sbn.key == callNotificationInfo?.key)
- ) {
- val newOngoingCallInfo =
- CallNotificationInfo(
- entry.sbn.key,
- entry.sbn.notification.getWhen(),
- // In this old listener pattern, we don't have access to the
- // notification icon.
- notificationIconView = null,
- entry.sbn.notification.contentIntent,
- entry.sbn.uid,
- entry.sbn.notification.extras.getInt(
- Notification.EXTRA_CALL_TYPE,
- -1,
- ) == CALL_TYPE_ONGOING,
- statusBarSwipedAway = callNotificationInfo?.statusBarSwipedAway ?: false,
- )
- if (newOngoingCallInfo == callNotificationInfo) {
- return
- }
-
- callNotificationInfo = newOngoingCallInfo
- if (newOngoingCallInfo.isOngoing) {
- logger.log(
- TAG,
- LogLevel.DEBUG,
- { str1 = newOngoingCallInfo.key },
- { "Call notif *is* ongoing -> showing chip. key=$str1" },
- )
- updateChip()
- } else {
- logger.log(
- TAG,
- LogLevel.DEBUG,
- { str1 = newOngoingCallInfo.key },
- { "Call notif not ongoing -> hiding chip. key=$str1" },
- )
- removeChip()
- }
- }
- }
-
- override fun onEntryRemoved(entry: NotificationEntry, reason: Int) {
- if (entry.sbn.key == callNotificationInfo?.key) {
- logger.log(
- TAG,
- LogLevel.DEBUG,
- { str1 = entry.sbn.key },
- { "Call notif removed -> hiding chip. key=$str1" },
- )
- removeChip()
- }
- }
- }
override fun start() {
- if (StatusBarChipsModernization.isEnabled)
- return
+ if (StatusBarChipsModernization.isEnabled) return
dumpManager.registerDumpable(this)
- if (Flags.statusBarUseReposForCallChip()) {
- scope.launch {
- // Listening to [ActiveNotificationsInteractor] instead of using
- // [NotifCollectionListener#onEntryUpdated] is better for two reasons:
- // 1. ActiveNotificationsInteractor automatically filters the notification list to
- // just notifications for the current user, which ensures we don't show a call chip
- // for User 1's call while User 2 is active (see b/328584859).
- // 2. ActiveNotificationsInteractor only emits notifications that are currently
- // present in the shade, which means we know we've already inflated the icon that we
- // might use for the call chip (see b/354930838).
- activeNotificationsInteractor.ongoingCallNotification.collect {
- updateInfoFromNotifModel(it)
- }
+ scope.launch {
+ // Listening to [ActiveNotificationsInteractor] instead of using
+ // [NotifCollectionListener#onEntryUpdated] is better for two reasons:
+ // 1. ActiveNotificationsInteractor automatically filters the notification list to
+ // just notifications for the current user, which ensures we don't show a call chip
+ // for User 1's call while User 2 is active (see b/328584859).
+ // 2. ActiveNotificationsInteractor only emits notifications that are currently
+ // present in the shade, which means we know we've already inflated the icon that we
+ // might use for the call chip (see b/354930838).
+ activeNotificationsInteractor.ongoingCallNotification.collect {
+ updateInfoFromNotifModel(it)
}
- } else {
- notifCollection.addCollectionListener(notifListener)
}
scope.launch {
@@ -244,21 +159,12 @@ constructor(
logger.log(
TAG,
LogLevel.DEBUG,
- {
- bool1 = Flags.statusBarCallChipNotificationIcon()
- bool2 = currentInfo.notificationIconView != null
- },
- { "Creating OngoingCallModel.InCall. notifIconFlag=$bool1 hasIcon=$bool2" },
+ { bool1 = currentInfo.notificationIconView != null },
+ { "Creating OngoingCallModel.InCall. hasIcon=$bool1" },
)
- val icon =
- if (Flags.statusBarCallChipNotificationIcon()) {
- currentInfo.notificationIconView
- } else {
- null
- }
return OngoingCallModel.InCall(
startTimeMs = currentInfo.callStartTime,
- notificationIconView = icon,
+ notificationIconView = currentInfo.notificationIconView,
intent = currentInfo.intent,
notificationKey = currentInfo.key,
)
@@ -597,8 +503,4 @@ constructor(
}
}
-private fun isCallNotification(entry: NotificationEntry): Boolean {
- return entry.sbn.notification.isStyle(Notification.CallStyle::class.java)
-}
-
private const val TAG = OngoingCallRepository.TAG
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/StatusBarUseReposForCallChip.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/StatusBarUseReposForCallChip.kt
deleted file mode 100644
index 4bdd90ebff3e..000000000000
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/StatusBarUseReposForCallChip.kt
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.statusbar.phone.ongoingcall
-
-import com.android.systemui.Flags
-import com.android.systemui.flags.FlagToken
-import com.android.systemui.flags.RefactorFlagUtils
-
-/** Helper for reading or using the status bar use repos for call chip flag state. */
-@Suppress("NOTHING_TO_INLINE")
-object StatusBarUseReposForCallChip {
- /** The aconfig flag name */
- const val FLAG_NAME = Flags.FLAG_STATUS_BAR_USE_REPOS_FOR_CALL_CHIP
-
- /** A token used for dependency declaration */
- val token: FlagToken
- get() = FlagToken(FLAG_NAME, isEnabled)
-
- /** Is the refactor enabled */
- @JvmStatic
- inline val isEnabled
- get() = Flags.statusBarUseReposForCallChip()
-
- /**
- * Called to ensure code is only run when the flag is enabled. This protects users from the
- * unintended behaviors caused by accidentally running new logic, while also crashing on an eng
- * build to ensure that the refactor author catches issues in testing.
- */
- @JvmStatic
- inline fun isUnexpectedlyInLegacyMode() =
- RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_NAME)
-
- /**
- * Called to ensure code is only run when the flag is disabled. This will throw an exception if
- * the flag is enabled to ensure that the refactor author catches issues in testing.
- */
- @JvmStatic
- inline fun assertInLegacyMode() = RefactorFlagUtils.assertInLegacyMode(isEnabled, FLAG_NAME)
-}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt
index 96666d83b39b..c71162a22d2f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt
@@ -56,8 +56,8 @@ import com.android.systemui.statusbar.pipeline.shared.data.repository.Connectivi
import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepositoryImpl
import com.android.systemui.statusbar.pipeline.shared.ui.binder.HomeStatusBarViewBinder
import com.android.systemui.statusbar.pipeline.shared.ui.binder.HomeStatusBarViewBinderImpl
-import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.HomeStatusBarViewModel
-import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.HomeStatusBarViewModelImpl
+import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.HomeStatusBarViewModel.HomeStatusBarViewModelFactory
+import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.HomeStatusBarViewModelImpl.HomeStatusBarViewModelFactoryImpl
import com.android.systemui.statusbar.pipeline.wifi.data.repository.RealWifiRepository
import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository
import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepositorySwitcher
@@ -148,7 +148,9 @@ abstract class StatusBarPipelineModule {
abstract fun bindCarrierConfigStartable(impl: CarrierConfigCoreStartable): CoreStartable
@Binds
- abstract fun homeStatusBarViewModel(impl: HomeStatusBarViewModelImpl): HomeStatusBarViewModel
+ abstract fun homeStatusBarViewModelFactory(
+ impl: HomeStatusBarViewModelFactoryImpl
+ ): HomeStatusBarViewModelFactory
@Binds
abstract fun homeStatusBarViewBinder(impl: HomeStatusBarViewBinderImpl): HomeStatusBarViewBinder
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/HomeStatusBarViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/HomeStatusBarViewBinder.kt
index 0dd7c8499861..31d6d86d1b37 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/HomeStatusBarViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/HomeStatusBarViewBinder.kt
@@ -45,6 +45,7 @@ import com.android.systemui.statusbar.phone.ongoingcall.StatusBarChipsModernizat
import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.HomeStatusBarViewModel
import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.HomeStatusBarViewModel.VisibilityModel
import javax.inject.Inject
+import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
/**
@@ -107,10 +108,9 @@ constructor(
}
if (NotificationsLiveDataStoreRefactor.isEnabled) {
- val displayId = view.display.displayId
val lightsOutView: View = view.requireViewById(R.id.notification_lights_out)
launch {
- viewModel.areNotificationsLightsOut(displayId).collect { show ->
+ viewModel.areNotificationsLightsOut.collect { show ->
animateLightsOutView(lightsOutView, show)
}
}
@@ -121,22 +121,26 @@ constructor(
!StatusBarNotifChips.isEnabled &&
!StatusBarChipsModernization.isEnabled
) {
- val primaryChipView: View =
- view.requireViewById(R.id.ongoing_activity_chip_primary)
+ val primaryChipViewBinding =
+ OngoingActivityChipBinder.createBinding(
+ view.requireViewById(R.id.ongoing_activity_chip_primary)
+ )
launch {
viewModel.primaryOngoingActivityChip.collect { primaryChipModel ->
OngoingActivityChipBinder.bind(
primaryChipModel,
- primaryChipView,
+ primaryChipViewBinding,
iconViewStore,
)
if (StatusBarRootModernization.isEnabled) {
when (primaryChipModel) {
is OngoingActivityChipModel.Shown ->
- primaryChipView.show(shouldAnimateChange = true)
+ primaryChipViewBinding.rootView.show(
+ shouldAnimateChange = true
+ )
is OngoingActivityChipModel.Hidden ->
- primaryChipView.hide(
+ primaryChipViewBinding.rootView.hide(
state = View.GONE,
shouldAnimateChange = primaryChipModel.shouldAnimate,
)
@@ -167,28 +171,34 @@ constructor(
StatusBarNotifChips.isEnabled &&
!StatusBarChipsModernization.isEnabled
) {
- val primaryChipView: View =
- view.requireViewById(R.id.ongoing_activity_chip_primary)
- val secondaryChipView: View =
- view.requireViewById(R.id.ongoing_activity_chip_secondary)
+ // Create view bindings here so we don't keep re-fetching child views each time
+ // the chip model changes.
+ val primaryChipViewBinding =
+ OngoingActivityChipBinder.createBinding(
+ view.requireViewById(R.id.ongoing_activity_chip_primary)
+ )
+ val secondaryChipViewBinding =
+ OngoingActivityChipBinder.createBinding(
+ view.requireViewById(R.id.ongoing_activity_chip_secondary)
+ )
launch {
- viewModel.ongoingActivityChips.collect { chips ->
+ viewModel.ongoingActivityChips.collectLatest { chips ->
OngoingActivityChipBinder.bind(
chips.primary,
- primaryChipView,
+ primaryChipViewBinding,
iconViewStore,
)
- // TODO(b/364653005): Don't show the secondary chip if there isn't
- // enough space for it.
OngoingActivityChipBinder.bind(
chips.secondary,
- secondaryChipView,
+ secondaryChipViewBinding,
iconViewStore,
)
if (StatusBarRootModernization.isEnabled) {
- primaryChipView.adjustVisibility(chips.primary.toVisibilityModel())
- secondaryChipView.adjustVisibility(
+ primaryChipViewBinding.rootView.adjustVisibility(
+ chips.primary.toVisibilityModel()
+ )
+ secondaryChipViewBinding.rootView.adjustVisibility(
chips.secondary.toVisibilityModel()
)
} else {
@@ -201,6 +211,18 @@ constructor(
shouldAnimate = true,
)
}
+
+ viewModel.contentArea.collect { _ ->
+ OngoingActivityChipBinder.resetPrimaryChipWidthRestrictions(
+ primaryChipViewBinding,
+ viewModel.ongoingActivityChips.value.primary,
+ )
+ OngoingActivityChipBinder.resetSecondaryChipWidthRestrictions(
+ secondaryChipViewBinding,
+ viewModel.ongoingActivityChips.value.secondary,
+ )
+ view.requestLayout()
+ }
}
}
}
@@ -218,7 +240,7 @@ constructor(
StatusBarOperatorNameViewBinder.bind(
operatorNameView,
viewModel.operatorNameViewModel,
- viewModel::areaTint,
+ viewModel.areaTint,
)
launch {
viewModel.shouldShowOperatorNameView.collect {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/StatusBarOperatorNameViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/StatusBarOperatorNameViewBinder.kt
index b7744d34560d..5dd76f4434f3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/StatusBarOperatorNameViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/StatusBarOperatorNameViewBinder.kt
@@ -32,19 +32,16 @@ object StatusBarOperatorNameViewBinder {
fun bind(
operatorFrameView: View,
viewModel: StatusBarOperatorNameViewModel,
- areaTint: (Int) -> Flow<StatusBarTintColor>,
+ areaTint: Flow<StatusBarTintColor>,
) {
operatorFrameView.repeatWhenAttached {
repeatOnLifecycle(Lifecycle.State.STARTED) {
- val displayId = operatorFrameView.display.displayId
-
val operatorNameText =
operatorFrameView.requireViewById<TextView>(R.id.operator_name)
launch { viewModel.operatorName.collect { operatorNameText.text = it } }
launch {
- val tint = areaTint(displayId)
- tint.collect { statusBarTintColors ->
+ areaTint.collect { statusBarTintColors ->
operatorNameText.setTextColor(
statusBarTintColors.tint(operatorNameText.viewBoundsOnScreen())
)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/composable/StatusBarRoot.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/composable/StatusBarRoot.kt
index f286a1a148fa..71e19188f309 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/composable/StatusBarRoot.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/composable/StatusBarRoot.kt
@@ -24,6 +24,7 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -33,9 +34,11 @@ import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.compose.ui.viewinterop.AndroidView
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.android.app.tracing.coroutines.launchTraced as launch
+import com.android.compose.theme.PlatformTheme
import com.android.keyguard.AlphaOptimizedLinearLayout
import com.android.systemui.plugins.DarkIconDispatcher
import com.android.systemui.res.R
+import com.android.systemui.statusbar.chips.ui.compose.OngoingActivityChips
import com.android.systemui.statusbar.data.repository.DarkIconDispatcherStore
import com.android.systemui.statusbar.events.domain.interactor.SystemStatusEventAnimationInteractor
import com.android.systemui.statusbar.featurepods.popups.StatusBarPopupChips
@@ -53,13 +56,14 @@ import com.android.systemui.statusbar.pipeline.shared.ui.binder.HomeStatusBarIco
import com.android.systemui.statusbar.pipeline.shared.ui.binder.HomeStatusBarViewBinder
import com.android.systemui.statusbar.pipeline.shared.ui.binder.StatusBarVisibilityChangeListener
import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.HomeStatusBarViewModel
+import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.HomeStatusBarViewModel.HomeStatusBarViewModelFactory
import javax.inject.Inject
/** Factory to simplify the dependency management for [StatusBarRoot] */
class StatusBarRootFactory
@Inject
constructor(
- private val homeStatusBarViewModel: HomeStatusBarViewModel,
+ private val homeStatusBarViewModelFactory: HomeStatusBarViewModelFactory,
private val homeStatusBarViewBinder: HomeStatusBarViewBinder,
private val notificationIconsBinder: NotificationIconContainerStatusBarViewBinder,
private val darkIconManagerFactory: DarkIconManager.Factory,
@@ -70,13 +74,14 @@ constructor(
) {
fun create(root: ViewGroup, andThen: (ViewGroup) -> Unit): ComposeView {
val composeView = ComposeView(root.context)
+ val displayId = root.context.displayId
val darkIconDispatcher =
darkIconDispatcherStore.forDisplay(root.context.displayId) ?: return composeView
composeView.apply {
setContent {
StatusBarRoot(
parent = root,
- statusBarViewModel = homeStatusBarViewModel,
+ statusBarViewModel = homeStatusBarViewModelFactory.create(displayId),
statusBarViewBinder = homeStatusBarViewBinder,
notificationIconsBinder = notificationIconsBinder,
darkIconManagerFactory = darkIconManagerFactory,
@@ -158,6 +163,45 @@ fun StatusBarRoot(
darkIconDispatcher,
)
iconController.addIconGroup(darkIconManager)
+
+ if (StatusBarChipsModernization.isEnabled) {
+ val startSideExceptHeadsUp =
+ phoneStatusBarView.requireViewById<LinearLayout>(
+ R.id.status_bar_start_side_except_heads_up
+ )
+
+ val composeView =
+ ComposeView(context).apply {
+ layoutParams =
+ LinearLayout.LayoutParams(
+ LinearLayout.LayoutParams.WRAP_CONTENT,
+ LinearLayout.LayoutParams.WRAP_CONTENT,
+ )
+
+ setViewCompositionStrategy(
+ ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed
+ )
+
+ setContent {
+ PlatformTheme {
+ val chips by
+ statusBarViewModel.ongoingActivityChips
+ .collectAsStateWithLifecycle()
+ OngoingActivityChips(chips = chips)
+ }
+ }
+ }
+
+ // Add the composable container for ongoingActivityChips before the
+ // notification_icon_area to maintain the same ordering for ongoing activity
+ // chips in the status bar layout.
+ val notificationIconAreaIndex =
+ startSideExceptHeadsUp.indexOfChild(
+ startSideExceptHeadsUp.findViewById(R.id.notification_icon_area)
+ )
+ startSideExceptHeadsUp.addView(composeView, notificationIconAreaIndex)
+ }
+
HomeStatusBarIconBlockListBinder.bind(
statusIconContainer,
darkIconManager,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModel.kt
index c9cc17389c17..d9d9a29ee2b6 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModel.kt
@@ -19,7 +19,6 @@ package com.android.systemui.statusbar.pipeline.shared.ui.viewmodel
import android.annotation.ColorInt
import android.graphics.Rect
import android.view.View
-import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
@@ -44,6 +43,7 @@ import com.android.systemui.statusbar.events.shared.model.SystemEventAnimationSt
import com.android.systemui.statusbar.events.shared.model.SystemEventAnimationState.Idle
import com.android.systemui.statusbar.featurepods.popups.shared.model.PopupChipModel
import com.android.systemui.statusbar.featurepods.popups.ui.viewmodel.StatusBarPopupChipsViewModel
+import com.android.systemui.statusbar.layout.ui.viewmodel.StatusBarContentInsetsViewModelStore
import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor
import com.android.systemui.statusbar.notification.domain.interactor.HeadsUpNotificationInteractor
import com.android.systemui.statusbar.notification.headsup.PinnedStatus
@@ -53,7 +53,9 @@ import com.android.systemui.statusbar.phone.domain.interactor.LightsOutInteracto
import com.android.systemui.statusbar.pipeline.shared.domain.interactor.HomeStatusBarIconBlockListInteractor
import com.android.systemui.statusbar.pipeline.shared.domain.interactor.HomeStatusBarInteractor
import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.HomeStatusBarViewModel.VisibilityModel
-import javax.inject.Inject
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
@@ -118,6 +120,7 @@ interface HomeStatusBarViewModel {
val shouldShowOperatorNameView: Flow<Boolean>
val isClockVisible: Flow<VisibilityModel>
val isNotificationIconContainerVisible: Flow<VisibilityModel>
+
/**
* Pair of (system info visibility, event animation state). The animation state can be used to
* respond to the system event chip animations. In all cases, system info visibility correctly
@@ -128,6 +131,9 @@ interface HomeStatusBarViewModel {
/** Which icons to block from the home status bar */
val iconBlockList: Flow<List<String>>
+ /** This status bar's current content area for the given rotation in absolute bounds. */
+ val contentArea: Flow<Rect>
+
/**
* Apps can request a low profile mode [android.view.View.SYSTEM_UI_FLAG_LOW_PROFILE] where
* status bar and navigation icons dim. In this mode, a notification dot appears where the
@@ -137,13 +143,13 @@ interface HomeStatusBarViewModel {
* whether there are notifications when the device is in
* [android.view.View.SYSTEM_UI_FLAG_LOW_PROFILE].
*/
- fun areNotificationsLightsOut(displayId: Int): Flow<Boolean>
+ val areNotificationsLightsOut: Flow<Boolean>
/**
- * Given a displayId, returns a flow of [StatusBarTintColor], a functional interface that will
- * allow a view to calculate its correct tint depending on location
+ * A flow of [StatusBarTintColor], a functional interface that will allow a view to calculate
+ * its correct tint depending on location
*/
- fun areaTint(displayId: Int): Flow<StatusBarTintColor>
+ val areaTint: Flow<StatusBarTintColor>
/** Models the current visibility for a specific child view of status bar. */
data class VisibilityModel(
@@ -157,17 +163,22 @@ interface HomeStatusBarViewModel {
val baseVisibility: VisibilityModel,
val animationState: SystemEventAnimationState,
)
+
+ /** Interface for the assisted factory, to allow for providing a fake in tests */
+ interface HomeStatusBarViewModelFactory {
+ fun create(displayId: Int): HomeStatusBarViewModel
+ }
}
-@SysUISingleton
class HomeStatusBarViewModelImpl
-@Inject
+@AssistedInject
constructor(
+ @Assisted thisDisplayId: Int,
homeStatusBarInteractor: HomeStatusBarInteractor,
homeStatusBarIconBlockListInteractor: HomeStatusBarIconBlockListInteractor,
- private val lightsOutInteractor: LightsOutInteractor,
- private val notificationsInteractor: ActiveNotificationsInteractor,
- private val darkIconInteractor: DarkIconInteractor,
+ lightsOutInteractor: LightsOutInteractor,
+ notificationsInteractor: ActiveNotificationsInteractor,
+ darkIconInteractor: DarkIconInteractor,
headsUpNotificationInteractor: HeadsUpNotificationInteractor,
keyguardTransitionInteractor: KeyguardTransitionInteractor,
keyguardInteractor: KeyguardInteractor,
@@ -178,6 +189,7 @@ constructor(
ongoingActivityChipsViewModel: OngoingActivityChipsViewModel,
statusBarPopupChipsViewModel: StatusBarPopupChipsViewModel,
animations: SystemStatusEventAnimationInteractor,
+ statusBarContentInsetsViewModelStore: StatusBarContentInsetsViewModelStore,
@Application coroutineScope: CoroutineScope,
) : HomeStatusBarViewModel {
override val isTransitioningFromLockscreenToOccluded: StateFlow<Boolean> =
@@ -211,22 +223,22 @@ constructor(
}
.stateIn(coroutineScope, SharingStarted.WhileSubscribed(), initialValue = false)
- override fun areNotificationsLightsOut(displayId: Int): Flow<Boolean> =
+ override val areNotificationsLightsOut: Flow<Boolean> =
if (NotificationsLiveDataStoreRefactor.isUnexpectedlyInLegacyMode()) {
emptyFlow()
} else {
combine(
notificationsInteractor.areAnyNotificationsPresent,
- lightsOutInteractor.isLowProfile(displayId) ?: flowOf(false),
+ lightsOutInteractor.isLowProfile(thisDisplayId) ?: flowOf(false),
) { hasNotifications, isLowProfile ->
hasNotifications && isLowProfile
}
.distinctUntilChanged()
}
- override fun areaTint(displayId: Int): Flow<StatusBarTintColor> =
+ override val areaTint: Flow<StatusBarTintColor> =
darkIconInteractor
- .darkState(displayId)
+ .darkState(thisDisplayId)
.map { (areas: Collection<Rect>, tint: Int) ->
StatusBarTintColor { viewBounds: Rect ->
if (DarkIconDispatcher.isInAreas(areas, viewBounds)) {
@@ -283,11 +295,12 @@ constructor(
override val shouldShowOperatorNameView: Flow<Boolean> =
combine(
shouldHomeStatusBarBeVisible,
- headsUpNotificationInteractor.statusBarHeadsUpState,
+ headsUpNotificationInteractor.statusBarHeadsUpStatus,
homeStatusBarInteractor.visibilityViaDisableFlags,
homeStatusBarInteractor.shouldShowOperatorName,
- ) { shouldStatusBarBeVisible, headsUpState, visibilityViaDisableFlags, shouldShowOperator ->
- val hideForHeadsUp = headsUpState == PinnedStatus.PinnedBySystem
+ ) { shouldStatusBarBeVisible, headsUpStatus, visibilityViaDisableFlags, shouldShowOperator
+ ->
+ val hideForHeadsUp = headsUpStatus == PinnedStatus.PinnedBySystem
shouldStatusBarBeVisible &&
!hideForHeadsUp &&
visibilityViaDisableFlags.isSystemInfoAllowed &&
@@ -297,10 +310,10 @@ constructor(
override val isClockVisible: Flow<VisibilityModel> =
combine(
shouldHomeStatusBarBeVisible,
- headsUpNotificationInteractor.statusBarHeadsUpState,
+ headsUpNotificationInteractor.statusBarHeadsUpStatus,
homeStatusBarInteractor.visibilityViaDisableFlags,
- ) { shouldStatusBarBeVisible, headsUpState, visibilityViaDisableFlags ->
- val hideClockForHeadsUp = headsUpState == PinnedStatus.PinnedBySystem
+ ) { shouldStatusBarBeVisible, headsUpStatus, visibilityViaDisableFlags ->
+ val hideClockForHeadsUp = headsUpStatus == PinnedStatus.PinnedBySystem
val showClock =
shouldStatusBarBeVisible &&
visibilityViaDisableFlags.isClockAllowed &&
@@ -356,6 +369,10 @@ constructor(
override val iconBlockList: Flow<List<String>> =
homeStatusBarIconBlockListInteractor.iconBlockList
+ override val contentArea: Flow<Rect> =
+ statusBarContentInsetsViewModelStore.forDisplay(thisDisplayId)?.contentArea
+ ?: flowOf(Rect(0, 0, 0, 0))
+
@View.Visibility
private fun Boolean.toVisibleOrGone(): Int {
return if (this) View.VISIBLE else View.GONE
@@ -364,6 +381,13 @@ constructor(
// Similar to the above, but uses INVISIBLE in place of GONE
@View.Visibility
private fun Boolean.toVisibleOrInvisible(): Int = if (this) View.VISIBLE else View.INVISIBLE
+
+ /** Inject this to create the display-dependent view model */
+ @AssistedFactory
+ interface HomeStatusBarViewModelFactoryImpl :
+ HomeStatusBarViewModel.HomeStatusBarViewModelFactory {
+ override fun create(displayId: Int): HomeStatusBarViewModelImpl
+ }
}
/** Lookup the color for a given view in the status bar */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyStateInflater.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyStateInflater.kt
index 56c9e9abbc36..cb26679434ef 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyStateInflater.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyStateInflater.kt
@@ -41,6 +41,7 @@ import android.view.ViewGroup
import android.view.accessibility.AccessibilityNodeInfo
import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction
import android.widget.Button
+import com.android.systemui.Flags
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.res.R
import com.android.systemui.shared.system.ActivityManagerWrapper
@@ -52,6 +53,7 @@ import com.android.systemui.statusbar.SmartReplyController
import com.android.systemui.statusbar.notification.collection.NotificationEntry
import com.android.systemui.statusbar.notification.headsup.HeadsUpManager
import com.android.systemui.statusbar.notification.logging.NotificationLogger
+import com.android.systemui.statusbar.notification.row.MagicActionBackgroundDrawable
import com.android.systemui.statusbar.phone.KeyguardDismissUtil
import com.android.systemui.statusbar.policy.InflatedSmartReplyState.SuppressedActions
import com.android.systemui.statusbar.policy.SmartReplyView.SmartActions
@@ -400,6 +402,15 @@ constructor(
.apply {
text = action.title
+ if (Flags.notificationMagicActionsTreatment()) {
+ if (
+ smartActions.fromAssistant &&
+ action.extras.getBoolean(Notification.Action.EXTRA_IS_MAGIC, false)
+ ) {
+ background = MagicActionBackgroundDrawable(parent.context)
+ }
+ }
+
// We received the Icon from the application - so use the Context of the application
// to
// reference icon resources.
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ui/StatusBarUiLayerModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/ui/StatusBarUiLayerModule.kt
new file mode 100644
index 000000000000..8e81d78d60f4
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/ui/StatusBarUiLayerModule.kt
@@ -0,0 +1,23 @@
+/*
+ * 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.ui
+
+import com.android.systemui.statusbar.layout.ui.viewmodel.StatusBarContentInsetsViewModelStoreModule
+import dagger.Module
+
+@Module(includes = [StatusBarContentInsetsViewModelStoreModule::class])
+object StatusBarUiLayerModule
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModel.kt
index 6175ea190697..a98a9e0c16d2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModel.kt
@@ -60,7 +60,7 @@ constructor(
private val showingHeadsUpStatusBar: Flow<Boolean> =
if (SceneContainerFlag.isEnabled) {
- headsUpNotificationInteractor.statusBarHeadsUpState.map { it.isPinned }
+ headsUpNotificationInteractor.statusBarHeadsUpStatus.map { it.isPinned }
} else {
flowOf(false)
}
diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/Utils.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/Utils.kt
index d9a2e956cc86..a88b127ae157 100644
--- a/packages/SystemUI/src/com/android/systemui/util/kotlin/Utils.kt
+++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/Utils.kt
@@ -17,10 +17,14 @@
package com.android.systemui.util.kotlin
import android.content.Context
+import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.stateIn
class Utils {
companion object {
@@ -32,6 +36,7 @@ class Utils {
fun <A, B, C, D> toQuad(a: A, bcd: Triple<B, C, D>) =
Quad(a, bcd.first, bcd.second, bcd.third)
+
fun <A, B, C, D> toQuad(abc: Triple<A, B, C>, d: D) =
Quad(abc.first, abc.second, abc.third, d)
@@ -51,7 +56,7 @@ class Utils {
bcdefg.third,
bcdefg.fourth,
bcdefg.fifth,
- bcdefg.sixth
+ bcdefg.sixth,
)
/**
@@ -81,7 +86,7 @@ class Utils {
fun <A, B, C, D> Flow<A>.sample(
b: Flow<B>,
c: Flow<C>,
- d: Flow<D>
+ d: Flow<D>,
): Flow<Quad<A, B, C, D>> {
return this.sample(combine(b, c, d, ::Triple), ::toQuad)
}
@@ -134,6 +139,20 @@ class Utils {
): Flow<Septuple<A, B, C, D, E, F, G>> {
return this.sample(combine(b, c, d, e, f, g, ::Sextuple), ::toSeptuple)
}
+
+ /**
+ * Combines 2 state flows, applying [transform] between the initial values to set the
+ * initial value of the resulting StateFlow.
+ */
+ fun <A, B, R> combineState(
+ f1: StateFlow<A>,
+ f2: StateFlow<B>,
+ scope: CoroutineScope,
+ sharingStarted: SharingStarted,
+ transform: (A, B) -> R,
+ ): StateFlow<R> =
+ combine(f1, f2) { a, b -> transform(a, b) }
+ .stateIn(scope, sharingStarted, transform(f1.value, f2.value))
}
}
@@ -144,7 +163,7 @@ data class Quint<A, B, C, D, E>(
val second: B,
val third: C,
val fourth: D,
- val fifth: E
+ val fifth: E,
)
data class Sextuple<A, B, C, D, E, F>(
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 e8d19dd5e0e4..96630ca36b97 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
@@ -51,6 +51,8 @@ import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.launch
private const val CLOSE_DRAWER_DELAY = 300L
+// Ensure roundness and color of button is updated when progress is changed by a minimum fraction.
+private const val BUTTON_MIN_VISIBLE_CHANGE = 0.05F
@OptIn(ExperimentalCoroutinesApi::class)
@VolumeDialogScope
@@ -58,12 +60,12 @@ class VolumeDialogRingerViewBinder
@Inject
constructor(private val viewModel: VolumeDialogRingerDrawerViewModel) {
private val roundnessSpringForce =
- SpringForce(0F).apply {
+ SpringForce(1F).apply {
stiffness = 800F
dampingRatio = 0.6F
}
private val colorSpringForce =
- SpringForce(0F).apply {
+ SpringForce(1F).apply {
stiffness = 3800F
dampingRatio = 1F
}
@@ -257,30 +259,35 @@ constructor(private val viewModel: VolumeDialogRingerDrawerViewModel) {
// 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.
- val selectedCornerRadius = (selectedButton.background as GradientDrawable).cornerRadius
- if (selectedCornerRadius.toInt() != selectedButtonUiModel.cornerRadius) {
- selectedButton.animateTo(
- selectedButtonUiModel,
- if (uiModel.currentButtonIndex == count - 1) {
- onProgressChanged
- } else {
- { _, _ -> }
- },
- )
- }
- val unselectedCornerRadius =
- (unselectedButton.background as GradientDrawable).cornerRadius
- if (unselectedCornerRadius.toInt() != unselectedButtonUiModel.cornerRadius) {
- unselectedButton.animateTo(
- unselectedButtonUiModel,
- if (previousIndex == count - 1) {
- onProgressChanged
- } else {
- { _, _ -> }
- },
- )
- }
coroutineScope {
+ val selectedCornerRadius =
+ (selectedButton.background as GradientDrawable).cornerRadius
+ if (selectedCornerRadius.toInt() != selectedButtonUiModel.cornerRadius) {
+ launch {
+ selectedButton.animateTo(
+ selectedButtonUiModel,
+ if (uiModel.currentButtonIndex == count - 1) {
+ onProgressChanged
+ } else {
+ { _, _ -> }
+ },
+ )
+ }
+ }
+ val unselectedCornerRadius =
+ (unselectedButton.background as GradientDrawable).cornerRadius
+ if (unselectedCornerRadius.toInt() != unselectedButtonUiModel.cornerRadius) {
+ launch {
+ unselectedButton.animateTo(
+ unselectedButtonUiModel,
+ if (previousIndex == count - 1) {
+ onProgressChanged
+ } else {
+ { _, _ -> }
+ },
+ )
+ }
+ }
launch {
delay(CLOSE_DRAWER_DELAY)
bindButtons(viewModel, uiModel, onAnimationEnd, isAnimated = true)
@@ -383,11 +390,14 @@ constructor(private val viewModel: VolumeDialogRingerDrawerViewModel) {
onProgressChanged: (Float, Boolean) -> Unit = { _, _ -> },
) {
val roundnessAnimation =
- SpringAnimation(FloatValueHolder(0F)).setSpring(roundnessSpringForce)
- val colorAnimation = SpringAnimation(FloatValueHolder(0F)).setSpring(colorSpringForce)
+ SpringAnimation(FloatValueHolder(0F), 1F).setSpring(roundnessSpringForce)
+ val colorAnimation = SpringAnimation(FloatValueHolder(0F), 1F).setSpring(colorSpringForce)
val radius = (background as GradientDrawable).cornerRadius
val cornerRadiusDiff =
ringerButtonUiModel.cornerRadius - (background as GradientDrawable).cornerRadius
+
+ roundnessAnimation.minimumVisibleChange = BUTTON_MIN_VISIBLE_CHANGE
+ colorAnimation.minimumVisibleChange = BUTTON_MIN_VISIBLE_CHANGE
coroutineScope {
launch {
colorAnimation.suspendAnimate { value ->
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 46d7d5f680ce..428dc6ecb5b6 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
@@ -80,7 +80,6 @@ constructor(
MutableStateFlow(WindowInsets.Builder().build())
// Root view of the Volume Dialog.
val root: MotionLayout = dialog.requireViewById(R.id.volume_dialog_root)
- root.alpha = 0f
animateVisibility(root, dialog, viewModel.dialogVisibilityModel)
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt
index 4abbbacd800b..bac2c47f51c7 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt
@@ -24,13 +24,15 @@ import android.view.ViewTreeObserver
import android.widget.FrameLayout
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
-import com.android.settingslib.notification.modes.TestModeBuilder.MANUAL_DND_INACTIVE
+import com.android.settingslib.notification.modes.TestModeBuilder.MANUAL_DND
import com.android.systemui.SysuiTestCase
import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.flags.Flags
+import com.android.systemui.flags.fakeFeatureFlagsClassic
import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
-import com.android.systemui.keyguard.domain.interactor.KeyguardInteractorFactory
+import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
import com.android.systemui.keyguard.shared.model.Edge
import com.android.systemui.keyguard.shared.model.KeyguardState.AOD
import com.android.systemui.keyguard.shared.model.KeyguardState.DOZING
@@ -97,8 +99,9 @@ import org.mockito.kotlin.clearInvocations
class ClockEventControllerTest : SysuiTestCase() {
private val kosmos = testKosmos()
- private val zenModeRepository = kosmos.fakeZenModeRepository
private val testScope = kosmos.testScope
+ private val zenModeRepository by lazy { kosmos.fakeZenModeRepository }
+ private val zenModeInteractor by lazy { kosmos.zenModeInteractor }
@JvmField @Rule val mockito = MockitoJUnit.rule()
@@ -106,7 +109,6 @@ class ClockEventControllerTest : SysuiTestCase() {
private lateinit var repository: FakeKeyguardRepository
private val clockBuffers = ClockMessageBuffers(LogcatOnlyMessageBuffer(LogLevel.DEBUG))
private lateinit var underTest: ClockEventController
- private lateinit var dndModeId: String
@Mock private lateinit var broadcastDispatcher: BroadcastDispatcher
@Mock private lateinit var batteryController: BatteryController
@@ -156,17 +158,12 @@ class ClockEventControllerTest : SysuiTestCase() {
whenever(largeClockController.theme).thenReturn(ThemeConfig(true, null))
whenever(userTracker.userId).thenReturn(1)
- dndModeId = MANUAL_DND_INACTIVE.id
- zenModeRepository.addMode(MANUAL_DND_INACTIVE)
+ repository = kosmos.fakeKeyguardRepository
- repository = FakeKeyguardRepository()
-
- val withDeps = KeyguardInteractorFactory.create(repository = repository)
-
- withDeps.featureFlags.apply { set(Flags.REGION_SAMPLING, false) }
+ kosmos.fakeFeatureFlagsClassic.set(Flags.REGION_SAMPLING, false)
underTest =
ClockEventController(
- withDeps.keyguardInteractor,
+ kosmos.keyguardInteractor,
keyguardTransitionInteractor,
broadcastDispatcher,
batteryController,
@@ -177,9 +174,9 @@ class ClockEventControllerTest : SysuiTestCase() {
mainExecutor,
bgExecutor,
clockBuffers,
- withDeps.featureFlags,
+ kosmos.fakeFeatureFlagsClassic,
zenModeController,
- kosmos.zenModeInteractor,
+ zenModeInteractor,
userTracker,
)
underTest.clock = clock
@@ -504,7 +501,7 @@ class ClockEventControllerTest : SysuiTestCase() {
runCurrent()
clearInvocations(events)
- zenModeRepository.activateMode(dndModeId)
+ zenModeRepository.activateMode(MANUAL_DND)
runCurrent()
verify(events)
@@ -512,7 +509,7 @@ class ClockEventControllerTest : SysuiTestCase() {
eq(ZenData(ZenMode.IMPORTANT_INTERRUPTIONS, R.string::dnd_is_on.name))
)
- zenModeRepository.deactivateMode(dndModeId)
+ zenModeRepository.deactivateMode(MANUAL_DND)
runCurrent()
verify(events).onZenDataChanged(eq(ZenData(ZenMode.OFF, R.string::dnd_is_off.name)))
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/FullscreenMagnificationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/FullscreenMagnificationControllerTest.java
index 9d9fb9c23a73..6ad2128759a0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/FullscreenMagnificationControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/FullscreenMagnificationControllerTest.java
@@ -30,9 +30,6 @@ import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.pm.ActivityInfo;
@@ -51,11 +48,8 @@ import android.view.SurfaceControlViewHost;
import android.view.View;
import android.view.WindowManager;
import android.view.accessibility.AccessibilityManager;
-import android.view.animation.DecelerateInterpolator;
-import android.view.animation.Interpolator;
import android.window.InputTransferToken;
-import androidx.annotation.NonNull;
import androidx.test.filters.FlakyTest;
import androidx.test.filters.SmallTest;
@@ -80,19 +74,17 @@ import java.util.function.Supplier;
@RunWith(AndroidTestingRunner.class)
@FlakyTest(bugId = 385115361)
public class FullscreenMagnificationControllerTest extends SysuiTestCase {
- private static final long ANIMATION_DURATION_MS = 100L;
private static final long WAIT_TIMEOUT_S = 5L * HW_TIMEOUT_MULTIPLIER;
- private static final long ANIMATION_TIMEOUT_MS =
- 5L * ANIMATION_DURATION_MS * HW_TIMEOUT_MULTIPLIER;
private static final String UNIQUE_DISPLAY_ID_PRIMARY = "000";
private static final String UNIQUE_DISPLAY_ID_SECONDARY = "111";
private static final int CORNER_RADIUS_PRIMARY = 10;
private static final int CORNER_RADIUS_SECONDARY = 20;
+ private static final int DISABLED = 0;
+ private static final int ENABLED = 3;
private FullscreenMagnificationController mFullscreenMagnificationController;
private SurfaceControlViewHost mSurfaceControlViewHost;
- private ValueAnimator mShowHideBorderAnimator;
private SurfaceControl.Transaction mTransaction;
private TestableWindowManager mWindowManager;
@Mock
@@ -136,7 +128,6 @@ public class FullscreenMagnificationControllerTest extends SysuiTestCase {
mContext.addMockSystemService(Context.WINDOW_SERVICE, mWindowManager);
mTransaction = new SurfaceControl.Transaction();
- mShowHideBorderAnimator = spy(newNullTargetObjectAnimator());
mFullscreenMagnificationController = new FullscreenMagnificationController(
mContext,
mContext.getMainThreadHandler(),
@@ -146,141 +137,68 @@ public class FullscreenMagnificationControllerTest extends SysuiTestCase {
mContext.getSystemService(WindowManager.class),
mIWindowManager,
scvhSupplier,
- mTransaction,
- mShowHideBorderAnimator);
+ mTransaction);
}
@After
public void tearDown() {
- getInstrumentation().runOnMainSync(
- () -> mFullscreenMagnificationController
- .onFullscreenMagnificationActivationChanged(false));
+ getInstrumentation().runOnMainSync(() ->
+ mFullscreenMagnificationController.cleanUpBorder());
+ }
+
+ @Test
+ public void createShowTargetAnimator_runAnimator_alphaIsEqualToOne() {
+ View view = new View(mContext);
+ view.setAlpha(0f);
+ ValueAnimator animator = mFullscreenMagnificationController.createShowTargetAnimator(view);
+ animator.end();
+ assertThat(view.getAlpha()).isEqualTo(1f);
+ }
+
+ @Test
+ public void createHideTargetAnimator_runAnimator_alphaIsEqualToZero() {
+ View view = new View(mContext);
+ view.setAlpha(1f);
+ ValueAnimator animator = mFullscreenMagnificationController.createHideTargetAnimator(view);
+ animator.end();
+ assertThat(view.getAlpha()).isEqualTo(0f);
}
@Test
- public void enableFullscreenMagnification_visibleBorder()
+ public void enableFullscreenMagnification_stateEnabled()
throws InterruptedException, RemoteException {
- CountDownLatch transactionCommittedLatch = new CountDownLatch(1);
- CountDownLatch animationEndLatch = new CountDownLatch(1);
- mTransaction.addTransactionCommittedListener(
- Runnable::run, transactionCommittedLatch::countDown);
- mShowHideBorderAnimator.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- animationEndLatch.countDown();
- }
- });
- getInstrumentation().runOnMainSync(() ->
- //Enable fullscreen magnification
- mFullscreenMagnificationController
- .onFullscreenMagnificationActivationChanged(true));
- assertWithMessage("Failed to wait for transaction committed")
- .that(transactionCommittedLatch.await(WAIT_TIMEOUT_S, TimeUnit.SECONDS))
- .isTrue();
- assertWithMessage("Failed to wait for animation to be finished")
- .that(animationEndLatch.await(ANIMATION_TIMEOUT_MS, TimeUnit.MILLISECONDS))
- .isTrue();
- verify(mShowHideBorderAnimator).start();
+ enableFullscreenMagnificationAndWaitForTransactionAndAnimation();
+
+ assertThat(mFullscreenMagnificationController.getState()).isEqualTo(ENABLED);
verify(mIWindowManager)
.watchRotation(any(IRotationWatcher.class), eq(Display.DEFAULT_DISPLAY));
- assertThat(mSurfaceControlViewHost.getView().isVisibleToUser()).isTrue();
}
@Test
- public void disableFullscreenMagnification_reverseAnimationAndReleaseScvh()
+ public void disableFullscreenMagnification_stateDisabled()
throws InterruptedException, RemoteException {
- CountDownLatch transactionCommittedLatch = new CountDownLatch(1);
- CountDownLatch enableAnimationEndLatch = new CountDownLatch(1);
- CountDownLatch disableAnimationEndLatch = new CountDownLatch(1);
- mTransaction.addTransactionCommittedListener(
- Runnable::run, transactionCommittedLatch::countDown);
- mShowHideBorderAnimator.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(@NonNull Animator animation, boolean isReverse) {
- if (isReverse) {
- disableAnimationEndLatch.countDown();
- } else {
- enableAnimationEndLatch.countDown();
- }
- }
- });
- getInstrumentation().runOnMainSync(() ->
- //Enable fullscreen magnification
- mFullscreenMagnificationController
- .onFullscreenMagnificationActivationChanged(true));
- assertWithMessage("Failed to wait for transaction committed")
- .that(transactionCommittedLatch.await(WAIT_TIMEOUT_S, TimeUnit.SECONDS))
- .isTrue();
- assertWithMessage("Failed to wait for enabling animation to be finished")
- .that(enableAnimationEndLatch.await(ANIMATION_TIMEOUT_MS, TimeUnit.MILLISECONDS))
- .isTrue();
- verify(mShowHideBorderAnimator).start();
+ enableFullscreenMagnificationAndWaitForTransactionAndAnimation();
- getInstrumentation().runOnMainSync(() ->
- // Disable fullscreen magnification
- mFullscreenMagnificationController
- .onFullscreenMagnificationActivationChanged(false));
+ getInstrumentation().runOnMainSync(() -> {
+ // Disable fullscreen magnification
+ mFullscreenMagnificationController
+ .onFullscreenMagnificationActivationChanged(false);
+ });
+ waitForIdleSync();
+ assertThat(mFullscreenMagnificationController.mShowHideBorderAnimator).isNotNull();
+ mFullscreenMagnificationController.mShowHideBorderAnimator.end();
+ waitForIdleSync();
- assertWithMessage("Failed to wait for disabling animation to be finished")
- .that(disableAnimationEndLatch.await(ANIMATION_TIMEOUT_MS, TimeUnit.MILLISECONDS))
- .isTrue();
- verify(mShowHideBorderAnimator).reverse();
+ assertThat(mFullscreenMagnificationController.getState()).isEqualTo(DISABLED);
verify(mSurfaceControlViewHost).release();
verify(mIWindowManager).removeRotationWatcher(any(IRotationWatcher.class));
}
@Test
- public void onFullscreenMagnificationActivationChangeTrue_deactivating_reverseAnimator()
- throws InterruptedException {
- // Simulate the hiding border animation is running
- when(mShowHideBorderAnimator.isRunning()).thenReturn(true);
- CountDownLatch transactionCommittedLatch = new CountDownLatch(1);
- CountDownLatch animationEndLatch = new CountDownLatch(1);
- mTransaction.addTransactionCommittedListener(
- Runnable::run, transactionCommittedLatch::countDown);
- mShowHideBorderAnimator.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- animationEndLatch.countDown();
- }
- });
-
- getInstrumentation().runOnMainSync(
- () -> mFullscreenMagnificationController
- .onFullscreenMagnificationActivationChanged(true));
-
- assertWithMessage("Failed to wait for transaction committed")
- .that(transactionCommittedLatch.await(WAIT_TIMEOUT_S, TimeUnit.SECONDS))
- .isTrue();
- assertWithMessage("Failed to wait for animation to be finished")
- .that(animationEndLatch.await(ANIMATION_TIMEOUT_MS, TimeUnit.MILLISECONDS))
- .isTrue();
- verify(mShowHideBorderAnimator).reverse();
- }
-
- @Test
public void onScreenSizeChanged_activated_borderChangedToExpectedSize()
throws InterruptedException {
- CountDownLatch transactionCommittedLatch = new CountDownLatch(1);
- CountDownLatch animationEndLatch = new CountDownLatch(1);
- mTransaction.addTransactionCommittedListener(
- Runnable::run, transactionCommittedLatch::countDown);
- mShowHideBorderAnimator.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- animationEndLatch.countDown();
- }
- });
- getInstrumentation().runOnMainSync(() ->
- //Enable fullscreen magnification
- mFullscreenMagnificationController
- .onFullscreenMagnificationActivationChanged(true));
- assertWithMessage("Failed to wait for transaction committed")
- .that(transactionCommittedLatch.await(WAIT_TIMEOUT_S, TimeUnit.SECONDS))
- .isTrue();
- assertWithMessage("Failed to wait for animation to be finished")
- .that(animationEndLatch.await(ANIMATION_TIMEOUT_MS, TimeUnit.MILLISECONDS))
- .isTrue();
+ enableFullscreenMagnificationAndWaitForTransactionAndAnimation();
+
final Rect testWindowBounds = new Rect(
mWindowManager.getCurrentWindowMetrics().getBounds());
testWindowBounds.set(testWindowBounds.left, testWindowBounds.top,
@@ -304,29 +222,8 @@ public class FullscreenMagnificationControllerTest extends SysuiTestCase {
@Test
public void enableFullscreenMagnification_applyPrimaryCornerRadius()
throws InterruptedException {
- CountDownLatch transactionCommittedLatch = new CountDownLatch(1);
- CountDownLatch animationEndLatch = new CountDownLatch(1);
- mTransaction.addTransactionCommittedListener(
- Runnable::run, transactionCommittedLatch::countDown);
- mShowHideBorderAnimator.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- animationEndLatch.countDown();
- }
- });
+ enableFullscreenMagnificationAndWaitForTransactionAndAnimation();
- getInstrumentation().runOnMainSync(() ->
- //Enable fullscreen magnification
- mFullscreenMagnificationController
- .onFullscreenMagnificationActivationChanged(true));
- assertWithMessage("Failed to wait for transaction committed")
- .that(transactionCommittedLatch.await(WAIT_TIMEOUT_S, TimeUnit.SECONDS))
- .isTrue();
- assertWithMessage("Failed to wait for animation to be finished")
- .that(animationEndLatch.await(ANIMATION_TIMEOUT_MS, TimeUnit.MILLISECONDS))
- .isTrue();
-
- // Verify the initial corner radius is applied
GradientDrawable backgroundDrawable =
(GradientDrawable) mSurfaceControlViewHost.getView().getBackground();
assertThat(backgroundDrawable.getCornerRadius()).isEqualTo(CORNER_RADIUS_PRIMARY);
@@ -334,28 +231,8 @@ public class FullscreenMagnificationControllerTest extends SysuiTestCase {
@EnableFlags(Flags.FLAG_UPDATE_CORNER_RADIUS_ON_DISPLAY_CHANGED)
@Test
- public void onDisplayChanged_updateCornerRadiusToSecondary() throws InterruptedException {
- CountDownLatch transactionCommittedLatch = new CountDownLatch(1);
- CountDownLatch animationEndLatch = new CountDownLatch(1);
- mTransaction.addTransactionCommittedListener(
- Runnable::run, transactionCommittedLatch::countDown);
- mShowHideBorderAnimator.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- animationEndLatch.countDown();
- }
- });
-
- getInstrumentation().runOnMainSync(() ->
- //Enable fullscreen magnification
- mFullscreenMagnificationController
- .onFullscreenMagnificationActivationChanged(true));
- assertWithMessage("Failed to wait for transaction committed")
- .that(transactionCommittedLatch.await(WAIT_TIMEOUT_S, TimeUnit.SECONDS))
- .isTrue();
- assertWithMessage("Failed to wait for animation to be finished")
- .that(animationEndLatch.await(ANIMATION_TIMEOUT_MS, TimeUnit.MILLISECONDS))
- .isTrue();
+ public void onDisplayChanged_applyCornerRadiusToBorder() throws InterruptedException {
+ enableFullscreenMagnificationAndWaitForTransactionAndAnimation();
ArgumentCaptor<DisplayManager.DisplayListener> displayListenerCaptor =
ArgumentCaptor.forClass(DisplayManager.DisplayListener.class);
@@ -372,22 +249,34 @@ public class FullscreenMagnificationControllerTest extends SysuiTestCase {
.addOverride(
com.android.internal.R.dimen.rounded_corner_radius,
CORNER_RADIUS_SECONDARY);
+
getInstrumentation().runOnMainSync(() ->
displayListenerCaptor.getValue().onDisplayChanged(Display.DEFAULT_DISPLAY));
waitForIdleSync();
+
// Verify the corner radius is updated
GradientDrawable backgroundDrawable2 =
(GradientDrawable) mSurfaceControlViewHost.getView().getBackground();
assertThat(backgroundDrawable2.getCornerRadius()).isEqualTo(CORNER_RADIUS_SECONDARY);
}
+ private void enableFullscreenMagnificationAndWaitForTransactionAndAnimation()
+ throws InterruptedException {
+ CountDownLatch transactionCommittedLatch = new CountDownLatch(1);
+ mTransaction.addTransactionCommittedListener(
+ Runnable::run, transactionCommittedLatch::countDown);
+
+ getInstrumentation().runOnMainSync(() ->
+ //Enable fullscreen magnification
+ mFullscreenMagnificationController
+ .onFullscreenMagnificationActivationChanged(true));
- private ValueAnimator newNullTargetObjectAnimator() {
- final ValueAnimator animator =
- ObjectAnimator.ofFloat(/* target= */ null, View.ALPHA, 0f, 1f);
- Interpolator interpolator = new DecelerateInterpolator(2.5f);
- animator.setInterpolator(interpolator);
- animator.setDuration(ANIMATION_DURATION_MS);
- return animator;
+ assertWithMessage("Failed to wait for transaction committed")
+ .that(transactionCommittedLatch.await(WAIT_TIMEOUT_S, TimeUnit.SECONDS))
+ .isTrue();
+ waitForIdleSync();
+ assertThat(mFullscreenMagnificationController.mShowHideBorderAnimator).isNotNull();
+ mFullscreenMagnificationController.mShowHideBorderAnimator.end();
+ waitForIdleSync();
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationControllerTest.java
index 856c37934251..9f6ad56335d7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationControllerTest.java
@@ -82,7 +82,7 @@ public class MenuAnimationControllerTest extends SysuiTestCase {
final WindowManager stubWindowManager = mContext.getSystemService(WindowManager.class);
final MenuViewAppearance stubMenuViewAppearance = new MenuViewAppearance(mContext,
stubWindowManager);
- final SecureSettings secureSettings = TestUtils.mockSecureSettings();
+ final SecureSettings secureSettings = TestUtils.mockSecureSettings(mContext);
final MenuViewModel stubMenuViewModel = new MenuViewModel(mContext, mAccessibilityManager,
secureSettings, mHearingAidDeviceManager);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java
index 33cfb3890e71..1500340c9d89 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java
@@ -144,7 +144,7 @@ public class MenuViewLayerTest extends SysuiTestCase {
private HearingAidDeviceManager mHearingAidDeviceManager;
@Mock
private PackageManager mMockPackageManager;
- private final SecureSettings mSecureSettings = TestUtils.mockSecureSettings();
+ private final SecureSettings mSecureSettings = TestUtils.mockSecureSettings(mContext);
private final NotificationManager mMockNotificationManager = mock(NotificationManager.class);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorTest.kt
index 5d622eaeb1aa..e61acc4e1d0b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorTest.kt
@@ -32,6 +32,7 @@ import com.android.systemui.kosmos.testScope
import com.android.systemui.plugins.activityStarter
import com.android.systemui.statusbar.phone.SystemUIDialog
import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest
@@ -224,6 +225,30 @@ class AudioSharingDeviceItemActionInteractorTest : SysuiTestCase() {
}
}
+ @Test
+ fun testOnActionIconClick_audioSharingMediaDevice_stopBroadcast() {
+ with(kosmos) {
+ testScope.runTest {
+ bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true)
+ actionInteractorImpl.onActionIconClick(inAudioSharingMediaDeviceItem) {}
+ assertThat(bluetoothTileDialogAudioSharingRepository.audioSharingStarted)
+ .isEqualTo(false)
+ }
+ }
+ }
+
+ @Test
+ fun testOnActionIconClick_availableAudioSharingMediaDevice_startBroadcast() {
+ with(kosmos) {
+ testScope.runTest {
+ bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true)
+ actionInteractorImpl.onActionIconClick(connectedAudioSharingMediaDeviceItem) {}
+ assertThat(bluetoothTileDialogAudioSharingRepository.audioSharingStarted)
+ .isEqualTo(true)
+ }
+ }
+ }
+
private companion object {
const val DEVICE_NAME = "device"
const val DEVICE_CONNECTION_SUMMARY = "active"
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegateTest.kt
index 6bfd08025833..4396b0a42ae6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegateTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegateTest.kt
@@ -32,6 +32,9 @@ import com.android.internal.logging.UiEventLogger
import com.android.settingslib.bluetooth.CachedBluetoothDevice
import com.android.systemui.SysuiTestCase
import com.android.systemui.animation.DialogTransitionAnimator
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.kosmos.testScope
import com.android.systemui.model.SysUiState
import com.android.systemui.res.R
import com.android.systemui.shade.data.repository.shadeDialogContextInteractor
@@ -43,9 +46,8 @@ import com.android.systemui.util.mockito.whenever
import com.android.systemui.util.time.FakeSystemClock
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.CoroutineDispatcher
-import kotlinx.coroutines.test.TestCoroutineScheduler
import kotlinx.coroutines.test.TestScope
-import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Rule
@@ -93,7 +95,6 @@ class BluetoothTileDialogDelegateTest : SysuiTestCase() {
private val fakeSystemClock = FakeSystemClock()
- private lateinit var scheduler: TestCoroutineScheduler
private lateinit var dispatcher: CoroutineDispatcher
private lateinit var testScope: TestScope
private lateinit var icon: Pair<Drawable, String>
@@ -104,9 +105,8 @@ class BluetoothTileDialogDelegateTest : SysuiTestCase() {
@Before
fun setUp() {
- scheduler = TestCoroutineScheduler()
- dispatcher = UnconfinedTestDispatcher(scheduler)
- testScope = TestScope(dispatcher)
+ dispatcher = kosmos.testDispatcher
+ testScope = kosmos.testScope
whenever(sysuiState.setFlag(anyLong(), anyBoolean())).thenReturn(sysuiState)
@@ -124,23 +124,19 @@ class BluetoothTileDialogDelegateTest : SysuiTestCase() {
kosmos.shadeDialogContextInteractor,
)
- whenever(
- sysuiDialogFactory.create(
- any(SystemUIDialog.Delegate::class.java),
- any()
- )
- ).thenAnswer {
- SystemUIDialog(
- mContext,
- 0,
- SystemUIDialog.DEFAULT_DISMISS_ON_DEVICE_LOCK,
- dialogManager,
- sysuiState,
- fakeBroadcastDispatcher,
- dialogTransitionAnimator,
- it.getArgument(0),
- )
- }
+ whenever(sysuiDialogFactory.create(any(SystemUIDialog.Delegate::class.java), any()))
+ .thenAnswer {
+ SystemUIDialog(
+ mContext,
+ 0,
+ SystemUIDialog.DEFAULT_DISMISS_ON_DEVICE_LOCK,
+ dialogManager,
+ sysuiState,
+ fakeBroadcastDispatcher,
+ dialogTransitionAnimator,
+ it.getArgument(0),
+ )
+ }
icon = Pair(drawable, DEVICE_NAME)
deviceItem =
@@ -194,20 +190,29 @@ class BluetoothTileDialogDelegateTest : SysuiTestCase() {
@Test
fun testDeviceItemViewHolder_cachedDeviceNotBusy() {
- deviceItem.isEnabled = true
-
- val view =
- LayoutInflater.from(mContext).inflate(R.layout.bluetooth_device_item, null, false)
- val viewHolder =
- mBluetoothTileDialogDelegate
- .Adapter(bluetoothTileDialogCallback)
- .DeviceItemViewHolder(view)
- viewHolder.bind(deviceItem, bluetoothTileDialogCallback)
- val container = view.requireViewById<View>(R.id.bluetooth_device_row)
-
- assertThat(container).isNotNull()
- assertThat(container.isEnabled).isTrue()
- assertThat(container.hasOnClickListeners()).isTrue()
+ testScope.runTest {
+ deviceItem.isEnabled = true
+
+ val view =
+ LayoutInflater.from(mContext).inflate(R.layout.bluetooth_device_item, null, false)
+ val viewHolder = mBluetoothTileDialogDelegate.Adapter().DeviceItemViewHolder(view)
+ viewHolder.bind(deviceItem)
+ val container = view.requireViewById<View>(R.id.bluetooth_device_row)
+
+ assertThat(container).isNotNull()
+ assertThat(container.isEnabled).isTrue()
+ assertThat(container.hasOnClickListeners()).isTrue()
+ val value by collectLastValue(mBluetoothTileDialogDelegate.deviceItemClick)
+ runCurrent()
+ container.performClick()
+ runCurrent()
+ assertThat(value).isNotNull()
+ value?.let {
+ assertThat(it.target).isEqualTo(DeviceItemClick.Target.ENTIRE_ROW)
+ assertThat(it.clickedView).isEqualTo(container)
+ assertThat(it.deviceItem).isEqualTo(deviceItem)
+ }
+ }
}
@Test
@@ -229,9 +234,9 @@ class BluetoothTileDialogDelegateTest : SysuiTestCase() {
sysuiDialogFactory,
kosmos.shadeDialogContextInteractor,
)
- .Adapter(bluetoothTileDialogCallback)
+ .Adapter()
.DeviceItemViewHolder(view)
- viewHolder.bind(deviceItem, bluetoothTileDialogCallback)
+ viewHolder.bind(deviceItem)
val container = view.requireViewById<View>(R.id.bluetooth_device_row)
assertThat(container).isNotNull()
@@ -240,6 +245,32 @@ class BluetoothTileDialogDelegateTest : SysuiTestCase() {
}
@Test
+ fun testDeviceItemViewHolder_clickActionIcon() {
+ testScope.runTest {
+ deviceItem.isEnabled = true
+
+ val view =
+ LayoutInflater.from(mContext).inflate(R.layout.bluetooth_device_item, null, false)
+ val viewHolder = mBluetoothTileDialogDelegate.Adapter().DeviceItemViewHolder(view)
+ viewHolder.bind(deviceItem)
+ val actionIconView = view.requireViewById<View>(R.id.gear_icon)
+
+ assertThat(actionIconView).isNotNull()
+ assertThat(actionIconView.hasOnClickListeners()).isTrue()
+ val value by collectLastValue(mBluetoothTileDialogDelegate.deviceItemClick)
+ runCurrent()
+ actionIconView.performClick()
+ runCurrent()
+ assertThat(value).isNotNull()
+ value?.let {
+ assertThat(it.target).isEqualTo(DeviceItemClick.Target.ACTION_ICON)
+ assertThat(it.clickedView).isEqualTo(actionIconView)
+ assertThat(it.deviceItem).isEqualTo(deviceItem)
+ }
+ }
+ }
+
+ @Test
fun testOnDeviceUpdated_hideSeeAll_showPairNew() {
testScope.runTest {
val dialog = mBluetoothTileDialogDelegate.createDialog()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactoryTest.kt
index 5bf15137b834..0aa5199cb20e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactoryTest.kt
@@ -118,6 +118,7 @@ class DeviceItemFactoryTest : SysuiTestCase() {
.isEqualTo(DeviceItemType.AVAILABLE_AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE)
assertThat(deviceItem.cachedBluetoothDevice).isEqualTo(cachedDevice)
assertThat(deviceItem.deviceName).isEqualTo(DEVICE_NAME)
+ assertThat(deviceItem.actionIconRes).isEqualTo(R.drawable.ic_add)
assertThat(deviceItem.isActive).isFalse()
assertThat(deviceItem.connectionSummary)
.isEqualTo(
@@ -292,6 +293,7 @@ class DeviceItemFactoryTest : SysuiTestCase() {
assertThat(deviceItem.cachedBluetoothDevice).isEqualTo(cachedDevice)
assertThat(deviceItem.deviceName).isEqualTo(DEVICE_NAME)
assertThat(deviceItem.connectionSummary).isEqualTo(CONNECTION_SUMMARY)
+ assertThat(deviceItem.actionIconRes).isEqualTo(R.drawable.ic_settings_24dp)
}
companion object {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/IconManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/IconManagerTest.kt
index 57a12df0cfee..c4ef4f978ff8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/IconManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/IconManagerTest.kt
@@ -35,7 +35,6 @@ import android.platform.test.annotations.EnableFlags
import androidx.test.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
-import com.android.systemui.Flags.FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON
import com.android.systemui.SysuiTestCase
import com.android.systemui.controls.controller.AuxiliaryPersistenceWrapperTest.Companion.any
import com.android.systemui.statusbar.core.StatusBarConnectedDisplays
@@ -112,20 +111,8 @@ class IconManagerTest : SysuiTestCase() {
}
@Test
- @DisableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON)
- fun testCreateIcons_chipNotifIconFlagDisabled_statusBarChipIconIsNull() {
- val entry =
- notificationEntry(hasShortcut = true, hasMessageSenderIcon = true, hasLargeIcon = true)
- entry?.let { iconManager.createIcons(it) }
- testScope.runCurrent()
-
- assertThat(entry?.icons?.statusBarChipIcon).isNull()
- }
-
- @Test
- @EnableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON)
@DisableFlags(StatusBarConnectedDisplays.FLAG_NAME)
- fun testCreateIcons_chipNotifIconFlagEnabled_cdFlagDisabled_statusBarChipIconIsNotNull() {
+ fun testCreateIcons_cdFlagDisabled_statusBarChipIconIsNotNull() {
val entry =
notificationEntry(hasShortcut = true, hasMessageSenderIcon = true, hasLargeIcon = true)
entry?.let { iconManager.createIcons(it) }
@@ -135,8 +122,8 @@ class IconManagerTest : SysuiTestCase() {
}
@Test
- @EnableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON, StatusBarConnectedDisplays.FLAG_NAME)
- fun testCreateIcons_chipNotifIconFlagEnabled_cdFlagEnabled_statusBarChipIconIsNull() {
+ @EnableFlags(StatusBarConnectedDisplays.FLAG_NAME)
+ fun testCreateIcons_cdFlagEnabled_statusBarChipIconIsNull() {
val entry =
notificationEntry(hasShortcut = true, hasMessageSenderIcon = true, hasLargeIcon = true)
entry?.let { iconManager.createIcons(it) }
@@ -217,7 +204,6 @@ class IconManagerTest : SysuiTestCase() {
}
@Test
- @EnableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON)
@DisableFlags(StatusBarConnectedDisplays.FLAG_NAME)
fun testCreateIcons_cdFlagDisabled_sensitiveImportantConversation() {
val entry =
@@ -233,7 +219,7 @@ class IconManagerTest : SysuiTestCase() {
}
@Test
- @EnableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON, StatusBarConnectedDisplays.FLAG_NAME)
+ @EnableFlags(StatusBarConnectedDisplays.FLAG_NAME)
fun testCreateIcons_cdFlagEnabled_sensitiveImportantConversation() {
val entry =
notificationEntry(hasShortcut = true, hasMessageSenderIcon = true, hasLargeIcon = false)
@@ -248,7 +234,6 @@ class IconManagerTest : SysuiTestCase() {
}
@Test
- @EnableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON)
@DisableFlags(StatusBarConnectedDisplays.FLAG_NAME)
fun testUpdateIcons_cdFlagDisabled_sensitiveImportantConversation() {
val entry =
@@ -266,7 +251,7 @@ class IconManagerTest : SysuiTestCase() {
}
@Test
- @EnableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON, StatusBarConnectedDisplays.FLAG_NAME)
+ @EnableFlags(StatusBarConnectedDisplays.FLAG_NAME)
fun testUpdateIcons_cdFlagEnabled_sensitiveImportantConversation() {
val entry =
notificationEntry(hasShortcut = true, hasMessageSenderIcon = true, hasLargeIcon = false)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java
index 3a99328fa8ed..30ab416b1cbd 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java
@@ -42,6 +42,7 @@ import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper.RunWithLooper;
import android.view.View;
+import androidx.annotation.NonNull;
import androidx.test.filters.SmallTest;
import com.android.keyguard.KeyguardUpdateMonitor;
@@ -77,6 +78,8 @@ import com.android.systemui.statusbar.phone.ui.DarkIconManager;
import com.android.systemui.statusbar.phone.ui.StatusBarIconController;
import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.FakeHomeStatusBarViewBinder;
import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.FakeHomeStatusBarViewModel;
+import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.HomeStatusBarViewModel;
+import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.HomeStatusBarViewModel.HomeStatusBarViewModelFactory;
import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.StatusBarOperatorNameViewModel;
import com.android.systemui.statusbar.policy.KeyguardStateController;
import com.android.systemui.statusbar.window.StatusBarWindowController;
@@ -1268,6 +1271,15 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest {
mock(StatusBarOperatorNameViewModel.class));
mCollapsedStatusBarViewBinder = new FakeHomeStatusBarViewBinder();
+ HomeStatusBarViewModelFactory homeStatusBarViewModelFactory =
+ new HomeStatusBarViewModelFactory() {
+ @NonNull
+ @Override
+ public HomeStatusBarViewModel create(int displayId) {
+ return mCollapsedStatusBarViewModel;
+ }
+ };
+
return new CollapsedStatusBarFragment(
mStatusBarFragmentComponentFactory,
mOngoingCallController,
@@ -1275,7 +1287,7 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest {
mShadeExpansionStateManager,
mStatusBarIconController,
mIconManagerFactory,
- mCollapsedStatusBarViewModel,
+ homeStatusBarViewModelFactory,
mCollapsedStatusBarViewBinder,
mStatusBarHideIconsForBouncerManager,
mKeyguardStateController,
diff --git a/packages/SystemUI/tests/utils/src/android/internal/statusbar/FakeStatusBarService.kt b/packages/SystemUI/tests/utils/src/android/internal/statusbar/FakeStatusBarService.kt
index 25d1c377ecbd..7ed736158a53 100644
--- a/packages/SystemUI/tests/utils/src/android/internal/statusbar/FakeStatusBarService.kt
+++ b/packages/SystemUI/tests/utils/src/android/internal/statusbar/FakeStatusBarService.kt
@@ -435,6 +435,8 @@ class FakeStatusBarService : IStatusBarService.Stub() {
override fun unbundleNotification(key: String) {}
+ override fun rebundleNotification(key: String) {}
+
companion object {
const val DEFAULT_DISPLAY_ID = Display.DEFAULT_DISPLAY
const val SECONDARY_DISPLAY_ID = 2
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/animation/ActivityTransitionAnimatorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/animation/ActivityTransitionAnimatorKosmos.kt
index 5ac41ec6741c..f380789968f5 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/animation/ActivityTransitionAnimatorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/animation/ActivityTransitionAnimatorKosmos.kt
@@ -23,11 +23,11 @@ import com.android.systemui.util.mockito.mock
val Kosmos.mockActivityTransitionAnimatorController by
Kosmos.Fixture { mock<ActivityTransitionAnimator.Controller>() }
-val Kosmos.activityTransitionAnimator by
+var Kosmos.activityTransitionAnimator by
Kosmos.Fixture {
ActivityTransitionAnimator(
// The main thread is checked in a bunch of places inside the different transitions
// animators, so we have to pass the real main executor here.
- mainExecutor = testCase.context.mainExecutor,
+ mainExecutor = testCase.context.mainExecutor
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/FakeAudioSharingRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/FakeAudioSharingRepository.kt
index a839f17aad82..c744eacfa3f4 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/FakeAudioSharingRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/FakeAudioSharingRepository.kt
@@ -33,6 +33,9 @@ class FakeAudioSharingRepository : AudioSharingRepository {
var sourceAdded: Boolean = false
private set
+ var audioSharingStarted: Boolean = false
+ private set
+
private var profile: LocalBluetoothLeBroadcast? = null
override val leAudioBroadcastProfile: LocalBluetoothLeBroadcast?
@@ -50,7 +53,13 @@ class FakeAudioSharingRepository : AudioSharingRepository {
override suspend fun setActive(cachedBluetoothDevice: CachedBluetoothDevice) {}
- override suspend fun startAudioSharing() {}
+ override suspend fun startAudioSharing() {
+ audioSharingStarted = true
+ }
+
+ override suspend fun stopAudioSharing() {
+ audioSharingStarted = false
+ }
fun setAudioSharingAvailable(available: Boolean) {
mutableAvailable = available
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorKosmos.kt
index 2a7e3e903737..490b89bf6b13 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorKosmos.kt
@@ -23,6 +23,7 @@ import com.android.systemui.biometrics.data.repository.fingerprintPropertyReposi
import com.android.systemui.dump.dumpManager
import com.android.systemui.keyevent.domain.interactor.keyEventInteractor
import com.android.systemui.keyguard.data.repository.biometricSettingsRepository
+import com.android.systemui.keyguard.domain.interactor.keyguardBypassInteractor
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.power.domain.interactor.powerInteractor
import com.android.systemui.util.time.systemClock
@@ -34,6 +35,8 @@ val Kosmos.deviceEntryHapticsInteractor by
DeviceEntryHapticsInteractor(
biometricSettingsRepository = biometricSettingsRepository,
deviceEntryBiometricAuthInteractor = deviceEntryBiometricAuthInteractor,
+ deviceEntryFaceAuthInteractor = deviceEntryFaceAuthInteractor,
+ keyguardBypassInteractor = keyguardBypassInteractor,
deviceEntryFingerprintAuthInteractor = deviceEntryFingerprintAuthInteractor,
deviceEntrySourceInteractor = deviceEntrySourceInteractor,
fingerprintPropertyRepository = fingerprintPropertyRepository,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/FakeDisplayRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/FakeDisplayRepository.kt
index 3fc60e339543..a64fc2413246 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/FakeDisplayRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/FakeDisplayRepository.kt
@@ -115,3 +115,6 @@ class FakeDisplayRepository @Inject constructor() : DisplayRepository {
interface FakeDisplayRepositoryModule {
@Binds fun bindFake(fake: FakeDisplayRepository): DisplayRepository
}
+
+val DisplayRepository.fake
+ get() = this as FakeDisplayRepository
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorFactory.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorFactory.kt
index 3de809308702..ee21bdc0b4c2 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorFactory.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorFactory.kt
@@ -24,8 +24,6 @@ import com.android.systemui.flags.FakeFeatureFlags
import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.model.TransitionStep
-import com.android.systemui.power.domain.interactor.PowerInteractor
-import com.android.systemui.power.domain.interactor.PowerInteractorFactory
import com.android.systemui.scene.domain.interactor.SceneInteractor
import com.android.systemui.shade.data.repository.FakeShadeRepository
import com.android.systemui.util.mockito.mock
@@ -55,7 +53,6 @@ object KeyguardInteractorFactory {
fromLockscreenTransitionInteractor: FromLockscreenTransitionInteractor = mock(),
fromOccludedTransitionInteractor: FromOccludedTransitionInteractor = mock(),
fromAlternateBouncerTransitionInteractor: FromAlternateBouncerTransitionInteractor = mock(),
- powerInteractor: PowerInteractor = PowerInteractorFactory.create().powerInteractor,
testScope: CoroutineScope = TestScope(),
): WithDependencies {
// Mock these until they are replaced by kosmos
@@ -73,10 +70,8 @@ object KeyguardInteractorFactory {
bouncerRepository = bouncerRepository,
configurationRepository = configurationRepository,
shadeRepository = shadeRepository,
- powerInteractor = powerInteractor,
KeyguardInteractor(
repository = repository,
- powerInteractor = powerInteractor,
bouncerRepository = bouncerRepository,
configurationInteractor = ConfigurationInteractorImpl(configurationRepository),
shadeRepository = shadeRepository,
@@ -99,7 +94,6 @@ object KeyguardInteractorFactory {
val bouncerRepository: FakeKeyguardBouncerRepository,
val configurationRepository: FakeConfigurationRepository,
val shadeRepository: FakeShadeRepository,
- val powerInteractor: PowerInteractor,
val keyguardInteractor: KeyguardInteractor,
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorKosmos.kt
index f5f8ef75065f..869bae236d5c 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorKosmos.kt
@@ -21,7 +21,6 @@ import com.android.systemui.common.ui.domain.interactor.configurationInteractor
import com.android.systemui.keyguard.data.repository.keyguardRepository
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.testScope
-import com.android.systemui.power.domain.interactor.powerInteractor
import com.android.systemui.scene.domain.interactor.sceneInteractor
import com.android.systemui.shade.data.repository.shadeRepository
@@ -29,7 +28,6 @@ val Kosmos.keyguardInteractor: KeyguardInteractor by
Kosmos.Fixture {
KeyguardInteractor(
repository = keyguardRepository,
- powerInteractor = powerInteractor,
bouncerRepository = keyguardBouncerRepository,
configurationInteractor = configurationInteractor,
shadeRepository = shadeRepository,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/transitions/FakeBouncerTransition.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/transitions/FakeBouncerTransition.kt
index 15d00d9f6994..edc1cce326c3 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/transitions/FakeBouncerTransition.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/transitions/FakeBouncerTransition.kt
@@ -20,4 +20,5 @@ import kotlinx.coroutines.flow.MutableStateFlow
class FakeBouncerTransition : PrimaryBouncerTransition {
override val windowBlurRadius: MutableStateFlow<Float> = MutableStateFlow(0.0f)
+ override val notificationBlurRadius: MutableStateFlow<Float> = MutableStateFlow(0.0f)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/GeneralKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/GeneralKosmos.kt
index afe48214832f..439df543b9fb 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/GeneralKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/GeneralKosmos.kt
@@ -54,8 +54,8 @@ var Kosmos.brightnessWarningToast: BrightnessWarningToast by
* Run this test body with a [Kosmos] as receiver, and using the [testScope] currently installed in
* that Kosmos instance
*/
-fun Kosmos.runTest(testBody: suspend Kosmos.() -> Unit) {
- testScope.runTestWithSnapshots testBody@{ this@runTest.testBody() }
+fun Kosmos.runTest(testBody: suspend Kosmos.() -> Unit) = let { kosmos ->
+ testScope.runTestWithSnapshots { kosmos.testBody() }
}
fun Kosmos.runCurrent() = testScope.runCurrent()
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/toolbar/EditModeButtonViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/toolbar/EditModeButtonViewModelKosmos.kt
index 8ae1332c387a..639bb691f455 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/toolbar/EditModeButtonViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/toolbar/EditModeButtonViewModelKosmos.kt
@@ -18,13 +18,18 @@ package com.android.systemui.qs.panels.ui.viewmodel.toolbar
import com.android.systemui.classifier.domain.interactor.falsingInteractor
import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.plugins.activityStarter
import com.android.systemui.qs.panels.ui.viewmodel.editModeViewModel
val Kosmos.editModeButtonViewModelFactory by
Kosmos.Fixture {
object : EditModeButtonViewModel.Factory {
override fun create(): EditModeButtonViewModel {
- return EditModeButtonViewModel(editModeViewModel, falsingInteractor)
+ return EditModeButtonViewModel(
+ editModeViewModel,
+ falsingInteractor,
+ activityStarter,
+ )
}
}
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/startable/SceneContainerStartableKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/startable/SceneContainerStartableKosmos.kt
index 82b5f6332b23..72c75000ebf4 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/startable/SceneContainerStartableKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/startable/SceneContainerStartableKosmos.kt
@@ -17,6 +17,7 @@
package com.android.systemui.scene.domain.startable
import com.android.internal.logging.uiEventLogger
+import com.android.systemui.animation.activityTransitionAnimator
import com.android.systemui.authentication.domain.interactor.authenticationInteractor
import com.android.systemui.bouncer.domain.interactor.alternateBouncerInteractor
import com.android.systemui.bouncer.domain.interactor.bouncerInteractor
@@ -85,5 +86,6 @@ val Kosmos.sceneContainerStartable by Fixture {
vibratorHelper = vibratorHelper,
msdlPlayer = msdlPlayer,
disabledContentInteractor = disabledContentInteractor,
+ activityTransitionAnimator = activityTransitionAnimator,
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeTestUtil.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeTestUtil.kt
index ab193d294b8c..b3d89dbb834d 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeTestUtil.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeTestUtil.kt
@@ -203,6 +203,7 @@ class ShadeTestUtilSceneImpl(
val isUserInputOngoing = MutableStateFlow(true)
override fun setShadeAndQsExpansion(shadeExpansion: Float, qsExpansion: Float) {
+ shadeRepository.setLegacyIsQsExpanded(qsExpansion > 0f)
if (shadeExpansion == 1f) {
setIdleScene(Scenes.Shade)
} else if (qsExpansion == 1f) {
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractorKosmos.kt
index 4af5e7d9d725..6e44df833582 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractorKosmos.kt
@@ -23,11 +23,21 @@ import com.android.systemui.kosmos.testScope
import com.android.systemui.shade.ShadeDisplayChangeLatencyTracker
import com.android.systemui.shade.ShadeWindowLayoutParams
import com.android.systemui.shade.data.repository.fakeShadeDisplaysRepository
+import java.util.Optional
+import org.mockito.kotlin.any
import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
val Kosmos.shadeLayoutParams by Kosmos.Fixture { ShadeWindowLayoutParams.create(mockedContext) }
-val Kosmos.mockedWindowContext by Kosmos.Fixture { mock<WindowContext>() }
+val Kosmos.mockedWindowContext by
+ Kosmos.Fixture {
+ mock<WindowContext>().apply {
+ whenever(reparentToDisplay(any())).thenAnswer { displayIdParam ->
+ whenever(displayId).thenReturn(displayIdParam.arguments[0] as Int)
+ }
+ }
+ }
val Kosmos.mockedShadeDisplayChangeLatencyTracker by
Kosmos.Fixture { mock<ShadeDisplayChangeLatencyTracker>() }
val Kosmos.shadeDisplaysInteractor by
@@ -38,5 +48,6 @@ val Kosmos.shadeDisplaysInteractor by
testScope.backgroundScope,
testScope.backgroundScope.coroutineContext,
mockedShadeDisplayChangeLatencyTracker,
+ Optional.of(shadeExpandedStateInteractor),
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeInteractorKosmos.kt
index af6d6249b4a8..1dc7229a6506 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeInteractorKosmos.kt
@@ -21,6 +21,7 @@ import com.android.systemui.keyguard.data.repository.keyguardRepository
import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.kosmos.testScope
import com.android.systemui.power.domain.interactor.powerInteractor
import com.android.systemui.scene.domain.interactor.sceneInteractor
import com.android.systemui.shade.ShadeModule
@@ -30,6 +31,7 @@ import com.android.systemui.statusbar.phone.dozeParameters
import com.android.systemui.statusbar.policy.data.repository.userSetupRepository
import com.android.systemui.statusbar.policy.domain.interactor.deviceProvisioningInteractor
import com.android.systemui.user.domain.interactor.userSwitcherInteractor
+import org.mockito.kotlin.mock
var Kosmos.baseShadeInteractor: BaseShadeInteractor by
Kosmos.Fixture {
@@ -71,3 +73,7 @@ val Kosmos.shadeInteractorImpl by
shadeModeInteractor = shadeModeInteractor,
)
}
+var Kosmos.mockShadeInteractor: ShadeInteractor by Kosmos.Fixture { mock() }
+val Kosmos.shadeExpandedStateInteractor by
+ Kosmos.Fixture { ShadeExpandedStateInteractorImpl(shadeInteractor, testScope.backgroundScope) }
+val Kosmos.fakeShadeExpandedStateInteractor by Kosmos.Fixture { FakeShadeExpandedStateInteractor() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/layout/StatusBarContentInsetsProviderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/layout/StatusBarContentInsetsProviderKosmos.kt
index 69e215dcba6a..90897faaa6f8 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/layout/StatusBarContentInsetsProviderKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/layout/StatusBarContentInsetsProviderKosmos.kt
@@ -16,13 +16,28 @@
package com.android.systemui.statusbar.layout
+import android.content.applicationContext
+import com.android.systemui.SysUICutoutProvider
+import com.android.systemui.dump.dumpManager
import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.statusbar.commandline.commandRegistry
+import com.android.systemui.statusbar.policy.configurationController
+import com.android.systemui.statusbar.policy.fake
import org.mockito.kotlin.mock
val Kosmos.mockStatusBarContentInsetsProvider by
Kosmos.Fixture { mock<StatusBarContentInsetsProvider>() }
-var Kosmos.statusBarContentInsetsProvider by Kosmos.Fixture { mockStatusBarContentInsetsProvider }
+val Kosmos.statusBarContentInsetsProvider by
+ Kosmos.Fixture {
+ StatusBarContentInsetsProviderImpl(
+ applicationContext,
+ configurationController.fake,
+ dumpManager,
+ commandRegistry,
+ mock<SysUICutoutProvider>(),
+ )
+ }
val Kosmos.fakeStatusBarContentInsetsProviderFactory by
Kosmos.Fixture { FakeStatusBarContentInsetsProviderFactory() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/layout/ui/viewmodel/StatusBarContentInsetsViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/layout/ui/viewmodel/StatusBarContentInsetsViewModelKosmos.kt
new file mode 100644
index 000000000000..889d469489f2
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/layout/ui/viewmodel/StatusBarContentInsetsViewModelKosmos.kt
@@ -0,0 +1,35 @@
+/*
+ * 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.layout.ui.viewmodel
+
+import com.android.systemui.display.data.repository.displayRepository
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.statusbar.data.repository.multiDisplayStatusBarContentInsetsProviderStore
+import com.android.systemui.statusbar.layout.statusBarContentInsetsProvider
+
+val Kosmos.statusBarContentInsetsViewModel by
+ Kosmos.Fixture { StatusBarContentInsetsViewModel(statusBarContentInsetsProvider) }
+
+val Kosmos.multiDisplayStatusBarContentInsetsViewModelStore by
+ Kosmos.Fixture {
+ MultiDisplayStatusBarContentInsetsViewModelStore(
+ applicationCoroutineScope,
+ displayRepository,
+ multiDisplayStatusBarContentInsetsProviderStore,
+ )
+ }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt
index d1619b7959f2..60e092c9709b 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt
@@ -57,6 +57,7 @@ import com.android.systemui.statusbar.notification.stack.domain.interactor.heads
import com.android.systemui.statusbar.notification.stack.domain.interactor.notificationStackAppearanceInteractor
import com.android.systemui.statusbar.notification.stack.domain.interactor.sharedNotificationContainerInteractor
import com.android.systemui.unfold.domain.interactor.unfoldTransitionInteractor
+import com.android.systemui.window.ui.viewmodel.fakeBouncerTransitions
import kotlinx.coroutines.ExperimentalCoroutinesApi
@OptIn(ExperimentalCoroutinesApi::class)
@@ -99,6 +100,7 @@ val Kosmos.sharedNotificationContainerViewModel by Fixture {
primaryBouncerToGoneTransitionViewModel = primaryBouncerToGoneTransitionViewModel,
primaryBouncerToLockscreenTransitionViewModel =
primaryBouncerToLockscreenTransitionViewModel,
+ primaryBouncerTransitions = fakeBouncerTransitions,
aodBurnInViewModel = aodBurnInViewModel,
communalSceneInteractor = communalSceneInteractor,
headsUpNotificationInteractor = { headsUpNotificationInteractor },
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelKosmos.kt
index b38a723f1fa7..db7e31bb2cb6 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelKosmos.kt
@@ -16,6 +16,7 @@
package com.android.systemui.statusbar.pipeline.shared.ui.viewmodel
+import android.content.testableContext
import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
import com.android.systemui.kosmos.Kosmos
@@ -26,6 +27,7 @@ import com.android.systemui.shade.domain.interactor.shadeInteractor
import com.android.systemui.statusbar.chips.ui.viewmodel.ongoingActivityChipsViewModel
import com.android.systemui.statusbar.events.domain.interactor.systemStatusEventAnimationInteractor
import com.android.systemui.statusbar.featurepods.popups.ui.viewmodel.statusBarPopupChipsViewModel
+import com.android.systemui.statusbar.layout.ui.viewmodel.multiDisplayStatusBarContentInsetsViewModelStore
import com.android.systemui.statusbar.notification.domain.interactor.activeNotificationsInteractor
import com.android.systemui.statusbar.notification.stack.domain.interactor.headsUpNotificationInteractor
import com.android.systemui.statusbar.phone.domain.interactor.darkIconInteractor
@@ -36,6 +38,7 @@ import com.android.systemui.statusbar.pipeline.shared.domain.interactor.homeStat
var Kosmos.homeStatusBarViewModel: HomeStatusBarViewModel by
Kosmos.Fixture {
HomeStatusBarViewModelImpl(
+ testableContext.displayId,
homeStatusBarInteractor,
homeStatusBarIconBlockListInteractor,
lightsOutInteractor,
@@ -51,6 +54,7 @@ var Kosmos.homeStatusBarViewModel: HomeStatusBarViewModel by
ongoingActivityChipsViewModel,
statusBarPopupChipsViewModel,
systemStatusEventAnimationInteractor,
+ multiDisplayStatusBarContentInsetsViewModelStore,
applicationCoroutineScope,
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/ConfigurationControllerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/ConfigurationControllerKosmos.kt
index 282f5947636c..1e4701333857 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/ConfigurationControllerKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/ConfigurationControllerKosmos.kt
@@ -25,3 +25,6 @@ val Kosmos.fakeConfigurationController: FakeConfigurationController by
Kosmos.Fixture { FakeConfigurationController() }
val Kosmos.statusBarConfigurationController: StatusBarConfigurationController by
Kosmos.Fixture { fakeConfigurationController }
+
+val ConfigurationController.fake
+ get() = this as FakeConfigurationController
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/data/repository/ZenModeRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/data/repository/ZenModeRepositoryKosmos.kt
index 1ba5ddbf0337..fc0c92e974f7 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/data/repository/ZenModeRepositoryKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/data/repository/ZenModeRepositoryKosmos.kt
@@ -20,5 +20,5 @@ import com.android.settingslib.notification.data.repository.FakeZenModeRepositor
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.Kosmos.Fixture
-val Kosmos.zenModeRepository by Fixture { fakeZenModeRepository }
+var Kosmos.zenModeRepository by Fixture { fakeZenModeRepository }
val Kosmos.fakeZenModeRepository by Fixture { FakeZenModeRepository() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/MediaOutputKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/MediaOutputKosmos.kt
index ed5322ed098e..db19d6ee9077 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/MediaOutputKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/MediaOutputKosmos.kt
@@ -39,7 +39,7 @@ val Kosmos.localMediaRepositoryFactory by
val Kosmos.mediaOutputActionsInteractor by
Kosmos.Fixture { MediaOutputActionsInteractor(mediaOutputDialogManager) }
-val Kosmos.mediaControllerRepository by Kosmos.Fixture { FakeMediaControllerRepository() }
+var Kosmos.mediaControllerRepository by Kosmos.Fixture { FakeMediaControllerRepository() }
val Kosmos.mediaOutputInteractor by
Kosmos.Fixture {
MediaOutputInteractor(
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/AudioRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/AudioRepositoryKosmos.kt
index 712ec41bbf2d..3f2b47948c1c 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/AudioRepositoryKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/AudioRepositoryKosmos.kt
@@ -19,4 +19,4 @@ package com.android.systemui.volume.data.repository
import com.android.systemui.kosmos.Kosmos
val Kosmos.fakeAudioRepository by Kosmos.Fixture { FakeAudioRepository() }
-val Kosmos.audioRepository by Kosmos.Fixture { fakeAudioRepository }
+var Kosmos.audioRepository by Kosmos.Fixture { fakeAudioRepository }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/VolumeDialogKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/VolumeDialogKosmos.kt
new file mode 100644
index 000000000000..e2431934bc40
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/VolumeDialogKosmos.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume.dialog
+
+import android.content.applicationContext
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.volume.dialog.dagger.volumeDialogComponentFactory
+import com.android.systemui.volume.dialog.domain.interactor.volumeDialogVisibilityInteractor
+
+val Kosmos.volumeDialog by
+ Kosmos.Fixture {
+ VolumeDialog(
+ context = applicationContext,
+ visibilityInteractor = volumeDialogVisibilityInteractor,
+ componentFactory = volumeDialogComponentFactory,
+ )
+ }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/dagger/VolumeDialogComponentKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/dagger/VolumeDialogComponentKosmos.kt
new file mode 100644
index 000000000000..73e5d8d40985
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/dagger/VolumeDialogComponentKosmos.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume.dialog.dagger
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.volume.dialog.sliders.dagger.VolumeDialogSliderComponent
+import com.android.systemui.volume.dialog.sliders.dagger.volumeDialogSliderComponentFactory
+import com.android.systemui.volume.dialog.ui.binder.VolumeDialogViewBinder
+import com.android.systemui.volume.dialog.ui.binder.volumeDialogViewBinder
+import kotlinx.coroutines.CoroutineScope
+
+val Kosmos.volumeDialogComponentFactory by
+ Kosmos.Fixture {
+ object : VolumeDialogComponent.Factory {
+ override fun create(scope: CoroutineScope): VolumeDialogComponent =
+ volumeDialogComponent
+ }
+ }
+val Kosmos.volumeDialogComponent by
+ Kosmos.Fixture {
+ object : VolumeDialogComponent {
+ override fun volumeDialogViewBinder(): VolumeDialogViewBinder = volumeDialogViewBinder
+
+ override fun sliderComponentFactory(): VolumeDialogSliderComponent.Factory =
+ volumeDialogSliderComponentFactory
+ }
+ }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/data/repository/VolumeDialogVisibilityRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/data/repository/VolumeDialogVisibilityRepositoryKosmos.kt
index 291dfc0430e2..3d5698b193e1 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/data/repository/VolumeDialogVisibilityRepositoryKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/data/repository/VolumeDialogVisibilityRepositoryKosmos.kt
@@ -19,4 +19,4 @@ package com.android.systemui.volume.dialog.data.repository
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.volume.dialog.data.VolumeDialogVisibilityRepository
-val Kosmos.volumeDialogVisibilityRepository by Kosmos.Fixture { VolumeDialogVisibilityRepository() }
+var Kosmos.volumeDialogVisibilityRepository by Kosmos.Fixture { VolumeDialogVisibilityRepository() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogCallbacksInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogCallbacksInteractorKosmos.kt
index db9c48d9be6f..8f122b57e9d4 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogCallbacksInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogCallbacksInteractorKosmos.kt
@@ -16,8 +16,6 @@
package com.android.systemui.volume.dialog.domain.interactor
-import android.os.Handler
-import android.os.looper
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.applicationCoroutineScope
import com.android.systemui.plugins.volumeDialogController
@@ -27,6 +25,6 @@ val Kosmos.volumeDialogCallbacksInteractor: VolumeDialogCallbacksInteractor by
VolumeDialogCallbacksInteractor(
volumeDialogController = volumeDialogController,
coroutineScope = applicationCoroutineScope,
- bgHandler = Handler(looper),
+ bgHandler = null,
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/VolumeDialogRingerViewBinderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/VolumeDialogRingerViewBinderKosmos.kt
new file mode 100644
index 000000000000..7cbdc3d9f6ee
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/VolumeDialogRingerViewBinderKosmos.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume.dialog.ringer
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.volume.dialog.ringer.ui.binder.VolumeDialogRingerViewBinder
+import com.android.systemui.volume.dialog.ringer.ui.viewmodel.volumeDialogRingerDrawerViewModel
+
+val Kosmos.volumeDialogRingerViewBinder by
+ Kosmos.Fixture { VolumeDialogRingerViewBinder(volumeDialogRingerDrawerViewModel) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/data/repository/VolumeDialogRingerFeedbackRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/data/repository/VolumeDialogRingerFeedbackRepositoryKosmos.kt
index 44371b4615df..cf357b498621 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/data/repository/VolumeDialogRingerFeedbackRepositoryKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/data/repository/VolumeDialogRingerFeedbackRepositoryKosmos.kt
@@ -20,5 +20,5 @@ import com.android.systemui.kosmos.Kosmos
val Kosmos.fakeVolumeDialogRingerFeedbackRepository by
Kosmos.Fixture { FakeVolumeDialogRingerFeedbackRepository() }
-val Kosmos.volumeDialogRingerFeedbackRepository by
+var Kosmos.volumeDialogRingerFeedbackRepository: VolumeDialogRingerFeedbackRepository by
Kosmos.Fixture { fakeVolumeDialogRingerFeedbackRepository }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/domain/VolumeDialogRingerInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/domain/VolumeDialogRingerInteractorKosmos.kt
index a494d04ec741..4bebf8911613 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/domain/VolumeDialogRingerInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/domain/VolumeDialogRingerInteractorKosmos.kt
@@ -21,7 +21,7 @@ import com.android.systemui.kosmos.applicationCoroutineScope
import com.android.systemui.plugins.volumeDialogController
import com.android.systemui.volume.data.repository.audioSystemRepository
import com.android.systemui.volume.dialog.domain.interactor.volumeDialogStateInteractor
-import com.android.systemui.volume.dialog.ringer.data.repository.fakeVolumeDialogRingerFeedbackRepository
+import com.android.systemui.volume.dialog.ringer.data.repository.volumeDialogRingerFeedbackRepository
val Kosmos.volumeDialogRingerInteractor by
Kosmos.Fixture {
@@ -30,6 +30,6 @@ val Kosmos.volumeDialogRingerInteractor by
volumeDialogStateInteractor = volumeDialogStateInteractor,
controller = volumeDialogController,
audioSystemRepository = audioSystemRepository,
- ringerFeedbackRepository = fakeVolumeDialogRingerFeedbackRepository,
+ ringerFeedbackRepository = volumeDialogRingerFeedbackRepository,
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/settings/domain/VolumeDialogSettingsButtonInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/settings/domain/VolumeDialogSettingsButtonInteractorKosmos.kt
new file mode 100644
index 000000000000..26b8bca6344b
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/settings/domain/VolumeDialogSettingsButtonInteractorKosmos.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume.dialog.settings.domain
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.statusbar.policy.deviceProvisionedController
+import com.android.systemui.volume.dialog.domain.interactor.volumeDialogVisibilityInteractor
+import com.android.systemui.volume.panel.domain.interactor.volumePanelGlobalStateInteractor
+
+val Kosmos.volumeDialogSettingsButtonInteractor by
+ Kosmos.Fixture {
+ VolumeDialogSettingsButtonInteractor(
+ applicationCoroutineScope,
+ deviceProvisionedController,
+ volumePanelGlobalStateInteractor,
+ volumeDialogVisibilityInteractor,
+ )
+ }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/settings/ui/binder/VolumeDialogSettingsButtonViewBinderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/settings/ui/binder/VolumeDialogSettingsButtonViewBinderKosmos.kt
new file mode 100644
index 000000000000..f9e128ddd810
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/settings/ui/binder/VolumeDialogSettingsButtonViewBinderKosmos.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume.dialog.settings.ui.binder
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.volume.dialog.settings.ui.viewmodel.volumeDialogSettingsButtonViewModel
+
+val Kosmos.volumeDialogSettingsButtonViewBinder by
+ Kosmos.Fixture { VolumeDialogSettingsButtonViewBinder(volumeDialogSettingsButtonViewModel) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/settings/ui/viewmodel/VolumeDialogSettingsButtonViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/settings/ui/viewmodel/VolumeDialogSettingsButtonViewModelKosmos.kt
new file mode 100644
index 000000000000..0ae3b037b50a
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/settings/ui/viewmodel/VolumeDialogSettingsButtonViewModelKosmos.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume.dialog.settings.ui.viewmodel
+
+import android.content.applicationContext
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.volume.dialog.settings.domain.volumeDialogSettingsButtonInteractor
+import com.android.systemui.volume.mediaDeviceSessionInteractor
+import com.android.systemui.volume.mediaOutputInteractor
+
+val Kosmos.volumeDialogSettingsButtonViewModel by
+ Kosmos.Fixture {
+ VolumeDialogSettingsButtonViewModel(
+ applicationContext,
+ testScope.testScheduler,
+ applicationCoroutineScope,
+ mediaOutputInteractor,
+ mediaDeviceSessionInteractor,
+ volumeDialogSettingsButtonInteractor,
+ )
+ }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/dagger/VolumeDialogSliderComponentKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/dagger/VolumeDialogSliderComponentKosmos.kt
new file mode 100644
index 000000000000..4f79f7b4b41a
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/dagger/VolumeDialogSliderComponentKosmos.kt
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume.dialog.sliders.dagger
+
+import android.content.applicationContext
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.plugins.volumeDialogController
+import com.android.systemui.statusbar.policy.data.repository.zenModeRepository
+import com.android.systemui.volume.data.repository.audioRepository
+import com.android.systemui.volume.dialog.data.repository.volumeDialogVisibilityRepository
+import com.android.systemui.volume.dialog.sliders.domain.model.VolumeDialogSliderType
+import com.android.systemui.volume.dialog.sliders.domain.model.volumeDialogSliderType
+import com.android.systemui.volume.dialog.sliders.ui.VolumeDialogOverscrollViewBinder
+import com.android.systemui.volume.dialog.sliders.ui.VolumeDialogSliderHapticsViewBinder
+import com.android.systemui.volume.dialog.sliders.ui.VolumeDialogSliderViewBinder
+import com.android.systemui.volume.dialog.sliders.ui.volumeDialogOverscrollViewBinder
+import com.android.systemui.volume.dialog.sliders.ui.volumeDialogSliderHapticsViewBinder
+import com.android.systemui.volume.dialog.sliders.ui.volumeDialogSliderViewBinder
+import com.android.systemui.volume.mediaControllerRepository
+import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.mediaControllerInteractor
+
+private val Kosmos.mutableSliderComponentKosmoses: MutableMap<VolumeDialogSliderType, Kosmos> by
+ Kosmos.Fixture { mutableMapOf() }
+
+val Kosmos.volumeDialogSliderComponentFactory by
+ Kosmos.Fixture {
+ object : VolumeDialogSliderComponent.Factory {
+ override fun create(sliderType: VolumeDialogSliderType): VolumeDialogSliderComponent =
+ volumeDialogSliderComponent(sliderType)
+ }
+ }
+
+fun Kosmos.volumeDialogSliderComponent(type: VolumeDialogSliderType): VolumeDialogSliderComponent {
+ return object : VolumeDialogSliderComponent {
+
+ private val localKosmos
+ get() =
+ mutableSliderComponentKosmoses.getOrPut(type) {
+ Kosmos().also {
+ it.setupVolumeDialogSliderComponent(this@volumeDialogSliderComponent, type)
+ }
+ }
+
+ override fun sliderViewBinder(): VolumeDialogSliderViewBinder =
+ localKosmos.volumeDialogSliderViewBinder
+
+ override fun sliderHapticsViewBinder(): VolumeDialogSliderHapticsViewBinder =
+ localKosmos.volumeDialogSliderHapticsViewBinder
+
+ override fun overscrollViewBinder(): VolumeDialogOverscrollViewBinder =
+ localKosmos.volumeDialogOverscrollViewBinder
+ }
+}
+
+private fun Kosmos.setupVolumeDialogSliderComponent(
+ parentKosmos: Kosmos,
+ type: VolumeDialogSliderType,
+) {
+ volumeDialogSliderType = type
+ applicationContext = parentKosmos.applicationContext
+ testScope = parentKosmos.testScope
+
+ volumeDialogController = parentKosmos.volumeDialogController
+ mediaControllerInteractor = parentKosmos.mediaControllerInteractor
+ mediaControllerRepository = parentKosmos.mediaControllerRepository
+ zenModeRepository = parentKosmos.zenModeRepository
+ volumeDialogVisibilityRepository = parentKosmos.volumeDialogVisibilityRepository
+ audioRepository = parentKosmos.audioRepository
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogOverscrollViewBinderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogOverscrollViewBinderKosmos.kt
new file mode 100644
index 000000000000..13d6ca9732d1
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogOverscrollViewBinderKosmos.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume.dialog.sliders.ui
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.volume.dialog.sliders.ui.viewmodel.volumeDialogOverscrollViewModel
+
+val Kosmos.volumeDialogOverscrollViewBinder by
+ Kosmos.Fixture { VolumeDialogOverscrollViewBinder(volumeDialogOverscrollViewModel) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderHapticsViewBinderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderHapticsViewBinderKosmos.kt
new file mode 100644
index 000000000000..d6845b1ff7e3
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderHapticsViewBinderKosmos.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume.dialog.sliders.ui
+
+import com.android.systemui.haptics.msdl.msdlPlayer
+import com.android.systemui.haptics.vibratorHelper
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.util.time.systemClock
+import com.android.systemui.volume.dialog.sliders.ui.viewmodel.volumeDialogSliderInputEventsViewModel
+
+val Kosmos.volumeDialogSliderHapticsViewBinder by
+ Kosmos.Fixture {
+ VolumeDialogSliderHapticsViewBinder(
+ volumeDialogSliderInputEventsViewModel,
+ vibratorHelper,
+ msdlPlayer,
+ systemClock,
+ )
+ }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinderKosmos.kt
new file mode 100644
index 000000000000..c6db717e004f
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinderKosmos.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume.dialog.sliders.ui
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.volume.dialog.sliders.ui.viewmodel.volumeDialogSliderInputEventsViewModel
+import com.android.systemui.volume.dialog.sliders.ui.viewmodel.volumeDialogSliderViewModel
+
+val Kosmos.volumeDialogSliderViewBinder by
+ Kosmos.Fixture {
+ VolumeDialogSliderViewBinder(
+ volumeDialogSliderViewModel,
+ volumeDialogSliderInputEventsViewModel,
+ )
+ }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSlidersViewBinderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSlidersViewBinderKosmos.kt
new file mode 100644
index 000000000000..83527d994e70
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSlidersViewBinderKosmos.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume.dialog.sliders.ui
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.volume.dialog.sliders.ui.viewmodel.volumeDialogSlidersViewModel
+
+val Kosmos.volumeDialogSlidersViewBinder by
+ Kosmos.Fixture { VolumeDialogSlidersViewBinder(volumeDialogSlidersViewModel) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogOverscrollViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogOverscrollViewModelKosmos.kt
new file mode 100644
index 000000000000..fe2f3d806b6a
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogOverscrollViewModelKosmos.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume.dialog.sliders.ui.viewmodel
+
+import android.content.applicationContext
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.volume.dialog.sliders.domain.interactor.volumeDialogSliderInputEventsInteractor
+
+val Kosmos.volumeDialogOverscrollViewModel by
+ Kosmos.Fixture {
+ VolumeDialogOverscrollViewModel(applicationContext, volumeDialogSliderInputEventsInteractor)
+ }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderIconProviderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderIconProviderKosmos.kt
new file mode 100644
index 000000000000..09f9f1c6362e
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderIconProviderKosmos.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume.dialog.sliders.ui.viewmodel
+
+import android.content.applicationContext
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.statusbar.policy.domain.interactor.zenModeInteractor
+import com.android.systemui.volume.domain.interactor.audioVolumeInteractor
+
+val Kosmos.volumeDialogSliderIconProvider by
+ Kosmos.Fixture {
+ VolumeDialogSliderIconProvider(
+ context = applicationContext,
+ audioVolumeInteractor = audioVolumeInteractor,
+ zenModeInteractor = zenModeInteractor,
+ )
+ }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderInputEventsViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderInputEventsViewModelKosmos.kt
new file mode 100644
index 000000000000..2de0e8f76a4b
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderInputEventsViewModelKosmos.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume.dialog.sliders.ui.viewmodel
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.volume.dialog.sliders.domain.interactor.volumeDialogSliderInputEventsInteractor
+
+val Kosmos.volumeDialogSliderInputEventsViewModel by
+ Kosmos.Fixture {
+ VolumeDialogSliderInputEventsViewModel(
+ applicationCoroutineScope,
+ volumeDialogSliderInputEventsInteractor,
+ )
+ }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderViewModelKosmos.kt
new file mode 100644
index 000000000000..63cd440a8633
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderViewModelKosmos.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume.dialog.sliders.ui.viewmodel
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.util.time.systemClock
+import com.android.systemui.volume.dialog.domain.interactor.volumeDialogVisibilityInteractor
+import com.android.systemui.volume.dialog.sliders.domain.interactor.volumeDialogSliderInteractor
+
+val Kosmos.volumeDialogSliderViewModel by
+ Kosmos.Fixture {
+ VolumeDialogSliderViewModel(
+ interactor = volumeDialogSliderInteractor,
+ visibilityInteractor = volumeDialogVisibilityInteractor,
+ coroutineScope = applicationCoroutineScope,
+ volumeDialogSliderIconProvider = volumeDialogSliderIconProvider,
+ systemClock = systemClock,
+ )
+ }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSlidersViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSlidersViewModelKosmos.kt
new file mode 100644
index 000000000000..5531f7608b69
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSlidersViewModelKosmos.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume.dialog.sliders.ui.viewmodel
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.volume.dialog.sliders.dagger.volumeDialogSliderComponentFactory
+import com.android.systemui.volume.dialog.sliders.domain.interactor.volumeDialogSlidersInteractor
+
+val Kosmos.volumeDialogSlidersViewModel by
+ Kosmos.Fixture {
+ VolumeDialogSlidersViewModel(
+ applicationCoroutineScope,
+ volumeDialogSlidersInteractor,
+ volumeDialogSliderComponentFactory,
+ )
+ }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogViewBinderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogViewBinderKosmos.kt
new file mode 100644
index 000000000000..dc09e3233b1e
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogViewBinderKosmos.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume.dialog.ui.binder
+
+import android.content.applicationContext
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.volume.dialog.ringer.volumeDialogRingerViewBinder
+import com.android.systemui.volume.dialog.settings.ui.binder.volumeDialogSettingsButtonViewBinder
+import com.android.systemui.volume.dialog.sliders.ui.volumeDialogSlidersViewBinder
+import com.android.systemui.volume.dialog.ui.utils.jankListenerFactory
+import com.android.systemui.volume.dialog.ui.viewmodel.volumeDialogViewModel
+import com.android.systemui.volume.dialog.utils.volumeTracer
+
+val Kosmos.volumeDialogViewBinder by
+ Kosmos.Fixture {
+ VolumeDialogViewBinder(
+ applicationContext.resources,
+ volumeDialogViewModel,
+ jankListenerFactory,
+ volumeTracer,
+ volumeDialogRingerViewBinder,
+ volumeDialogSlidersViewBinder,
+ volumeDialogSettingsButtonViewBinder,
+ )
+ }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ui/utils/JankListenerFactoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ui/utils/JankListenerFactoryKosmos.kt
new file mode 100644
index 000000000000..35ec5d3cc9af
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ui/utils/JankListenerFactoryKosmos.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume.dialog.ui.utils
+
+import com.android.systemui.jank.interactionJankMonitor
+import com.android.systemui.kosmos.Kosmos
+
+val Kosmos.jankListenerFactory by Kosmos.Fixture { JankListenerFactory(interactionJankMonitor) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ui/viewmodel/VolumeDialogViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ui/viewmodel/VolumeDialogViewModelKosmos.kt
new file mode 100644
index 000000000000..05ef462d4998
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ui/viewmodel/VolumeDialogViewModelKosmos.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume.dialog.ui.viewmodel
+
+import android.content.applicationContext
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.statusbar.policy.configurationController
+import com.android.systemui.statusbar.policy.devicePostureController
+import com.android.systemui.volume.dialog.domain.interactor.volumeDialogStateInteractor
+import com.android.systemui.volume.dialog.domain.interactor.volumeDialogVisibilityInteractor
+import com.android.systemui.volume.dialog.sliders.domain.interactor.volumeDialogSlidersInteractor
+
+val Kosmos.volumeDialogViewModel by
+ Kosmos.Fixture {
+ VolumeDialogViewModel(
+ applicationContext,
+ volumeDialogVisibilityInteractor,
+ volumeDialogSlidersInteractor,
+ volumeDialogStateInteractor,
+ devicePostureController,
+ configurationController,
+ )
+ }
diff --git a/packages/Vcn/service-b/Android.bp b/packages/Vcn/service-b/Android.bp
index 1370b0678cc5..97574e6e35e3 100644
--- a/packages/Vcn/service-b/Android.bp
+++ b/packages/Vcn/service-b/Android.bp
@@ -39,9 +39,7 @@ java_library {
name: "connectivity-utils-service-vcn-internal",
sdk_version: "module_current",
min_sdk_version: "30",
- srcs: [
- ":framework-connectivity-shared-srcs",
- ],
+ srcs: ["service-utils/**/*.java"],
libs: [
"framework-annotations-lib",
"unsupportedappusage",
diff --git a/packages/Vcn/service-b/service-utils/android/util/LocalLog.java b/packages/Vcn/service-b/service-utils/android/util/LocalLog.java
new file mode 100644
index 000000000000..5955d930aab1
--- /dev/null
+++ b/packages/Vcn/service-b/service-utils/android/util/LocalLog.java
@@ -0,0 +1,148 @@
+/*
+ * 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 android.util;
+
+import android.compat.annotation.UnsupportedAppUsage;
+import android.os.Build;
+import android.os.SystemClock;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.time.Duration;
+import java.time.Instant;
+import java.time.LocalDateTime;
+import java.util.ArrayDeque;
+import java.util.Deque;
+import java.util.Iterator;
+
+/**
+ * @hide
+ */
+// Exported to Mainline modules; cannot use annotations
+// @android.ravenwood.annotation.RavenwoodKeepWholeClass
+// TODO: b/374174952 This is an exact copy of frameworks/base/core/java/android/util/LocalLog.java.
+// This file is only used in "service-connectivity-b-platform" before the VCN modularization flag
+// is fully ramped. When the flag is fully ramped and the development is finalized, this file can
+// be removed.
+public final class LocalLog {
+
+ private final Deque<String> mLog;
+ private final int mMaxLines;
+
+ /**
+ * {@code true} to use log timestamps expressed in local date/time, {@code false} to use log
+ * timestamped expressed with the elapsed realtime clock and UTC system clock. {@code false} is
+ * useful when logging behavior that modifies device time zone or system clock.
+ */
+ private final boolean mUseLocalTimestamps;
+
+ @UnsupportedAppUsage
+ public LocalLog(int maxLines) {
+ this(maxLines, true /* useLocalTimestamps */);
+ }
+
+ public LocalLog(int maxLines, boolean useLocalTimestamps) {
+ mMaxLines = Math.max(0, maxLines);
+ mLog = new ArrayDeque<>(mMaxLines);
+ mUseLocalTimestamps = useLocalTimestamps;
+ }
+
+ @UnsupportedAppUsage
+ public void log(String msg) {
+ if (mMaxLines <= 0) {
+ return;
+ }
+ final String logLine;
+ if (mUseLocalTimestamps) {
+ logLine = LocalDateTime.now() + " - " + msg;
+ } else {
+ logLine = Duration.ofMillis(SystemClock.elapsedRealtime())
+ + " / " + Instant.now() + " - " + msg;
+ }
+ append(logLine);
+ }
+
+ private synchronized void append(String logLine) {
+ while (mLog.size() >= mMaxLines) {
+ mLog.remove();
+ }
+ mLog.add(logLine);
+ }
+
+ @UnsupportedAppUsage
+ public synchronized void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+ dump(pw);
+ }
+
+ public synchronized void dump(PrintWriter pw) {
+ dump("", pw);
+ }
+
+ /**
+ * Dumps the content of local log to print writer with each log entry predeced with indent
+ *
+ * @param indent indent that precedes each log entry
+ * @param pw printer writer to write into
+ */
+ public synchronized void dump(String indent, PrintWriter pw) {
+ Iterator<String> itr = mLog.iterator();
+ while (itr.hasNext()) {
+ pw.printf("%s%s\n", indent, itr.next());
+ }
+ }
+
+ public synchronized void reverseDump(FileDescriptor fd, PrintWriter pw, String[] args) {
+ reverseDump(pw);
+ }
+
+ public synchronized void reverseDump(PrintWriter pw) {
+ Iterator<String> itr = mLog.descendingIterator();
+ while (itr.hasNext()) {
+ pw.println(itr.next());
+ }
+ }
+
+ // @VisibleForTesting(otherwise = VisibleForTesting.NONE)
+ public synchronized void clear() {
+ mLog.clear();
+ }
+
+ public static class ReadOnlyLocalLog {
+ private final LocalLog mLog;
+ ReadOnlyLocalLog(LocalLog log) {
+ mLog = log;
+ }
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+ mLog.dump(pw);
+ }
+ public void dump(PrintWriter pw) {
+ mLog.dump(pw);
+ }
+ public void reverseDump(FileDescriptor fd, PrintWriter pw, String[] args) {
+ mLog.reverseDump(pw);
+ }
+ public void reverseDump(PrintWriter pw) {
+ mLog.reverseDump(pw);
+ }
+ }
+
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ public ReadOnlyLocalLog readOnlyLocalLog() {
+ return new ReadOnlyLocalLog(this);
+ }
+} \ No newline at end of file
diff --git a/packages/Vcn/service-b/service-utils/com/android/internal/util/WakeupMessage.java b/packages/Vcn/service-b/service-utils/com/android/internal/util/WakeupMessage.java
new file mode 100644
index 000000000000..7db62f8e9ffc
--- /dev/null
+++ b/packages/Vcn/service-b/service-utils/com/android/internal/util/WakeupMessage.java
@@ -0,0 +1,142 @@
+/*
+ * 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.internal.util;
+
+import android.app.AlarmManager;
+import android.content.Context;
+import android.os.Handler;
+import android.os.Message;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+ /**
+ * An AlarmListener that sends the specified message to a Handler and keeps the system awake until
+ * the message is processed.
+ *
+ * This is useful when using the AlarmManager direct callback interface to wake up the system and
+ * request that an object whose API consists of messages (such as a StateMachine) perform some
+ * action.
+ *
+ * In this situation, using AlarmManager.onAlarmListener by itself will wake up the system to send
+ * the message, but does not guarantee that the system will be awake until the target object has
+ * processed it. This is because as soon as the onAlarmListener sends the message and returns, the
+ * AlarmManager releases its wakelock and the system is free to go to sleep again.
+ */
+// TODO: b/374174952 This is an exact copy of
+// frameworks/base/core/java/com/android/internal/util/WakeupMessage.java.
+// This file is only used in "service-connectivity-b-platform" before the VCN modularization flag
+// is fully ramped. When the flag is fully ramped and the development is finalized, this file can
+// be removed.
+public class WakeupMessage implements AlarmManager.OnAlarmListener {
+ private final AlarmManager mAlarmManager;
+
+ @VisibleForTesting
+ protected final Handler mHandler;
+ @VisibleForTesting
+ protected final String mCmdName;
+ @VisibleForTesting
+ protected final int mCmd, mArg1, mArg2;
+ @VisibleForTesting
+ protected final Object mObj;
+ private final Runnable mRunnable;
+ private boolean mScheduled;
+
+ public WakeupMessage(Context context, Handler handler,
+ String cmdName, int cmd, int arg1, int arg2, Object obj) {
+ mAlarmManager = getAlarmManager(context);
+ mHandler = handler;
+ mCmdName = cmdName;
+ mCmd = cmd;
+ mArg1 = arg1;
+ mArg2 = arg2;
+ mObj = obj;
+ mRunnable = null;
+ }
+
+ public WakeupMessage(Context context, Handler handler, String cmdName, int cmd, int arg1) {
+ this(context, handler, cmdName, cmd, arg1, 0, null);
+ }
+
+ public WakeupMessage(Context context, Handler handler,
+ String cmdName, int cmd, int arg1, int arg2) {
+ this(context, handler, cmdName, cmd, arg1, arg2, null);
+ }
+
+ public WakeupMessage(Context context, Handler handler, String cmdName, int cmd) {
+ this(context, handler, cmdName, cmd, 0, 0, null);
+ }
+
+ public WakeupMessage(Context context, Handler handler, String cmdName, Runnable runnable) {
+ mAlarmManager = getAlarmManager(context);
+ mHandler = handler;
+ mCmdName = cmdName;
+ mCmd = 0;
+ mArg1 = 0;
+ mArg2 = 0;
+ mObj = null;
+ mRunnable = runnable;
+ }
+
+ private static AlarmManager getAlarmManager(Context context) {
+ return (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
+ }
+
+ /**
+ * Schedule the message to be delivered at the time in milliseconds of the
+ * {@link android.os.SystemClock#elapsedRealtime SystemClock.elapsedRealtime()} clock and wakeup
+ * the device when it goes off. If schedule is called multiple times without the message being
+ * dispatched then the alarm is rescheduled to the new time.
+ */
+ public synchronized void schedule(long when) {
+ mAlarmManager.setExact(
+ AlarmManager.ELAPSED_REALTIME_WAKEUP, when, mCmdName, this, mHandler);
+ mScheduled = true;
+ }
+
+ /**
+ * Cancel all pending messages. This includes alarms that may have been fired, but have not been
+ * run on the handler yet.
+ */
+ public synchronized void cancel() {
+ if (mScheduled) {
+ mAlarmManager.cancel(this);
+ mScheduled = false;
+ }
+ }
+
+ @Override
+ public void onAlarm() {
+ // Once this method is called the alarm has already been fired and removed from
+ // AlarmManager (it is still partially tracked, but only for statistics). The alarm can now
+ // be marked as unscheduled so that it can be rescheduled in the message handler.
+ final boolean stillScheduled;
+ synchronized (this) {
+ stillScheduled = mScheduled;
+ mScheduled = false;
+ }
+ if (stillScheduled) {
+ Message msg;
+ if (mRunnable == null) {
+ msg = mHandler.obtainMessage(mCmd, mArg1, mArg2, mObj);
+ } else {
+ msg = Message.obtain(mHandler, mRunnable);
+ }
+ mHandler.dispatchMessage(msg);
+ msg.recycle();
+ }
+ }
+} \ No newline at end of file
diff --git a/packages/Vcn/service-b/service-vcn-platform-jarjar-rules.txt b/packages/Vcn/service-b/service-vcn-platform-jarjar-rules.txt
index 36307277b4b9..6ec39d953266 100644
--- a/packages/Vcn/service-b/service-vcn-platform-jarjar-rules.txt
+++ b/packages/Vcn/service-b/service-vcn-platform-jarjar-rules.txt
@@ -1,5 +1,2 @@
-rule android.util.IndentingPrintWriter android.net.vcn.module.repackaged.android.util.IndentingPrintWriter
rule android.util.LocalLog android.net.vcn.module.repackaged.android.util.LocalLog
-rule com.android.internal.util.IndentingPrintWriter android.net.vcn.module.repackaged.com.android.internal.util.IndentingPrintWriter
-rule com.android.internal.util.MessageUtils android.net.vcn.module.repackaged.com.android.internal.util.MessageUtils
rule com.android.internal.util.WakeupMessage android.net.vcn.module.repackaged.com.android.internal.util.WakeupMessage \ No newline at end of file
diff --git a/ravenwood/Android.bp b/ravenwood/Android.bp
index 59043a8356ae..8e998426685b 100644
--- a/ravenwood/Android.bp
+++ b/ravenwood/Android.bp
@@ -345,14 +345,41 @@ java_library {
],
}
+// We define our own version of platform_compat_config's here, because:
+// - The original version (e.g. "framework-platform-compat-config) is built from
+// the output file of the device side jar, rather than the host jar, meaning
+// they're slow to build because they depend on D8/R8 output.
+// - The original services one ("services-platform-compat-config") is built from services.jar,
+// which includes service.permission, which is very slow to rebuild because of kotlin.
+//
+// Because we're re-defining the same compat-IDs that are defined elsewhere,
+// they should all have `include_in_merged_xml: false`. Otherwise, generating
+// merged_compat_config.xml would fail due to duplicate IDs.
+//
+// These module names must end with "compat-config" because these will be used as the filename,
+// and at runtime, we only loads files that match `*compat-config.xml`.
+platform_compat_config {
+ name: "ravenwood-framework-platform-compat-config",
+ src: ":framework-minus-apex-for-host",
+ include_in_merged_xml: false,
+ visibility: ["//visibility:private"],
+}
+
+platform_compat_config {
+ name: "ravenwood-services.core-platform-compat-config",
+ src: ":services.core-for-host",
+ include_in_merged_xml: false,
+ visibility: ["//visibility:private"],
+}
+
filegroup {
name: "ravenwood-data",
device_common_srcs: [
":system-build.prop",
":framework-res",
":ravenwood-empty-res",
- ":framework-platform-compat-config",
- ":services-platform-compat-config",
+ ":ravenwood-framework-platform-compat-config",
+ ":ravenwood-services.core-platform-compat-config",
"texts/ravenwood-build.prop",
],
device_first_srcs: [
@@ -616,6 +643,10 @@ android_ravenwood_libgroup {
"android.test.mock.ravenwood",
"ravenwood-helper-runtime",
"hoststubgen-helper-runtime.ravenwood",
+
+ // Note, when we include other services.* jars, we'll need to add
+ // platform_compat_config for that module too.
+ // See ravenwood-services.core-platform-compat-config above.
"services.core.ravenwood-jarjar",
"services.fakes.ravenwood-jarjar",
diff --git a/ravenwood/CleanSpec.mk b/ravenwood/CleanSpec.mk
new file mode 100644
index 000000000000..50d2fab40326
--- /dev/null
+++ b/ravenwood/CleanSpec.mk
@@ -0,0 +1,45 @@
+# Copyright (C) 2024 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+# If you don't need to do a full clean build but would like to touch
+# a file or delete some intermediate files, add a clean step to the end
+# of the list. These steps will only be run once, if they haven't been
+# run before.
+#
+# E.g.:
+# $(call add-clean-step, touch -c external/sqlite/sqlite3.h)
+# $(call add-clean-step, rm -rf $(PRODUCT_OUT)/obj/STATIC_LIBRARIES/libz_intermediates)
+#
+# Always use "touch -c" and "rm -f" or "rm -rf" to gracefully deal with
+# files that are missing or have been moved.
+#
+# Use $(PRODUCT_OUT) to get to the "out/target/product/blah/" directory.
+# Use $(OUT_DIR) to refer to the "out" directory.
+#
+# If you need to re-do something that's already mentioned, just copy
+# the command and add it to the bottom of the list. E.g., if a change
+# that you made last week required touching a file and a change you
+# made today requires touching the same file, just copy the old
+# touch step and add it to the end of the list.
+#
+# *****************************************************************
+# NEWER CLEAN STEPS MUST BE AT THE END OF THE LIST ABOVE THE BANNER
+# *****************************************************************
+
+$(call add-clean-step, rm -rf $(OUT_DIR)/host/linux-x86/testcases/ravenwood-runtime)
+
+# ******************************************************************
+# NEWER CLEAN STEPS MUST BE AT THE END OF THE LIST ABOVE THIS BANNER
+# ******************************************************************
diff --git a/ravenwood/tools/hoststubgen/scripts/dump-jar b/ravenwood/tools/hoststubgen/scripts/dump-jar
index 87652451359d..998357b70dff 100755
--- a/ravenwood/tools/hoststubgen/scripts/dump-jar
+++ b/ravenwood/tools/hoststubgen/scripts/dump-jar
@@ -89,7 +89,7 @@ filter_output() {
# - Some other transient lines
# - Sometimes the javap shows mysterious warnings, so remove them too.
#
- # `/PATTERN-1/,/PATTERN-1/{//!d}` is a trick to delete lines between two patterns, without
+ # `/PATTERN-1/,/PATTERN-2/{//!d}` is a trick to delete lines between two patterns, without
# the start and the end lines.
sed -e 's/#[0-9][0-9]*/#x/g' \
-e 's/^\( *\)[0-9][0-9]*:/\1x:/' \
diff --git a/ravenwood/tools/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt b/ravenwood/tools/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt
index 6d8d7b768b91..cc704b2b32ed 100644
--- a/ravenwood/tools/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt
+++ b/ravenwood/tools/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt
@@ -27,7 +27,7 @@ import com.android.hoststubgen.filters.ImplicitOutputFilter
import com.android.hoststubgen.filters.KeepNativeFilter
import com.android.hoststubgen.filters.OutputFilter
import com.android.hoststubgen.filters.SanitizationFilter
-import com.android.hoststubgen.filters.TextFileFilterPolicyParser
+import com.android.hoststubgen.filters.TextFileFilterPolicyBuilder
import com.android.hoststubgen.filters.printAsTextPolicy
import com.android.hoststubgen.utils.ClassFilter
import com.android.hoststubgen.visitors.BaseAdapter
@@ -179,9 +179,9 @@ class HostStubGen(val options: HostStubGenOptions) {
// Next, "text based" filter, which allows to override polices without touching
// the target code.
if (options.policyOverrideFiles.isNotEmpty()) {
- val parser = TextFileFilterPolicyParser(allClasses, filter)
- options.policyOverrideFiles.forEach(parser::parse)
- filter = parser.createOutputFilter()
+ val builder = TextFileFilterPolicyBuilder(allClasses, filter)
+ options.policyOverrideFiles.forEach(builder::parse)
+ filter = builder.createOutputFilter()
}
// Apply the implicit filter.
diff --git a/ravenwood/tools/hoststubgen/src/com/android/hoststubgen/filters/TextFileFilterPolicyParser.kt b/ravenwood/tools/hoststubgen/src/com/android/hoststubgen/filters/TextFileFilterPolicyParser.kt
index 7462a8ce12c5..be1b6ca93869 100644
--- a/ravenwood/tools/hoststubgen/src/com/android/hoststubgen/filters/TextFileFilterPolicyParser.kt
+++ b/ravenwood/tools/hoststubgen/src/com/android/hoststubgen/filters/TextFileFilterPolicyParser.kt
@@ -23,10 +23,12 @@ import com.android.hoststubgen.asm.toJvmClassName
import com.android.hoststubgen.log
import com.android.hoststubgen.normalizeTextLine
import com.android.hoststubgen.whitespaceRegex
-import java.io.File
+import org.objectweb.asm.tree.ClassNode
+import java.io.BufferedReader
+import java.io.FileReader
import java.io.PrintWriter
+import java.io.Reader
import java.util.regex.Pattern
-import org.objectweb.asm.tree.ClassNode
/**
* Print a class node as a "keep" policy.
@@ -48,7 +50,7 @@ fun printAsTextPolicy(pw: PrintWriter, cn: ClassNode) {
private const val FILTER_REASON = "file-override"
-private enum class SpecialClass {
+enum class SpecialClass {
NotSpecial,
Aidl,
FeatureFlags,
@@ -56,10 +58,58 @@ private enum class SpecialClass {
RFile,
}
-class TextFileFilterPolicyParser(
+/**
+ * This receives [TextFileFilterPolicyBuilder] parsing result.
+ */
+interface PolicyFileProcessor {
+ /** "package" directive. */
+ fun onPackage(name: String, policy: FilterPolicyWithReason)
+
+ /** "rename" directive. */
+ fun onRename(pattern: Pattern, prefix: String)
+
+ /** "class" directive. */
+ fun onSimpleClassStart(className: String)
+ fun onSimpleClassPolicy(className: String, policy: FilterPolicyWithReason)
+ fun onSimpleClassEnd(className: String)
+
+ fun onSubClassPolicy(superClassName: String, policy: FilterPolicyWithReason)
+ fun onRedirectionClass(fromClassName: String, toClassName: String)
+ fun onClassLoadHook(className: String, callback: String)
+ fun onSpecialClassPolicy(type: SpecialClass, policy: FilterPolicyWithReason)
+
+ /** "field" directive. */
+ fun onField(className: String, fieldName: String, policy: FilterPolicyWithReason)
+
+ /** "method" directive. */
+ fun onSimpleMethodPolicy(
+ className: String,
+ methodName: String,
+ methodDesc: String,
+ policy: FilterPolicyWithReason,
+ )
+ fun onMethodInClassReplace(
+ className: String,
+ methodName: String,
+ methodDesc: String,
+ targetName: String,
+ policy: FilterPolicyWithReason,
+ )
+ fun onMethodOutClassReplace(
+ className: String,
+ methodName: String,
+ methodDesc: String,
+ replaceSpec: TextFilePolicyMethodReplaceFilter.MethodCallReplaceSpec,
+ policy: FilterPolicyWithReason,
+ )
+}
+
+class TextFileFilterPolicyBuilder(
private val classes: ClassNodes,
fallback: OutputFilter
) {
+ private val parser = TextFileFilterPolicyParser()
+
private val subclassFilter = SubclassFilter(classes, fallback)
private val packageFilter = PackageFilter(subclassFilter)
private val imf = InMemoryOutputFilter(classes, packageFilter)
@@ -71,30 +121,19 @@ class TextFileFilterPolicyParser(
private val methodReplaceSpec =
mutableListOf<TextFilePolicyMethodReplaceFilter.MethodCallReplaceSpec>()
- private lateinit var currentClassName: String
-
/**
- * Read a given "policy" file and return as an [OutputFilter]
+ * Parse a given policy file. This method can be called multiple times to read from
+ * multiple files. To get the resulting filter, use [createOutputFilter]
*/
fun parse(file: String) {
- log.i("Loading offloaded annotations from $file ...")
- log.withIndent {
- var lineNo = 0
- try {
- File(file).forEachLine {
- lineNo++
- val line = normalizeTextLine(it)
- if (line.isEmpty()) {
- return@forEachLine // skip empty lines.
- }
- parseLine(line)
- }
- } catch (e: ParseException) {
- throw e.withSourceInfo(file, lineNo)
- }
- }
+ // We may parse multiple files, but we reuse the same parser, because the parser
+ // will make sure there'll be no dupplicating "special class" policies.
+ parser.parse(FileReader(file), file, Processor())
}
+ /**
+ * Generate the resulting [OutputFilter].
+ */
fun createOutputFilter(): OutputFilter {
var ret: OutputFilter = imf
if (typeRenameSpec.isNotEmpty()) {
@@ -112,14 +151,200 @@ class TextFileFilterPolicyParser(
return ret
}
+ private inner class Processor : PolicyFileProcessor {
+ override fun onPackage(name: String, policy: FilterPolicyWithReason) {
+ packageFilter.addPolicy(name, policy)
+ }
+
+ override fun onRename(pattern: Pattern, prefix: String) {
+ typeRenameSpec += TextFilePolicyRemapperFilter.TypeRenameSpec(
+ pattern, prefix
+ )
+ }
+
+ override fun onSimpleClassStart(className: String) {
+ }
+
+ override fun onSimpleClassEnd(className: String) {
+ }
+
+ override fun onSimpleClassPolicy(className: String, policy: FilterPolicyWithReason) {
+ imf.setPolicyForClass(className, policy)
+ }
+
+ override fun onSubClassPolicy(
+ superClassName: String,
+ policy: FilterPolicyWithReason,
+ ) {
+ log.i("class extends $superClassName")
+ subclassFilter.addPolicy( superClassName, policy)
+ }
+
+ override fun onRedirectionClass(fromClassName: String, toClassName: String) {
+ imf.setRedirectionClass(fromClassName, toClassName)
+ }
+
+ override fun onClassLoadHook(className: String, callback: String) {
+ imf.setClassLoadHook(className, callback)
+ }
+
+ override fun onSpecialClassPolicy(
+ type: SpecialClass,
+ policy: FilterPolicyWithReason,
+ ) {
+ log.i("class special $type $policy")
+ when (type) {
+ SpecialClass.NotSpecial -> {} // Shouldn't happen
+
+ SpecialClass.Aidl -> {
+ aidlPolicy = policy
+ }
+
+ SpecialClass.FeatureFlags -> {
+ featureFlagsPolicy = policy
+ }
+
+ SpecialClass.Sysprops -> {
+ syspropsPolicy = policy
+ }
+
+ SpecialClass.RFile -> {
+ rFilePolicy = policy
+ }
+ }
+ }
+
+ override fun onField(className: String, fieldName: String, policy: FilterPolicyWithReason) {
+ imf.setPolicyForField(className, fieldName, policy)
+ }
+
+ override fun onSimpleMethodPolicy(
+ className: String,
+ methodName: String,
+ methodDesc: String,
+ policy: FilterPolicyWithReason,
+ ) {
+ imf.setPolicyForMethod(className, methodName, methodDesc, policy)
+ }
+
+ override fun onMethodInClassReplace(
+ className: String,
+ methodName: String,
+ methodDesc: String,
+ targetName: String,
+ policy: FilterPolicyWithReason,
+ ) {
+ imf.setPolicyForMethod(className, methodName, methodDesc, policy)
+
+ // Make sure to keep the target method.
+ imf.setPolicyForMethod(
+ className,
+ targetName,
+ methodDesc,
+ FilterPolicy.Keep.withReason(FILTER_REASON)
+ )
+ // Set up the rename.
+ imf.setRenameTo(className, targetName, methodDesc, methodName)
+ }
+
+ override fun onMethodOutClassReplace(
+ className: String,
+ methodName: String,
+ methodDesc: String,
+ replaceSpec: TextFilePolicyMethodReplaceFilter.MethodCallReplaceSpec,
+ policy: FilterPolicyWithReason,
+ ) {
+ imf.setPolicyForMethod(className, methodName, methodDesc, policy)
+ methodReplaceSpec.add(replaceSpec)
+ }
+ }
+}
+
+/**
+ * Parses a filer policy text file.
+ */
+class TextFileFilterPolicyParser {
+ private lateinit var processor: PolicyFileProcessor
+ private var currentClassName: String? = null
+
+ private var aidlPolicy: FilterPolicyWithReason? = null
+ private var featureFlagsPolicy: FilterPolicyWithReason? = null
+ private var syspropsPolicy: FilterPolicyWithReason? = null
+ private var rFilePolicy: FilterPolicyWithReason? = null
+
+ /** Name of the file that's currently being processed. */
+ var filename: String? = null
+ private set
+
+ /** 1-based line number in the current file */
+ var lineNumber = -1
+ private set
+
+ /**
+ * Parse a given "policy" file.
+ */
+ fun parse(reader: Reader, inputName: String, processor: PolicyFileProcessor) {
+ filename = inputName
+
+ log.i("Parsing text policy file $inputName ...")
+ this.processor = processor
+ BufferedReader(reader).use { rd ->
+ lineNumber = 0
+ try {
+ while (true) {
+ var line = rd.readLine()
+ if (line == null) {
+ break
+ }
+ lineNumber++
+ line = normalizeTextLine(line) // Remove comment and trim.
+ if (line.isEmpty()) {
+ continue
+ }
+ parseLine(line)
+ }
+ finishLastClass()
+ } catch (e: ParseException) {
+ throw e.withSourceInfo(inputName, lineNumber)
+ }
+ }
+ }
+
+ private fun finishLastClass() {
+ currentClassName?.let { className ->
+ processor.onSimpleClassEnd(className)
+ currentClassName = null
+ }
+ }
+
+ private fun ensureInClass(directive: String): String {
+ return currentClassName ?:
+ throw ParseException("Directive '$directive' must follow a 'class' directive")
+ }
+
private fun parseLine(line: String) {
val fields = line.split(whitespaceRegex).toTypedArray()
when (fields[0].lowercase()) {
- "p", "package" -> parsePackage(fields)
- "c", "class" -> parseClass(fields)
- "f", "field" -> parseField(fields)
- "m", "method" -> parseMethod(fields)
- "r", "rename" -> parseRename(fields)
+ "p", "package" -> {
+ finishLastClass()
+ parsePackage(fields)
+ }
+ "c", "class" -> {
+ finishLastClass()
+ parseClass(fields)
+ }
+ "f", "field" -> {
+ ensureInClass("field")
+ parseField(fields)
+ }
+ "m", "method" -> {
+ ensureInClass("method")
+ parseMethod(fields)
+ }
+ "r", "rename" -> {
+ finishLastClass()
+ parseRename(fields)
+ }
else -> throw ParseException("Unknown directive \"${fields[0]}\"")
}
}
@@ -184,20 +409,20 @@ class TextFileFilterPolicyParser(
if (!policy.isUsableWithClasses) {
throw ParseException("Package can't have policy '$policy'")
}
- packageFilter.addPolicy(name, policy.withReason(FILTER_REASON))
+ processor.onPackage(name, policy.withReason(FILTER_REASON))
}
private fun parseClass(fields: Array<String>) {
if (fields.size < 3) {
throw ParseException("Class ('c') expects 2 fields.")
}
- currentClassName = fields[1]
+ val className = fields[1]
// superClass is set when the class name starts with a "*".
- val superClass = resolveExtendingClass(currentClassName)
+ val superClass = resolveExtendingClass(className)
// :aidl, etc?
- val classType = resolveSpecialClass(currentClassName)
+ val classType = resolveSpecialClass(className)
if (fields[2].startsWith("!")) {
if (classType != SpecialClass.NotSpecial) {
@@ -208,7 +433,8 @@ class TextFileFilterPolicyParser(
}
// It's a redirection class.
val toClass = fields[2].substring(1)
- imf.setRedirectionClass(currentClassName, toClass)
+
+ processor.onRedirectionClass(className, toClass)
} else if (fields[2].startsWith("~")) {
if (classType != SpecialClass.NotSpecial) {
// We could support it, but not needed at least for now.
@@ -218,7 +444,8 @@ class TextFileFilterPolicyParser(
}
// It's a class-load hook
val callback = fields[2].substring(1)
- imf.setClassLoadHook(currentClassName, callback)
+
+ processor.onClassLoadHook(className, callback)
} else {
val policy = parsePolicy(fields[2])
if (!policy.isUsableWithClasses) {
@@ -229,26 +456,27 @@ class TextFileFilterPolicyParser(
SpecialClass.NotSpecial -> {
// TODO: Duplicate check, etc
if (superClass == null) {
- imf.setPolicyForClass(
- currentClassName, policy.withReason(FILTER_REASON)
- )
+ currentClassName = className
+ processor.onSimpleClassStart(className)
+ processor.onSimpleClassPolicy(className, policy.withReason(FILTER_REASON))
} else {
- subclassFilter.addPolicy(
+ processor.onSubClassPolicy(
superClass,
- policy.withReason("extends $superClass")
+ policy.withReason("extends $superClass"),
)
}
}
-
SpecialClass.Aidl -> {
if (aidlPolicy != null) {
throw ParseException(
"Policy for AIDL classes already defined"
)
}
- aidlPolicy = policy.withReason(
+ val p = policy.withReason(
"$FILTER_REASON (special-class AIDL)"
)
+ processor.onSpecialClassPolicy(classType, p)
+ aidlPolicy = p
}
SpecialClass.FeatureFlags -> {
@@ -257,9 +485,11 @@ class TextFileFilterPolicyParser(
"Policy for feature flags already defined"
)
}
- featureFlagsPolicy = policy.withReason(
+ val p = policy.withReason(
"$FILTER_REASON (special-class feature flags)"
)
+ processor.onSpecialClassPolicy(classType, p)
+ featureFlagsPolicy = p
}
SpecialClass.Sysprops -> {
@@ -268,9 +498,11 @@ class TextFileFilterPolicyParser(
"Policy for sysprops already defined"
)
}
- syspropsPolicy = policy.withReason(
+ val p = policy.withReason(
"$FILTER_REASON (special-class sysprops)"
)
+ processor.onSpecialClassPolicy(classType, p)
+ syspropsPolicy = p
}
SpecialClass.RFile -> {
@@ -279,9 +511,11 @@ class TextFileFilterPolicyParser(
"Policy for R file already defined"
)
}
- rFilePolicy = policy.withReason(
+ val p = policy.withReason(
"$FILTER_REASON (special-class R file)"
)
+ processor.onSpecialClassPolicy(classType, p)
+ rFilePolicy = p
}
}
}
@@ -296,17 +530,16 @@ class TextFileFilterPolicyParser(
if (!policy.isUsableWithFields) {
throw ParseException("Field can't have policy '$policy'")
}
- require(this::currentClassName.isInitialized)
// TODO: Duplicate check, etc
- imf.setPolicyForField(currentClassName, name, policy.withReason(FILTER_REASON))
+ processor.onField(currentClassName!!, name, policy.withReason(FILTER_REASON))
}
private fun parseMethod(fields: Array<String>) {
if (fields.size < 3 || fields.size > 4) {
throw ParseException("Method ('m') expects 3 or 4 fields.")
}
- val name = fields[1]
+ val methodName = fields[1]
val signature: String
val policyStr: String
if (fields.size <= 3) {
@@ -323,44 +556,48 @@ class TextFileFilterPolicyParser(
throw ParseException("Method can't have policy '$policy'")
}
- require(this::currentClassName.isInitialized)
+ val className = currentClassName!!
- imf.setPolicyForMethod(
- currentClassName, name, signature,
- policy.withReason(FILTER_REASON)
- )
- if (policy == FilterPolicy.Substitute) {
- val fromName = policyStr.substring(1)
+ val policyWithReason = policy.withReason(FILTER_REASON)
+ if (policy != FilterPolicy.Substitute) {
+ processor.onSimpleMethodPolicy(className, methodName, signature, policyWithReason)
+ } else {
+ val targetName = policyStr.substring(1)
- if (fromName == name) {
+ if (targetName == methodName) {
throw ParseException(
"Substitution must have a different name"
)
}
- // Set the policy for the "from" method.
- imf.setPolicyForMethod(
- currentClassName, fromName, signature,
- FilterPolicy.Keep.withReason(FILTER_REASON)
- )
-
- val classAndMethod = splitWithLastPeriod(fromName)
+ val classAndMethod = splitWithLastPeriod(targetName)
if (classAndMethod != null) {
// If the substitution target contains a ".", then
// it's a method call redirect.
- methodReplaceSpec.add(
- TextFilePolicyMethodReplaceFilter.MethodCallReplaceSpec(
- currentClassName.toJvmClassName(),
- name,
+ val spec = TextFilePolicyMethodReplaceFilter.MethodCallReplaceSpec(
+ currentClassName!!.toJvmClassName(),
+ methodName,
signature,
classAndMethod.first.toJvmClassName(),
classAndMethod.second,
)
+ processor.onMethodOutClassReplace(
+ className,
+ methodName,
+ signature,
+ spec,
+ policyWithReason,
)
} else {
// It's an in-class replace.
// ("@RavenwoodReplace" equivalent)
- imf.setRenameTo(currentClassName, fromName, signature, name)
+ processor.onMethodInClassReplace(
+ className,
+ methodName,
+ signature,
+ targetName,
+ policyWithReason,
+ )
}
}
}
@@ -378,7 +615,7 @@ class TextFileFilterPolicyParser(
// applied. (Which is needed for services.jar)
val prefix = fields[2].trimStart('/')
- typeRenameSpec += TextFilePolicyRemapperFilter.TypeRenameSpec(
+ processor.onRename(
pattern, prefix
)
}
diff --git a/ravenwood/tools/hoststubgen/test-tiny-framework/diff-and-update-golden.sh b/ravenwood/tools/hoststubgen/test-tiny-framework/diff-and-update-golden.sh
index b389a67a8e4c..8408a18142eb 100755
--- a/ravenwood/tools/hoststubgen/test-tiny-framework/diff-and-update-golden.sh
+++ b/ravenwood/tools/hoststubgen/test-tiny-framework/diff-and-update-golden.sh
@@ -34,10 +34,11 @@ source "${0%/*}"/../common.sh
SCRIPT_NAME="${0##*/}"
-GOLDEN_DIR=golden-output
+GOLDEN_DIR=${GOLDEN_DIR:-golden-output}
mkdir -p $GOLDEN_DIR
-DIFF_CMD=${DIFF:-diff -u --ignore-blank-lines --ignore-space-change}
+# TODO(b/388562869) We shouldn't need `--ignore-matching-lines`, but the golden files may not have the "Constant pool:" lines.
+DIFF_CMD=${DIFF_CMD:-diff -u --ignore-blank-lines --ignore-space-change --ignore-matching-lines='^\(Constant.pool:\|{\)$'}
update=0
three_way=0
@@ -62,7 +63,7 @@ done
shift $(($OPTIND - 1))
# Build the dump files, which are the input of this test.
-run m dump-jar tiny-framework-dump-test
+run ${BUILD_CMD:=m} dump-jar tiny-framework-dump-test
# Get the path to the generate text files. (not the golden files.)
diff --git a/ravenwood/tools/hoststubgen/test-tiny-framework/tiny-framework-dump-test.py b/ravenwood/tools/hoststubgen/test-tiny-framework/tiny-framework-dump-test.py
index 88fa492addb8..c35d6d106c8d 100755
--- a/ravenwood/tools/hoststubgen/test-tiny-framework/tiny-framework-dump-test.py
+++ b/ravenwood/tools/hoststubgen/test-tiny-framework/tiny-framework-dump-test.py
@@ -28,8 +28,11 @@ GOLDEN_DIRS = [
# Run diff.
def run_diff(file1, file2):
+ # TODO(b/388562869) We shouldn't need `--ignore-matching-lines`, but the golden files may not have the "Constant pool:" lines.
command = ['diff', '-u', '--ignore-blank-lines',
- '--ignore-space-change', file1, file2]
+ '--ignore-space-change',
+ '--ignore-matching-lines=^\(Constant.pool:\|{\)$',
+ file1, file2]
print(' '.join(command))
result = subprocess.run(command, stderr=sys.stdout)
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
index 6cd1f721d215..8e037c3ba90c 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
@@ -542,7 +542,8 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
MagnificationController magnificationController,
@Nullable AccessibilityInputFilter inputFilter,
ProxyManager proxyManager,
- PermissionEnforcer permissionEnforcer) {
+ PermissionEnforcer permissionEnforcer,
+ HearingDevicePhoneCallNotificationController hearingDeviceNotificationController) {
super(permissionEnforcer);
mContext = context;
mPowerManager = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
@@ -571,8 +572,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
mVisibleBgUserIds = null;
mInputManager = context.getSystemService(InputManager.class);
if (com.android.settingslib.flags.Flags.hearingDevicesInputRoutingControl()) {
- mHearingDeviceNotificationController = new HearingDevicePhoneCallNotificationController(
- context);
+ mHearingDeviceNotificationController = hearingDeviceNotificationController;
} else {
mHearingDeviceNotificationController = null;
}
diff --git a/services/backup/java/com/android/server/backup/BackupManagerService.java b/services/backup/java/com/android/server/backup/BackupManagerService.java
index 3f6ede95eaf9..8804faf2d312 100644
--- a/services/backup/java/com/android/server/backup/BackupManagerService.java
+++ b/services/backup/java/com/android/server/backup/BackupManagerService.java
@@ -22,9 +22,9 @@ import android.Manifest;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UserIdInt;
-import android.app.ActivityManager;
import android.app.admin.DevicePolicyManager;
import android.app.backup.BackupManager;
+import android.app.backup.BackupManagerInternal;
import android.app.backup.BackupRestoreEventLogger.DataTypeResult;
import android.app.backup.IBackupManager;
import android.app.backup.IBackupManagerMonitor;
@@ -60,6 +60,7 @@ import android.util.SparseArray;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.DumpUtils;
+import com.android.server.LocalServices;
import com.android.server.SystemConfig;
import com.android.server.SystemService;
import com.android.server.backup.utils.RandomAccessFileUtils;
@@ -91,7 +92,7 @@ import java.util.Set;
* privileged callers (currently {@link DevicePolicyManager}). If called on {@link
* UserHandle#USER_SYSTEM}, backup is disabled for all users.
*/
-public class BackupManagerService extends IBackupManager.Stub {
+public class BackupManagerService extends IBackupManager.Stub implements BackupManagerInternal {
public static final String TAG = "BackupManagerService";
public static final boolean DEBUG = true;
public static final boolean MORE_DEBUG = false;
@@ -191,7 +192,6 @@ public class BackupManagerService extends IBackupManager.Stub {
}
}
- // TODO: Remove this when we implement DI by injecting in the construtor.
@VisibleForTesting
Handler getBackupHandler() {
return mHandler;
@@ -637,51 +637,28 @@ public class BackupManagerService extends IBackupManager.Stub {
}
@Override
- public void agentConnectedForUser(int userId, String packageName, IBinder agent)
- throws RemoteException {
- if (isUserReadyForBackup(userId)) {
- agentConnected(userId, packageName, agent);
+ public void agentConnectedForUser(String packageName, @UserIdInt int userId, IBinder agent) {
+ if (!isUserReadyForBackup(userId)) {
+ return;
}
- }
- @Override
- public void agentConnected(String packageName, IBinder agent) throws RemoteException {
- agentConnectedForUser(binderGetCallingUserId(), packageName, agent);
- }
-
- /**
- * Callback: a requested backup agent has been instantiated. This should only be called from the
- * {@link ActivityManager}.
- */
- public void agentConnected(@UserIdInt int userId, String packageName, IBinder agentBinder) {
- UserBackupManagerService userBackupManagerService =
- getServiceForUserIfCallerHasPermission(userId, "agentConnected()");
+ UserBackupManagerService userBackupManagerService = getServiceForUserIfCallerHasPermission(
+ userId, "agentConnected()");
if (userBackupManagerService != null) {
userBackupManagerService.getBackupAgentConnectionManager().agentConnected(packageName,
- agentBinder);
+ agent);
}
}
@Override
- public void agentDisconnectedForUser(int userId, String packageName) throws RemoteException {
- if (isUserReadyForBackup(userId)) {
- agentDisconnected(userId, packageName);
+ public void agentDisconnectedForUser(String packageName, @UserIdInt int userId) {
+ if (!isUserReadyForBackup(userId)) {
+ return;
}
- }
- @Override
- public void agentDisconnected(String packageName) throws RemoteException {
- agentDisconnectedForUser(binderGetCallingUserId(), packageName);
- }
-
- /**
- * Callback: a backup agent has failed to come up, or has unexpectedly quit. This should only be
- * called from the {@link ActivityManager}.
- */
- public void agentDisconnected(@UserIdInt int userId, String packageName) {
- UserBackupManagerService userBackupManagerService =
- getServiceForUserIfCallerHasPermission(userId, "agentDisconnected()");
+ UserBackupManagerService userBackupManagerService = getServiceForUserIfCallerHasPermission(
+ userId, "agentDisconnected()");
if (userBackupManagerService != null) {
userBackupManagerService.getBackupAgentConnectionManager().agentDisconnected(
@@ -1688,7 +1665,7 @@ public class BackupManagerService extends IBackupManager.Stub {
* @param userId User id on which the backup operation is being requested.
* @param message A message to include in the exception if it is thrown.
*/
- void enforceCallingPermissionOnUserId(@UserIdInt int userId, String message) {
+ private void enforceCallingPermissionOnUserId(@UserIdInt int userId, String message) {
if (binderGetCallingUserId() != userId) {
mContext.enforceCallingOrSelfPermission(
Manifest.permission.INTERACT_ACROSS_USERS_FULL, message);
@@ -1697,6 +1674,8 @@ public class BackupManagerService extends IBackupManager.Stub {
/** Implementation to receive lifecycle event callbacks for system services. */
public static class Lifecycle extends SystemService {
+ private final BackupManagerService mBackupManagerService;
+
public Lifecycle(Context context) {
this(context, new BackupManagerService(context));
}
@@ -1704,12 +1683,14 @@ public class BackupManagerService extends IBackupManager.Stub {
@VisibleForTesting
Lifecycle(Context context, BackupManagerService backupManagerService) {
super(context);
+ mBackupManagerService = backupManagerService;
sInstance = backupManagerService;
+ LocalServices.addService(BackupManagerInternal.class, mBackupManagerService);
}
@Override
public void onStart() {
- publishService(Context.BACKUP_SERVICE, BackupManagerService.sInstance);
+ publishService(Context.BACKUP_SERVICE, mBackupManagerService);
}
@Override
@@ -1717,17 +1698,17 @@ public class BackupManagerService extends IBackupManager.Stub {
// Starts the backup service for this user if backup is active for this user. Offloads
// work onto the handler thread {@link #mHandlerThread} to keep unlock time low since
// backup is not essential for device functioning.
- sInstance.postToHandler(
+ mBackupManagerService.postToHandler(
() -> {
- sInstance.updateDefaultBackupUserIdIfNeeded();
- sInstance.startServiceForUser(user.getUserIdentifier());
- sInstance.mHasFirstUserUnlockedSinceBoot = true;
+ mBackupManagerService.updateDefaultBackupUserIdIfNeeded();
+ mBackupManagerService.startServiceForUser(user.getUserIdentifier());
+ mBackupManagerService.mHasFirstUserUnlockedSinceBoot = true;
});
}
@Override
public void onUserStopping(@NonNull TargetUser user) {
- sInstance.onStopUser(user.getUserIdentifier());
+ mBackupManagerService.onStopUser(user.getUserIdentifier());
}
@VisibleForTesting
diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java
index aeb2f5e9be84..40726b4331e2 100644
--- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java
+++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java
@@ -767,7 +767,7 @@ public class VirtualDeviceManagerService extends SystemService {
params,
/* activityListener= */ null,
/* soundEffectListener= */ null);
- return new VirtualDeviceManager.VirtualDevice(mImpl, getContext(), virtualDevice);
+ return new VirtualDeviceManager.VirtualDevice(getContext(), virtualDevice);
}
@Override
diff --git a/services/core/java/com/android/server/BinaryTransparencyService.java b/services/core/java/com/android/server/BinaryTransparencyService.java
index 31f6ef9fc062..1d914c89c570 100644
--- a/services/core/java/com/android/server/BinaryTransparencyService.java
+++ b/services/core/java/com/android/server/BinaryTransparencyService.java
@@ -729,8 +729,10 @@ public class BinaryTransparencyService extends SystemService {
private void printModuleDetails(ModuleInfo moduleInfo, final PrintWriter pw) {
pw.println("--- Module Details ---");
pw.println("Module name: " + moduleInfo.getName());
- pw.println("Module visibility: "
- + (moduleInfo.isHidden() ? "hidden" : "visible"));
+ if (!android.content.pm.Flags.removeHiddenModuleUsage()) {
+ pw.println("Module visibility: "
+ + (moduleInfo.isHidden() ? "hidden" : "visible"));
+ }
}
private void printAppDetails(PackageInfo packageInfo,
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index b536dc524a80..0603c4506cd1 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -256,7 +256,7 @@ import android.app.ServiceStartNotAllowedException;
import android.app.WaitResult;
import android.app.assist.ActivityId;
import android.app.backup.BackupAnnotations.BackupDestination;
-import android.app.backup.IBackupManager;
+import android.app.backup.BackupManagerInternal;
import android.app.compat.CompatChanges;
import android.app.job.JobParameters;
import android.app.usage.UsageEvents;
@@ -4490,11 +4490,8 @@ public class ActivityManagerService extends IActivityManager.Stub
final int userId = app.userId;
final String packageName = app.info.packageName;
mHandler.post(() -> {
- try {
- getBackupManager().agentDisconnectedForUser(userId, packageName);
- } catch (RemoteException e) {
- // Can't happen; the backup manager is local
- }
+ LocalServices.getService(BackupManagerInternal.class).agentDisconnectedForUser(
+ packageName, userId);
});
}
} else {
@@ -12864,6 +12861,28 @@ public class ActivityManagerService extends IActivityManager.Stub
}
}
+ final long kernelCmaUsage = Debug.getKernelCmaUsageKb();
+ if (kernelCmaUsage >= 0) {
+ pw.print(" Kernel CMA: ");
+ pw.println(stringifyKBSize(kernelCmaUsage));
+ // CMA memory can be in one of the following four states:
+ //
+ // 1. Free, in which case it is accounted for as part of MemFree, which
+ // is already considered in the lostRAM calculation below.
+ //
+ // 2. Allocated as part of a userspace allocated, in which case it is
+ // already accounted for in the total PSS value that was computed.
+ //
+ // 3. Allocated for storing compressed memory (ZRAM) on Android kernels.
+ // This is accounted for by calculating the amount of memory ZRAM
+ // consumes and including it in the lostRAM calculuation.
+ //
+ // 4. Allocated by a kernel driver, in which case, it is currently not
+ // attributed to any term that has been derived thus far. Since the
+ // allocations come from a kernel driver, add it to kernelUsed.
+ kernelUsed += kernelCmaUsage;
+ }
+
// Note: ION/DMA-BUF heap pools are reclaimable and hence, they are included as part of
// memInfo.getCachedSizeKb().
final long lostRAM = memInfo.getTotalSizeKb()
@@ -13381,12 +13400,32 @@ public class ActivityManagerService extends IActivityManager.Stub
proto.write(MemInfoDumpProto.CACHED_KERNEL_KB, memInfo.getCachedSizeKb());
proto.write(MemInfoDumpProto.FREE_KB, memInfo.getFreeSizeKb());
}
+ // CMA memory can be in one of the following four states:
+ //
+ // 1. Free, in which case it is accounted for as part of MemFree, which
+ // is already considered in the lostRAM calculation below.
+ //
+ // 2. Allocated as part of a userspace allocated, in which case it is
+ // already accounted for in the total PSS value that was computed.
+ //
+ // 3. Allocated for storing compressed memory (ZRAM) on Android Kernels.
+ // This is accounted for by calculating hte amount of memory ZRAM
+ // consumes and including it in the lostRAM calculation.
+ //
+ // 4. Allocated by a kernel driver, in which case, it is currently not
+ // attributed to any term that has been derived thus far, so subtract
+ // it from lostRAM.
+ long kernelCmaUsage = Debug.getKernelCmaUsageKb();
+ if (kernelCmaUsage < 0) {
+ kernelCmaUsage = 0;
+ }
long lostRAM = memInfo.getTotalSizeKb()
- (ss[INDEX_TOTAL_PSS] - ss[INDEX_TOTAL_SWAP_PSS])
- memInfo.getFreeSizeKb() - memInfo.getCachedSizeKb()
// NR_SHMEM is subtracted twice (getCachedSizeKb() and getKernelUsedSizeKb())
+ memInfo.getShmemSizeKb()
- - memInfo.getKernelUsedSizeKb() - memInfo.getZramTotalSizeKb();
+ - memInfo.getKernelUsedSizeKb() - memInfo.getZramTotalSizeKb()
+ - kernelCmaUsage;
proto.write(MemInfoDumpProto.USED_PSS_KB, ss[INDEX_TOTAL_PSS] - cachedPss);
proto.write(MemInfoDumpProto.USED_KERNEL_KB, memInfo.getKernelUsedSizeKb());
proto.write(MemInfoDumpProto.LOST_RAM_KB, lostRAM);
@@ -13505,11 +13544,8 @@ public class ActivityManagerService extends IActivityManager.Stub
if (DEBUG_BACKUP || DEBUG_CLEANUP) Slog.d(TAG_CLEANUP, "App "
+ backupTarget.appInfo + " died during backup");
mHandler.post(() -> {
- try {
- getBackupManager().agentDisconnectedForUser(app.userId, app.info.packageName);
- } catch (RemoteException e) {
- // can't happen; backup manager is local
- }
+ LocalServices.getService(BackupManagerInternal.class).agentDisconnectedForUser(
+ app.info.packageName, app.userId);
});
}
@@ -14223,9 +14259,8 @@ public class ActivityManagerService extends IActivityManager.Stub
final long oldIdent = Binder.clearCallingIdentity();
try {
- getBackupManager().agentConnectedForUser(userId, agentPackageName, agent);
- } catch (RemoteException e) {
- // can't happen; the backup manager service is local
+ LocalServices.getService(BackupManagerInternal.class).agentConnectedForUser(
+ agentPackageName, userId, agent);
} catch (Exception e) {
Slog.w(TAG, "Exception trying to deliver BackupAgent binding: ");
e.printStackTrace();
@@ -14565,7 +14600,7 @@ public class ActivityManagerService extends IActivityManager.Stub
app.mProfile.addHostingComponentType(HOSTING_COMPONENT_TYPE_INSTRUMENTATION);
}
- app.setActiveInstrumentation(activeInstr);
+ mProcessStateController.setActiveInstrumentation(app, activeInstr);
activeInstr.mFinished = false;
activeInstr.mSourceUid = callingUid;
activeInstr.mRunningProcesses.add(app);
@@ -14711,7 +14746,7 @@ public class ActivityManagerService extends IActivityManager.Stub
abiOverride,
ZYGOTE_POLICY_FLAG_EMPTY);
- app.setActiveInstrumentation(activeInstr);
+ mProcessStateController.setActiveInstrumentation(app, activeInstr);
activeInstr.mFinished = false;
activeInstr.mSourceUid = callingUid;
activeInstr.mRunningProcesses.add(app);
@@ -14848,7 +14883,7 @@ public class ActivityManagerService extends IActivityManager.Stub
}
instr.removeProcess(app);
- app.setActiveInstrumentation(null);
+ mProcessStateController.setActiveInstrumentation(app, null);
}
app.mProfile.clearHostingComponentType(HOSTING_COMPONENT_TYPE_INSTRUMENTATION);
@@ -16617,7 +16652,7 @@ public class ActivityManagerService extends IActivityManager.Stub
}
@Override
- public void onUserRemoved(@UserIdInt int userId) {
+ public void onUserRemoving(@UserIdInt int userId) {
// Clean up any ActivityTaskManager state (by telling it the user is stopped)
mAtmInternal.onUserStopped(userId);
// Clean up various services by removing the user
@@ -16631,6 +16666,12 @@ public class ActivityManagerService extends IActivityManager.Stub
}
@Override
+ public void onUserRemoved(int userId) {
+ // Clean up UserController state
+ mUserController.onUserRemoved(userId);
+ }
+
+ @Override
public boolean startUserInBackground(final int userId) {
return ActivityManagerService.this.startUserInBackground(userId);
}
@@ -19374,7 +19415,7 @@ public class ActivityManagerService extends IActivityManager.Stub
}
if (preventIntentRedirectCollectNestedKeysOnServerIfNotCollected()) {
// this flag will be ramped to public.
- intent.collectExtraIntentKeys();
+ intent.collectExtraIntentKeys(true);
}
}
@@ -19440,8 +19481,4 @@ public class ActivityManagerService extends IActivityManager.Stub
}
return token;
}
-
- private IBackupManager getBackupManager() {
- return IBackupManager.Stub.asInterface(ServiceManager.getService(Context.BACKUP_SERVICE));
- }
}
diff --git a/services/core/java/com/android/server/am/AppProfiler.java b/services/core/java/com/android/server/am/AppProfiler.java
index 6b24df4a1fa8..225c7ca2ca9e 100644
--- a/services/core/java/com/android/server/am/AppProfiler.java
+++ b/services/core/java/com/android/server/am/AppProfiler.java
@@ -2477,13 +2477,15 @@ public class AppProfiler {
// This is the wildcard mode, where every process brought up for
// the target instrumentation should be included.
if (aInstr.mTargetInfo.packageName.equals(app.info.packageName)) {
- app.setActiveInstrumentation(aInstr);
+ mService.mProcessStateController.setActiveInstrumentation(app,
+ aInstr);
aInstr.mRunningProcesses.add(app);
}
} else {
for (String proc : aInstr.mTargetProcesses) {
if (proc.equals(app.processName)) {
- app.setActiveInstrumentation(aInstr);
+ mService.mProcessStateController.setActiveInstrumentation(app,
+ aInstr);
aInstr.mRunningProcesses.add(app);
break;
}
diff --git a/services/core/java/com/android/server/am/OomAdjuster.java b/services/core/java/com/android/server/am/OomAdjuster.java
index 9c569db99797..3abcd4e7a143 100644
--- a/services/core/java/com/android/server/am/OomAdjuster.java
+++ b/services/core/java/com/android/server/am/OomAdjuster.java
@@ -156,7 +156,6 @@ import android.os.Process;
import android.os.RemoteException;
import android.os.SystemClock;
import android.os.Trace;
-import android.os.UserHandle;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.Slog;
@@ -3401,13 +3400,10 @@ public class OomAdjuster {
}
private static int getCpuCapability(ProcessRecord app, long nowUptime) {
+ // Note: persistent processes get all capabilities, including CPU_TIME.
final UidRecord uidRec = app.getUidRecord();
if (uidRec != null && uidRec.isCurAllowListed()) {
- // Process has user visible activities.
- return PROCESS_CAPABILITY_CPU_TIME;
- }
- if (UserHandle.isCore(app.uid)) {
- // Make sure all system components are not frozen.
+ // Process is in the power allowlist.
return PROCESS_CAPABILITY_CPU_TIME;
}
if (app.mState.getCachedHasVisibleActivities()) {
@@ -3418,6 +3414,12 @@ public class OomAdjuster {
// It running a short fgs, just give it cpu time.
return PROCESS_CAPABILITY_CPU_TIME;
}
+ if (app.mReceivers.numberOfCurReceivers() > 0) {
+ return PROCESS_CAPABILITY_CPU_TIME;
+ }
+ if (app.hasActiveInstrumentation()) {
+ return PROCESS_CAPABILITY_CPU_TIME;
+ }
// TODO(b/370817323): Populate this method with all of the reasons to keep a process
// unfrozen.
return 0;
diff --git a/services/core/java/com/android/server/am/ProcessStateController.java b/services/core/java/com/android/server/am/ProcessStateController.java
index 57899228e6ad..f44fb06727cf 100644
--- a/services/core/java/com/android/server/am/ProcessStateController.java
+++ b/services/core/java/com/android/server/am/ProcessStateController.java
@@ -246,12 +246,11 @@ public class ProcessStateController {
}
/**
- * Set what sched group to grant a process due to running a broadcast.
- * {@link ProcessList.SCHED_GROUP_UNDEFINED} means the process is not running a broadcast.
+ * Sets an active instrumentation running within the given process.
*/
- public void setBroadcastSchedGroup(@NonNull ProcessRecord proc, int schedGroup) {
- // TODO(b/302575389): Migrate state pulled from BroadcastQueue to a pushed model
- throw new UnsupportedOperationException("Not implemented yet");
+ public void setActiveInstrumentation(@NonNull ProcessRecord proc,
+ ActiveInstrumentation activeInstrumentation) {
+ proc.setActiveInstrumentation(activeInstrumentation);
}
/********************* Process Visibility State Events *********************/
@@ -587,6 +586,34 @@ public class ProcessStateController {
psr.updateHasTopStartedAlmostPerceptibleServices();
}
+ /************************ Broadcast Receiver State Events **************************/
+ /**
+ * Set what sched group to grant a process due to running a broadcast.
+ * {@link ProcessList.SCHED_GROUP_UNDEFINED} means the process is not running a broadcast.
+ */
+ public void setBroadcastSchedGroup(@NonNull ProcessRecord proc, int schedGroup) {
+ // TODO(b/302575389): Migrate state pulled from BroadcastQueue to a pushed model
+ throw new UnsupportedOperationException("Not implemented yet");
+ }
+
+ /**
+ * Note that the process has started processing a broadcast receiver.
+ */
+ public boolean incrementCurReceivers(@NonNull ProcessRecord app) {
+ // TODO(b/302575389): Migrate state pulled from ATMS to a pushed model
+ // maybe used ActivityStateFlags instead.
+ throw new UnsupportedOperationException("Not implemented yet");
+ }
+
+ /**
+ * Note that the process has finished processing a broadcast receiver.
+ */
+ public boolean decrementCurReceivers(@NonNull ProcessRecord app) {
+ // TODO(b/302575389): Migrate state pulled from ATMS to a pushed model
+ // maybe used ActivityStateFlags instead.
+ throw new UnsupportedOperationException("Not implemented yet");
+ }
+
/**
* Builder for ProcessStateController.
*/
diff --git a/services/core/java/com/android/server/am/UserController.java b/services/core/java/com/android/server/am/UserController.java
index ec74f60539a2..d76c04ac7f31 100644
--- a/services/core/java/com/android/server/am/UserController.java
+++ b/services/core/java/com/android/server/am/UserController.java
@@ -125,7 +125,6 @@ import android.view.Display;
import com.android.internal.R;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
-import com.android.internal.policy.IKeyguardDismissCallback;
import com.android.internal.util.ArrayUtils;
import com.android.internal.util.FrameworkStatsLog;
import com.android.internal.util.ObjectUtils;
@@ -161,7 +160,6 @@ import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.BiConsumer;
-import java.util.function.Consumer;
/**
* Helper class for {@link ActivityManagerService} responsible for multi-user functionality.
@@ -227,14 +225,6 @@ class UserController implements Handler.Callback {
private static final int USER_SWITCH_CALLBACKS_TIMEOUT_MS = 5 * 1000;
/**
- * Amount of time waited for {@link WindowManagerService#dismissKeyguard} callbacks to be
- * called after dismissing the keyguard.
- * Otherwise, we should move on to dismiss the dialog {@link #dismissUserSwitchDialog()}
- * and report user switch is complete {@link #REPORT_USER_SWITCH_COMPLETE_MSG}.
- */
- private static final int DISMISS_KEYGUARD_TIMEOUT_MS = 2 * 1000;
-
- /**
* Time after last scheduleOnUserCompletedEvent() call at which USER_COMPLETED_EVENT_MSG will be
* scheduled (although it may fire sooner instead).
* When it fires, {@link #reportOnUserCompletedEvent} will be processed.
@@ -455,11 +445,6 @@ class UserController implements Handler.Callback {
public void onUserCreated(UserInfo user, Object token) {
onUserAdded(user);
}
-
- @Override
- public void onUserRemoved(UserInfo user) {
- UserController.this.onUserRemoved(user.id);
- }
};
UserController(ActivityManagerService service) {
@@ -2010,7 +1995,7 @@ class UserController implements Handler.Callback {
mInjector.getWindowManager().setSwitchingUser(true);
// Only lock if the user has a secure keyguard PIN/Pattern/Pwd
if (mInjector.getKeyguardManager().isDeviceSecure(userId)) {
- // Make sure the device is locked before moving on with the user switch
+ Slogf.d(TAG, "Locking the device before moving on with the user switch");
mInjector.lockDeviceNowAndWaitForKeyguardShown();
}
}
@@ -2640,7 +2625,7 @@ class UserController implements Handler.Callback {
EventLog.writeEvent(EventLogTags.UC_CONTINUE_USER_SWITCH, oldUserId, newUserId);
- // Do the keyguard dismiss and dismiss the user switching dialog later
+ // Dismiss the user switching dialog and complete the user switch
mHandler.removeMessages(COMPLETE_USER_SWITCH_MSG);
mHandler.sendMessage(mHandler.obtainMessage(
COMPLETE_USER_SWITCH_MSG, oldUserId, newUserId));
@@ -2655,31 +2640,17 @@ class UserController implements Handler.Callback {
@VisibleForTesting
void completeUserSwitch(int oldUserId, int newUserId) {
- final boolean isUserSwitchUiEnabled = isUserSwitchUiEnabled();
- // serialize each conditional step
- await(
- // STEP 1 - If there is no challenge set, dismiss the keyguard right away
- isUserSwitchUiEnabled && !mInjector.getKeyguardManager().isDeviceSecure(newUserId),
- mInjector::dismissKeyguard,
- () -> await(
- // STEP 2 - If user switch ui was enabled, dismiss user switch dialog
- isUserSwitchUiEnabled,
- this::dismissUserSwitchDialog,
- () -> {
- // STEP 3 - Send REPORT_USER_SWITCH_COMPLETE_MSG to broadcast
- // ACTION_USER_SWITCHED & call UserSwitchObservers.onUserSwitchComplete
- mHandler.removeMessages(REPORT_USER_SWITCH_COMPLETE_MSG);
- mHandler.sendMessage(mHandler.obtainMessage(
- REPORT_USER_SWITCH_COMPLETE_MSG, oldUserId, newUserId));
- }
- ));
- }
-
- private void await(boolean condition, Consumer<Runnable> conditionalStep, Runnable nextStep) {
- if (condition) {
- conditionalStep.accept(nextStep);
+ final Runnable runnable = () -> {
+ // Send REPORT_USER_SWITCH_COMPLETE_MSG to broadcast ACTION_USER_SWITCHED and call
+ // onUserSwitchComplete on UserSwitchObservers.
+ mHandler.removeMessages(REPORT_USER_SWITCH_COMPLETE_MSG);
+ mHandler.sendMessage(mHandler.obtainMessage(
+ REPORT_USER_SWITCH_COMPLETE_MSG, oldUserId, newUserId));
+ };
+ if (isUserSwitchUiEnabled()) {
+ dismissUserSwitchDialog(runnable);
} else {
- nextStep.run();
+ runnable.run();
}
}
@@ -3381,10 +3352,12 @@ class UserController implements Handler.Callback {
if (mUserProfileGroupIds.keyAt(i) == userId
|| mUserProfileGroupIds.valueAt(i) == userId) {
mUserProfileGroupIds.removeAt(i);
-
}
}
mCurrentProfileIds = ArrayUtils.removeInt(mCurrentProfileIds, userId);
+ mUserLru.remove((Integer) userId);
+ mStartedUsers.remove(userId);
+ updateStartedUserArrayLU();
}
}
@@ -4127,33 +4100,6 @@ class UserController implements Handler.Callback {
return IStorageManager.Stub.asInterface(ServiceManager.getService("mount"));
}
- protected void dismissKeyguard(Runnable runnable) {
- final AtomicBoolean isFirst = new AtomicBoolean(true);
- final Runnable runOnce = () -> {
- if (isFirst.getAndSet(false)) {
- runnable.run();
- }
- };
-
- mHandler.postDelayed(runOnce, DISMISS_KEYGUARD_TIMEOUT_MS);
- getWindowManager().dismissKeyguard(new IKeyguardDismissCallback.Stub() {
- @Override
- public void onDismissError() throws RemoteException {
- mHandler.post(runOnce);
- }
-
- @Override
- public void onDismissSucceeded() throws RemoteException {
- mHandler.post(runOnce);
- }
-
- @Override
- public void onDismissCancelled() throws RemoteException {
- mHandler.post(runOnce);
- }
- }, /* message= */ null);
- }
-
boolean isHeadlessSystemUserMode() {
return UserManager.isHeadlessSystemUserMode();
}
@@ -4178,6 +4124,7 @@ class UserController implements Handler.Callback {
void lockDeviceNowAndWaitForKeyguardShown() {
if (getWindowManager().isKeyguardLocked()) {
+ Slogf.w(TAG, "Not locking the device since the keyguard is already locked");
return;
}
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index 3f6484f0f58e..b9b06701a11b 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -47,6 +47,7 @@ import static android.media.AudioManager.RINGER_MODE_NORMAL;
import static android.media.AudioManager.RINGER_MODE_SILENT;
import static android.media.AudioManager.RINGER_MODE_VIBRATE;
import static android.media.AudioManager.STREAM_SYSTEM;
+import static android.media.IAudioManagerNative.HardeningType;
import static android.media.audio.Flags.autoPublicVolumeApiHardening;
import static android.media.audio.Flags.automaticBtDeviceType;
import static android.media.audio.Flags.concurrentAudioRecordBypassPermission;
@@ -151,6 +152,7 @@ import android.media.BluetoothProfileConnectionInfo;
import android.media.FadeManagerConfiguration;
import android.media.IAudioDeviceVolumeDispatcher;
import android.media.IAudioFocusDispatcher;
+import android.media.IAudioManagerNative;
import android.media.IAudioModeDispatcher;
import android.media.IAudioRoutesObserver;
import android.media.IAudioServerStateDispatcher;
@@ -835,6 +837,18 @@ public class AudioService extends IAudioService.Stub
private final UserRestrictionsListener mUserRestrictionsListener =
new AudioServiceUserRestrictionsListener();
+ private final IAudioManagerNative mNativeShim = new IAudioManagerNative.Stub() {
+ // oneway
+ @Override
+ public void playbackHardeningEvent(int uid, byte type, boolean bypassed) {
+ }
+
+ @Override
+ public void permissionUpdateBarrier() {
+ AudioService.this.permissionUpdateBarrier();
+ }
+ };
+
// List of binder death handlers for setMode() client processes.
// The last process to have called setMode() is at the top of the list.
// package-private so it can be accessed in AudioDeviceBroker.getSetModeDeathHandlers
@@ -2811,6 +2825,11 @@ public class AudioService extends IAudioService.Stub
args, callback, resultReceiver);
}
+ @Override
+ public IAudioManagerNative getNativeInterface() {
+ return mNativeShim;
+ }
+
/** @see AudioManager#getSurroundFormats() */
@Override
public Map<Integer, Boolean> getSurroundFormats() {
@@ -7214,7 +7233,7 @@ public class AudioService extends IAudioService.Stub
final int pid = Binder.getCallingPid();
final String eventSource = new StringBuilder("setBluetoothA2dpOn(").append(on)
.append(") from u/pid:").append(uid).append("/")
- .append(pid).toString();
+ .append(pid).append(" src:AudioService.setBtA2dpOn").toString();
new MediaMetrics.Item(MediaMetrics.Name.AUDIO_DEVICE
+ MediaMetrics.SEPARATOR + "setBluetoothA2dpOn")
diff --git a/services/core/java/com/android/server/input/InputGestureManager.java b/services/core/java/com/android/server/input/InputGestureManager.java
index 24296406da00..93fdbc787ed0 100644
--- a/services/core/java/com/android/server/input/InputGestureManager.java
+++ b/services/core/java/com/android/server/input/InputGestureManager.java
@@ -316,6 +316,31 @@ final class InputGestureManager {
}
}
+ @Nullable
+ public InputGestureData getInputGesture(int userId, InputGestureData.Trigger trigger) {
+ synchronized (mGestureLock) {
+ if (mBlockListedTriggers.contains(trigger)) {
+ return new InputGestureData.Builder().setTrigger(trigger).setKeyGestureType(
+ KeyGestureEvent.KEY_GESTURE_TYPE_SYSTEM_RESERVED).build();
+ }
+ if (trigger instanceof InputGestureData.KeyTrigger keyTrigger) {
+ if (KeyEvent.isModifierKey(keyTrigger.getKeycode()) ||
+ KeyEvent.isSystemKey(keyTrigger.getKeycode())) {
+ return new InputGestureData.Builder().setTrigger(trigger).setKeyGestureType(
+ KeyGestureEvent.KEY_GESTURE_TYPE_SYSTEM_RESERVED).build();
+ }
+ }
+ InputGestureData gestureData = mSystemShortcuts.get(trigger);
+ if (gestureData != null) {
+ return gestureData;
+ }
+ if (!mCustomInputGestures.contains(userId)) {
+ return null;
+ }
+ return mCustomInputGestures.get(userId).get(trigger);
+ }
+ }
+
@InputManager.CustomInputGestureResult
public int addCustomInputGesture(int userId, InputGestureData newGesture) {
synchronized (mGestureLock) {
diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java
index b2c35e1f362e..2ba35d6a70d2 100644
--- a/services/core/java/com/android/server/input/InputManagerService.java
+++ b/services/core/java/com/android/server/input/InputManagerService.java
@@ -3061,6 +3061,16 @@ public class InputManagerService extends IInputManager.Stub
@Override
@PermissionManuallyEnforced
+ public AidlInputGestureData getInputGesture(@UserIdInt int userId,
+ @NonNull AidlInputGestureData.Trigger trigger) {
+ enforceManageKeyGesturePermission();
+
+ Objects.requireNonNull(trigger);
+ return mKeyGestureController.getInputGesture(userId, trigger);
+ }
+
+ @Override
+ @PermissionManuallyEnforced
public int addCustomInputGesture(@UserIdInt int userId,
@NonNull AidlInputGestureData inputGestureData) {
enforceManageKeyGesturePermission();
diff --git a/services/core/java/com/android/server/input/KeyGestureController.java b/services/core/java/com/android/server/input/KeyGestureController.java
index 5f7ad2797368..fb5ce5b4e5fa 100644
--- a/services/core/java/com/android/server/input/KeyGestureController.java
+++ b/services/core/java/com/android/server/input/KeyGestureController.java
@@ -18,6 +18,8 @@ package com.android.server.input;
import static android.content.pm.PackageManager.FEATURE_LEANBACK;
import static android.content.pm.PackageManager.FEATURE_WATCH;
+import static android.os.UserManager.isVisibleBackgroundUsersEnabled;
+import static android.view.Display.DEFAULT_DISPLAY;
import static android.view.WindowManagerPolicyConstants.FLAG_INTERACTIVE;
import static com.android.hardware.input.Flags.enableNew25q2Keycodes;
@@ -55,7 +57,6 @@ import android.util.IndentingPrintWriter;
import android.util.Log;
import android.util.Slog;
import android.util.SparseArray;
-import android.view.Display;
import android.view.InputDevice;
import android.view.KeyCharacterMap;
import android.view.KeyEvent;
@@ -64,6 +65,8 @@ import com.android.internal.R;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.policy.IShortcutService;
+import com.android.server.LocalServices;
+import com.android.server.pm.UserManagerInternal;
import com.android.server.policy.KeyCombinationManager;
import java.util.ArrayDeque;
@@ -159,6 +162,10 @@ final class KeyGestureController {
/** Currently fully consumed key codes per device */
private final SparseArray<Set<Integer>> mConsumedKeysForDevice = new SparseArray<>();
+ private final UserManagerInternal mUserManagerInternal;
+
+ private final boolean mVisibleBackgroundUsersEnabled = isVisibleBackgroundUsersEnabled();
+
KeyGestureController(Context context, Looper looper, InputDataStore inputDataStore) {
mContext = context;
mHandler = new Handler(looper, this::handleMessage);
@@ -180,6 +187,7 @@ final class KeyGestureController {
mAppLaunchShortcutManager = new AppLaunchShortcutManager(mContext);
mInputGestureManager = new InputGestureManager(mContext);
mInputDataStore = inputDataStore;
+ mUserManagerInternal = LocalServices.getService(UserManagerInternal.class);
initBehaviors();
initKeyCombinationRules();
}
@@ -449,6 +457,9 @@ final class KeyGestureController {
}
public boolean interceptKeyBeforeQueueing(KeyEvent event, int policyFlags) {
+ if (mVisibleBackgroundUsersEnabled && shouldIgnoreKeyEventForVisibleBackgroundUser(event)) {
+ return false;
+ }
final boolean interactive = (policyFlags & FLAG_INTERACTIVE) != 0;
if (InputSettings.doesKeyGestureEventHandlerSupportMultiKeyGestures()
&& (event.getFlags() & KeyEvent.FLAG_FALLBACK) == 0) {
@@ -457,6 +468,24 @@ final class KeyGestureController {
return false;
}
+ private boolean shouldIgnoreKeyEventForVisibleBackgroundUser(KeyEvent event) {
+ final int displayAssignedUserId = mUserManagerInternal.getUserAssignedToDisplay(
+ event.getDisplayId());
+ final int currentUserId;
+ synchronized (mUserLock) {
+ currentUserId = mCurrentUserId;
+ }
+ if (currentUserId != displayAssignedUserId
+ && !KeyEvent.isVisibleBackgroundUserAllowedKey(event.getKeyCode())) {
+ if (DEBUG) {
+ Slog.w(TAG, "Ignored key event [" + event + "] for visible background user ["
+ + displayAssignedUserId + "]");
+ }
+ return true;
+ }
+ return false;
+ }
+
public long interceptKeyBeforeDispatching(IBinder focusedToken, KeyEvent event,
int policyFlags) {
// TODO(b/358569822): Handle shortcuts trigger logic here and pass it to appropriate
@@ -895,7 +924,7 @@ final class KeyGestureController {
private void handleMultiKeyGesture(int[] keycodes,
@KeyGestureEvent.KeyGestureType int gestureType, int action, int flags) {
handleKeyGesture(KeyCharacterMap.VIRTUAL_KEYBOARD, keycodes, /* modifierState= */0,
- gestureType, action, Display.DEFAULT_DISPLAY, /* focusedToken = */null, flags,
+ gestureType, action, DEFAULT_DISPLAY, /* focusedToken = */null, flags,
/* appLaunchData = */null);
}
@@ -903,7 +932,7 @@ final class KeyGestureController {
@Nullable AppLaunchData appLaunchData) {
handleKeyGesture(KeyCharacterMap.VIRTUAL_KEYBOARD, new int[0], /* modifierState= */0,
keyGestureType, KeyGestureEvent.ACTION_GESTURE_COMPLETE,
- Display.DEFAULT_DISPLAY, /* focusedToken = */null, /* flags = */0, appLaunchData);
+ DEFAULT_DISPLAY, /* focusedToken = */null, /* flags = */0, appLaunchData);
}
@VisibleForTesting
@@ -915,6 +944,11 @@ final class KeyGestureController {
}
private boolean handleKeyGesture(AidlKeyGestureEvent event, @Nullable IBinder focusedToken) {
+ if (mVisibleBackgroundUsersEnabled && event.displayId != DEFAULT_DISPLAY
+ && shouldIgnoreGestureEventForVisibleBackgroundUser(event.gestureType,
+ event.displayId)) {
+ return false;
+ }
synchronized (mKeyGestureHandlerRecords) {
for (KeyGestureHandlerRecord handler : mKeyGestureHandlerRecords.values()) {
if (handler.handleKeyGesture(event, focusedToken)) {
@@ -927,6 +961,24 @@ final class KeyGestureController {
return false;
}
+ private boolean shouldIgnoreGestureEventForVisibleBackgroundUser(
+ @KeyGestureEvent.KeyGestureType int gestureType, int displayId) {
+ final int displayAssignedUserId = mUserManagerInternal.getUserAssignedToDisplay(displayId);
+ final int currentUserId;
+ synchronized (mUserLock) {
+ currentUserId = mCurrentUserId;
+ }
+ if (currentUserId != displayAssignedUserId
+ && !KeyGestureEvent.isVisibleBackgrounduserAllowedGesture(gestureType)) {
+ if (DEBUG) {
+ Slog.w(TAG, "Ignored gesture event [" + gestureType
+ + "] for visible background user [" + displayAssignedUserId + "]");
+ }
+ return true;
+ }
+ return false;
+ }
+
private boolean isKeyGestureSupported(@KeyGestureEvent.KeyGestureType int gestureType) {
synchronized (mKeyGestureHandlerRecords) {
for (KeyGestureHandlerRecord handler : mKeyGestureHandlerRecords.values()) {
@@ -943,7 +995,7 @@ final class KeyGestureController {
// TODO(b/358569822): Once we move the gesture detection logic to IMS, we ideally
// should not rely on PWM to tell us about the gesture start and end.
AidlKeyGestureEvent event = createKeyGestureEvent(deviceId, keycodes, modifierState,
- gestureType, KeyGestureEvent.ACTION_GESTURE_COMPLETE, Display.DEFAULT_DISPLAY,
+ gestureType, KeyGestureEvent.ACTION_GESTURE_COMPLETE, DEFAULT_DISPLAY,
/* flags = */0, /* appLaunchData = */null);
mHandler.obtainMessage(MSG_NOTIFY_KEY_GESTURE_EVENT, event).sendToTarget();
}
@@ -951,7 +1003,7 @@ final class KeyGestureController {
public void handleKeyGesture(int deviceId, int[] keycodes, int modifierState,
@KeyGestureEvent.KeyGestureType int gestureType) {
AidlKeyGestureEvent event = createKeyGestureEvent(deviceId, keycodes, modifierState,
- gestureType, KeyGestureEvent.ACTION_GESTURE_COMPLETE, Display.DEFAULT_DISPLAY,
+ gestureType, KeyGestureEvent.ACTION_GESTURE_COMPLETE, DEFAULT_DISPLAY,
/* flags = */0, /* appLaunchData = */null);
handleKeyGesture(event, null /*focusedToken*/);
}
@@ -1069,6 +1121,18 @@ final class KeyGestureController {
}
@BinderThread
+ @Nullable
+ public AidlInputGestureData getInputGesture(@UserIdInt int userId,
+ @NonNull AidlInputGestureData.Trigger trigger) {
+ InputGestureData gestureData = mInputGestureManager.getInputGesture(userId,
+ InputGestureData.createTriggerFromAidlTrigger(trigger));
+ if (gestureData == null) {
+ return null;
+ }
+ return gestureData.getAidlData();
+ }
+
+ @BinderThread
@InputManager.CustomInputGestureResult
public int addCustomInputGesture(@UserIdInt int userId,
@NonNull AidlInputGestureData inputGestureData) {
diff --git a/services/core/java/com/android/server/inputmethod/ImeVisibilityStateComputer.java b/services/core/java/com/android/server/inputmethod/ImeVisibilityStateComputer.java
index b0dff22c6f03..281db0ae9518 100644
--- a/services/core/java/com/android/server/inputmethod/ImeVisibilityStateComputer.java
+++ b/services/core/java/com/android/server/inputmethod/ImeVisibilityStateComputer.java
@@ -387,8 +387,9 @@ public final class ImeVisibilityStateComputer {
@GuardedBy("ImfLock.class")
void setWindowState(IBinder windowToken, @NonNull ImeTargetWindowState newState) {
final ImeTargetWindowState state = mRequestWindowStateMap.get(windowToken);
- if (state != null && newState.hasEditorFocused()
- && newState.mToolType != MotionEvent.TOOL_TYPE_STYLUS) {
+ if (state != null && newState.hasEditorFocused() && (
+ newState.mToolType != MotionEvent.TOOL_TYPE_STYLUS
+ || Flags.refactorInsetsController())) {
// Inherit the last requested IME visible state when the target window is still
// focused with an editor.
newState.setRequestedImeVisible(state.mRequestedImeVisible);
diff --git a/services/core/java/com/android/server/location/contexthub/ContextHubEndpointBroker.java b/services/core/java/com/android/server/location/contexthub/ContextHubEndpointBroker.java
index 87d809b5e850..1e54beeb2d64 100644
--- a/services/core/java/com/android/server/location/contexthub/ContextHubEndpointBroker.java
+++ b/services/core/java/com/android/server/location/contexthub/ContextHubEndpointBroker.java
@@ -32,7 +32,10 @@ import android.hardware.location.ContextHubTransaction;
import android.hardware.location.IContextHubTransactionCallback;
import android.os.Binder;
import android.os.IBinder;
+import android.os.PowerManager;
+import android.os.PowerManager.WakeLock;
import android.os.RemoteException;
+import android.os.WorkSource;
import android.util.Log;
import android.util.SparseArray;
@@ -54,6 +57,16 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub
/** Message used by noteOp when this client receives a message from an endpoint. */
private static final String RECEIVE_MSG_NOTE = "ContextHubEndpointMessageDelivery";
+ /** The duration of wakelocks acquired during HAL callbacks */
+ private static final long WAKELOCK_TIMEOUT_MILLIS = 5 * 1000;
+
+ /*
+ * Internal interface used to invoke client callbacks.
+ */
+ interface CallbackConsumer {
+ void accept(IContextHubEndpointCallback callback) throws RemoteException;
+ }
+
/** The context of the service. */
private final Context mContext;
@@ -134,6 +147,9 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub
private final int mUid;
+ /** Wakelock held while nanoapp message are in flight to the client */
+ private final WakeLock mWakeLock;
+
/* package */ ContextHubEndpointBroker(
Context context,
IEndpointCommunication hubInterface,
@@ -158,6 +174,11 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub
mAppOpsManager = context.getSystemService(AppOpsManager.class);
mAppOpsManager.startWatchingMode(AppOpsManager.OP_NONE, mPackageName, this);
+
+ PowerManager powerManager = context.getSystemService(PowerManager.class);
+ mWakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);
+ mWakeLock.setWorkSource(new WorkSource(mUid, mPackageName));
+ mWakeLock.setReferenceCounted(true);
}
@Override
@@ -227,6 +248,7 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub
}
}
mEndpointManager.unregisterEndpoint(mEndpointInfo.getIdentifier().getEndpoint());
+ releaseWakeLockOnExit();
}
@Override
@@ -302,6 +324,13 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub
}
}
+ @Override
+ @android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_CONTEXT_HUB)
+ public void onCallbackFinished() {
+ super.onCallbackFinished_enforcePermission();
+ releaseWakeLock();
+ }
+
/** Invoked when the underlying binder of this broker has died at the client process. */
@Override
public void binderDied() {
@@ -357,15 +386,13 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub
mSessionInfoMap.put(sessionId, new SessionInfo(initiator, true));
}
- if (mContextHubEndpointCallback != null) {
- try {
- mContextHubEndpointCallback.onSessionOpenRequest(
- sessionId, initiator, serviceDescriptor);
- } catch (RemoteException e) {
- Log.e(TAG, "RemoteException while calling onSessionOpenRequest", e);
- cleanupSessionResources(sessionId);
- return;
- }
+ boolean success =
+ invokeCallback(
+ (consumer) ->
+ consumer.onSessionOpenRequest(
+ sessionId, initiator, serviceDescriptor));
+ if (!success) {
+ cleanupSessionResources(sessionId);
}
}
@@ -374,14 +401,11 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub
Log.w(TAG, "Unknown session ID in onCloseEndpointSession: id=" + sessionId);
return;
}
- if (mContextHubEndpointCallback != null) {
- try {
- mContextHubEndpointCallback.onSessionClosed(
- sessionId, ContextHubServiceUtil.toAppHubEndpointReason(reason));
- } catch (RemoteException e) {
- Log.e(TAG, "RemoteException while calling onSessionClosed", e);
- }
- }
+
+ invokeCallback(
+ (consumer) ->
+ consumer.onSessionClosed(
+ sessionId, ContextHubServiceUtil.toAppHubEndpointReason(reason)));
}
/* package */ void onEndpointSessionOpenComplete(int sessionId) {
@@ -392,16 +416,30 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub
}
mSessionInfoMap.get(sessionId).setSessionState(SessionInfo.SessionState.ACTIVE);
}
- if (mContextHubEndpointCallback != null) {
- try {
- mContextHubEndpointCallback.onSessionOpenComplete(sessionId);
- } catch (RemoteException e) {
- Log.e(TAG, "RemoteException while calling onSessionClosed", e);
- }
- }
+
+ invokeCallback((consumer) -> consumer.onSessionOpenComplete(sessionId));
}
/* package */ void onMessageReceived(int sessionId, HubMessage message) {
+ byte code = onMessageReceivedInternal(sessionId, message);
+ if (code != ErrorCode.OK && message.isResponseRequired()) {
+ sendMessageDeliveryStatus(
+ sessionId, message.getMessageSequenceNumber(), code);
+ }
+ }
+
+ /* package */ void onMessageDeliveryStatusReceived(
+ int sessionId, int sequenceNumber, byte errorCode) {
+ mTransactionManager.onMessageDeliveryResponse(sequenceNumber, errorCode == ErrorCode.OK);
+ }
+
+ /* package */ boolean hasSessionId(int sessionId) {
+ synchronized (mOpenSessionLock) {
+ return mSessionInfoMap.contains(sessionId);
+ }
+ }
+
+ private byte onMessageReceivedInternal(int sessionId, HubMessage message) {
HubEndpointInfo remote;
synchronized (mOpenSessionLock) {
if (!isSessionActive(sessionId)) {
@@ -411,9 +449,7 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub
+ sessionId
+ ") with message: "
+ message);
- sendMessageDeliveryStatus(
- sessionId, message.getMessageSequenceNumber(), ErrorCode.PERMANENT_ERROR);
- return;
+ return ErrorCode.PERMANENT_ERROR;
}
remote = mSessionInfoMap.get(sessionId).getRemoteEndpointInfo();
}
@@ -435,31 +471,12 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub
+ ". "
+ mPackageName
+ " doesn't have permission");
- sendMessageDeliveryStatus(
- sessionId, message.getMessageSequenceNumber(), ErrorCode.PERMISSION_DENIED);
- return;
- }
-
- if (mContextHubEndpointCallback != null) {
- try {
- mContextHubEndpointCallback.onMessageReceived(sessionId, message);
- } catch (RemoteException e) {
- Log.e(TAG, "RemoteException while calling onMessageReceived", e);
- sendMessageDeliveryStatus(
- sessionId, message.getMessageSequenceNumber(), ErrorCode.TRANSIENT_ERROR);
- }
+ return ErrorCode.PERMISSION_DENIED;
}
- }
-
- /* package */ void onMessageDeliveryStatusReceived(
- int sessionId, int sequenceNumber, byte errorCode) {
- mTransactionManager.onMessageDeliveryResponse(sequenceNumber, errorCode == ErrorCode.OK);
- }
- /* package */ boolean hasSessionId(int sessionId) {
- synchronized (mOpenSessionLock) {
- return mSessionInfoMap.contains(sessionId);
- }
+ boolean success =
+ invokeCallback((consumer) -> consumer.onMessageReceived(sessionId, message));
+ return success ? ErrorCode.OK : ErrorCode.TRANSIENT_ERROR;
}
/**
@@ -520,4 +537,63 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub
Collection<String> requiredPermissions = targetEndpointInfo.getRequiredPermissions();
return ContextHubServiceUtil.hasPermissions(mContext, mPid, mUid, requiredPermissions);
}
+
+ private void acquireWakeLock() {
+ Binder.withCleanCallingIdentity(
+ () -> {
+ if (mIsRegistered.get()) {
+ mWakeLock.acquire(WAKELOCK_TIMEOUT_MILLIS);
+ }
+ });
+ }
+
+ private void releaseWakeLock() {
+ Binder.withCleanCallingIdentity(
+ () -> {
+ if (mWakeLock.isHeld()) {
+ try {
+ mWakeLock.release();
+ } catch (RuntimeException e) {
+ Log.e(TAG, "Releasing the wakelock fails - ", e);
+ }
+ }
+ });
+ }
+
+ private void releaseWakeLockOnExit() {
+ Binder.withCleanCallingIdentity(
+ () -> {
+ while (mWakeLock.isHeld()) {
+ try {
+ mWakeLock.release();
+ } catch (RuntimeException e) {
+ Log.e(
+ TAG,
+ "Releasing the wakelock for all acquisitions fails - ",
+ e);
+ break;
+ }
+ }
+ });
+ }
+
+ /**
+ * Invokes a callback and acquires a wakelock.
+ *
+ * @param consumer The callback invoke
+ * @return false if the callback threw a RemoteException
+ */
+ private boolean invokeCallback(CallbackConsumer consumer) {
+ if (mContextHubEndpointCallback != null) {
+ acquireWakeLock();
+ try {
+ consumer.accept(mContextHubEndpointCallback);
+ } catch (RemoteException e) {
+ Log.e(TAG, "RemoteException while calling endpoint callback", e);
+ releaseWakeLock();
+ return false;
+ }
+ }
+ return true;
+ }
}
diff --git a/services/core/java/com/android/server/location/fudger/LocationFudger.java b/services/core/java/com/android/server/location/fudger/LocationFudger.java
index 27577641ad1d..28e21b71dcc9 100644
--- a/services/core/java/com/android/server/location/fudger/LocationFudger.java
+++ b/services/core/java/com/android/server/location/fudger/LocationFudger.java
@@ -302,6 +302,15 @@ public class LocationFudger {
// requires latitude since longitudinal distances change with distance from equator.
private static double metersToDegreesLongitude(double distance, double lat) {
- return distance / APPROXIMATE_METERS_PER_DEGREE_AT_EQUATOR / Math.cos(Math.toRadians(lat));
+ // Needed to convert from longitude distance to longitude degree.
+ // X meters near the poles is more degrees than at the equator.
+ double cosLat = Math.cos(Math.toRadians(lat));
+ // If we are right on top of the pole, the degree is always 0.
+ // We return a very small value instead to avoid divide by zero errors
+ // later on.
+ if (cosLat == 0.0) {
+ return 0.0001;
+ }
+ return distance / APPROXIMATE_METERS_PER_DEGREE_AT_EQUATOR / cosLat;
}
}
diff --git a/services/core/java/com/android/server/locksettings/LockSettingsService.java b/services/core/java/com/android/server/locksettings/LockSettingsService.java
index 286238e7888c..0d0cdd83cc73 100644
--- a/services/core/java/com/android/server/locksettings/LockSettingsService.java
+++ b/services/core/java/com/android/server/locksettings/LockSettingsService.java
@@ -438,9 +438,9 @@ public class LockSettingsService extends ILockSettings.Stub {
}
LockscreenCredential credential =
LockscreenCredential.createUnifiedProfilePassword(newPassword);
- Arrays.fill(newPasswordChars, '\u0000');
- Arrays.fill(newPassword, (byte) 0);
- Arrays.fill(randomLockSeed, (byte) 0);
+ LockPatternUtils.zeroize(newPasswordChars);
+ LockPatternUtils.zeroize(newPassword);
+ LockPatternUtils.zeroize(randomLockSeed);
return credential;
}
@@ -1537,7 +1537,7 @@ public class LockSettingsService extends ILockSettings.Stub {
+ userId);
}
} finally {
- Arrays.fill(password, (byte) 0);
+ LockPatternUtils.zeroize(password);
}
}
@@ -1570,7 +1570,7 @@ public class LockSettingsService extends ILockSettings.Stub {
decryptionResult = cipher.doFinal(encryptedPassword);
LockscreenCredential credential = LockscreenCredential.createUnifiedProfilePassword(
decryptionResult);
- Arrays.fill(decryptionResult, (byte) 0);
+ LockPatternUtils.zeroize(decryptionResult);
try {
long parentSid = getGateKeeperService().getSecureUserId(
mUserManager.getProfileParent(userId).id);
@@ -2263,7 +2263,7 @@ public class LockSettingsService extends ILockSettings.Stub {
} catch (RemoteException e) {
Slogf.wtf(TAG, e, "Failed to unlock CE storage for %s user %d", userType, userId);
} finally {
- Arrays.fill(secret, (byte) 0);
+ LockPatternUtils.zeroize(secret);
}
}
diff --git a/services/core/java/com/android/server/locksettings/UnifiedProfilePasswordCache.java b/services/core/java/com/android/server/locksettings/UnifiedProfilePasswordCache.java
index 21caf76d30d0..3d64f1890073 100644
--- a/services/core/java/com/android/server/locksettings/UnifiedProfilePasswordCache.java
+++ b/services/core/java/com/android/server/locksettings/UnifiedProfilePasswordCache.java
@@ -26,6 +26,7 @@ import android.util.SparseArray;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.ArrayUtils;
+import com.android.internal.widget.LockPatternUtils;
import com.android.internal.widget.LockscreenCredential;
import java.security.GeneralSecurityException;
@@ -154,7 +155,7 @@ public class UnifiedProfilePasswordCache {
}
LockscreenCredential result =
LockscreenCredential.createUnifiedProfilePassword(credential);
- Arrays.fill(credential, (byte) 0);
+ LockPatternUtils.zeroize(credential);
return result;
}
}
@@ -175,7 +176,7 @@ public class UnifiedProfilePasswordCache {
Slog.d(TAG, "Cannot delete key", e);
}
if (mEncryptedPasswords.contains(userId)) {
- Arrays.fill(mEncryptedPasswords.get(userId), (byte) 0);
+ LockPatternUtils.zeroize(mEncryptedPasswords.get(userId));
mEncryptedPasswords.remove(userId);
}
}
diff --git a/services/core/java/com/android/server/locksettings/recoverablekeystore/KeySyncTask.java b/services/core/java/com/android/server/locksettings/recoverablekeystore/KeySyncTask.java
index bf1b3c3f0b35..85dc811a7811 100644
--- a/services/core/java/com/android/server/locksettings/recoverablekeystore/KeySyncTask.java
+++ b/services/core/java/com/android/server/locksettings/recoverablekeystore/KeySyncTask.java
@@ -162,7 +162,7 @@ public class KeySyncTask implements Runnable {
Log.e(TAG, "Unexpected exception thrown during KeySyncTask", e);
} finally {
if (mCredential != null) {
- Arrays.fill(mCredential, (byte) 0); // no longer needed.
+ LockPatternUtils.zeroize(mCredential); // no longer needed.
}
}
}
@@ -506,7 +506,7 @@ public class KeySyncTask implements Runnable {
try {
byte[] hash = MessageDigest.getInstance(LOCK_SCREEN_HASH_ALGORITHM).digest(bytes);
- Arrays.fill(bytes, (byte) 0);
+ LockPatternUtils.zeroize(bytes);
return hash;
} catch (NoSuchAlgorithmException e) {
// Impossible, SHA-256 must be supported on Android.
diff --git a/services/core/java/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManager.java b/services/core/java/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManager.java
index 54303c01890a..7d8300a8148a 100644
--- a/services/core/java/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManager.java
+++ b/services/core/java/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManager.java
@@ -1082,7 +1082,7 @@ public class RecoverableKeyStoreManager {
int keyguardCredentialsType = lockPatternUtilsToKeyguardType(savedCredentialType);
try (LockscreenCredential credential =
createLockscreenCredential(keyguardCredentialsType, decryptedCredentials)) {
- Arrays.fill(decryptedCredentials, (byte) 0);
+ LockPatternUtils.zeroize(decryptedCredentials);
decryptedCredentials = null;
VerifyCredentialResponse verifyResponse =
lockSettingsService.verifyCredential(credential, userId, 0);
diff --git a/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverySessionStorage.java b/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverySessionStorage.java
index 0e66746f4160..f1ef333d223a 100644
--- a/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverySessionStorage.java
+++ b/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverySessionStorage.java
@@ -19,8 +19,9 @@ package com.android.server.locksettings.recoverablekeystore.storage;
import android.annotation.Nullable;
import android.util.SparseArray;
+import com.android.internal.widget.LockPatternUtils;
+
import java.util.ArrayList;
-import java.util.Arrays;
import javax.security.auth.Destroyable;
@@ -187,8 +188,8 @@ public class RecoverySessionStorage implements Destroyable {
*/
@Override
public void destroy() {
- Arrays.fill(mLskfHash, (byte) 0);
- Arrays.fill(mKeyClaimant, (byte) 0);
+ LockPatternUtils.zeroize(mLskfHash);
+ LockPatternUtils.zeroize(mKeyClaimant);
}
}
}
diff --git a/services/core/java/com/android/server/media/MediaRouterService.java b/services/core/java/com/android/server/media/MediaRouterService.java
index 68e195d7f079..35bb19943a24 100644
--- a/services/core/java/com/android/server/media/MediaRouterService.java
+++ b/services/core/java/com/android/server/media/MediaRouterService.java
@@ -302,7 +302,9 @@ public final class MediaRouterService extends IMediaRouterService.Stub
final long token = Binder.clearCallingIdentity();
try {
- mAudioService.setBluetoothA2dpOn(on);
+ if (!Flags.disableSetBluetoothAd2pOnCalls()) {
+ mAudioService.setBluetoothA2dpOn(on);
+ }
} catch (RemoteException ex) {
Slog.w(TAG, "RemoteException while calling setBluetoothA2dpOn. on=" + on);
} finally {
@@ -677,7 +679,9 @@ public final class MediaRouterService extends IMediaRouterService.Stub
if (DEBUG) {
Slog.d(TAG, "restoreBluetoothA2dp(" + a2dpOn + ")");
}
- mAudioService.setBluetoothA2dpOn(a2dpOn);
+ if (!Flags.disableSetBluetoothAd2pOnCalls()) {
+ mAudioService.setBluetoothA2dpOn(a2dpOn);
+ }
}
} catch (RemoteException e) {
Slog.w(TAG, "RemoteException while calling setBluetoothA2dpOn.");
diff --git a/services/core/java/com/android/server/media/TEST_MAPPING b/services/core/java/com/android/server/media/TEST_MAPPING
index 43e2afd8827d..dbf9915c6e0c 100644
--- a/services/core/java/com/android/server/media/TEST_MAPPING
+++ b/services/core/java/com/android/server/media/TEST_MAPPING
@@ -1,7 +1,10 @@
{
"presubmit": [
{
- "name": "CtsMediaBetterTogetherTestCases"
+ "name": "CtsMediaRouterTestCases"
+ },
+ {
+ "name": "CtsMediaSessionTestCases"
},
{
"name": "MediaRouterServiceTests"
diff --git a/services/core/java/com/android/server/media/quality/MediaQualityService.java b/services/core/java/com/android/server/media/quality/MediaQualityService.java
index 86fc732e9d04..d440d3ab3521 100644
--- a/services/core/java/com/android/server/media/quality/MediaQualityService.java
+++ b/services/core/java/com/android/server/media/quality/MediaQualityService.java
@@ -21,6 +21,7 @@ import android.content.Context;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
+import android.hardware.tv.mediaquality.IMediaQuality;
import android.media.quality.AmbientBacklightSettings;
import android.media.quality.IAmbientBacklightCallback;
import android.media.quality.IMediaQualityManager;
@@ -35,9 +36,11 @@ import android.media.quality.SoundProfile;
import android.media.quality.SoundProfileHandle;
import android.os.Binder;
import android.os.Bundle;
+import android.os.IBinder;
import android.os.PersistableBundle;
import android.os.RemoteCallbackList;
import android.os.RemoteException;
+import android.os.ServiceManager;
import android.os.UserHandle;
import android.util.Log;
import android.util.Pair;
@@ -45,6 +48,7 @@ import android.util.Slog;
import android.util.SparseArray;
import com.android.server.SystemService;
+import com.android.server.utils.Slogf;
import org.json.JSONException;
import org.json.JSONObject;
@@ -74,6 +78,7 @@ public class MediaQualityService extends SystemService {
private final BiMap<Long, String> mSoundProfileTempIdMap;
private final PackageManager mPackageManager;
private final SparseArray<UserState> mUserStates = new SparseArray<>();
+ private IMediaQuality mMediaQuality;
public MediaQualityService(Context context) {
super(context);
@@ -88,6 +93,12 @@ public class MediaQualityService extends SystemService {
@Override
public void onStart() {
+ IBinder binder = ServiceManager.getService(IMediaQuality.DESCRIPTOR + "/default");
+ if (binder != null) {
+ Slogf.d(TAG, "binder is not null");
+ mMediaQuality = IMediaQuality.Stub.asInterface(binder);
+ }
+
publishBinderService(Context.MEDIA_QUALITY_SERVICE, new BinderService());
}
@@ -809,10 +820,29 @@ public class MediaQualityService extends SystemService {
if (!hasGlobalPictureQualityServicePermission()) {
//TODO: error handling
}
+
+ try {
+ if (mMediaQuality != null) {
+ mMediaQuality.setAutoPqEnabled(enabled);
+ }
+ } catch (UnsupportedOperationException e) {
+ Slog.e(TAG, "The current device is not supported");
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Failed to set auto picture quality", e);
+ }
}
@Override
public boolean isAutoPictureQualityEnabled(UserHandle user) {
+ try {
+ if (mMediaQuality != null) {
+ return mMediaQuality.getAutoPqEnabled();
+ }
+ } catch (UnsupportedOperationException e) {
+ Slog.e(TAG, "The current device is not supported");
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Failed to get auto picture quality", e);
+ }
return false;
}
@@ -821,10 +851,29 @@ public class MediaQualityService extends SystemService {
if (!hasGlobalPictureQualityServicePermission()) {
//TODO: error handling
}
+
+ try {
+ if (mMediaQuality != null) {
+ mMediaQuality.setAutoSrEnabled(enabled);
+ }
+ } catch (UnsupportedOperationException e) {
+ Slog.e(TAG, "The current device is not supported");
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Failed to set auto super resolution", e);
+ }
}
@Override
public boolean isSuperResolutionEnabled(UserHandle user) {
+ try {
+ if (mMediaQuality != null) {
+ return mMediaQuality.getAutoSrEnabled();
+ }
+ } catch (UnsupportedOperationException e) {
+ Slog.e(TAG, "The current device is not supported");
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Failed to get auto super resolution", e);
+ }
return false;
}
@@ -833,10 +882,29 @@ public class MediaQualityService extends SystemService {
if (!hasGlobalSoundQualityServicePermission()) {
//TODO: error handling
}
+
+ try {
+ if (mMediaQuality != null) {
+ mMediaQuality.setAutoAqEnabled(enabled);
+ }
+ } catch (UnsupportedOperationException e) {
+ Slog.e(TAG, "The current device is not supported");
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Failed to set auto audio quality", e);
+ }
}
@Override
public boolean isAutoSoundQualityEnabled(UserHandle user) {
+ try {
+ if (mMediaQuality != null) {
+ return mMediaQuality.getAutoAqEnabled();
+ }
+ } catch (UnsupportedOperationException e) {
+ Slog.e(TAG, "The current device is not supported");
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Failed to get auto audio quality", e);
+ }
return false;
}
diff --git a/services/core/java/com/android/server/notification/GroupHelper.java b/services/core/java/com/android/server/notification/GroupHelper.java
index 4b41696a4390..e47f8ae9d3a5 100644
--- a/services/core/java/com/android/server/notification/GroupHelper.java
+++ b/services/core/java/com/android/server/notification/GroupHelper.java
@@ -583,6 +583,15 @@ public class GroupHelper {
final FullyQualifiedGroupKey fullAggregateGroupKey = new FullyQualifiedGroupKey(
record.getUserId(), pkgName, sectioner);
+ // The notification was part of a different section => trigger regrouping
+ final FullyQualifiedGroupKey prevSectionKey = getPreviousValidSectionKey(record);
+ if (prevSectionKey != null && !fullAggregateGroupKey.equals(prevSectionKey)) {
+ if (DEBUG) {
+ Slog.i(TAG, "Section changed for: " + record);
+ }
+ maybeUngroupOnSectionChanged(record, prevSectionKey);
+ }
+
// This notification is already aggregated
if (record.getGroupKey().equals(fullAggregateGroupKey.toString())) {
return false;
@@ -652,10 +661,33 @@ public class GroupHelper {
}
/**
+ * A notification was added that was previously part of a different section and needs to trigger
+ * GH state cleanup.
+ */
+ private void maybeUngroupOnSectionChanged(NotificationRecord record,
+ FullyQualifiedGroupKey prevSectionKey) {
+ maybeUngroupWithSections(record, prevSectionKey);
+ if (record.getGroupKey().equals(prevSectionKey.toString())) {
+ record.setOverrideGroupKey(null);
+ }
+ }
+
+ /**
* A notification was added that is app-grouped.
*/
private void maybeUngroupOnAppGrouped(NotificationRecord record) {
- maybeUngroupWithSections(record, getSectionGroupKeyWithFallback(record));
+ FullyQualifiedGroupKey currentSectionKey = getSectionGroupKeyWithFallback(record);
+
+ // The notification was part of a different section => trigger regrouping
+ final FullyQualifiedGroupKey prevSectionKey = getPreviousValidSectionKey(record);
+ if (prevSectionKey != null && !prevSectionKey.equals(currentSectionKey)) {
+ if (DEBUG) {
+ Slog.i(TAG, "Section changed for: " + record);
+ }
+ currentSectionKey = prevSectionKey;
+ }
+
+ maybeUngroupWithSections(record, currentSectionKey);
}
/**
diff --git a/services/core/java/com/android/server/notification/NotificationDelegate.java b/services/core/java/com/android/server/notification/NotificationDelegate.java
index 7cbbe2938fd5..5a425057ea89 100644
--- a/services/core/java/com/android/server/notification/NotificationDelegate.java
+++ b/services/core/java/com/android/server/notification/NotificationDelegate.java
@@ -107,4 +107,9 @@ public interface NotificationDelegate {
* @param key the notification key
*/
void unbundleNotification(String key);
+ /**
+ * Called when the notification should be rebundled.
+ * @param key the notification key
+ */
+ void rebundleNotification(String key);
}
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index 9567c818fa18..341038f878d9 100644
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -1888,6 +1888,36 @@ public class NotificationManagerService extends SystemService {
}
}
}
+
+ @Override
+ public void rebundleNotification(String key) {
+ if (!(notificationClassification() && notificationRegroupOnClassification())) {
+ return;
+ }
+ synchronized (mNotificationLock) {
+ NotificationRecord r = mNotificationsByKey.get(key);
+ if (r == null) {
+ return;
+ }
+
+ if (DBG) {
+ Slog.v(TAG, "rebundleNotification: " + r);
+ }
+
+ if (r.getBundleType() != Adjustment.TYPE_OTHER) {
+ final Bundle classifBundle = new Bundle();
+ classifBundle.putInt(KEY_TYPE, r.getBundleType());
+ Adjustment adj = new Adjustment(r.getSbn().getPackageName(), r.getKey(),
+ classifBundle, "rebundle", r.getUserId());
+ applyAdjustmentLocked(r, adj, /* isPosted= */ true);
+ mRankingHandler.requestSort();
+ } else {
+ if (DBG) {
+ Slog.w(TAG, "Can't rebundle. No valid bundle type for: " + r);
+ }
+ }
+ }
+ }
};
NotificationManagerPrivate mNotificationManagerPrivate = new NotificationManagerPrivate() {
@@ -3162,6 +3192,7 @@ public class NotificationManagerService extends SystemService {
mAssistants.onBootPhaseAppsCanStart();
mConditionProviders.onBootPhaseAppsCanStart();
mHistoryManager.onBootPhaseAppsCanStart();
+ mPreferencesHelper.onBootPhaseAppsCanStart();
migrateDefaultNAS();
maybeShowInitialReviewPermissionsNotification();
@@ -7133,6 +7164,7 @@ public class NotificationManagerService extends SystemService {
adjustments.putParcelable(KEY_TYPE, newChannel);
logClassificationChannelAdjustmentReceived(r, isPosted, classification);
+ r.setBundleType(classification);
}
}
r.addAdjustment(adjustment);
@@ -9536,7 +9568,8 @@ public class NotificationManagerService extends SystemService {
|| !Objects.equals(oldSbn.getNotification().getGroup(),
n.getNotification().getGroup())
|| oldSbn.getNotification().flags
- != n.getNotification().flags) {
+ != n.getNotification().flags
+ || !old.getChannel().getId().equals(r.getChannel().getId())) {
synchronized (mNotificationLock) {
final String autogroupName =
notificationForceGrouping() ?
diff --git a/services/core/java/com/android/server/notification/NotificationRecord.java b/services/core/java/com/android/server/notification/NotificationRecord.java
index 93f512bc7e17..81af0d8a6d80 100644
--- a/services/core/java/com/android/server/notification/NotificationRecord.java
+++ b/services/core/java/com/android/server/notification/NotificationRecord.java
@@ -36,10 +36,7 @@ import android.app.KeyguardManager;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.Person;
-import android.content.ContentProvider;
-import android.content.ContentResolver;
import android.content.Context;
-import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.PackageManagerInternal;
import android.content.pm.ShortcutInfo;
@@ -48,7 +45,6 @@ import android.media.AudioAttributes;
import android.media.AudioSystem;
import android.metrics.LogMaker;
import android.net.Uri;
-import android.os.Binder;
import android.os.Build;
import android.os.Bundle;
import android.os.IBinder;
@@ -226,6 +222,9 @@ public final class NotificationRecord {
// lifetime extended.
private boolean mCanceledAfterLifetimeExtension = false;
+ // type of the bundle if the notification was classified
+ private @Adjustment.Types int mBundleType = Adjustment.TYPE_OTHER;
+
public NotificationRecord(Context context, StatusBarNotification sbn,
NotificationChannel channel) {
this.sbn = sbn;
@@ -471,6 +470,10 @@ public final class NotificationRecord {
}
}
+ if (android.service.notification.Flags.notificationClassification()) {
+ mBundleType = previous.mBundleType;
+ }
+
// Don't copy importance information or mGlobalSortKey, recompute them.
}
@@ -1493,23 +1496,14 @@ public final class NotificationRecord {
final Notification notification = getNotification();
notification.visitUris((uri) -> {
- if (com.android.server.notification.Flags.notificationVerifyChannelSoundUri()) {
- visitGrantableUri(uri, false, false);
- } else {
- oldVisitGrantableUri(uri, false, false);
- }
+ visitGrantableUri(uri, false, false);
});
if (notification.getChannelId() != null) {
NotificationChannel channel = getChannel();
if (channel != null) {
- if (com.android.server.notification.Flags.notificationVerifyChannelSoundUri()) {
- visitGrantableUri(channel.getSound(), (channel.getUserLockedFields()
- & NotificationChannel.USER_LOCKED_SOUND) != 0, true);
- } else {
- oldVisitGrantableUri(channel.getSound(), (channel.getUserLockedFields()
- & NotificationChannel.USER_LOCKED_SOUND) != 0, true);
- }
+ visitGrantableUri(channel.getSound(), (channel.getUserLockedFields()
+ & NotificationChannel.USER_LOCKED_SOUND) != 0, true);
}
}
} finally {
@@ -1525,53 +1519,6 @@ public final class NotificationRecord {
* {@link #mGrantableUris}. Otherwise, this will either log or throw
* {@link SecurityException} depending on target SDK of enqueuing app.
*/
- private void oldVisitGrantableUri(Uri uri, boolean userOverriddenUri, boolean isSound) {
- if (uri == null || !ContentResolver.SCHEME_CONTENT.equals(uri.getScheme())) return;
-
- if (mGrantableUris != null && mGrantableUris.contains(uri)) {
- return; // already verified this URI
- }
-
- final int sourceUid = getSbn().getUid();
- final long ident = Binder.clearCallingIdentity();
- try {
- // This will throw a SecurityException if the caller can't grant.
- mUgmInternal.checkGrantUriPermission(sourceUid, null,
- ContentProvider.getUriWithoutUserId(uri),
- Intent.FLAG_GRANT_READ_URI_PERMISSION,
- ContentProvider.getUserIdFromUri(uri, UserHandle.getUserId(sourceUid)));
-
- if (mGrantableUris == null) {
- mGrantableUris = new ArraySet<>();
- }
- mGrantableUris.add(uri);
- } catch (SecurityException e) {
- if (!userOverriddenUri) {
- if (isSound) {
- mSound = Settings.System.DEFAULT_NOTIFICATION_URI;
- Log.w(TAG, "Replacing " + uri + " from " + sourceUid + ": " + e.getMessage());
- } else {
- if (mTargetSdkVersion >= Build.VERSION_CODES.P) {
- throw e;
- } else {
- Log.w(TAG,
- "Ignoring " + uri + " from " + sourceUid + ": " + e.getMessage());
- }
- }
- }
- } finally {
- Binder.restoreCallingIdentity(ident);
- }
- }
-
- /**
- * Note the presence of a {@link Uri} that should have permission granted to
- * whoever will be rendering it.
- * <p>
- * If the enqueuing app has the ability to grant access, it will be added to
- * {@link #mGrantableUris}. Otherwise, this will either log or throw
- * {@link SecurityException} depending on target SDK of enqueuing app.
- */
private void visitGrantableUri(Uri uri, boolean userOverriddenUri,
boolean isSound) {
if (mGrantableUris != null && mGrantableUris.contains(uri)) {
@@ -1689,6 +1636,14 @@ public final class NotificationRecord {
mCanceledAfterLifetimeExtension = canceledAfterLifetimeExtension;
}
+ public @Adjustment.Types int getBundleType() {
+ return mBundleType;
+ }
+
+ public void setBundleType(@Adjustment.Types int bundleType) {
+ mBundleType = bundleType;
+ }
+
/**
* Whether this notification is a conversation notification.
*/
diff --git a/services/core/java/com/android/server/notification/PreferencesHelper.java b/services/core/java/com/android/server/notification/PreferencesHelper.java
index 15377d6b269a..36eabae69b22 100644
--- a/services/core/java/com/android/server/notification/PreferencesHelper.java
+++ b/services/core/java/com/android/server/notification/PreferencesHelper.java
@@ -82,7 +82,6 @@ import android.text.format.DateUtils;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.IntArray;
-import android.util.Log;
import android.util.Pair;
import android.util.Slog;
import android.util.SparseBooleanArray;
@@ -272,6 +271,15 @@ public class PreferencesHelper implements RankingConfig {
updateMediaNotificationFilteringEnabled();
}
+ void onBootPhaseAppsCanStart() {
+ // IpcDataCaches must be invalidated once data becomes available, as queries will only
+ // begin to be cached after the first invalidation signal. At this point, we know about all
+ // notification channels.
+ if (android.app.Flags.nmBinderPerfCacheChannels()) {
+ invalidateNotificationChannelCache();
+ }
+ }
+
public void readXml(TypedXmlPullParser parser, boolean forRestore, int userId)
throws XmlPullParserException, IOException {
int type = parser.getEventType();
@@ -531,12 +539,14 @@ public class PreferencesHelper implements RankingConfig {
private PackagePreferences getOrCreatePackagePreferencesLocked(String pkg,
@UserIdInt int userId, int uid, int importance, int priority, int visibility,
boolean showBadge, int bubblePreference, long creationTime) {
+ boolean created = false;
final String key = packagePreferencesKey(pkg, uid);
PackagePreferences
r = (uid == UNKNOWN_UID)
? mRestoredWithoutUids.get(unrestoredPackageKey(pkg, userId))
: mPackagePreferences.get(key);
if (r == null) {
+ created = true;
r = new PackagePreferences();
r.pkg = pkg;
r.uid = uid;
@@ -572,6 +582,9 @@ public class PreferencesHelper implements RankingConfig {
mRestoredWithoutUids.remove(unrestoredPackageKey(pkg, userId));
}
}
+ if (android.app.Flags.nmBinderPerfCacheChannels() && created) {
+ invalidateNotificationChannelCache();
+ }
return r;
}
@@ -664,6 +677,9 @@ public class PreferencesHelper implements RankingConfig {
}
NotificationChannel channel = new NotificationChannel(channelId, label, IMPORTANCE_LOW);
p.channels.put(channelId, channel);
+ if (android.app.Flags.nmBinderPerfCacheChannels()) {
+ invalidateNotificationChannelCache();
+ }
return channel;
}
@@ -1171,9 +1187,7 @@ public class PreferencesHelper implements RankingConfig {
// Verify that the app has permission to read the sound Uri
// Only check for new channels, as regular apps can only set sound
// before creating. See: {@link NotificationChannel#setSound}
- if (Flags.notificationVerifyChannelSoundUri()) {
- PermissionHelper.grantUriPermission(mUgmInternal, channel.getSound(), uid);
- }
+ PermissionHelper.grantUriPermission(mUgmInternal, channel.getSound(), uid);
channel.setImportanceLockedByCriticalDeviceFunction(
r.defaultAppLockedImportance || r.fixedImportance);
@@ -1208,6 +1222,10 @@ public class PreferencesHelper implements RankingConfig {
updateCurrentUserHasChannelsBypassingDnd(callingUid, fromSystemOrSystemUi);
}
+ if (android.app.Flags.nmBinderPerfCacheChannels() && needsPolicyFileChange) {
+ invalidateNotificationChannelCache();
+ }
+
return needsPolicyFileChange;
}
@@ -1229,6 +1247,9 @@ public class PreferencesHelper implements RankingConfig {
}
channel.unlockFields(USER_LOCKED_IMPORTANCE);
}
+ if (android.app.Flags.nmBinderPerfCacheChannels()) {
+ invalidateNotificationChannelCache();
+ }
}
@@ -1301,6 +1322,9 @@ public class PreferencesHelper implements RankingConfig {
updateCurrentUserHasChannelsBypassingDnd(callingUid, fromSystemOrSystemUi);
}
if (changed) {
+ if (android.app.Flags.nmBinderPerfCacheChannels()) {
+ invalidateNotificationChannelCache();
+ }
updateConfig();
}
}
@@ -1537,6 +1561,10 @@ public class PreferencesHelper implements RankingConfig {
if (channelBypassedDnd) {
updateCurrentUserHasChannelsBypassingDnd(callingUid, fromSystemOrSystemUi);
}
+
+ if (android.app.Flags.nmBinderPerfCacheChannels() && deletedChannel) {
+ invalidateNotificationChannelCache();
+ }
return deletedChannel;
}
@@ -1566,6 +1594,9 @@ public class PreferencesHelper implements RankingConfig {
}
r.channels.remove(channelId);
}
+ if (android.app.Flags.nmBinderPerfCacheChannels()) {
+ invalidateNotificationChannelCache();
+ }
}
@Override
@@ -1576,13 +1607,18 @@ public class PreferencesHelper implements RankingConfig {
if (r == null) {
return;
}
+ boolean deleted = false;
int N = r.channels.size() - 1;
for (int i = N; i >= 0; i--) {
String key = r.channels.keyAt(i);
if (!DEFAULT_CHANNEL_ID.equals(key)) {
r.channels.remove(key);
+ deleted = true;
}
}
+ if (android.app.Flags.nmBinderPerfCacheChannels() && deleted) {
+ invalidateNotificationChannelCache();
+ }
}
}
@@ -1613,6 +1649,9 @@ public class PreferencesHelper implements RankingConfig {
}
}
}
+ if (android.app.Flags.nmBinderPerfCacheChannels()) {
+ invalidateNotificationChannelCache();
+ }
}
public void updateDefaultApps(int userId, ArraySet<String> toRemove,
@@ -1642,6 +1681,9 @@ public class PreferencesHelper implements RankingConfig {
}
}
}
+ if (android.app.Flags.nmBinderPerfCacheChannels()) {
+ invalidateNotificationChannelCache();
+ }
}
public NotificationChannelGroup getNotificationChannelGroupWithChannels(String pkg,
@@ -1757,6 +1799,9 @@ public class PreferencesHelper implements RankingConfig {
if (groupBypassedDnd) {
updateCurrentUserHasChannelsBypassingDnd(callingUid, fromSystemOrSystemUi);
}
+ if (android.app.Flags.nmBinderPerfCacheChannels() && deletedChannels.size() > 0) {
+ invalidateNotificationChannelCache();
+ }
return deletedChannels;
}
@@ -1902,8 +1947,13 @@ public class PreferencesHelper implements RankingConfig {
}
}
}
- if (!deletedChannelIds.isEmpty() && mCurrentUserHasChannelsBypassingDnd) {
- updateCurrentUserHasChannelsBypassingDnd(callingUid, fromSystemOrSystemUi);
+ if (!deletedChannelIds.isEmpty()) {
+ if (mCurrentUserHasChannelsBypassingDnd) {
+ updateCurrentUserHasChannelsBypassingDnd(callingUid, fromSystemOrSystemUi);
+ }
+ if (android.app.Flags.nmBinderPerfCacheChannels()) {
+ invalidateNotificationChannelCache();
+ }
}
return deletedChannelIds;
}
@@ -2196,6 +2246,11 @@ public class PreferencesHelper implements RankingConfig {
PackagePreferences prefs = getOrCreatePackagePreferencesLocked(sourcePkg, sourceUid);
prefs.delegate = new Delegate(delegatePkg, delegateUid, true);
}
+ if (android.app.Flags.nmBinderPerfCacheChannels()) {
+ // If package delegates change, then which packages can get what channel information
+ // also changes, so we need to clear the cache.
+ invalidateNotificationChannelCache();
+ }
}
/**
@@ -2208,6 +2263,9 @@ public class PreferencesHelper implements RankingConfig {
prefs.delegate.mEnabled = false;
}
}
+ if (android.app.Flags.nmBinderPerfCacheChannels()) {
+ invalidateNotificationChannelCache();
+ }
}
/**
@@ -2811,18 +2869,24 @@ public class PreferencesHelper implements RankingConfig {
public void onUserRemoved(int userId) {
synchronized (mLock) {
+ boolean removed = false;
int N = mPackagePreferences.size();
for (int i = N - 1; i >= 0; i--) {
PackagePreferences PackagePreferences = mPackagePreferences.valueAt(i);
if (UserHandle.getUserId(PackagePreferences.uid) == userId) {
mPackagePreferences.removeAt(i);
+ removed = true;
}
}
+ if (android.app.Flags.nmBinderPerfCacheChannels() && removed) {
+ invalidateNotificationChannelCache();
+ }
}
}
protected void onLocaleChanged(Context context, int userId) {
synchronized (mLock) {
+ boolean updated = false;
int N = mPackagePreferences.size();
for (int i = 0; i < N; i++) {
PackagePreferences PackagePreferences = mPackagePreferences.valueAt(i);
@@ -2833,10 +2897,14 @@ public class PreferencesHelper implements RankingConfig {
DEFAULT_CHANNEL_ID).setName(
context.getResources().getString(
R.string.default_notification_channel_label));
+ updated = true;
}
// TODO (b/346396459): Localize all reserved channels
}
}
+ if (android.app.Flags.nmBinderPerfCacheChannels() && updated) {
+ invalidateNotificationChannelCache();
+ }
}
}
@@ -2884,7 +2952,7 @@ public class PreferencesHelper implements RankingConfig {
channel.getAudioAttributes().getUsage());
if (Settings.System.DEFAULT_NOTIFICATION_URI.equals(
restoredUri)) {
- Log.w(TAG,
+ Slog.w(TAG,
"Could not restore sound: " + uri + " for channel: "
+ channel);
}
@@ -2922,6 +2990,9 @@ public class PreferencesHelper implements RankingConfig {
if (updated) {
updateConfig();
+ if (android.app.Flags.nmBinderPerfCacheChannels()) {
+ invalidateNotificationChannelCache();
+ }
}
return updated;
}
@@ -2939,6 +3010,9 @@ public class PreferencesHelper implements RankingConfig {
p.priority = DEFAULT_PRIORITY;
p.visibility = DEFAULT_VISIBILITY;
p.showBadge = DEFAULT_SHOW_BADGE;
+ if (android.app.Flags.nmBinderPerfCacheChannels()) {
+ invalidateNotificationChannelCache();
+ }
}
}
}
@@ -3123,6 +3197,9 @@ public class PreferencesHelper implements RankingConfig {
}
}
}
+ if (android.app.Flags.nmBinderPerfCacheChannels()) {
+ invalidateNotificationChannelCache();
+ }
}
public void migrateNotificationPermissions(List<UserInfo> users) {
@@ -3154,6 +3231,12 @@ public class PreferencesHelper implements RankingConfig {
mRankingHandler.requestSort();
}
+ @VisibleForTesting
+ // Utility method for overriding in tests to confirm that the cache gets cleared.
+ protected void invalidateNotificationChannelCache() {
+ NotificationManager.invalidateNotificationChannelCache();
+ }
+
private static String packagePreferencesKey(String pkg, int uid) {
return pkg + "|" + uid;
}
diff --git a/services/core/java/com/android/server/notification/flags.aconfig b/services/core/java/com/android/server/notification/flags.aconfig
index 2b4d71e85dc0..c1ca9c23aef5 100644
--- a/services/core/java/com/android/server/notification/flags.aconfig
+++ b/services/core/java/com/android/server/notification/flags.aconfig
@@ -172,16 +172,6 @@ flag {
}
flag {
- name: "notification_verify_channel_sound_uri"
- namespace: "systemui"
- description: "Verify Uri permission for sound when creating a notification channel"
- bug: "337775777"
- metadata {
- purpose: PURPOSE_BUGFIX
- }
-}
-
-flag {
name: "notification_vibration_in_sound_uri_for_channel"
namespace: "systemui"
description: "Enables sound uri with vibration source in notification channel"
diff --git a/services/core/java/com/android/server/pm/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java
index 4c70d2347fb7..a0fbc008475c 100644
--- a/services/core/java/com/android/server/pm/InstallPackageHelper.java
+++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java
@@ -125,7 +125,6 @@ import android.content.pm.SharedLibraryInfo;
import android.content.pm.Signature;
import android.content.pm.SigningDetails;
import android.content.pm.VerifierInfo;
-import android.content.pm.dex.DexMetadataHelper;
import android.content.pm.parsing.result.ParseResult;
import android.content.pm.parsing.result.ParseTypeImpl;
import android.net.Uri;
@@ -171,7 +170,6 @@ import com.android.internal.pm.pkg.component.ParsedIntentInfo;
import com.android.internal.pm.pkg.component.ParsedPermission;
import com.android.internal.pm.pkg.component.ParsedPermissionGroup;
import com.android.internal.pm.pkg.parsing.ParsingPackageUtils;
-import com.android.internal.security.VerityUtils;
import com.android.internal.util.ArrayUtils;
import com.android.internal.util.CollectionUtils;
import com.android.server.EventLogTags;
@@ -186,7 +184,6 @@ import com.android.server.pm.pkg.AndroidPackage;
import com.android.server.pm.pkg.PackageStateInternal;
import com.android.server.pm.pkg.SharedLibraryWrapper;
import com.android.server.rollback.RollbackManagerInternal;
-import com.android.server.security.FileIntegrityService;
import com.android.server.utils.WatchedArrayMap;
import com.android.server.utils.WatchedLongSparseArray;
@@ -195,7 +192,6 @@ import dalvik.system.VMRuntime;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
-import java.security.DigestException;
import java.security.DigestInputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
@@ -1165,11 +1161,8 @@ final class InstallPackageHelper {
}
try {
doRenameLI(request, parsedPackage);
- setUpFsVerity(parsedPackage);
- } catch (Installer.InstallerException | IOException | DigestException
- | NoSuchAlgorithmException | PrepareFailure e) {
- request.setError(PackageManagerException.INTERNAL_ERROR_VERITY_SETUP,
- "Failed to set up verity: " + e);
+ } catch (PrepareFailure e) {
+ request.setError(e);
return false;
}
@@ -2322,68 +2315,6 @@ final class InstallPackageHelper {
}
}
- /**
- * Set up fs-verity for the given package. For older devices that do not support fs-verity,
- * this is a no-op.
- */
- private void setUpFsVerity(AndroidPackage pkg) throws Installer.InstallerException,
- PrepareFailure, IOException, DigestException, NoSuchAlgorithmException {
- if (!PackageManagerServiceUtils.isApkVerityEnabled()) {
- return;
- }
-
- if (isIncrementalPath(pkg.getPath()) && IncrementalManager.getVersion()
- < IncrementalManager.MIN_VERSION_TO_SUPPORT_FSVERITY) {
- return;
- }
-
- // Collect files we care for fs-verity setup.
- ArrayMap<String, String> fsverityCandidates = new ArrayMap<>();
- fsverityCandidates.put(pkg.getBaseApkPath(),
- VerityUtils.getFsveritySignatureFilePath(pkg.getBaseApkPath()));
-
- final String dmPath = DexMetadataHelper.buildDexMetadataPathForApk(
- pkg.getBaseApkPath());
- if (new File(dmPath).exists()) {
- fsverityCandidates.put(dmPath, VerityUtils.getFsveritySignatureFilePath(dmPath));
- }
-
- for (String path : pkg.getSplitCodePaths()) {
- fsverityCandidates.put(path, VerityUtils.getFsveritySignatureFilePath(path));
-
- final String splitDmPath = DexMetadataHelper.buildDexMetadataPathForApk(path);
- if (new File(splitDmPath).exists()) {
- fsverityCandidates.put(splitDmPath,
- VerityUtils.getFsveritySignatureFilePath(splitDmPath));
- }
- }
-
- var fis = FileIntegrityService.getService();
- for (Map.Entry<String, String> entry : fsverityCandidates.entrySet()) {
- try {
- final String filePath = entry.getKey();
- if (VerityUtils.hasFsverity(filePath)) {
- continue;
- }
-
- final String signaturePath = entry.getValue();
- if (new File(signaturePath).exists()) {
- // If signature is provided, enable fs-verity first so that the file can be
- // measured for signature check below.
- VerityUtils.setUpFsverity(filePath);
-
- if (!fis.verifyPkcs7DetachedSignature(signaturePath, filePath)) {
- throw new PrepareFailure(PackageManager.INSTALL_FAILED_BAD_SIGNATURE,
- "fs-verity signature does not verify against a known key");
- }
- }
- } catch (IOException e) {
- throw new PrepareFailure(PackageManager.INSTALL_FAILED_BAD_SIGNATURE,
- "Failed to enable fs-verity: " + e);
- }
- }
- }
-
private PackageFreezer freezePackageForInstall(String packageName, int userId, int installFlags,
String killReason, int exitInfoReason, InstallRequest request) {
if ((installFlags & PackageManager.INSTALL_DONT_KILL_APP) != 0) {
@@ -3060,6 +2991,8 @@ final class InstallPackageHelper {
}
if (succeeded) {
+ Slog.i(TAG, "installation completed:" + packageName);
+
if (Flags.aslInApkAppMetadataSource()
&& pkgSetting.getAppMetadataSource() == APP_METADATA_SOURCE_APK) {
if (!extractAppMetadataFromApk(request.getPkg(),
diff --git a/services/core/java/com/android/server/pm/PackageInstallerSession.java b/services/core/java/com/android/server/pm/PackageInstallerSession.java
index c6760431116e..1b41c3617a05 100644
--- a/services/core/java/com/android/server/pm/PackageInstallerSession.java
+++ b/services/core/java/com/android/server/pm/PackageInstallerSession.java
@@ -26,7 +26,6 @@ import static android.content.pm.PackageInstaller.UNARCHIVAL_OK;
import static android.content.pm.PackageInstaller.UNARCHIVAL_STATUS_UNSET;
import static android.content.pm.PackageItemInfo.MAX_SAFE_LABEL_LENGTH;
import static android.content.pm.PackageManager.INSTALL_FAILED_ABORTED;
-import static android.content.pm.PackageManager.INSTALL_FAILED_BAD_SIGNATURE;
import static android.content.pm.PackageManager.INSTALL_FAILED_INSUFFICIENT_STORAGE;
import static android.content.pm.PackageManager.INSTALL_FAILED_INTERNAL_ERROR;
import static android.content.pm.PackageManager.INSTALL_FAILED_INVALID_APK;
@@ -824,8 +823,6 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub {
@GuardedBy("mLock")
private File mInheritedFilesBase;
- @GuardedBy("mLock")
- private boolean mVerityFoundForApks;
/**
* Both flags should be guarded with mLock whenever changes need to be in lockstep.
@@ -864,7 +861,6 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub {
} else {
if (DexMetadataHelper.isDexMetadataFile(file)) return false;
}
- if (VerityUtils.isFsveritySignatureFile(file)) return false;
if (ApkChecksums.isDigestOrDigestSignatureFile(file)) return false;
return true;
}
@@ -3565,13 +3561,6 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub {
"Missing existing base package");
}
- // Default to require only if existing base apk has fs-verity signature.
- mVerityFoundForApks = PackageManagerServiceUtils.isApkVerityEnabled()
- && params.mode == SessionParams.MODE_INHERIT_EXISTING
- && VerityUtils.hasFsverity(pkgInfo.applicationInfo.getBaseCodePath())
- && (new File(VerityUtils.getFsveritySignatureFilePath(
- pkgInfo.applicationInfo.getBaseCodePath()))).exists();
-
final List<File> removedFiles = getRemovedFilesLocked();
final List<String> removeSplitList = new ArrayList<>();
if (!removedFiles.isEmpty()) {
@@ -3972,24 +3961,6 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub {
}
@GuardedBy("mLock")
- private void maybeStageFsveritySignatureLocked(File origFile, File targetFile,
- boolean fsVerityRequired) throws PackageManagerException {
- if (android.security.Flags.deprecateFsvSig()) {
- return;
- }
- final File originalSignature = new File(
- VerityUtils.getFsveritySignatureFilePath(origFile.getPath()));
- if (originalSignature.exists()) {
- final File stagedSignature = new File(
- VerityUtils.getFsveritySignatureFilePath(targetFile.getPath()));
- stageFileLocked(originalSignature, stagedSignature);
- } else if (fsVerityRequired) {
- throw new PackageManagerException(INSTALL_FAILED_BAD_SIGNATURE,
- "Missing corresponding fs-verity signature to " + origFile);
- }
- }
-
- @GuardedBy("mLock")
private void maybeStageV4SignatureLocked(File origFile, File targetFile)
throws PackageManagerException {
final File originalSignature = new File(origFile.getPath() + V4Signature.EXT);
@@ -4015,11 +3986,6 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub {
DexMetadataHelper.buildDexMetadataPathForApk(targetFile.getName()));
stageFileLocked(dexMetadataFile, targetDexMetadataFile);
-
- // Also stage .dm.fsv_sig. .dm may be required to install with fs-verity signature on
- // supported on older devices.
- maybeStageFsveritySignatureLocked(dexMetadataFile, targetDexMetadataFile,
- DexMetadataHelper.isFsVerityRequired());
}
@FlaggedApi(com.android.art.flags.Flags.FLAG_ART_SERVICE_V3)
@@ -4105,44 +4071,10 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub {
}
@GuardedBy("mLock")
- private boolean isFsVerityRequiredForApk(File origFile, File targetFile)
- throws PackageManagerException {
- if (mVerityFoundForApks) {
- return true;
- }
-
- // We haven't seen .fsv_sig for any APKs. Treat it as not required until we see one.
- final File originalSignature = new File(
- VerityUtils.getFsveritySignatureFilePath(origFile.getPath()));
- if (!originalSignature.exists()) {
- return false;
- }
- mVerityFoundForApks = true;
-
- // When a signature is found, also check any previous staged APKs since they also need to
- // have fs-verity signature consistently.
- for (File file : mResolvedStagedFiles) {
- if (!file.getName().endsWith(".apk")) {
- continue;
- }
- // Ignore the current targeting file.
- if (targetFile.getName().equals(file.getName())) {
- continue;
- }
- throw new PackageManagerException(INSTALL_FAILED_BAD_SIGNATURE,
- "Previously staged apk is missing fs-verity signature");
- }
- return true;
- }
-
- @GuardedBy("mLock")
private void resolveAndStageFileLocked(File origFile, File targetFile, String splitName,
List<String> artManagedFilePaths) throws PackageManagerException {
stageFileLocked(origFile, targetFile);
- // Stage APK's fs-verity signature if present.
- maybeStageFsveritySignatureLocked(origFile, targetFile,
- isFsVerityRequiredForApk(origFile, targetFile));
// Stage APK's v4 signature if present, and fs-verity is supported.
if (android.security.Flags.extendVbChainToUpdatedApk()
&& VerityUtils.isFsVeritySupported()) {
@@ -4160,16 +4092,6 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub {
}
@GuardedBy("mLock")
- private void maybeInheritFsveritySignatureLocked(File origFile) {
- // Inherit the fsverity signature file if present.
- final File fsveritySignatureFile = new File(
- VerityUtils.getFsveritySignatureFilePath(origFile.getPath()));
- if (fsveritySignatureFile.exists()) {
- mResolvedInheritedFiles.add(fsveritySignatureFile);
- }
- }
-
- @GuardedBy("mLock")
private void maybeInheritV4SignatureLocked(File origFile) {
// Inherit the v4 signature file if present.
final File v4SignatureFile = new File(origFile.getPath() + V4Signature.EXT);
@@ -4182,7 +4104,6 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub {
private void inheritFileLocked(File origFile, List<String> artManagedFilePaths) {
mResolvedInheritedFiles.add(origFile);
- maybeInheritFsveritySignatureLocked(origFile);
if (android.security.Flags.extendVbChainToUpdatedApk()) {
maybeInheritV4SignatureLocked(origFile);
}
@@ -4193,13 +4114,11 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub {
artManagedFilePaths, origFile.getPath())) {
File artManagedFile = new File(path);
mResolvedInheritedFiles.add(artManagedFile);
- maybeInheritFsveritySignatureLocked(artManagedFile);
}
} else {
final File dexMetadataFile = DexMetadataHelper.findDexMetadataForFile(origFile);
if (dexMetadataFile != null) {
mResolvedInheritedFiles.add(dexMetadataFile);
- maybeInheritFsveritySignatureLocked(dexMetadataFile);
}
}
// Inherit the digests if present.
diff --git a/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java b/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java
index 7af39f74d0d6..3e376b6958ec 100644
--- a/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java
+++ b/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java
@@ -520,22 +520,6 @@ public class PackageManagerServiceUtils {
}
}
- /** Default is to not use fs-verity since it depends on kernel support. */
- private static final int FSVERITY_DISABLED = 0;
-
- /** Standard fs-verity. */
- private static final int FSVERITY_ENABLED = 2;
-
- /** Returns true if standard APK Verity is enabled. */
- static boolean isApkVerityEnabled() {
- if (android.security.Flags.deprecateFsvSig()) {
- return false;
- }
- return Build.VERSION.DEVICE_INITIAL_SDK_INT >= Build.VERSION_CODES.R
- || SystemProperties.getInt("ro.apk_verity.mode", FSVERITY_DISABLED)
- == FSVERITY_ENABLED;
- }
-
/**
* Verifies that signatures match.
* @returns {@code true} if the compat signatures were matched; otherwise, {@code false}.
diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java
index 8249d65868cd..81956fbb55e6 100644
--- a/services/core/java/com/android/server/pm/UserManagerService.java
+++ b/services/core/java/com/android/server/pm/UserManagerService.java
@@ -6661,7 +6661,7 @@ public class UserManagerService extends IUserManager.Stub {
+ userId);
}
new Thread(() -> {
- getActivityManagerInternal().onUserRemoved(userId);
+ getActivityManagerInternal().onUserRemoving(userId);
removeUserState(userId);
}).start();
}
@@ -6701,6 +6701,7 @@ public class UserManagerService extends IUserManager.Stub {
synchronized (mUsersLock) {
removeUserDataLU(userId);
mIsUserManaged.delete(userId);
+ getActivityManagerInternal().onUserRemoved(userId);
}
synchronized (mUserStates) {
mUserStates.delete(userId);
diff --git a/services/core/java/com/android/server/power/PowerManagerService.java b/services/core/java/com/android/server/power/PowerManagerService.java
index 23383a9c55c0..f9e4022f04a0 100644
--- a/services/core/java/com/android/server/power/PowerManagerService.java
+++ b/services/core/java/com/android/server/power/PowerManagerService.java
@@ -1395,7 +1395,9 @@ public final class PowerManagerService extends SystemService
DisplayGroupPowerChangeListener displayGroupPowerChangeListener =
new DisplayGroupPowerChangeListener();
mDisplayManagerInternal.registerDisplayGroupListener(displayGroupPowerChangeListener);
- mDisplayManager.registerDisplayListener(new DisplayListener(), mHandler);
+ if (mFeatureFlags.isScreenTimeoutPolicyListenerApiEnabled()) {
+ mDisplayManager.registerDisplayListener(new DisplayListener(), mHandler);
+ }
if(mDreamManager != null){
// This DreamManager method does not acquire a lock, so it should be safe to call.
@@ -3852,6 +3854,10 @@ public final class PowerManagerService extends SystemService
@GuardedBy("mLock")
private void notifyScreenTimeoutPolicyChangesLocked() {
+ if (!mFeatureFlags.isScreenTimeoutPolicyListenerApiEnabled()) {
+ return;
+ }
+
for (int idx = 0; idx < mPowerGroups.size(); idx++) {
final int powerGroupId = mPowerGroups.keyAt(idx);
final PowerGroup powerGroup = mPowerGroups.valueAt(idx);
@@ -6011,6 +6017,11 @@ public final class PowerManagerService extends SystemService
@Override // Binder call
public void addScreenTimeoutPolicyListener(int displayId,
IScreenTimeoutPolicyListener listener) {
+ if (!mFeatureFlags.isScreenTimeoutPolicyListenerApiEnabled()) {
+ throw new IllegalStateException("Screen timeout policy listener API flag "
+ + "is not enabled");
+ }
+
mContext.enforceCallingOrSelfPermission(android.Manifest.permission.DEVICE_POWER,
null);
@@ -6042,6 +6053,11 @@ public final class PowerManagerService extends SystemService
@Override // Binder call
public void removeScreenTimeoutPolicyListener(int displayId,
IScreenTimeoutPolicyListener listener) {
+ if (!mFeatureFlags.isScreenTimeoutPolicyListenerApiEnabled()) {
+ throw new IllegalStateException("Screen timeout policy listener API flag "
+ + "is not enabled");
+ }
+
mContext.enforceCallingOrSelfPermission(android.Manifest.permission.DEVICE_POWER,
null);
diff --git a/services/core/java/com/android/server/power/ThermalManagerService.java b/services/core/java/com/android/server/power/ThermalManagerService.java
index 42dbb7974fe2..f46fa446a0ba 100644
--- a/services/core/java/com/android/server/power/ThermalManagerService.java
+++ b/services/core/java/com/android/server/power/ThermalManagerService.java
@@ -155,6 +155,9 @@ public class ThermalManagerService extends SystemService {
@VisibleForTesting
final TemperatureWatcher mTemperatureWatcher;
+ @VisibleForTesting
+ final AtomicBoolean mIsHalSkinForecastSupported = new AtomicBoolean(false);
+
private final ThermalHalWrapper.WrapperThermalChangedCallback mWrapperCallback =
new ThermalHalWrapper.WrapperThermalChangedCallback() {
@Override
@@ -254,6 +257,18 @@ public class ThermalManagerService extends SystemService {
}
onTemperatureMapChangedLocked();
mTemperatureWatcher.getAndUpdateThresholds();
+ // we only check forecast if a single SKIN sensor threshold is reported
+ synchronized (mTemperatureWatcher.mSamples) {
+ if (mTemperatureWatcher.mSevereThresholds.size() == 1) {
+ try {
+ mIsHalSkinForecastSupported.set(
+ Flags.allowThermalHalSkinForecast()
+ && !Float.isNaN(mHalWrapper.forecastSkinTemperature(10)));
+ } catch (UnsupportedOperationException e) {
+ Slog.i(TAG, "Thermal HAL does not support forecastSkinTemperature");
+ }
+ }
+ }
mHalReady.set(true);
}
}
@@ -1092,6 +1107,8 @@ public class ThermalManagerService extends SystemService {
protected abstract List<TemperatureThreshold> getTemperatureThresholds(boolean shouldFilter,
int type);
+ protected abstract float forecastSkinTemperature(int forecastSeconds);
+
protected abstract boolean connectToHal();
protected abstract void dump(PrintWriter pw, String prefix);
@@ -1124,8 +1141,16 @@ public class ThermalManagerService extends SystemService {
@VisibleForTesting
static class ThermalHalAidlWrapper extends ThermalHalWrapper implements IBinder.DeathRecipient {
/* Proxy object for the Thermal HAL AIDL service. */
+
+ @GuardedBy("mHalLock")
private IThermal mInstance = null;
+ private IThermal getHalInstance() {
+ synchronized (mHalLock) {
+ return mInstance;
+ }
+ }
+
/** Callback for Thermal HAL AIDL. */
private final IThermalChangedCallback mThermalCallbackAidl =
new IThermalChangedCallback.Stub() {
@@ -1169,154 +1194,183 @@ public class ThermalManagerService extends SystemService {
@Override
protected List<Temperature> getCurrentTemperatures(boolean shouldFilter,
int type) {
- synchronized (mHalLock) {
- final List<Temperature> ret = new ArrayList<>();
- if (mInstance == null) {
+ final IThermal instance = getHalInstance();
+ final List<Temperature> ret = new ArrayList<>();
+ if (instance == null) {
+ return ret;
+ }
+ try {
+ final android.hardware.thermal.Temperature[] halRet =
+ shouldFilter ? instance.getTemperaturesWithType(type)
+ : instance.getTemperatures();
+ if (halRet == null) {
return ret;
}
- try {
- final android.hardware.thermal.Temperature[] halRet =
- shouldFilter ? mInstance.getTemperaturesWithType(type)
- : mInstance.getTemperatures();
- if (halRet == null) {
- return ret;
+ for (android.hardware.thermal.Temperature t : halRet) {
+ if (!Temperature.isValidStatus(t.throttlingStatus)) {
+ Slog.e(TAG, "Invalid temperature status " + t.throttlingStatus
+ + " received from AIDL HAL");
+ t.throttlingStatus = Temperature.THROTTLING_NONE;
}
- for (android.hardware.thermal.Temperature t : halRet) {
- if (!Temperature.isValidStatus(t.throttlingStatus)) {
- Slog.e(TAG, "Invalid temperature status " + t.throttlingStatus
- + " received from AIDL HAL");
- t.throttlingStatus = Temperature.THROTTLING_NONE;
- }
- if (shouldFilter && t.type != type) {
- continue;
- }
- ret.add(new Temperature(t.value, t.type, t.name, t.throttlingStatus));
+ if (shouldFilter && t.type != type) {
+ continue;
}
- } catch (IllegalArgumentException | IllegalStateException e) {
- Slog.e(TAG, "Couldn't getCurrentCoolingDevices due to invalid status", e);
- } catch (RemoteException e) {
- Slog.e(TAG, "Couldn't getCurrentTemperatures, reconnecting", e);
- connectToHal();
+ ret.add(new Temperature(t.value, t.type, t.name, t.throttlingStatus));
+ }
+ } catch (IllegalArgumentException | IllegalStateException e) {
+ Slog.e(TAG, "Couldn't getCurrentCoolingDevices due to invalid status", e);
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Couldn't getCurrentTemperatures, reconnecting", e);
+ synchronized (mHalLock) {
+ connectToHalIfNeededLocked(instance);
}
- return ret;
}
+ return ret;
}
@Override
protected List<CoolingDevice> getCurrentCoolingDevices(boolean shouldFilter,
int type) {
- synchronized (mHalLock) {
- final List<CoolingDevice> ret = new ArrayList<>();
- if (mInstance == null) {
+ final IThermal instance = getHalInstance();
+ final List<CoolingDevice> ret = new ArrayList<>();
+ if (instance == null) {
+ return ret;
+ }
+ try {
+ final android.hardware.thermal.CoolingDevice[] halRet = shouldFilter
+ ? instance.getCoolingDevicesWithType(type)
+ : instance.getCoolingDevices();
+ if (halRet == null) {
return ret;
}
- try {
- final android.hardware.thermal.CoolingDevice[] halRet = shouldFilter
- ? mInstance.getCoolingDevicesWithType(type)
- : mInstance.getCoolingDevices();
- if (halRet == null) {
- return ret;
+ for (android.hardware.thermal.CoolingDevice t : halRet) {
+ if (!CoolingDevice.isValidType(t.type)) {
+ Slog.e(TAG, "Invalid cooling device type " + t.type + " from AIDL HAL");
+ continue;
}
- for (android.hardware.thermal.CoolingDevice t : halRet) {
- if (!CoolingDevice.isValidType(t.type)) {
- Slog.e(TAG, "Invalid cooling device type " + t.type + " from AIDL HAL");
- continue;
- }
- if (shouldFilter && t.type != type) {
- continue;
- }
- ret.add(new CoolingDevice(t.value, t.type, t.name));
+ if (shouldFilter && t.type != type) {
+ continue;
}
- } catch (IllegalArgumentException | IllegalStateException e) {
- Slog.e(TAG, "Couldn't getCurrentCoolingDevices due to invalid status", e);
- } catch (RemoteException e) {
- Slog.e(TAG, "Couldn't getCurrentCoolingDevices, reconnecting", e);
- connectToHal();
+ ret.add(new CoolingDevice(t.value, t.type, t.name));
+ }
+ } catch (IllegalArgumentException | IllegalStateException e) {
+ Slog.e(TAG, "Couldn't getCurrentCoolingDevices due to invalid status", e);
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Couldn't getCurrentCoolingDevices, reconnecting", e);
+ synchronized (mHalLock) {
+ connectToHalIfNeededLocked(instance);
}
- return ret;
}
+ return ret;
}
@Override
@NonNull
protected List<TemperatureThreshold> getTemperatureThresholds(
boolean shouldFilter, int type) {
- synchronized (mHalLock) {
- final List<TemperatureThreshold> ret = new ArrayList<>();
- if (mInstance == null) {
+ final IThermal instance = getHalInstance();
+ final List<TemperatureThreshold> ret = new ArrayList<>();
+ if (instance == null) {
+ return ret;
+ }
+ try {
+ final TemperatureThreshold[] halRet =
+ shouldFilter ? instance.getTemperatureThresholdsWithType(type)
+ : instance.getTemperatureThresholds();
+ if (halRet == null) {
return ret;
}
- try {
- final TemperatureThreshold[] halRet =
- shouldFilter ? mInstance.getTemperatureThresholdsWithType(type)
- : mInstance.getTemperatureThresholds();
- if (halRet == null) {
- return ret;
- }
- if (shouldFilter) {
- return Arrays.stream(halRet).filter(t -> t.type == type).collect(
- Collectors.toList());
- }
- return Arrays.asList(halRet);
- } catch (IllegalArgumentException | IllegalStateException e) {
- Slog.e(TAG, "Couldn't getTemperatureThresholds due to invalid status", e);
- } catch (RemoteException e) {
- Slog.e(TAG, "Couldn't getTemperatureThresholds, reconnecting...", e);
- connectToHal();
+ if (shouldFilter) {
+ return Arrays.stream(halRet).filter(t -> t.type == type).collect(
+ Collectors.toList());
+ }
+ return Arrays.asList(halRet);
+ } catch (IllegalArgumentException | IllegalStateException e) {
+ Slog.e(TAG, "Couldn't getTemperatureThresholds due to invalid status", e);
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Couldn't getTemperatureThresholds, reconnecting...", e);
+ synchronized (mHalLock) {
+ connectToHalIfNeededLocked(instance);
}
- return ret;
}
+ return ret;
+ }
+
+ @Override
+ protected float forecastSkinTemperature(int forecastSeconds) {
+ final IThermal instance = getHalInstance();
+ if (instance == null) {
+ return Float.NaN;
+ }
+ try {
+ return instance.forecastSkinTemperature(forecastSeconds);
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Couldn't forecastSkinTemperature, reconnecting...", e);
+ synchronized (mHalLock) {
+ connectToHalIfNeededLocked(instance);
+ }
+ }
+ return Float.NaN;
}
@Override
protected boolean connectToHal() {
synchronized (mHalLock) {
- IBinder binder = Binder.allowBlocking(ServiceManager.waitForDeclaredService(
- IThermal.DESCRIPTOR + "/default"));
- initProxyAndRegisterCallback(binder);
+ return connectToHalIfNeededLocked(mInstance);
}
+ }
+
+ @GuardedBy("mHalLock")
+ protected boolean connectToHalIfNeededLocked(IThermal instance) {
+ if (instance != mInstance) {
+ // instance has been updated since last used
+ return true;
+ }
+ IBinder binder = Binder.allowBlocking(ServiceManager.waitForDeclaredService(
+ IThermal.DESCRIPTOR + "/default"));
+ initProxyAndRegisterCallbackLocked(binder);
return mInstance != null;
}
@VisibleForTesting
void initProxyAndRegisterCallback(IBinder binder) {
synchronized (mHalLock) {
- if (binder != null) {
- mInstance = IThermal.Stub.asInterface(binder);
+ initProxyAndRegisterCallbackLocked(binder);
+ }
+ }
+
+ @GuardedBy("mHalLock")
+ protected void initProxyAndRegisterCallbackLocked(IBinder binder) {
+ if (binder != null) {
+ mInstance = IThermal.Stub.asInterface(binder);
+ try {
+ binder.linkToDeath(this, 0);
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Unable to connect IThermal AIDL instance", e);
+ connectToHal();
+ }
+ if (mInstance != null) {
try {
- binder.linkToDeath(this, 0);
+ Slog.i(TAG, "Thermal HAL AIDL service connected with version "
+ + mInstance.getInterfaceVersion());
} catch (RemoteException e) {
- Slog.e(TAG, "Unable to connect IThermal AIDL instance", e);
+ Slog.e(TAG, "Unable to read interface version from Thermal HAL", e);
connectToHal();
+ return;
}
- if (mInstance != null) {
- try {
- Slog.i(TAG, "Thermal HAL AIDL service connected with version "
- + mInstance.getInterfaceVersion());
- } catch (RemoteException e) {
- Slog.e(TAG, "Unable to read interface version from Thermal HAL", e);
- connectToHal();
- return;
- }
- registerThermalChangedCallback();
+ try {
+ mInstance.registerThermalChangedCallback(mThermalCallbackAidl);
+ } catch (IllegalArgumentException | IllegalStateException e) {
+ Slog.e(TAG, "Couldn't registerThermalChangedCallback due to invalid status",
+ e);
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Unable to connect IThermal AIDL instance", e);
+ connectToHal();
}
}
}
}
- @VisibleForTesting
- void registerThermalChangedCallback() {
- try {
- mInstance.registerThermalChangedCallback(mThermalCallbackAidl);
- } catch (IllegalArgumentException | IllegalStateException e) {
- Slog.e(TAG, "Couldn't registerThermalChangedCallback due to invalid status",
- e);
- } catch (RemoteException e) {
- Slog.e(TAG, "Unable to connect IThermal AIDL instance", e);
- connectToHal();
- }
- }
-
@Override
protected void dump(PrintWriter pw, String prefix) {
synchronized (mHalLock) {
@@ -1445,6 +1499,11 @@ public class ThermalManagerService extends SystemService {
}
@Override
+ protected float forecastSkinTemperature(int forecastSeconds) {
+ throw new UnsupportedOperationException("Not supported in Thermal HAL 1.0");
+ }
+
+ @Override
protected void dump(PrintWriter pw, String prefix) {
synchronized (mHalLock) {
pw.print(prefix);
@@ -1583,6 +1642,11 @@ public class ThermalManagerService extends SystemService {
}
@Override
+ protected float forecastSkinTemperature(int forecastSeconds) {
+ throw new UnsupportedOperationException("Not supported in Thermal HAL 1.1");
+ }
+
+ @Override
protected void dump(PrintWriter pw, String prefix) {
synchronized (mHalLock) {
pw.print(prefix);
@@ -1749,6 +1813,11 @@ public class ThermalManagerService extends SystemService {
}
@Override
+ protected float forecastSkinTemperature(int forecastSeconds) {
+ throw new UnsupportedOperationException("Not supported in Thermal HAL 2.0");
+ }
+
+ @Override
protected void dump(PrintWriter pw, String prefix) {
synchronized (mHalLock) {
pw.print(prefix);
@@ -1977,6 +2046,39 @@ public class ThermalManagerService extends SystemService {
float getForecast(int forecastSeconds) {
synchronized (mSamples) {
+ // If we don't have any thresholds, we can't normalize the temperatures,
+ // so return early
+ if (mSevereThresholds.isEmpty()) {
+ Slog.e(TAG, "No temperature thresholds found");
+ FrameworkStatsLog.write(FrameworkStatsLog.THERMAL_HEADROOM_CALLED,
+ Binder.getCallingUid(),
+ THERMAL_HEADROOM_CALLED__API_STATUS__NO_TEMPERATURE_THRESHOLD,
+ Float.NaN, forecastSeconds);
+ return Float.NaN;
+ }
+ }
+ if (mIsHalSkinForecastSupported.get()) {
+ float threshold = -1f;
+ synchronized (mSamples) {
+ // we only do forecast if a single SKIN sensor threshold is reported
+ if (mSevereThresholds.size() == 1) {
+ threshold = mSevereThresholds.valueAt(0);
+ }
+ }
+ if (threshold > 0) {
+ try {
+ final float forecastTemperature =
+ mHalWrapper.forecastSkinTemperature(forecastSeconds);
+ return normalizeTemperature(forecastTemperature, threshold);
+ } catch (UnsupportedOperationException e) {
+ Slog.wtf(TAG, "forecastSkinTemperature returns unsupported");
+ } catch (Exception e) {
+ Slog.e(TAG, "forecastSkinTemperature fails");
+ }
+ return Float.NaN;
+ }
+ }
+ synchronized (mSamples) {
mLastForecastCallTimeMillis = SystemClock.elapsedRealtime();
if (mSamples.isEmpty()) {
getAndUpdateTemperatureSamples();
@@ -1993,17 +2095,6 @@ public class ThermalManagerService extends SystemService {
return Float.NaN;
}
- // If we don't have any thresholds, we can't normalize the temperatures,
- // so return early
- if (mSevereThresholds.isEmpty()) {
- Slog.e(TAG, "No temperature thresholds found");
- FrameworkStatsLog.write(FrameworkStatsLog.THERMAL_HEADROOM_CALLED,
- Binder.getCallingUid(),
- THERMAL_HEADROOM_CALLED__API_STATUS__NO_TEMPERATURE_THRESHOLD,
- Float.NaN, forecastSeconds);
- return Float.NaN;
- }
-
if (mCachedHeadrooms.contains(forecastSeconds)) {
// TODO(b/360486877): replace with metrics
Slog.d(TAG,
diff --git a/services/core/java/com/android/server/power/feature/PowerManagerFlags.java b/services/core/java/com/android/server/power/feature/PowerManagerFlags.java
index 5cd7dee35e5f..42b44013bea2 100644
--- a/services/core/java/com/android/server/power/feature/PowerManagerFlags.java
+++ b/services/core/java/com/android/server/power/feature/PowerManagerFlags.java
@@ -37,6 +37,11 @@ public class PowerManagerFlags {
Flags.FLAG_ENABLE_EARLY_SCREEN_TIMEOUT_DETECTOR,
Flags::enableEarlyScreenTimeoutDetector);
+ private final FlagState mEnableScreenTimeoutPolicyListenerApi = new FlagState(
+ Flags.FLAG_ENABLE_SCREEN_TIMEOUT_POLICY_LISTENER_API,
+ Flags::enableScreenTimeoutPolicyListenerApi
+ );
+
private final FlagState mImproveWakelockLatency = new FlagState(
Flags.FLAG_IMPROVE_WAKELOCK_LATENCY,
Flags::improveWakelockLatency
@@ -63,6 +68,11 @@ public class PowerManagerFlags {
return mEarlyScreenTimeoutDetectorFlagState.isEnabled();
}
+ /** Returns whether screen timeout policy listener APIs are enabled on not. */
+ public boolean isScreenTimeoutPolicyListenerApiEnabled() {
+ return mEnableScreenTimeoutPolicyListenerApi.isEnabled();
+ }
+
/**
* @return Whether to improve the wakelock acquire/release latency or not
*/
diff --git a/services/core/java/com/android/server/power/feature/power_flags.aconfig b/services/core/java/com/android/server/power/feature/power_flags.aconfig
index a975da32f2fd..613daf820e34 100644
--- a/services/core/java/com/android/server/power/feature/power_flags.aconfig
+++ b/services/core/java/com/android/server/power/feature/power_flags.aconfig
@@ -12,6 +12,17 @@ flag {
}
flag {
+ name: "enable_screen_timeout_policy_listener_api"
+ namespace: "power"
+ description: "Enables APIs that allow to listen to screen timeout policy changes"
+ bug: "363174979"
+ is_fixed_read_only: true
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+flag {
name: "improve_wakelock_latency"
namespace: "power"
description: "Feature flag for tracking the optimizations to improve the latency of acquiring and releasing a wakelock."
diff --git a/services/core/java/com/android/server/power/hint/HintManagerService.java b/services/core/java/com/android/server/power/hint/HintManagerService.java
index a6f2a3757dcb..1cf24fcd8594 100644
--- a/services/core/java/com/android/server/power/hint/HintManagerService.java
+++ b/services/core/java/com/android/server/power/hint/HintManagerService.java
@@ -20,7 +20,6 @@ import static android.os.Flags.adpfUseFmqChannel;
import static com.android.internal.util.ConcurrentUtils.DIRECT_EXECUTOR;
import static com.android.server.power.hint.Flags.adpfSessionTag;
-import static com.android.server.power.hint.Flags.cpuHeadroomAffinityCheck;
import static com.android.server.power.hint.Flags.powerhintThreadCleanup;
import static com.android.server.power.hint.Flags.resetOnForkEnabled;
@@ -1604,8 +1603,7 @@ public final class HintManagerService extends SystemService {
}
}
}
- if (cpuHeadroomAffinityCheck() && mCheckHeadroomAffinity
- && params.tids.length > 1) {
+ if (mCheckHeadroomAffinity && params.tids.length > 1) {
checkThreadAffinityForTids(params.tids);
}
halParams.tids = params.tids;
diff --git a/services/core/java/com/android/server/resources/ResourcesManagerShellCommand.java b/services/core/java/com/android/server/resources/ResourcesManagerShellCommand.java
index 17739712d65a..a75d110e3cd1 100644
--- a/services/core/java/com/android/server/resources/ResourcesManagerShellCommand.java
+++ b/services/core/java/com/android/server/resources/ResourcesManagerShellCommand.java
@@ -88,5 +88,6 @@ public class ResourcesManagerShellCommand extends ShellCommand {
out.println(" Print this help text.");
out.println(" dump <PROCESS>");
out.println(" Dump the Resources objects in use as well as the history of Resources");
+
}
}
diff --git a/services/core/java/com/android/server/rollback/RollbackStore.java b/services/core/java/com/android/server/rollback/RollbackStore.java
index 14539d544bf9..50db1e4ac30e 100644
--- a/services/core/java/com/android/server/rollback/RollbackStore.java
+++ b/services/core/java/com/android/server/rollback/RollbackStore.java
@@ -84,8 +84,12 @@ class RollbackStore {
*/
private static List<Rollback> loadRollbacks(File rollbackDataDir) {
List<Rollback> rollbacks = new ArrayList<>();
- rollbackDataDir.mkdirs();
- for (File rollbackDir : rollbackDataDir.listFiles()) {
+ File[] rollbackDirs = rollbackDataDir.listFiles();
+ if (rollbackDirs == null) {
+ Slog.e(TAG, "Folder doesn't exist: " + rollbackDataDir);
+ return rollbacks;
+ }
+ for (File rollbackDir : rollbackDirs) {
if (rollbackDir.isDirectory()) {
try {
rollbacks.add(loadRollback(rollbackDir));
diff --git a/services/core/java/com/android/server/selinux/SelinuxAuditLogBuilder.java b/services/core/java/com/android/server/selinux/SelinuxAuditLogBuilder.java
index d69150d88e4f..a1f72be7a039 100644
--- a/services/core/java/com/android/server/selinux/SelinuxAuditLogBuilder.java
+++ b/services/core/java/com/android/server/selinux/SelinuxAuditLogBuilder.java
@@ -15,7 +15,6 @@
*/
package com.android.server.selinux;
-import android.provider.DeviceConfig;
import android.text.TextUtils;
import android.util.Slog;
@@ -34,10 +33,6 @@ class SelinuxAuditLogBuilder {
private static final String TAG = "SelinuxAuditLogs";
- // This config indicates which Selinux logs for source domains to collect. The string will be
- // inserted into a regex, so it must follow the regex syntax. For example, a valid value would
- // be "system_server|untrusted_app".
- @VisibleForTesting static final String CONFIG_SELINUX_AUDIT_DOMAIN = "selinux_audit_domain";
private static final Matcher NO_OP_MATCHER = Pattern.compile("no-op^").matcher("");
private static final String TCONTEXT_PATTERN =
"u:object_r:(?<ttype>\\w+):s0(:c)?(?<tcategories>((,c)?\\d+)+)*";
@@ -50,7 +45,7 @@ class SelinuxAuditLogBuilder {
private Iterator<String> mTokens;
private final SelinuxAuditLog mAuditLog = new SelinuxAuditLog();
- SelinuxAuditLogBuilder() {
+ SelinuxAuditLogBuilder(String auditDomain) {
Matcher scontextMatcher = NO_OP_MATCHER;
Matcher tcontextMatcher = NO_OP_MATCHER;
Matcher pathMatcher = NO_OP_MATCHER;
@@ -59,10 +54,7 @@ class SelinuxAuditLogBuilder {
Pattern.compile(
TextUtils.formatSimple(
"u:r:(?<stype>%s):s0(:c)?(?<scategories>((,c)?\\d+)+)*",
- DeviceConfig.getString(
- DeviceConfig.NAMESPACE_ADSERVICES,
- CONFIG_SELINUX_AUDIT_DOMAIN,
- "no_match^")))
+ auditDomain))
.matcher("");
tcontextMatcher = Pattern.compile(TCONTEXT_PATTERN).matcher("");
pathMatcher = Pattern.compile(PATH_PATTERN).matcher("");
diff --git a/services/core/java/com/android/server/selinux/SelinuxAuditLogsCollector.java b/services/core/java/com/android/server/selinux/SelinuxAuditLogsCollector.java
index c655d46eb9f4..0aa705892376 100644
--- a/services/core/java/com/android/server/selinux/SelinuxAuditLogsCollector.java
+++ b/services/core/java/com/android/server/selinux/SelinuxAuditLogsCollector.java
@@ -15,6 +15,7 @@
*/
package com.android.server.selinux;
+import android.provider.DeviceConfig;
import android.util.EventLog;
import android.util.EventLog.Event;
import android.util.Log;
@@ -32,6 +33,7 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.Supplier;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -43,9 +45,16 @@ class SelinuxAuditLogsCollector {
private static final String SELINUX_PATTERN = "^.*\\bavc:\\s+(?<denial>.*)$";
+ // This config indicates which Selinux logs for source domains to collect. The string will be
+ // inserted into a regex, so it must follow the regex syntax. For example, a valid value would
+ // be "system_server|untrusted_app".
+ @VisibleForTesting static final String CONFIG_SELINUX_AUDIT_DOMAIN = "selinux_audit_domain";
+ @VisibleForTesting static final String DEFAULT_SELINUX_AUDIT_DOMAIN = "no_match^";
+
@VisibleForTesting
static final Matcher SELINUX_MATCHER = Pattern.compile(SELINUX_PATTERN).matcher("");
+ private final Supplier<String> mAuditDomainSupplier;
private final RateLimiter mRateLimiter;
private final QuotaLimiter mQuotaLimiter;
@@ -53,11 +62,26 @@ class SelinuxAuditLogsCollector {
AtomicBoolean mStopRequested = new AtomicBoolean(false);
- SelinuxAuditLogsCollector(RateLimiter rateLimiter, QuotaLimiter quotaLimiter) {
+ SelinuxAuditLogsCollector(
+ Supplier<String> auditDomainSupplier,
+ RateLimiter rateLimiter,
+ QuotaLimiter quotaLimiter) {
+ mAuditDomainSupplier = auditDomainSupplier;
mRateLimiter = rateLimiter;
mQuotaLimiter = quotaLimiter;
}
+ SelinuxAuditLogsCollector(RateLimiter rateLimiter, QuotaLimiter quotaLimiter) {
+ this(
+ () ->
+ DeviceConfig.getString(
+ DeviceConfig.NAMESPACE_ADSERVICES,
+ CONFIG_SELINUX_AUDIT_DOMAIN,
+ DEFAULT_SELINUX_AUDIT_DOMAIN),
+ rateLimiter,
+ quotaLimiter);
+ }
+
public void setStopRequested(boolean stopRequested) {
mStopRequested.set(stopRequested);
}
@@ -108,7 +132,8 @@ class SelinuxAuditLogsCollector {
}
private boolean writeAuditLogs(Queue<Event> logLines) {
- final SelinuxAuditLogBuilder auditLogBuilder = new SelinuxAuditLogBuilder();
+ final SelinuxAuditLogBuilder auditLogBuilder =
+ new SelinuxAuditLogBuilder(mAuditDomainSupplier.get());
int auditsWritten = 0;
while (!mStopRequested.get() && !logLines.isEmpty()) {
diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
index 4ed5f90f2852..a19a3422af06 100644
--- a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
+++ b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
@@ -2199,6 +2199,19 @@ public class StatusBarManagerService extends IStatusBarService.Stub implements D
});
}
+ /**
+ * Called when the notification should be rebundled.
+ * @param key the notification key
+ */
+ @Override
+ public void rebundleNotification(String key) {
+ enforceStatusBarService();
+ enforceValidCallingUser();
+ Binder.withCleanCallingIdentity(() -> {
+ mNotificationDelegate.rebundleNotification(key);
+ });
+ }
+
@Override
public void onShellCommand(FileDescriptor in, FileDescriptor out, FileDescriptor err,
diff --git a/services/core/java/com/android/server/timezonedetector/NotifyingTimeZoneChangeListener.java b/services/core/java/com/android/server/timezonedetector/NotifyingTimeZoneChangeListener.java
new file mode 100644
index 000000000000..2e73829ca143
--- /dev/null
+++ b/services/core/java/com/android/server/timezonedetector/NotifyingTimeZoneChangeListener.java
@@ -0,0 +1,649 @@
+/*
+ * 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.timezonedetector;
+
+import static android.app.PendingIntent.FLAG_CANCEL_CURRENT;
+import static android.app.PendingIntent.FLAG_IMMUTABLE;
+import static android.app.PendingIntent.FLAG_UPDATE_CURRENT;
+import static android.content.Context.RECEIVER_NOT_EXPORTED;
+import static android.provider.Settings.ACTION_DATE_SETTINGS;
+
+import static com.android.server.timezonedetector.TimeZoneDetectorStrategy.ORIGIN_LOCATION;
+import static com.android.server.timezonedetector.TimeZoneDetectorStrategy.ORIGIN_MANUAL;
+import static com.android.server.timezonedetector.TimeZoneDetectorStrategy.ORIGIN_TELEPHONY;
+import static com.android.server.timezonedetector.TimeZoneDetectorStrategy.ORIGIN_UNKNOWN;
+
+import android.annotation.DurationMillisLong;
+import android.annotation.IntDef;
+import android.annotation.RequiresPermission;
+import android.annotation.UserIdInt;
+import android.app.ActivityManagerInternal;
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.res.Resources;
+import android.icu.text.DateFormat;
+import android.icu.text.SimpleDateFormat;
+import android.icu.util.TimeZone;
+import android.os.Handler;
+import android.os.SystemClock;
+import android.os.UserHandle;
+import android.util.IndentingPrintWriter;
+import android.util.Log;
+
+import com.android.internal.R;
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.notification.SystemNotificationChannels;
+import com.android.server.LocalServices;
+import com.android.server.flags.Flags;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.time.Duration;
+import java.util.Objects;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * An implementation of {@link TimeZoneChangeListener} that fires notifications.
+ */
+public class NotifyingTimeZoneChangeListener implements TimeZoneChangeListener {
+ @IntDef({STATUS_UNKNOWN, STATUS_UNTRACKED, STATUS_REJECTED,
+ STATUS_ACCEPTED, STATUS_SUPERSEDED})
+ @Retention(RetentionPolicy.SOURCE)
+ @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER})
+ @interface TimeZoneChangeStatus {}
+
+ /** Used to indicate the status could not be inferred. */
+ @TimeZoneChangeStatus
+ static final int STATUS_UNKNOWN = 0;
+ /** Used to indicate the change is not one that needs to be tracked. */
+ @TimeZoneChangeStatus
+ static final int STATUS_UNTRACKED = 1;
+ @TimeZoneChangeStatus
+ static final int STATUS_REJECTED = 2;
+ @TimeZoneChangeStatus
+ static final int STATUS_ACCEPTED = 3;
+ /** Used to indicate a change was superseded before its status could be determined. */
+ @TimeZoneChangeStatus
+ static final int STATUS_SUPERSEDED = 4;
+
+ @IntDef({SIGNAL_TYPE_UNKNOWN, SIGNAL_TYPE_NONE, SIGNAL_TYPE_NOTIFICATION,
+ SIGNAL_TYPE_HEURISTIC})
+ @Retention(RetentionPolicy.SOURCE)
+ @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER})
+ @interface SignalType {}
+
+ /** Used when the signal type cannot be inferred. */
+ @SignalType
+ static final int SIGNAL_TYPE_UNKNOWN = 0;
+ /** Used when the status is not one that needs a signal type. */
+ @SignalType
+ static final int SIGNAL_TYPE_NONE = 1;
+ @SignalType
+ static final int SIGNAL_TYPE_NOTIFICATION = 2;
+ @SignalType
+ static final int SIGNAL_TYPE_HEURISTIC = 3;
+
+ private static final int MAX_EVENTS_TO_TRACK = 10;
+
+ @VisibleForTesting
+ @DurationMillisLong
+ static final long AUTO_REVERT_THRESHOLD = Duration.ofMinutes(15).toMillis();
+
+ private static final String TAG = "TimeZoneChangeTracker";
+ private static final String NOTIFICATION_TAG = "TimeZoneDetector";
+ private static final int TZ_CHANGE_NOTIFICATION_ID = 1001;
+
+ private static final String ACTION_NOTIFICATION_DELETED =
+ "com.android.server.timezonedetector.TimeZoneNotificationDeleted";
+
+ private static final String NOTIFICATION_INTENT_EXTRA_USER_ID = "user_id";
+ private static final String NOTIFICATION_INTENT_EXTRA_CHANGE_ID = "change_id";
+
+ private final Context mContext;
+ private final NotificationManager mNotificationManager;
+ private final ActivityManagerInternal mActivityManagerInternal;
+
+ // For scheduling callbacks
+ private final Handler mHandler;
+ private final ServiceConfigAccessor mServiceConfigAccessor;
+ private final AtomicInteger mNextChangeEventId = new AtomicInteger(1);
+
+ private final Resources mRes = Resources.getSystem();
+
+ @GuardedBy("mTimeZoneChangeRecord")
+ private final ReferenceWithHistory<TimeZoneChangeRecord> mTimeZoneChangeRecord =
+ new ReferenceWithHistory<>(MAX_EVENTS_TO_TRACK);
+
+ private final BroadcastReceiver mNotificationReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ switch (intent.getAction()) {
+ case ACTION_NOTIFICATION_DELETED:
+ int notifiedUserId = intent.getIntExtra(
+ NOTIFICATION_INTENT_EXTRA_USER_ID, UserHandle.USER_NULL);
+ int changeEventId = intent.getIntExtra(
+ NOTIFICATION_INTENT_EXTRA_CHANGE_ID, 0);
+ notificationSwipedAway(notifiedUserId, changeEventId);
+ break;
+ default:
+ Log.d(TAG, "Unknown intent action received: " + intent.getAction());
+ }
+ }
+ };
+
+ private final Object mConfigurationLock = new Object();
+ @GuardedBy("mConfigurationLock")
+ private ConfigurationInternal mConfigurationInternal;
+ @GuardedBy("mConfigurationLock")
+ private boolean mIsRegistered;
+
+ private int mAcceptedManualChanges;
+ private int mAcceptedTelephonyChanges;
+ private int mAcceptedLocationChanges;
+ private int mAcceptedUnknownChanges;
+ private int mRejectedTelephonyChanges;
+ private int mRejectedLocationChanges;
+ private int mRejectedUnknownChanges;
+
+ /** Create and initialise a new {@code TimeZoneChangeTrackerImpl} */
+ @RequiresPermission("android.permission.INTERACT_ACROSS_USERS_FULL")
+ public static NotifyingTimeZoneChangeListener create(Handler handler, Context context,
+ ServiceConfigAccessor serviceConfigAccessor) {
+ NotifyingTimeZoneChangeListener changeTracker =
+ new NotifyingTimeZoneChangeListener(handler,
+ context,
+ serviceConfigAccessor,
+ context.getSystemService(NotificationManager.class));
+
+ // Pretend there was an update to initialize configuration.
+ changeTracker.handleConfigurationUpdate();
+
+ return changeTracker;
+ }
+
+ @VisibleForTesting
+ NotifyingTimeZoneChangeListener(
+ Handler handler, Context context, ServiceConfigAccessor serviceConfigAccessor,
+ NotificationManager notificationManager) {
+ mHandler = Objects.requireNonNull(handler);
+ mContext = Objects.requireNonNull(context);
+ mServiceConfigAccessor = Objects.requireNonNull(serviceConfigAccessor);
+ mServiceConfigAccessor.addConfigurationInternalChangeListener(
+ this::handleConfigurationUpdate);
+ mActivityManagerInternal = LocalServices.getService(ActivityManagerInternal.class);
+ mNotificationManager = notificationManager;
+ }
+
+ @RequiresPermission("android.permission.INTERACT_ACROSS_USERS_FULL")
+ private void handleConfigurationUpdate() {
+ synchronized (mConfigurationLock) {
+ ConfigurationInternal oldConfigurationInternal = mConfigurationInternal;
+ mConfigurationInternal = mServiceConfigAccessor.getCurrentUserConfigurationInternal();
+
+ if (areNotificationsEnabled() && isNotificationTrackingSupported()) {
+ if (!mIsRegistered) {
+ IntentFilter intentFilter = new IntentFilter();
+ intentFilter.addAction(ACTION_NOTIFICATION_DELETED);
+ mContext.registerReceiverForAllUsers(mNotificationReceiver, intentFilter,
+ /* broadcastPermission= */ null, mHandler, RECEIVER_NOT_EXPORTED);
+ mIsRegistered = true;
+ }
+ } else if (mIsRegistered) {
+ mContext.unregisterReceiver(mNotificationReceiver);
+ mIsRegistered = false;
+ }
+
+ if (oldConfigurationInternal != null) {
+ boolean userChanged =
+ oldConfigurationInternal.getUserId() != mConfigurationInternal.getUserId();
+
+ if (!areNotificationsEnabled() || userChanged) {
+ // Clear any notifications that are no longer needed.
+ clearNotificationForUser(oldConfigurationInternal.getUserId());
+ }
+ }
+ }
+ }
+
+ private void notificationSwipedAway(@UserIdInt int userId, int changeEventId) {
+ // User swiping away a notification is interpreted as "user accepted the change".
+ if (isNotificationTrackingSupported()) {
+ markChangeAsAccepted(changeEventId, userId, SIGNAL_TYPE_NOTIFICATION);
+ }
+ }
+
+ private boolean areNotificationsEnabled() {
+ synchronized (mConfigurationLock) {
+ return mConfigurationInternal.getNotificationsEnabledBehavior();
+ }
+ }
+
+ private boolean isNotificationTrackingSupported() {
+ synchronized (mConfigurationLock) {
+ return mConfigurationInternal.isNotificationTrackingSupported();
+ }
+ }
+
+ private boolean isManualChangeTrackingSupported() {
+ synchronized (mConfigurationLock) {
+ return mConfigurationInternal.isManualChangeTrackingSupported();
+ }
+ }
+
+ /**
+ * Marks a change event as accepted by the user
+ *
+ * <p>A change event is said to be accepted when the client does not revert an automatic time
+ * zone change by manually changing the time zone within {@code AUTO_REVERT_THRESHOLD} of the
+ * notification being received.
+ */
+ private void markChangeAsAccepted(int changeEventId, @UserIdInt int userId,
+ @SignalType int signalType) {
+ if (!isUserIdCurrentUser(userId)) {
+ return;
+ }
+
+ synchronized (mTimeZoneChangeRecord) {
+ TimeZoneChangeRecord lastTimeZoneChangeRecord = mTimeZoneChangeRecord.get();
+ if (lastTimeZoneChangeRecord != null) {
+ if (lastTimeZoneChangeRecord.getId() != changeEventId) {
+ // To be accepted, the change being accepted has to still be the latest.
+ return;
+ }
+ if (lastTimeZoneChangeRecord.getStatus() != STATUS_UNKNOWN) {
+ // Change status has already been set.
+ return;
+ }
+ lastTimeZoneChangeRecord.setAccepted(signalType);
+
+ switch (lastTimeZoneChangeRecord.getEvent().getOrigin()) {
+ case ORIGIN_MANUAL:
+ mAcceptedManualChanges += 1;
+ break;
+ case ORIGIN_TELEPHONY:
+ mAcceptedTelephonyChanges += 1;
+ break;
+ case ORIGIN_LOCATION:
+ mAcceptedLocationChanges += 1;
+ break;
+ default:
+ mAcceptedUnknownChanges += 1;
+ break;
+ }
+ }
+ }
+ }
+
+ private boolean isUserIdCurrentUser(@UserIdInt int userId) {
+ synchronized (mConfigurationLock) {
+ return userId == mConfigurationInternal.getUserId();
+ }
+ }
+
+ /**
+ * Marks a change event as rejected by the user
+ *
+ * <p>A change event is said to be rejected when the client reverts an automatic time zone
+ * change by manually changing the time zone within {@code AUTO_REVERT_THRESHOLD} of the
+ * notification being received.
+ */
+ @GuardedBy("mTimeZoneChangeRecord")
+ private void markChangeAsRejected(int changeEventId, @UserIdInt int userId,
+ @SignalType int signalType) {
+ if (!isUserIdCurrentUser(userId)) {
+ return;
+ }
+
+ TimeZoneChangeRecord lastTimeZoneChangeRecord = mTimeZoneChangeRecord.get();
+ if (lastTimeZoneChangeRecord != null) {
+ if (lastTimeZoneChangeRecord.getId() != changeEventId) {
+ // To be accepted, the change being accepted has to still be the latest.
+ return;
+ }
+ if (lastTimeZoneChangeRecord.getStatus() != STATUS_UNKNOWN) {
+ // Change status has already been set.
+ return;
+ }
+ lastTimeZoneChangeRecord.setRejected(signalType);
+
+ switch (lastTimeZoneChangeRecord.getEvent().getOrigin()) {
+ case ORIGIN_TELEPHONY:
+ mRejectedTelephonyChanges += 1;
+ break;
+ case ORIGIN_LOCATION:
+ mRejectedLocationChanges += 1;
+ break;
+ default:
+ mRejectedUnknownChanges += 1;
+ break;
+ }
+ }
+ }
+
+ @Override
+ public void process(TimeZoneChangeEvent changeEvent) {
+ final TimeZoneChangeRecord trackedChangeEvent;
+
+ synchronized (mTimeZoneChangeRecord) {
+ fixPotentialHistoryCorruption(changeEvent);
+
+ TimeZoneChangeRecord lastTimeZoneChangeRecord = mTimeZoneChangeRecord.get();
+ int changeEventId = mNextChangeEventId.getAndIncrement();
+ trackedChangeEvent = new TimeZoneChangeRecord(changeEventId, changeEvent);
+
+ if (isManualChangeTrackingSupported()) {
+ // Time-based heuristic for "user is undoing a mistake made by the time zone
+ // detector".
+ if (lastTimeZoneChangeRecord != null
+ && lastTimeZoneChangeRecord.getStatus() == STATUS_UNKNOWN) {
+ TimeZoneChangeEvent lastChangeEvent = lastTimeZoneChangeRecord.getEvent();
+
+ if (shouldRejectChangeEvent(changeEvent, lastChangeEvent)) {
+ markChangeAsRejected(lastTimeZoneChangeRecord.getId(),
+ changeEvent.getUserId(), SIGNAL_TYPE_HEURISTIC);
+ }
+ }
+
+ // Schedule a callback for the new time zone so that we can implement "user accepted
+ // the change because they didn't revert it"
+ scheduleChangeAcceptedHeuristicCallback(trackedChangeEvent, AUTO_REVERT_THRESHOLD);
+ }
+
+ if (lastTimeZoneChangeRecord != null
+ && lastTimeZoneChangeRecord.getStatus() == STATUS_UNKNOWN) {
+ lastTimeZoneChangeRecord.setStatus(STATUS_SUPERSEDED, SIGNAL_TYPE_NONE);
+ }
+
+ if (changeEvent.getOrigin() == ORIGIN_MANUAL) {
+ trackedChangeEvent.setStatus(STATUS_UNTRACKED, SIGNAL_TYPE_NONE);
+ }
+
+ mTimeZoneChangeRecord.set(trackedChangeEvent);
+ }
+
+ if (areNotificationsEnabled()) {
+ int currentUserId;
+ synchronized (mConfigurationLock) {
+ currentUserId = mConfigurationInternal.getUserId();
+ }
+
+ if (changeEvent.getOrigin() == ORIGIN_MANUAL) {
+ // Just clear any existing notification.
+ clearNotificationForUser(currentUserId);
+ } else {
+ notifyOfTimeZoneChange(currentUserId, trackedChangeEvent);
+ }
+ }
+ }
+
+ /**
+ * Checks if the history of time zone change events is corrupted and fixes it, if needed
+ *
+ * <p>The history of changes is considered corrupted if a transition is missing. That is, if
+ * {@code events[i-1].newTimeZoneId != events[i].oldTimeZoneId}. In that case, a "synthetic"
+ * event is added to the history to bridge the gap between the last reported time zone ID and
+ * the time zone ID that the new event is replacing.
+ *
+ * <p>Note: we are not expecting this method to be required often (if ever) but in the
+ * eventuality that an event gets lost, we want to keep the history coherent.
+ */
+ @GuardedBy("mTimeZoneChangeRecord")
+ private void fixPotentialHistoryCorruption(TimeZoneChangeEvent changeEvent) {
+ TimeZoneChangeRecord lastTimeZoneChangeRecord = mTimeZoneChangeRecord.get();
+
+ if (lastTimeZoneChangeRecord != null) {
+ // The below block takes care of the case where we are missing record(s) of time
+ // zone changes
+ TimeZoneChangeEvent lastChangeEvent = lastTimeZoneChangeRecord.getEvent();
+ if (!changeEvent.getOldZoneId().equals(lastChangeEvent.getNewZoneId())) {
+ int changeEventId = mNextChangeEventId.getAndIncrement();
+ TimeZoneChangeEvent syntheticChangeEvent = new TimeZoneChangeEvent(
+ SystemClock.elapsedRealtime(), System.currentTimeMillis(),
+ ORIGIN_UNKNOWN, UserHandle.USER_NULL, lastChangeEvent.getNewZoneId(),
+ changeEvent.getOldZoneId(), 0, "Synthetic");
+ TimeZoneChangeRecord syntheticTrackedChangeEvent =
+ new TimeZoneChangeRecord(changeEventId, syntheticChangeEvent);
+ syntheticTrackedChangeEvent.setStatus(STATUS_SUPERSEDED, SIGNAL_TYPE_NONE);
+
+ mTimeZoneChangeRecord.set(syntheticTrackedChangeEvent);
+
+ // Housekeeping for the last reported time zone change: try to ensure it has
+ // a status too.
+ if (lastTimeZoneChangeRecord.getStatus() == STATUS_UNKNOWN) {
+ lastTimeZoneChangeRecord.setStatus(STATUS_SUPERSEDED, SIGNAL_TYPE_NONE);
+ }
+ }
+ }
+ }
+
+ private static boolean shouldRejectChangeEvent(TimeZoneChangeEvent changeEvent,
+ TimeZoneChangeEvent lastChangeEvent) {
+ return changeEvent.getOrigin() == ORIGIN_MANUAL
+ && lastChangeEvent.getOrigin() != ORIGIN_MANUAL
+ && (changeEvent.getElapsedRealtimeMillis()
+ - lastChangeEvent.getElapsedRealtimeMillis() < AUTO_REVERT_THRESHOLD);
+ }
+
+ private void scheduleChangeAcceptedHeuristicCallback(
+ TimeZoneChangeRecord trackedChangeEvent,
+ @DurationMillisLong long delayMillis) {
+ mHandler.postDelayed(
+ () -> changeAcceptedTimeHeuristicCallback(trackedChangeEvent.getId()), delayMillis);
+ }
+
+ private void changeAcceptedTimeHeuristicCallback(int changeEventId) {
+ if (isManualChangeTrackingSupported()) {
+ int currentUserId = mActivityManagerInternal.getCurrentUserId();
+ markChangeAsAccepted(changeEventId, currentUserId, SIGNAL_TYPE_HEURISTIC);
+ }
+ }
+
+ private void clearNotificationForUser(@UserIdInt int userId) {
+ mNotificationManager.cancelAsUser(NOTIFICATION_TAG, TZ_CHANGE_NOTIFICATION_ID,
+ UserHandle.of(userId));
+ }
+
+ private void notifyOfTimeZoneChange(@UserIdInt int userId,
+ TimeZoneChangeRecord trackedChangeEvent) {
+ TimeZoneChangeEvent changeEvent = trackedChangeEvent.getEvent();
+
+ if (!Flags.datetimeNotifications() || !areNotificationsEnabled()) {
+ return;
+ }
+
+ TimeZone oldTimeZone = TimeZone.getTimeZone(changeEvent.getOldZoneId());
+ TimeZone newTimeZone = TimeZone.getTimeZone(changeEvent.getNewZoneId());
+ long unixEpochTimeMillis = changeEvent.getUnixEpochTimeMillis();
+ boolean hasOffsetChanged = newTimeZone.getOffset(unixEpochTimeMillis)
+ == oldTimeZone.getOffset(unixEpochTimeMillis);
+
+ if (hasOffsetChanged) {
+ // If the time zone ID changes but not the offset, we do not send a notification to
+ // the user. This is to prevent spamming users and reduce the number of notification
+ // we send overall.
+ Log.d(TAG, "The time zone ID has changed but the offset remains the same.");
+ return;
+ }
+
+ final CharSequence title = mRes.getString(R.string.time_zone_change_notification_title);
+ final CharSequence body = getNotificationBody(newTimeZone, unixEpochTimeMillis);
+
+ final Intent clickNotificationIntent = new Intent(ACTION_DATE_SETTINGS)
+ .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK
+ | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
+ | Intent.FLAG_ACTIVITY_CLEAR_TOP);
+
+ final Intent clearNotificationIntent = new Intent(ACTION_NOTIFICATION_DELETED)
+ .putExtra(NOTIFICATION_INTENT_EXTRA_USER_ID, userId)
+ .putExtra(NOTIFICATION_INTENT_EXTRA_CHANGE_ID, trackedChangeEvent.getId());
+
+ Notification notification = new Notification.Builder(mContext,
+ SystemNotificationChannels.TIME)
+ .setSmallIcon(R.drawable.btn_clock_material)
+ .setStyle(new Notification.BigTextStyle().bigText(body))
+ .setOnlyAlertOnce(true)
+ .setColor(mContext.getColor(R.color.system_notification_accent_color))
+ .setTicker(title)
+ .setContentTitle(title)
+ .setContentText(body)
+ .setContentIntent(PendingIntent.getActivityAsUser(
+ mContext,
+ /* requestCode= */ 0,
+ clickNotificationIntent,
+ /* flags= */ FLAG_CANCEL_CURRENT | FLAG_IMMUTABLE,
+ /* options= */ null,
+ UserHandle.of(userId)))
+ .setDeleteIntent(PendingIntent.getBroadcast(
+ mContext,
+ /* requestCode= */ 0,
+ clearNotificationIntent,
+ /* flags= */ FLAG_UPDATE_CURRENT | FLAG_IMMUTABLE))
+ .setAutoCancel(true) // auto-clear notification on selection
+ .build();
+
+ mNotificationManager.notifyAsUser(NOTIFICATION_TAG,
+ TZ_CHANGE_NOTIFICATION_ID, notification, UserHandle.of(userId));
+ }
+
+ private CharSequence getNotificationBody(TimeZone newTimeZone, long unixEpochTimeMillis) {
+ DateFormat timeFormat = SimpleDateFormat.getInstanceForSkeleton("zzzz");
+ DateFormat offsetFormat = SimpleDateFormat.getInstanceForSkeleton("ZZZZ");
+
+ String newTime = formatInZone(timeFormat, newTimeZone, unixEpochTimeMillis);
+ String newOffset = formatInZone(offsetFormat, newTimeZone, unixEpochTimeMillis);
+
+ return mRes.getString(R.string.time_zone_change_notification_body, newTime, newOffset);
+ }
+
+ private static String formatInZone(DateFormat timeFormat, TimeZone timeZone,
+ long unixEpochTimeMillis) {
+ timeFormat.setTimeZone(timeZone);
+ return timeFormat.format(unixEpochTimeMillis);
+ }
+
+ @Override
+ public void dump(IndentingPrintWriter pw) {
+ synchronized (mConfigurationLock) {
+ pw.println("currentUserId=" + mConfigurationInternal.getUserId());
+ pw.println("notificationsEnabledBehavior="
+ + mConfigurationInternal.getNotificationsEnabledBehavior());
+ pw.println("notificationTrackingSupported="
+ + mConfigurationInternal.isNotificationTrackingSupported());
+ pw.println("manualChangeTrackingSupported="
+ + mConfigurationInternal.isManualChangeTrackingSupported());
+ }
+
+ pw.println("mAcceptedLocationChanges=" + mAcceptedLocationChanges);
+ pw.println("mAcceptedManualChanges=" + mAcceptedManualChanges);
+ pw.println("mAcceptedTelephonyChanges=" + mAcceptedTelephonyChanges);
+ pw.println("mAcceptedUnknownChanges=" + mAcceptedUnknownChanges);
+ pw.println("mRejectedLocationChanges=" + mRejectedLocationChanges);
+ pw.println("mRejectedTelephonyChanges=" + mRejectedTelephonyChanges);
+ pw.println("mRejectedUnknownChanges=" + mRejectedUnknownChanges);
+ pw.println("mNextChangeEventId=" + mNextChangeEventId);
+
+ pw.println("mTimeZoneChangeRecord:");
+ pw.increaseIndent();
+ synchronized (mTimeZoneChangeRecord) {
+ mTimeZoneChangeRecord.dump(pw);
+ }
+ pw.decreaseIndent();
+ }
+
+ @VisibleForTesting
+ static class TimeZoneChangeRecord {
+
+ private final int mId;
+ private final TimeZoneChangeEvent mEvent;
+ private @TimeZoneChangeStatus int mStatus = STATUS_UNKNOWN;
+ private @SignalType int mSignalType = SIGNAL_TYPE_UNKNOWN;
+
+ TimeZoneChangeRecord(int id, TimeZoneChangeEvent event) {
+ mId = id;
+ mEvent = Objects.requireNonNull(event);
+ }
+
+ public int getId() {
+ return mId;
+ }
+
+ public @TimeZoneChangeStatus int getStatus() {
+ return mStatus;
+ }
+
+ public void setAccepted(int signalType) {
+ setStatus(STATUS_ACCEPTED, signalType);
+ }
+
+ public void setRejected(int signalType) {
+ setStatus(STATUS_REJECTED, signalType);
+ }
+
+ public void setStatus(@TimeZoneChangeStatus int status, @SignalType int signalType) {
+ mStatus = status;
+ mSignalType = signalType;
+ }
+
+ public TimeZoneChangeEvent getEvent() {
+ return mEvent;
+ }
+
+ @Override
+ public String toString() {
+ return "TrackedTimeZoneChangeEvent{"
+ + "mId=" + mId
+ + ", mEvent=" + mEvent
+ + ", mStatus=" + mStatus
+ + ", mSignalType=" + mSignalType
+ + '}';
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o instanceof TimeZoneChangeRecord that) {
+ return mId == that.mId
+ && mEvent.equals(that.mEvent)
+ && mStatus == that.mStatus
+ && mSignalType == that.mSignalType;
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mId, mEvent, mStatus, mSignalType);
+ }
+ }
+
+ @VisibleForTesting
+ TimeZoneChangeRecord getLastTimeZoneChangeRecord() {
+ synchronized (mTimeZoneChangeRecord) {
+ return mTimeZoneChangeRecord.get();
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/timezonedetector/TimeZoneChangeListener.java b/services/core/java/com/android/server/timezonedetector/TimeZoneChangeListener.java
index e14326cc2d53..d340ed470591 100644
--- a/services/core/java/com/android/server/timezonedetector/TimeZoneChangeListener.java
+++ b/services/core/java/com/android/server/timezonedetector/TimeZoneChangeListener.java
@@ -102,5 +102,29 @@ public interface TimeZoneChangeListener {
+ ", mCause='" + mCause + '\''
+ '}';
}
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o instanceof TimeZoneChangeEvent that) {
+ return mElapsedRealtimeMillis == that.mElapsedRealtimeMillis
+ && mUnixEpochTimeMillis == that.mUnixEpochTimeMillis
+ && mOrigin == that.mOrigin
+ && mUserId == that.mUserId
+ && Objects.equals(mOldZoneId, that.mOldZoneId)
+ && Objects.equals(mNewZoneId, that.mNewZoneId)
+ && mNewConfidence == that.mNewConfidence
+ && Objects.equals(mCause, that.mCause);
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mElapsedRealtimeMillis, mUnixEpochTimeMillis, mOrigin, mUserId,
+ mOldZoneId, mNewZoneId, mNewConfidence, mCause);
+ }
}
}
diff --git a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorStrategyImpl.java b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorStrategyImpl.java
index 19a28ddcdaeb..b2b06b0af5fa 100644
--- a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorStrategyImpl.java
+++ b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorStrategyImpl.java
@@ -42,6 +42,7 @@ import android.app.timezonedetector.ManualTimeZoneSuggestion;
import android.app.timezonedetector.TelephonyTimeZoneSuggestion;
import android.content.Context;
import android.os.Handler;
+import android.os.SystemClock;
import android.os.TimestampedValue;
import android.os.UserHandle;
import android.util.IndentingPrintWriter;
@@ -50,6 +51,7 @@ import android.util.Slog;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.SystemTimeZone.TimeZoneConfidence;
+import com.android.server.flags.Flags;
import com.android.server.timezonedetector.ConfigurationInternal.DetectionMode;
import java.io.PrintWriter;
@@ -215,6 +217,13 @@ public final class TimeZoneDetectorStrategyImpl implements TimeZoneDetectorStrat
private final List<StateChangeListener> mStateChangeListeners = new ArrayList<>();
/**
+ * A component adjunct to the detection behavior that tracks time zone changes and implements
+ * behavior associated with time zone changes.
+ */
+ @NonNull
+ private final TimeZoneChangeListener mChangeTracker;
+
+ /**
* A snapshot of the current detector status. A local copy is cached because it is relatively
* heavyweight to obtain and is used more often than it is expected to change.
*/
@@ -256,15 +265,20 @@ public final class TimeZoneDetectorStrategyImpl implements TimeZoneDetectorStrat
@NonNull ServiceConfigAccessor serviceConfigAccessor) {
Environment environment = new EnvironmentImpl(handler);
- return new TimeZoneDetectorStrategyImpl(serviceConfigAccessor, environment);
+ TimeZoneChangeListener changeEventTracker =
+ NotifyingTimeZoneChangeListener.create(handler, context, serviceConfigAccessor);
+ return new TimeZoneDetectorStrategyImpl(
+ serviceConfigAccessor, environment, changeEventTracker);
}
@VisibleForTesting
public TimeZoneDetectorStrategyImpl(
@NonNull ServiceConfigAccessor serviceConfigAccessor,
- @NonNull Environment environment) {
+ @NonNull Environment environment,
+ @NonNull TimeZoneChangeListener changeEventTracker) {
mEnvironment = Objects.requireNonNull(environment);
mServiceConfigAccessor = Objects.requireNonNull(serviceConfigAccessor);
+ mChangeTracker = Objects.requireNonNull(changeEventTracker);
// Start with telephony fallback enabled.
mTelephonyTimeZoneFallbackEnabled =
@@ -833,6 +847,17 @@ public final class TimeZoneDetectorStrategyImpl implements TimeZoneDetectorStrat
Slog.d(LOG_TAG, logInfo);
}
mEnvironment.setDeviceTimeZoneAndConfidence(newZoneId, newConfidence, logInfo);
+
+ if (Flags.datetimeNotifications()) {
+ // Record the fact that the time zone was changed so that it can be tracked, i.e.
+ // whether the device / user sticks with it.
+ TimeZoneChangeListener.TimeZoneChangeEvent changeEvent =
+ new TimeZoneChangeListener.TimeZoneChangeEvent(
+ SystemClock.elapsedRealtime(), System.currentTimeMillis(), origin,
+ userId,
+ currentZoneId, newZoneId, newConfidence, cause);
+ mChangeTracker.process(changeEvent);
+ }
}
@GuardedBy("this")
@@ -947,6 +972,14 @@ public final class TimeZoneDetectorStrategyImpl implements TimeZoneDetectorStrat
ipw.increaseIndent(); // level 2
mTelephonySuggestionsBySlotIndex.dump(ipw);
ipw.decreaseIndent(); // level 2
+
+ if (Flags.datetimeNotifications()) {
+ ipw.println("Time zone change tracker:");
+ ipw.increaseIndent(); // level 2
+ mChangeTracker.dump(ipw);
+ ipw.decreaseIndent(); // level 2
+ }
+
ipw.decreaseIndent(); // level 1
}
diff --git a/services/core/java/com/android/server/vibrator/BasicToPwleSegmentAdapter.java b/services/core/java/com/android/server/vibrator/BasicToPwleSegmentAdapter.java
index 54ae047a2858..0b676ff7d590 100644
--- a/services/core/java/com/android/server/vibrator/BasicToPwleSegmentAdapter.java
+++ b/services/core/java/com/android/server/vibrator/BasicToPwleSegmentAdapter.java
@@ -100,6 +100,11 @@ final class BasicToPwleSegmentAdapter implements VibrationSegmentsAdapter {
}
VibratorInfo.FrequencyProfile frequencyProfile = info.getFrequencyProfile();
+ if (frequencyProfile.isEmpty()) {
+ // The frequency profile has an invalid frequency range, so keep the segments unchanged.
+ return repeatIndex;
+ }
+
float[] frequenciesHz = frequencyProfile.getFrequenciesHz();
float[] accelerationsGs = frequencyProfile.getOutputAccelerationsGs();
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index 3e6315635571..d31aed2aee37 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -232,6 +232,7 @@ import static com.android.server.wm.IdentifierProto.USER_ID;
import static com.android.server.wm.StartingData.AFTER_TRANSACTION_COPY_TO_CLIENT;
import static com.android.server.wm.StartingData.AFTER_TRANSACTION_IDLE;
import static com.android.server.wm.StartingData.AFTER_TRANSACTION_REMOVE_DIRECTLY;
+import static com.android.server.wm.StartingData.AFTER_TRANSITION_FINISH;
import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_APP_TRANSITION;
import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_PREDICT_BACK;
import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_WINDOW_ANIMATION;
@@ -2814,9 +2815,27 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A
attachStartingSurfaceToAssociatedTask();
}
+ /**
+ * If the device is locked and the app does not request showWhenLocked,
+ * defer removing the starting window until the transition is complete.
+ * This prevents briefly appearing the app context and causing secure concern.
+ */
+ void deferStartingWindowRemovalForKeyguardUnoccluding() {
+ if (mStartingData.mRemoveAfterTransaction != AFTER_TRANSITION_FINISH
+ && isKeyguardLocked() && !canShowWhenLockedInner(this) && !isVisibleRequested()
+ && mTransitionController.inTransition(this)) {
+ mStartingData.mRemoveAfterTransaction = AFTER_TRANSITION_FINISH;
+ }
+ }
+
void removeStartingWindow() {
boolean prevEligibleForLetterboxEducation = isEligibleForLetterboxEducation();
+ if (mStartingData != null
+ && mStartingData.mRemoveAfterTransaction == AFTER_TRANSITION_FINISH) {
+ return;
+ }
+
if (transferSplashScreenIfNeeded()) {
return;
}
@@ -4261,7 +4280,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A
}
void finishRelaunching() {
- mAppCompatController.getAppCompatOrientationOverrides()
+ mAppCompatController.getOrientationOverrides()
.setRelaunchingAfterRequestedOrientationChanged(false);
mTaskSupervisor.getActivityMetricsLogger().notifyActivityRelaunched(this);
@@ -4655,6 +4674,9 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A
tStartingWindow.mToken = this;
tStartingWindow.mActivityRecord = this;
+ if (mStartingData.mRemoveAfterTransaction == AFTER_TRANSITION_FINISH) {
+ mStartingData.mRemoveAfterTransaction = AFTER_TRANSACTION_IDLE;
+ }
if (mStartingData.mRemoveAfterTransaction == AFTER_TRANSACTION_REMOVE_DIRECTLY) {
// The removal of starting window should wait for window drawn of current
// activity.
@@ -8125,10 +8147,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A
if (task != null && requestedOrientation == SCREEN_ORIENTATION_BEHIND) {
// We use Task here because we want to be consistent with what happens in
// multi-window mode where other tasks orientations are ignored.
- final ActivityRecord belowCandidate = task.getActivity(
- a -> a.canDefineOrientationForActivitiesAbove() /* callback */,
- this /* boundary */, false /* includeBoundary */,
- true /* traverseTopToBottom */);
+ final ActivityRecord belowCandidate = task.getActivityBelowForDefiningOrientation(this);
if (belowCandidate != null) {
return belowCandidate.getRequestedConfigurationOrientation(forDisplay);
}
@@ -8222,7 +8241,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A
mLastReportedConfiguration.getMergedConfiguration())) {
ensureActivityConfiguration(false /* ignoreVisibility */);
if (mPendingRelaunchCount > originalRelaunchingCount) {
- mAppCompatController.getAppCompatOrientationOverrides()
+ mAppCompatController.getOrientationOverrides()
.setRelaunchingAfterRequestedOrientationChanged(true);
}
if (mTransitionController.inPlayingTransition(this)) {
@@ -8976,13 +8995,12 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A
|| orientationRespectedWithInsets)) {
return;
}
- final AppCompatDisplayInsets mAppCompatDisplayInsets = getAppCompatDisplayInsets();
+ final AppCompatDisplayInsets appCompatDisplayInsets = getAppCompatDisplayInsets();
final AppCompatSizeCompatModePolicy scmPolicy =
mAppCompatController.getAppCompatSizeCompatModePolicy();
- if (scmPolicy.hasAppCompatDisplayInsetsWithoutInheritance()
- && mAppCompatDisplayInsets != null
- && !mAppCompatDisplayInsets.mIsInFixedOrientationOrAspectRatioLetterbox) {
+ if (appCompatDisplayInsets != null
+ && !appCompatDisplayInsets.mIsInFixedOrientationOrAspectRatioLetterbox) {
// App prefers to keep its original size.
// If the size compat is from previous fixed orientation letterboxing, we may want to
// have fixed orientation letterbox again, otherwise it will show the size compat
@@ -9034,8 +9052,8 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A
.applyDesiredAspectRatio(newParentConfig, parentBounds, resolvedBounds,
containingBoundsWithInsets, containingBounds);
- if (scmPolicy.hasAppCompatDisplayInsetsWithoutInheritance()) {
- mAppCompatDisplayInsets.getBoundsByRotation(mTmpBounds,
+ if (appCompatDisplayInsets != null) {
+ appCompatDisplayInsets.getBoundsByRotation(mTmpBounds,
newParentConfig.windowConfiguration.getRotation());
if (resolvedBounds.width() != mTmpBounds.width()
|| resolvedBounds.height() != mTmpBounds.height()) {
@@ -9058,7 +9076,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A
// Calculate app bounds using fixed orientation bounds because they will be needed later
// for comparison with size compat app bounds in {@link resolveSizeCompatModeConfiguration}.
- mResolveConfigHint.mTmpCompatInsets = mAppCompatDisplayInsets;
+ mResolveConfigHint.mTmpCompatInsets = appCompatDisplayInsets;
computeConfigByResolveHint(getResolvedOverrideConfiguration(), newParentConfig);
mAppCompatController.getAppCompatAspectRatioPolicy()
.setLetterboxBoundsForFixedOrientationAndAspectRatio(new Rect(resolvedBounds));
@@ -10217,7 +10235,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A
mAppCompatController.getAppCompatAspectRatioOverrides()
.shouldOverrideMinAspectRatio());
proto.write(SHOULD_IGNORE_ORIENTATION_REQUEST_LOOP,
- mAppCompatController.getAppCompatOrientationOverrides()
+ mAppCompatController.getOrientationOverrides()
.shouldIgnoreOrientationRequestLoop());
proto.write(SHOULD_OVERRIDE_FORCE_RESIZE_APP,
mAppCompatController.getResizeOverrides().shouldOverrideForceResizeApp());
diff --git a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
index ef6f92317b2c..12c8f9ccac7c 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
@@ -2538,7 +2538,7 @@ public class ActivityTaskSupervisor implements RecentTasks.Callbacks {
void wakeUp(int displayId, String reason) {
mPowerManager.wakeUp(SystemClock.uptimeMillis(), PowerManager.WAKE_REASON_APPLICATION,
- "android.server.am:TURN_ON:" + reason, displayId);
+ "android.server.wm:TURN_ON:" + reason, displayId);
}
/** Starts a batch of visibility updates. */
diff --git a/services/core/java/com/android/server/wm/AppCompatController.java b/services/core/java/com/android/server/wm/AppCompatController.java
index c748264a833e..6d0e8eacd438 100644
--- a/services/core/java/com/android/server/wm/AppCompatController.java
+++ b/services/core/java/com/android/server/wm/AppCompatController.java
@@ -89,8 +89,8 @@ class AppCompatController {
}
@NonNull
- AppCompatOrientationOverrides getAppCompatOrientationOverrides() {
- return mAppCompatOverrides.getAppCompatOrientationOverrides();
+ AppCompatOrientationOverrides getOrientationOverrides() {
+ return mAppCompatOverrides.getOrientationOverrides();
}
@NonNull
diff --git a/services/core/java/com/android/server/wm/AppCompatLetterboxPolicy.java b/services/core/java/com/android/server/wm/AppCompatLetterboxPolicy.java
index 8866e39fecc9..449458665b63 100644
--- a/services/core/java/com/android/server/wm/AppCompatLetterboxPolicy.java
+++ b/services/core/java/com/android/server/wm/AppCompatLetterboxPolicy.java
@@ -154,7 +154,7 @@ class AppCompatLetterboxPolicy {
@VisibleForTesting
boolean shouldShowLetterboxUi(@NonNull WindowState mainWindow) {
- if (mActivityRecord.mAppCompatController.getAppCompatOrientationOverrides()
+ if (mActivityRecord.mAppCompatController.getOrientationOverrides()
.getIsRelaunchingAfterRequestedOrientationChanged()) {
return mLastShouldShowLetterboxUi;
}
diff --git a/services/core/java/com/android/server/wm/AppCompatOrientationOverrides.java b/services/core/java/com/android/server/wm/AppCompatOrientationOverrides.java
index c84711d4be51..af83668f1188 100644
--- a/services/core/java/com/android/server/wm/AppCompatOrientationOverrides.java
+++ b/services/core/java/com/android/server/wm/AppCompatOrientationOverrides.java
@@ -113,7 +113,7 @@ class AppCompatOrientationOverrides {
// Task to ensure that Activity Embedding is excluded.
return mActivityRecord.isVisibleRequested() && mActivityRecord.getTaskFragment() != null
&& mActivityRecord.getTaskFragment().getWindowingMode() == WINDOWING_MODE_FULLSCREEN
- && mActivityRecord.mAppCompatController.getAppCompatOrientationOverrides()
+ && mActivityRecord.mAppCompatController.getOrientationOverrides()
.isOverrideRespectRequestedOrientationEnabled();
}
diff --git a/services/core/java/com/android/server/wm/AppCompatOrientationPolicy.java b/services/core/java/com/android/server/wm/AppCompatOrientationPolicy.java
index 16e20297dcf3..fc758ef90995 100644
--- a/services/core/java/com/android/server/wm/AppCompatOrientationPolicy.java
+++ b/services/core/java/com/android/server/wm/AppCompatOrientationPolicy.java
@@ -94,7 +94,7 @@ class AppCompatOrientationPolicy {
return SCREEN_ORIENTATION_PORTRAIT;
}
- if (mAppCompatOverrides.getAppCompatOrientationOverrides()
+ if (mAppCompatOverrides.getOrientationOverrides()
.isAllowOrientationOverrideOptOut()) {
return candidate;
}
@@ -108,7 +108,7 @@ class AppCompatOrientationPolicy {
}
final AppCompatOrientationOverrides.OrientationOverridesState capabilityState =
- mAppCompatOverrides.getAppCompatOrientationOverrides()
+ mAppCompatOverrides.getOrientationOverrides()
.mOrientationOverridesState;
if (capabilityState.mIsOverrideToReverseLandscapeOrientationEnabled
@@ -170,7 +170,7 @@ class AppCompatOrientationPolicy {
boolean shouldIgnoreRequestedOrientation(
@ActivityInfo.ScreenOrientation int requestedOrientation) {
final AppCompatOrientationOverrides orientationOverrides =
- mAppCompatOverrides.getAppCompatOrientationOverrides();
+ mAppCompatOverrides.getOrientationOverrides();
if (orientationOverrides.shouldEnableIgnoreOrientationRequest()) {
if (orientationOverrides.getIsRelaunchingAfterRequestedOrientationChanged()) {
Slog.w(TAG, "Ignoring orientation update to "
diff --git a/services/core/java/com/android/server/wm/AppCompatOverrides.java b/services/core/java/com/android/server/wm/AppCompatOverrides.java
index 9effae6aa640..9fb54db23d55 100644
--- a/services/core/java/com/android/server/wm/AppCompatOverrides.java
+++ b/services/core/java/com/android/server/wm/AppCompatOverrides.java
@@ -27,7 +27,7 @@ import com.android.server.wm.utils.OptPropFactory;
public class AppCompatOverrides {
@NonNull
- private final AppCompatOrientationOverrides mAppCompatOrientationOverrides;
+ private final AppCompatOrientationOverrides mOrientationOverrides;
@NonNull
private final AppCompatCameraOverrides mAppCompatCameraOverrides;
@NonNull
@@ -48,7 +48,7 @@ public class AppCompatOverrides {
@NonNull AppCompatDeviceStateQuery appCompatDeviceStateQuery) {
mAppCompatCameraOverrides = new AppCompatCameraOverrides(activityRecord,
appCompatConfiguration, optPropBuilder);
- mAppCompatOrientationOverrides = new AppCompatOrientationOverrides(activityRecord,
+ mOrientationOverrides = new AppCompatOrientationOverrides(activityRecord,
appCompatConfiguration, optPropBuilder, mAppCompatCameraOverrides);
mReachabilityOverrides = new AppCompatReachabilityOverrides(activityRecord,
appCompatConfiguration, appCompatDeviceStateQuery);
@@ -64,8 +64,8 @@ public class AppCompatOverrides {
}
@NonNull
- AppCompatOrientationOverrides getAppCompatOrientationOverrides() {
- return mAppCompatOrientationOverrides;
+ AppCompatOrientationOverrides getOrientationOverrides() {
+ return mOrientationOverrides;
}
@NonNull
diff --git a/services/core/java/com/android/server/wm/DisplayArea.java b/services/core/java/com/android/server/wm/DisplayArea.java
index f40d636b522a..b932ef362aca 100644
--- a/services/core/java/com/android/server/wm/DisplayArea.java
+++ b/services/core/java/com/android/server/wm/DisplayArea.java
@@ -264,7 +264,7 @@ public class DisplayArea<T extends WindowContainer> extends WindowContainer<T> {
// that should be respected, Check all activities in display to make sure any eligible
// activity should be respected.
final ActivityRecord activity = mDisplayContent.getActivity((r) ->
- r.mAppCompatController.getAppCompatOrientationOverrides()
+ r.mAppCompatController.getOrientationOverrides()
.shouldRespectRequestedOrientationDueToOverride());
return activity != null;
}
diff --git a/services/core/java/com/android/server/wm/DisplayAreaPolicy.java b/services/core/java/com/android/server/wm/DisplayAreaPolicy.java
index d49a507c9e11..5bec4424269a 100644
--- a/services/core/java/com/android/server/wm/DisplayAreaPolicy.java
+++ b/services/core/java/com/android/server/wm/DisplayAreaPolicy.java
@@ -25,6 +25,8 @@ import static android.view.WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL;
import static android.view.WindowManager.LayoutParams.TYPE_NOTIFICATION_SHADE;
import static android.view.WindowManager.LayoutParams.TYPE_SECURE_SYSTEM_OVERLAY;
import static android.view.WindowManager.LayoutParams.TYPE_STATUS_BAR;
+import static android.view.WindowManager.LayoutParams.TYPE_WALLPAPER;
+import static android.window.DisplayAreaOrganizer.FEATURE_APP_ZOOM_OUT;
import static android.window.DisplayAreaOrganizer.FEATURE_DEFAULT_TASK_CONTAINER;
import static android.window.DisplayAreaOrganizer.FEATURE_FULLSCREEN_MAGNIFICATION;
import static android.window.DisplayAreaOrganizer.FEATURE_HIDE_DISPLAY_CUTOUT;
@@ -151,6 +153,12 @@ public abstract class DisplayAreaPolicy {
.all()
.except(TYPE_NAVIGATION_BAR, TYPE_NAVIGATION_BAR_PANEL,
TYPE_SECURE_SYSTEM_OVERLAY)
+ .build())
+ .addFeature(new Feature.Builder(wmService.mPolicy, "AppZoomOut",
+ FEATURE_APP_ZOOM_OUT)
+ .all()
+ .except(TYPE_NAVIGATION_BAR, TYPE_NAVIGATION_BAR_PANEL,
+ TYPE_STATUS_BAR, TYPE_NOTIFICATION_SHADE, TYPE_WALLPAPER)
.build());
}
rootHierarchy
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index 145c7b37fcdc..d32c31f1c1c7 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -1822,7 +1822,13 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp
*/
private void applyFixedRotationForNonTopVisibleActivityIfNeeded(@NonNull ActivityRecord ar,
@ActivityInfo.ScreenOrientation int topOrientation) {
- final int orientation = ar.getRequestedOrientation();
+ int orientation = ar.getRequestedOrientation();
+ if (orientation == ActivityInfo.SCREEN_ORIENTATION_BEHIND) {
+ final ActivityRecord nextCandidate = getActivityBelowForDefiningOrientation(ar);
+ if (nextCandidate != null) {
+ orientation = nextCandidate.getRequestedOrientation();
+ }
+ }
if (orientation == topOrientation || ar.inMultiWindowMode()
|| ar.getRequestedConfigurationOrientation() == ORIENTATION_UNDEFINED) {
return;
@@ -1864,9 +1870,7 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp
return ROTATION_UNDEFINED;
}
if (activityOrientation == ActivityInfo.SCREEN_ORIENTATION_BEHIND) {
- final ActivityRecord nextCandidate = getActivity(
- a -> a.canDefineOrientationForActivitiesAbove() /* callback */,
- r /* boundary */, false /* includeBoundary */, true /* traverseTopToBottom */);
+ final ActivityRecord nextCandidate = getActivityBelowForDefiningOrientation(r);
if (nextCandidate != null) {
r = nextCandidate;
activityOrientation = r.getOverrideOrientation();
@@ -2964,7 +2968,7 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp
if (!handlesOrientationChangeFromDescendant(orientation)) {
ActivityRecord topActivity = topRunningActivity(/* considerKeyguardState= */ true);
if (topActivity != null && topActivity.mAppCompatController
- .getAppCompatOrientationOverrides()
+ .getOrientationOverrides()
.shouldUseDisplayLandscapeNaturalOrientation()) {
ProtoLog.v(WM_DEBUG_ORIENTATION,
"Display id=%d is ignoring orientation request for %d, return %d"
@@ -4291,7 +4295,7 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp
return target;
}
if (android.view.inputmethod.Flags.refactorInsetsController()) {
- final DisplayContent defaultDc = mWmService.getDefaultDisplayContentLocked();
+ final DisplayContent defaultDc = getUserMainDisplayContent();
return defaultDc.mRemoteInsetsControlTarget;
} else {
return getImeFallback();
@@ -4301,11 +4305,26 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp
InsetsControlTarget getImeFallback() {
// host is in non-default display that doesn't support system decor, default to
// default display's StatusBar to control IME (when available), else let system control it.
- final DisplayContent defaultDc = mWmService.getDefaultDisplayContentLocked();
- WindowState statusBar = defaultDc.getDisplayPolicy().getStatusBar();
+ final DisplayContent defaultDc = getUserMainDisplayContent();
+ final WindowState statusBar = defaultDc.getDisplayPolicy().getStatusBar();
return statusBar != null ? statusBar : defaultDc.mRemoteInsetsControlTarget;
}
+ private DisplayContent getUserMainDisplayContent() {
+ final DisplayContent defaultDc;
+ if (android.view.inputmethod.Flags.fallbackDisplayForSecondaryUserOnSecondaryDisplay()) {
+ final int userId = mWmService.mUmInternal.getUserAssignedToDisplay(mDisplayId);
+ defaultDc = mWmService.getUserMainDisplayContentLocked(userId);
+ if (defaultDc == null) {
+ throw new IllegalStateException(
+ "No default display was assigned to user " + userId);
+ }
+ } else {
+ defaultDc = mWmService.getDefaultDisplayContentLocked();
+ }
+ return defaultDc;
+ }
+
/**
* Returns the corresponding IME insets control target according the IME target type.
*
@@ -4841,8 +4860,15 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp
// The control target could be the RemoteInsetsControlTarget (if the focussed
// view is on a virtual display that can not show the IME (and therefore it will
// be shown on the default display)
- if (isDefaultDisplay && mRemoteInsetsControlTarget != null) {
- return mRemoteInsetsControlTarget;
+ if (android.view.inputmethod.Flags
+ .fallbackDisplayForSecondaryUserOnSecondaryDisplay()) {
+ if (isUserMainDisplay() && mRemoteInsetsControlTarget != null) {
+ return mRemoteInsetsControlTarget;
+ }
+ } else {
+ if (isDefaultDisplay && mRemoteInsetsControlTarget != null) {
+ return mRemoteInsetsControlTarget;
+ }
}
}
return null;
@@ -4858,6 +4884,16 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp
}
/**
+ * Returns {@code true} if {@link #mDisplayId} corresponds to the user's main display.
+ *
+ * <p>Visible background users may have other than DEFAULT_DISPLAY marked as their main display.
+ */
+ private boolean isUserMainDisplay() {
+ final int userId = mWmService.mUmInternal.getUserAssignedToDisplay(mDisplayId);
+ return mDisplayId == mWmService.mUmInternal.getMainDisplayAssignedToUser(userId);
+ }
+
+ /**
* Computes the window the IME should be attached to.
*/
@VisibleForTesting
diff --git a/services/core/java/com/android/server/wm/DragDropController.java b/services/core/java/com/android/server/wm/DragDropController.java
index 3c60d8296577..db058cafe5fe 100644
--- a/services/core/java/com/android/server/wm/DragDropController.java
+++ b/services/core/java/com/android/server/wm/DragDropController.java
@@ -215,7 +215,8 @@ class DragDropController {
mDragState.mOriginalAlpha = alpha;
mDragState.mAnimatedScale = callingWin.mGlobalScale;
mDragState.mToken = dragToken;
- mDragState.mDisplayContent = displayContent;
+ mDragState.mStartDragDisplayContent = displayContent;
+ mDragState.mCurrentDisplayContent = displayContent;
mDragState.mData = data;
mDragState.mCallingTaskIdToHide = shouldMoveCallingTaskToBack(callingWin,
flags);
@@ -273,7 +274,7 @@ class DragDropController {
InputManagerGlobal.getInstance().setPointerIcon(
PointerIcon.getSystemIcon(
mService.mContext, PointerIcon.TYPE_GRABBING),
- mDragState.mDisplayContent.getDisplayId(), touchDeviceId,
+ mDragState.mCurrentDisplayContent.getDisplayId(), touchDeviceId,
touchPointerId, mDragState.getInputToken());
}
// remember the thumb offsets for later
diff --git a/services/core/java/com/android/server/wm/DragState.java b/services/core/java/com/android/server/wm/DragState.java
index 3a0e41a5f9f8..d48b9b4a5d10 100644
--- a/services/core/java/com/android/server/wm/DragState.java
+++ b/services/core/java/com/android/server/wm/DragState.java
@@ -45,6 +45,7 @@ import android.annotation.Nullable;
import android.content.ClipData;
import android.content.ClipDescription;
import android.graphics.Point;
+import android.graphics.PointF;
import android.graphics.Rect;
import android.os.Binder;
import android.os.Build;
@@ -70,6 +71,7 @@ import com.android.internal.protolog.ProtoLog;
import com.android.internal.view.IDragAndDropPermissions;
import com.android.server.LocalServices;
import com.android.server.pm.UserManagerInternal;
+import com.android.window.flags.Flags;
import java.util.ArrayList;
import java.util.concurrent.CompletableFuture;
@@ -127,10 +129,17 @@ class DragState {
*/
volatile boolean mAnimationCompleted = false;
/**
+ * The display on which the drag originally started. Note that it's possible for either/both
+ * mStartDragDisplayContent and mCurrentDisplayContent to be invalid if DisplayTopology was
+ * changed or removed in the middle of the drag. In this case, drag will also be cancelled as
+ * soon as listener is notified.
+ */
+ DisplayContent mStartDragDisplayContent;
+ /**
* The display on which the drag is happening. If it goes into a different display this will
* be updated.
*/
- DisplayContent mDisplayContent;
+ DisplayContent mCurrentDisplayContent;
@Nullable private ValueAnimator mAnimator;
private final Interpolator mCubicEaseOutInterpolator = new DecelerateInterpolator(1.5f);
@@ -179,7 +188,7 @@ class DragState {
.setContainerLayer()
.setName("Drag and Drop Input Consumer")
.setCallsite("DragState.showInputSurface")
- .setParent(mDisplayContent.getOverlayLayer())
+ .setParent(mCurrentDisplayContent.getOverlayLayer())
.build();
}
final InputWindowHandle h = getInputWindowHandle();
@@ -244,7 +253,8 @@ class DragState {
}
}
DragEvent event = DragEvent.obtain(DragEvent.ACTION_DRAG_ENDED, inWindowX,
- inWindowY, mThumbOffsetX, mThumbOffsetY, mFlags, null, null, null,
+ inWindowY, mThumbOffsetX, mThumbOffsetY,
+ mCurrentDisplayContent.getDisplayId(), mFlags, null, null, null,
dragSurface, null, mDragResult);
try {
if (DEBUG_DRAG) Slog.d(TAG_WM, "Sending DRAG_ENDED to " + ws);
@@ -542,10 +552,26 @@ class DragState {
}
}
ClipDescription description = data != null ? data.getDescription() : mDataDescription;
+
+ // Note this can be negative numbers if touch coords are left or top of the window.
+ PointF relativeToWindowCoords = new PointF(newWin.translateToWindowX(touchX),
+ newWin.translateToWindowY(touchY));
+ if (Flags.enableConnectedDisplaysDnd()
+ && mCurrentDisplayContent.getDisplayId() != newWin.getDisplayId()) {
+ // Currently DRAG_STARTED coords are sent relative to the window target in **px**
+ // coordinates. However, this cannot be extended to connected displays scenario,
+ // as there's only global **dp** coordinates and no global **px** coordinates.
+ // Hence, the coords sent here will only try to indicate that drag started outside
+ // this window display, but relative distance should not be calculated or depended
+ // on.
+ relativeToWindowCoords = new PointF(-newWin.getBounds().left - 1,
+ -newWin.getBounds().top - 1);
+ }
+
DragEvent event = obtainDragEvent(DragEvent.ACTION_DRAG_STARTED,
- newWin.translateToWindowX(touchX), newWin.translateToWindowY(touchY),
- description, data, false /* includeDragSurface */,
- true /* includeDragFlags */, null /* dragAndDropPermission */);
+ relativeToWindowCoords.x, relativeToWindowCoords.y, description, data,
+ false /* includeDragSurface */, true /* includeDragFlags */,
+ null /* dragAndDropPermission */);
try {
newWin.mClient.dispatchDragEvent(event);
// track each window that we've notified that the drag is starting
@@ -702,6 +728,20 @@ class DragState {
mCurrentDisplayX = displayX;
mCurrentDisplayY = displayY;
+ final DisplayContent lastSetDisplayContent = mCurrentDisplayContent;
+ boolean cursorMovedToDifferentDisplay = false;
+ // Keep latest display up-to-date even when drag has stopped.
+ if (Flags.enableConnectedDisplaysDnd() && mCurrentDisplayContent.mDisplayId != displayId) {
+ final DisplayContent newDisplay = mService.mRoot.getDisplayContent(displayId);
+ if (newDisplay == null) {
+ Slog.e(TAG_WM, "Target displayId=" + displayId + " was not found, ending drag.");
+ endDragLocked(false /* dropConsumed */,
+ false /* relinquishDragSurfaceToDropTarget */);
+ return;
+ }
+ cursorMovedToDifferentDisplay = true;
+ mCurrentDisplayContent = newDisplay;
+ }
if (!keepHandling) {
return;
}
@@ -710,6 +750,24 @@ class DragState {
if (SHOW_LIGHT_TRANSACTIONS) {
Slog.i(TAG_WM, ">>> OPEN TRANSACTION notifyMoveLocked");
}
+ if (cursorMovedToDifferentDisplay) {
+ mAnimatedScale = mAnimatedScale * mCurrentDisplayContent.mBaseDisplayDensity
+ / lastSetDisplayContent.mBaseDisplayDensity;
+ mThumbOffsetX = mThumbOffsetX * mCurrentDisplayContent.mBaseDisplayDensity
+ / lastSetDisplayContent.mBaseDisplayDensity;
+ mThumbOffsetY = mThumbOffsetY * mCurrentDisplayContent.mBaseDisplayDensity
+ / lastSetDisplayContent.mBaseDisplayDensity;
+ mTransaction.reparent(mSurfaceControl, mCurrentDisplayContent.getSurfaceControl());
+ mTransaction.setScale(mSurfaceControl, mAnimatedScale, mAnimatedScale);
+
+ final InputWindowHandle inputWindowHandle = getInputWindowHandle();
+ if (inputWindowHandle == null) {
+ Slog.w(TAG_WM, "Drag is in progress but there is no drag window handle.");
+ return;
+ }
+ inputWindowHandle.displayId = displayId;
+ mTransaction.setInputWindowInfo(mInputSurface, inputWindowHandle);
+ }
mTransaction.setPosition(mSurfaceControl, displayX - mThumbOffsetX,
displayY - mThumbOffsetY).apply();
ProtoLog.i(WM_SHOW_TRANSACTIONS, "DRAG %s: displayId=%d, pos=(%d,%d)", mSurfaceControl,
@@ -734,10 +792,10 @@ class DragState {
ClipData data, boolean includeDragSurface, boolean includeDragFlags,
IDragAndDropPermissions dragAndDropPermissions) {
return DragEvent.obtain(action, x, y, mThumbOffsetX, mThumbOffsetY,
- includeDragFlags ? mFlags : 0,
+ mCurrentDisplayContent.getDisplayId(), includeDragFlags ? mFlags : 0,
null /* localState */, description, data,
- includeDragSurface ? mSurfaceControl : null,
- dragAndDropPermissions, false /* result */);
+ includeDragSurface ? mSurfaceControl : null, dragAndDropPermissions,
+ false /* result */);
}
private ValueAnimator createReturnAnimationLocked() {
diff --git a/services/core/java/com/android/server/wm/PageSizeMismatchDialog.java b/services/core/java/com/android/server/wm/PageSizeMismatchDialog.java
index 8c50913dd563..29922f0f85c5 100644
--- a/services/core/java/com/android/server/wm/PageSizeMismatchDialog.java
+++ b/services/core/java/com/android/server/wm/PageSizeMismatchDialog.java
@@ -57,9 +57,11 @@ class PageSizeMismatchDialog extends AppWarnings.BaseDialog {
final AlertDialog.Builder builder =
new AlertDialog.Builder(context)
- .setPositiveButton(
- R.string.ok,
- (dialog, which) -> {/* Do nothing */})
+ .setPositiveButton(R.string.ok, (dialog, which) ->
+ manager.setPackageFlag(
+ mUserId, mPackageName,
+ AppWarnings.FLAG_HIDE_PAGE_SIZE_MISMATCH,
+ true))
.setMessage(Html.fromHtml(warning, FROM_HTML_MODE_COMPACT))
.setTitle(label);
diff --git a/services/core/java/com/android/server/wm/PersisterQueue.java b/services/core/java/com/android/server/wm/PersisterQueue.java
index 9dc3d6a81338..bc16a566bfef 100644
--- a/services/core/java/com/android/server/wm/PersisterQueue.java
+++ b/services/core/java/com/android/server/wm/PersisterQueue.java
@@ -86,6 +86,34 @@ class PersisterQueue {
mLazyTaskWriterThread = new LazyTaskWriterThread("LazyTaskWriterThread");
}
+ /**
+ * Busy wait until {@link #mLazyTaskWriterThread} is in {@link Thread.State#WAITING}, or
+ * times out. This indicates the thread is waiting for new tasks to appear. If the wait
+ * succeeds, this queue waits at least {@link #mPreTaskDelayMs} milliseconds before running the
+ * next task.
+ *
+ * <p>This is for testing purposes only.
+ *
+ * @param timeoutMillis the maximum time of waiting in milliseconds
+ * @return {@code true} if the thread is in {@link Thread.State#WAITING} at return
+ */
+ @VisibleForTesting
+ boolean waitUntilWritingThreadIsWaiting(long timeoutMillis) {
+ final long timeoutTime = SystemClock.uptimeMillis() + timeoutMillis;
+ do {
+ Thread.State state;
+ synchronized (this) {
+ state = mLazyTaskWriterThread.getState();
+ }
+ if (state == Thread.State.WAITING) {
+ return true;
+ }
+ Thread.yield();
+ } while (SystemClock.uptimeMillis() < timeoutTime);
+
+ return false;
+ }
+
synchronized void startPersisting() {
if (!mLazyTaskWriterThread.isAlive()) {
mLazyTaskWriterThread.start();
diff --git a/services/core/java/com/android/server/wm/SnapshotPersistQueue.java b/services/core/java/com/android/server/wm/SnapshotPersistQueue.java
index a5454546341b..3eb13c52cca6 100644
--- a/services/core/java/com/android/server/wm/SnapshotPersistQueue.java
+++ b/services/core/java/com/android/server/wm/SnapshotPersistQueue.java
@@ -407,10 +407,8 @@ class SnapshotPersistQueue {
bitmap.recycle();
final File file = mPersistInfoProvider.getHighResolutionBitmapFile(mId, mUserId);
- try {
- FileOutputStream fos = new FileOutputStream(file);
+ try (FileOutputStream fos = new FileOutputStream(file)) {
swBitmap.compress(JPEG, COMPRESS_QUALITY, fos);
- fos.close();
} catch (IOException e) {
Slog.e(TAG, "Unable to open " + file + " for persisting.", e);
return false;
@@ -428,10 +426,8 @@ class SnapshotPersistQueue {
swBitmap.recycle();
final File lowResFile = mPersistInfoProvider.getLowResolutionBitmapFile(mId, mUserId);
- try {
- FileOutputStream lowResFos = new FileOutputStream(lowResFile);
+ try (FileOutputStream lowResFos = new FileOutputStream(lowResFile)) {
lowResBitmap.compress(JPEG, COMPRESS_QUALITY, lowResFos);
- lowResFos.close();
} catch (IOException e) {
Slog.e(TAG, "Unable to open " + lowResFile + " for persisting.", e);
return false;
diff --git a/services/core/java/com/android/server/wm/StartingData.java b/services/core/java/com/android/server/wm/StartingData.java
index 7349224ddcd8..1a7a6196cf85 100644
--- a/services/core/java/com/android/server/wm/StartingData.java
+++ b/services/core/java/com/android/server/wm/StartingData.java
@@ -31,11 +31,18 @@ public abstract class StartingData {
static final int AFTER_TRANSACTION_REMOVE_DIRECTLY = 1;
/** Do copy splash screen to client after transaction done. */
static final int AFTER_TRANSACTION_COPY_TO_CLIENT = 2;
+ /**
+ * Remove the starting window after transition finish.
+ * Used when activity doesn't request show when locked, so the app window should never show to
+ * the user if device is locked.
+ **/
+ static final int AFTER_TRANSITION_FINISH = 3;
@IntDef(prefix = { "AFTER_TRANSACTION" }, value = {
AFTER_TRANSACTION_IDLE,
AFTER_TRANSACTION_REMOVE_DIRECTLY,
AFTER_TRANSACTION_COPY_TO_CLIENT,
+ AFTER_TRANSITION_FINISH,
})
@interface AfterTransaction {}
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index d92301ba4f6f..fe478c60bc32 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -5230,9 +5230,15 @@ class Task extends TaskFragment {
// to ensure any necessary pause logic occurs. In the case where the Activity will be
// shown regardless of the lock screen, the call to
// {@link ActivityTaskSupervisor#checkReadyForSleepLocked} is skipped.
- final ActivityRecord next = topRunningActivity(true /* focusableOnly */);
- if (next == null || !next.canTurnScreenOn()) {
- checkReadyForSleep();
+ if (shouldSleepActivities()) {
+ final ActivityRecord next = topRunningActivity(true /* focusableOnly */);
+ if (next != null && next.canTurnScreenOn()
+ && !mWmService.mPowerManager.isInteractive()) {
+ mTaskSupervisor.wakeUp(getDisplayId(), "resumeTop-turnScreenOnFlag");
+ next.setCurrentLaunchCanTurnScreenOn(false);
+ } else {
+ checkReadyForSleep();
+ }
}
} finally {
mInResumeTopActivity = false;
@@ -5255,6 +5261,10 @@ class Task extends TaskFragment {
return false;
}
+ if (!mTaskSupervisor.readyToResume()) {
+ return false;
+ }
+
final ActivityRecord topActivity = topRunningActivity(true /* focusableOnly */);
if (topActivity == null) {
// There are no activities left in this task, let's look somewhere else.
diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java
index a9646783b92d..f4a455a9c2dd 100644
--- a/services/core/java/com/android/server/wm/Transition.java
+++ b/services/core/java/com/android/server/wm/Transition.java
@@ -70,6 +70,8 @@ import static com.android.server.wm.ActivityRecord.State.RESUMED;
import static com.android.server.wm.ActivityTaskManagerInternal.APP_TRANSITION_RECENTS_ANIM;
import static com.android.server.wm.ActivityTaskManagerInternal.APP_TRANSITION_SPLASH_SCREEN;
import static com.android.server.wm.ActivityTaskManagerInternal.APP_TRANSITION_WINDOWS_DRAWN;
+import static com.android.server.wm.StartingData.AFTER_TRANSACTION_IDLE;
+import static com.android.server.wm.StartingData.AFTER_TRANSITION_FINISH;
import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_PREDICT_BACK;
import static com.android.server.wm.WindowContainer.AnimationFlags.PARENTS;
import static com.android.server.wm.WindowState.BLAST_TIMEOUT_DURATION;
@@ -1374,6 +1376,13 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener {
enterAutoPip = true;
}
}
+
+ if (ar.mStartingData != null && ar.mStartingData.mRemoveAfterTransaction
+ == AFTER_TRANSITION_FINISH
+ && (!ar.isVisible() || !ar.mTransitionController.inTransition(ar))) {
+ ar.mStartingData.mRemoveAfterTransaction = AFTER_TRANSACTION_IDLE;
+ ar.removeStartingWindow();
+ }
final ChangeInfo changeInfo = mChanges.get(ar);
// Due to transient-hide, there may be some activities here which weren't in the
// transition.
diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java
index aa60f939f9aa..e761e024b3ca 100644
--- a/services/core/java/com/android/server/wm/WindowContainer.java
+++ b/services/core/java/com/android/server/wm/WindowContainer.java
@@ -117,6 +117,7 @@ import com.android.server.wm.SurfaceAnimator.Animatable;
import com.android.server.wm.SurfaceAnimator.AnimationType;
import com.android.server.wm.SurfaceAnimator.OnAnimationFinishedCallback;
import com.android.server.wm.utils.AlwaysTruePredicate;
+import com.android.window.flags.Flags;
import java.io.PrintWriter;
import java.lang.ref.WeakReference;
@@ -1659,6 +1660,12 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer<
return ORIENTATION_UNDEFINED;
}
+ @Nullable
+ ActivityRecord getActivityBelowForDefiningOrientation(ActivityRecord from) {
+ return getActivity(ActivityRecord::canDefineOrientationForActivitiesAbove,
+ from /* boundary */, false /* includeBoundary */, true /* traverseTopToBottom */);
+ }
+
/**
* Calls {@link #setOrientation(int, WindowContainer)} with {@code null} to the last 2
* parameters.
@@ -2730,6 +2737,13 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer<
if (!mTransitionController.canAssignLayers(this)) return;
final boolean changed = layer != mLastLayer || mLastRelativeToLayer != null;
if (mSurfaceControl != null && changed) {
+ if (Flags.useSelfSyncTransactionForLayer() && mSyncState != SYNC_STATE_NONE) {
+ // When this container needs to be synced, assign layer with its own sync
+ // transaction to avoid out of ordering when merge.
+ // Still use the passed-in transaction for non-sync case, such as building finish
+ // transaction.
+ t = getSyncTransaction();
+ }
setLayer(t, layer);
mLastLayer = layer;
mLastRelativeToLayer = null;
@@ -2740,6 +2754,13 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer<
boolean forceUpdate) {
final boolean changed = layer != mLastLayer || mLastRelativeToLayer != relativeTo;
if (mSurfaceControl != null && (changed || forceUpdate)) {
+ if (Flags.useSelfSyncTransactionForLayer() && mSyncState != SYNC_STATE_NONE) {
+ // When this container needs to be synced, assign layer with its own sync
+ // transaction to avoid out of ordering when merge.
+ // Still use the passed-in transaction for non-sync case, such as building finish
+ // transaction.
+ t = getSyncTransaction();
+ }
setRelativeLayer(t, relativeTo, layer);
mLastLayer = layer;
mLastRelativeToLayer = relativeTo;
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index 92e0931993d1..e4ef3d186bdb 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -7473,6 +7473,23 @@ public class WindowManagerService extends IWindowManager.Stub
return mRoot.getDisplayContent(DEFAULT_DISPLAY);
}
+ /**
+ * Returns the main display content for the user passed as parameter.
+ *
+ * <p>Visible background users may have their own designated main display, distinct from the
+ * system default display (DEFAULT_DISPLAY). Visible background users operate independently
+ * with their own main displays. These secondary user main displays host the secondary home
+ * activities.
+ */
+ @Nullable
+ DisplayContent getUserMainDisplayContentLocked(@UserIdInt int userId) {
+ final int userMainDisplayId = mUmInternal.getMainDisplayAssignedToUser(userId);
+ if (userMainDisplayId == -1) {
+ return null;
+ }
+ return mRoot.getDisplayContent(userMainDisplayId);
+ }
+
public void onOverlayChanged() {
// Post to display thread so it can get the latest display info.
mH.post(() -> {
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index b43e334d6e1a..85e3d89730a3 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -126,6 +126,7 @@ import static com.android.server.wm.IdentifierProto.USER_ID;
import static com.android.server.wm.MoveAnimationSpecProto.DURATION_MS;
import static com.android.server.wm.MoveAnimationSpecProto.FROM;
import static com.android.server.wm.MoveAnimationSpecProto.TO;
+import static com.android.server.wm.StartingData.AFTER_TRANSITION_FINISH;
import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_ALL;
import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_APP_TRANSITION;
import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_STARTING_REVEAL;
@@ -1920,6 +1921,13 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
}
final ActivityRecord atoken = mActivityRecord;
if (atoken != null) {
+ if (atoken.mStartingData != null && mAttrs.type != TYPE_APPLICATION_STARTING
+ && atoken.mStartingData.mRemoveAfterTransaction
+ == AFTER_TRANSITION_FINISH) {
+ // Preventing app window from visible during un-occluding animation playing due to
+ // alpha blending.
+ return false;
+ }
final boolean isVisible = isStartingWindowAssociatedToTask()
? mStartingData.mAssociatedTask.isVisible() : atoken.isVisible();
return ((!isParentWindowHidden() && isVisible)
@@ -2925,7 +2933,14 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
final int mask = FLAG_SHOW_WHEN_LOCKED | FLAG_DISMISS_KEYGUARD
| FLAG_ALLOW_LOCK_WHILE_SCREEN_ON;
WindowManager.LayoutParams sa = mActivityRecord.mStartingWindow.mAttrs;
+ final boolean wasShowWhenLocked = (sa.flags & FLAG_SHOW_WHEN_LOCKED) != 0;
+ final boolean removeShowWhenLocked = (mAttrs.flags & FLAG_SHOW_WHEN_LOCKED) == 0;
sa.flags = (sa.flags & ~mask) | (mAttrs.flags & mask);
+ if (Flags.keepAppWindowHideWhileLocked() && wasShowWhenLocked && removeShowWhenLocked) {
+ // Trigger unoccluding animation if needed.
+ mActivityRecord.checkKeyguardFlagsChanged();
+ mActivityRecord.deferStartingWindowRemovalForKeyguardUnoccluding();
+ }
}
}
@@ -5424,7 +5439,7 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
// change then delay the position update until it has redrawn to avoid any flickers.
final boolean isLetterboxedAndRelaunching = activityRecord != null
&& activityRecord.areBoundsLetterboxed()
- && activityRecord.mAppCompatController.getAppCompatOrientationOverrides()
+ && activityRecord.mAppCompatController.getOrientationOverrides()
.getIsRelaunchingAfterRequestedOrientationChanged();
if (surfaceResizedWithoutMoveAnimation || isLetterboxedAndRelaunching) {
applyWithNextDraw(mSetSurfacePositionConsumer);
@@ -5547,6 +5562,13 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
@Override
void assignLayer(Transaction t, int layer) {
if (mStartingData != null) {
+ if (Flags.useSelfSyncTransactionForLayer() && mSyncState != SYNC_STATE_NONE) {
+ // When this container needs to be synced, assign layer with its own sync
+ // transaction to avoid out of ordering when merge.
+ // Still use the passed-in transaction for non-sync case, such as building finish
+ // transaction.
+ t = getSyncTransaction();
+ }
// The starting window should cover the task.
t.setLayer(mSurfaceControl, Integer.MAX_VALUE);
return;
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index e69a7414dd76..0ce25db6ea55 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -8909,11 +8909,15 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub {
if (parent) {
Preconditions.checkCallAuthorization(
- isProfileOwnerOfOrganizationOwnedDevice(getCallerIdentity().getUserId()));
+ isProfileOwnerOfOrganizationOwnedDevice(caller.getUserId()));
+ // If a DPC is querying on the parent instance, make sure it's only querying the parent
+ // user of itself. Querying any other user is not allowed.
+ Preconditions.checkArgument(caller.getUserId() == userHandle);
}
+ int affectedUserId = parent ? getProfileParentId(userHandle) : userHandle;
Boolean disallowed = mDevicePolicyEngine.getResolvedPolicy(
PolicyDefinition.SCREEN_CAPTURE_DISABLED,
- userHandle);
+ affectedUserId);
return disallowed != null && disallowed;
}
@@ -10466,6 +10470,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub {
// Reset some of the user-specific policies.
final DevicePolicyData policy = getUserData(userId);
policy.mPermissionPolicy = DevicePolicyManager.PERMISSION_POLICY_PROMPT;
+ mPolicyCache.setPermissionPolicy(userId, policy.mPermissionPolicy);
// Clear delegations.
policy.mDelegationMap.clear();
policy.mStatusBarDisabled = false;
@@ -14669,7 +14674,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub {
@Override
public void setSecondaryLockscreenEnabled(ComponentName who, boolean enabled,
PersistableBundle options) {
- if (Flags.secondaryLockscreenApiEnabled()) {
+ if (Flags.secondaryLockscreenApiEnabled() && mSupervisionManagerInternal != null) {
final CallerIdentity caller = getCallerIdentity();
final boolean isRoleHolder = isCallerSystemSupervisionRoleHolder(caller);
synchronized (getLockObject()) {
@@ -14680,16 +14685,8 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub {
caller.getUserId());
}
- if (mSupervisionManagerInternal != null) {
- mSupervisionManagerInternal.setSupervisionLockscreenEnabledForUser(
- caller.getUserId(), enabled, options);
- } else {
- synchronized (getLockObject()) {
- DevicePolicyData policy = getUserData(caller.getUserId());
- policy.mSecondaryLockscreenEnabled = enabled;
- saveSettingsLocked(caller.getUserId());
- }
- }
+ mSupervisionManagerInternal.setSupervisionLockscreenEnabledForUser(
+ caller.getUserId(), enabled, options);
} else {
Objects.requireNonNull(who, "ComponentName is null");
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index c5d42ad9f081..8e06ed8cc283 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -2714,16 +2714,18 @@ public final class SystemServer implements Dumpable {
mSystemServiceManager.startService(AuthService.class);
t.traceEnd();
- if (android.security.Flags.secureLockdown()) {
- t.traceBegin("StartSecureLockDeviceService.Lifecycle");
- mSystemServiceManager.startService(SecureLockDeviceService.Lifecycle.class);
- t.traceEnd();
- }
+ if (!isWatch && !isTv && !isAutomotive) {
+ if (android.security.Flags.secureLockdown()) {
+ t.traceBegin("StartSecureLockDeviceService.Lifecycle");
+ mSystemServiceManager.startService(SecureLockDeviceService.Lifecycle.class);
+ t.traceEnd();
+ }
- if (android.adaptiveauth.Flags.enableAdaptiveAuth()) {
- t.traceBegin("StartAuthenticationPolicyService");
- mSystemServiceManager.startService(AuthenticationPolicyService.class);
- t.traceEnd();
+ if (android.adaptiveauth.Flags.enableAdaptiveAuth()) {
+ t.traceBegin("StartAuthenticationPolicyService");
+ mSystemServiceManager.startService(AuthenticationPolicyService.class);
+ t.traceEnd();
+ }
}
if (!isWatch) {
diff --git a/services/print/java/com/android/server/print/flags.aconfig b/services/print/java/com/android/server/print/flags.aconfig
index 0210791cfeda..42d142545660 100644
--- a/services/print/java/com/android/server/print/flags.aconfig
+++ b/services/print/java/com/android/server/print/flags.aconfig
@@ -3,7 +3,7 @@ container: "system"
flag {
name: "do_not_include_capabilities"
- namespace: "print"
+ namespace: "printing"
description: "Do not use the flag Context.BIND_INCLUDE_CAPABILITIES when binding to the service"
bug: "291281543"
}
diff --git a/services/robotests/backup/src/com/android/server/backup/BackupManagerServiceRoboTest.java b/services/robotests/backup/src/com/android/server/backup/BackupManagerServiceRoboTest.java
index 4e9fff230bac..b80d68de0f2e 100644
--- a/services/robotests/backup/src/com/android/server/backup/BackupManagerServiceRoboTest.java
+++ b/services/robotests/backup/src/com/android/server/backup/BackupManagerServiceRoboTest.java
@@ -37,6 +37,7 @@ import static org.testng.Assert.expectThrows;
import android.annotation.UserIdInt;
import android.app.Application;
+import android.app.backup.BackupManagerInternal;
import android.app.backup.IBackupManagerMonitor;
import android.app.backup.IBackupObserver;
import android.app.backup.IFullBackupRestoreObserver;
@@ -52,6 +53,7 @@ import android.os.UserManager;
import android.platform.test.annotations.Presubmit;
import android.util.SparseArray;
+import com.android.server.LocalServices;
import com.android.server.SystemService.TargetUser;
import com.android.server.backup.testing.TransportData;
import com.android.server.testing.shadows.ShadowApplicationPackageManager;
@@ -229,7 +231,7 @@ public class BackupManagerServiceRoboTest {
setCallerAndGrantInteractUserPermission(mUserOneId, /* shouldGrantPermission */ false);
IBinder agentBinder = mock(IBinder.class);
- backupManagerService.agentConnected(mUserOneId, TEST_PACKAGE, agentBinder);
+ backupManagerService.agentConnectedForUser(TEST_PACKAGE, mUserOneId, agentBinder);
verify(mUserOneBackupAgentConnectionManager).agentConnected(TEST_PACKAGE, agentBinder);
}
@@ -242,7 +244,7 @@ public class BackupManagerServiceRoboTest {
setCallerAndGrantInteractUserPermission(mUserTwoId, /* shouldGrantPermission */ false);
IBinder agentBinder = mock(IBinder.class);
- backupManagerService.agentConnected(mUserTwoId, TEST_PACKAGE, agentBinder);
+ backupManagerService.agentConnectedForUser(TEST_PACKAGE, mUserTwoId, agentBinder);
verify(mUserOneBackupAgentConnectionManager, never()).agentConnected(TEST_PACKAGE,
agentBinder);
@@ -1549,6 +1551,7 @@ public class BackupManagerServiceRoboTest {
@Test
public void testOnStart_publishesService() {
BackupManagerService backupManagerService = mock(BackupManagerService.class);
+ LocalServices.removeServiceForTest(BackupManagerInternal.class);
BackupManagerService.Lifecycle lifecycle =
spy(new BackupManagerService.Lifecycle(mContext, backupManagerService));
doNothing().when(lifecycle).publishService(anyString(), any());
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/ActivityManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/am/ActivityManagerServiceTest.java
index 6defadf44d05..35ab2d233563 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/ActivityManagerServiceTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/ActivityManagerServiceTest.java
@@ -1454,6 +1454,18 @@ public class ActivityManagerServiceTest {
assertThat(tokenForFullIntent.getKeyFields()).isEqualTo(tokenForCloneIntent.getKeyFields());
}
+ @Test
+ public void testCanLaunchClipDataIntent() {
+ ClipData clipData = ClipData.newIntent("test", new Intent("test"));
+ clipData.prepareToLeaveProcess(true);
+ // skip mimicking sending clipData to another app because it will just be parceled and
+ // un-parceled.
+ Intent intent = clipData.getItemAt(0).getIntent();
+ // default intent redirect protection won't block an intent nested in a top level ClipData.
+ assertThat(intent.getExtendedFlags()
+ & Intent.EXTENDED_FLAG_MISSING_CREATOR_OR_INVALID_TOKEN).isEqualTo(0);
+ }
+
private void verifyWaitingForNetworkStateUpdate(long curProcStateSeq,
long lastNetworkUpdatedProcStateSeq,
final long procStateSeqToWait, boolean expectWait) throws Exception {
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java b/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java
index 4a09802fc822..fe7cc923d3d1 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java
@@ -743,6 +743,43 @@ public class MockingOomAdjusterTests {
@SuppressWarnings("GuardedBy")
@Test
+ @EnableFlags(Flags.FLAG_USE_CPU_TIME_CAPABILITY)
+ public void testUpdateOomAdjFreezeState_receivers() {
+ final ProcessRecord app = makeDefaultProcessRecord(MOCKAPP_PID, MOCKAPP_UID,
+ MOCKAPP_PROCESSNAME, MOCKAPP_PACKAGENAME, true);
+
+ updateOomAdj(app);
+ assertNoCpuTime(app);
+
+ app.mReceivers.incrementCurReceivers();
+ updateOomAdj(app);
+ assertCpuTime(app);
+
+ app.mReceivers.decrementCurReceivers();
+ updateOomAdj(app);
+ assertNoCpuTime(app);
+ }
+
+ @SuppressWarnings("GuardedBy")
+ @Test
+ @EnableFlags(Flags.FLAG_USE_CPU_TIME_CAPABILITY)
+ public void testUpdateOomAdjFreezeState_activeInstrumentation() {
+ ProcessRecord app = makeDefaultProcessRecord(MOCKAPP_PID, MOCKAPP_UID, MOCKAPP_PROCESSNAME,
+ MOCKAPP_PACKAGENAME, true);
+ updateOomAdj(app);
+ assertNoCpuTime(app);
+
+ mProcessStateController.setActiveInstrumentation(app, mock(ActiveInstrumentation.class));
+ updateOomAdj(app);
+ assertCpuTime(app);
+
+ mProcessStateController.setActiveInstrumentation(app, null);
+ updateOomAdj(app);
+ assertNoCpuTime(app);
+ }
+
+ @SuppressWarnings("GuardedBy")
+ @Test
public void testUpdateOomAdj_DoOne_OverlayUi() {
ProcessRecord app = spy(makeDefaultProcessRecord(MOCKAPP_PID, MOCKAPP_UID,
MOCKAPP_PROCESSNAME, MOCKAPP_PACKAGENAME, true));
diff --git a/services/tests/mockingservicestests/src/com/android/server/backup/BackupManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/backup/BackupManagerServiceTest.java
index c4a042370de5..f1f4a0e83a79 100644
--- a/services/tests/mockingservicestests/src/com/android/server/backup/BackupManagerServiceTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/backup/BackupManagerServiceTest.java
@@ -39,6 +39,7 @@ import static org.mockito.Mockito.when;
import android.Manifest;
import android.app.backup.BackupManager;
+import android.app.backup.BackupManagerInternal;
import android.app.backup.ISelectBackupTransportCallback;
import android.app.job.JobScheduler;
import android.content.ComponentName;
@@ -59,6 +60,7 @@ import androidx.test.filters.SmallTest;
import com.android.dx.mockito.inline.extended.ExtendedMockito;
import com.android.internal.util.DumpUtils;
+import com.android.server.LocalServices;
import com.android.server.SystemService;
import com.android.server.backup.utils.RandomAccessFileUtils;
@@ -716,7 +718,7 @@ public class BackupManagerServiceTest {
// Create BMS *before* setting a main user to simulate the main user being created after
// BMS, which can happen for the first ever boot of a new device.
mService = new BackupManagerServiceTestable(mContextMock);
- mServiceLifecycle = new BackupManagerService.Lifecycle(mContextMock, mService);
+ createBackupServiceLifecycle(mContextMock, mService);
when(mUserManagerMock.getMainUser()).thenReturn(UserHandle.of(NON_SYSTEM_USER));
assertFalse(mService.isBackupServiceActive(NON_SYSTEM_USER));
@@ -730,7 +732,7 @@ public class BackupManagerServiceTest {
// Create BMS *before* setting a main user to simulate the main user being created after
// BMS, which can happen for the first ever boot of a new device.
mService = new BackupManagerServiceTestable(mContextMock);
- mServiceLifecycle = new BackupManagerService.Lifecycle(mContextMock, mService);
+ createBackupServiceLifecycle(mContextMock, mService);
when(mUserManagerMock.getMainUser()).thenReturn(UserHandle.of(NON_SYSTEM_USER));
assertFalse(mService.isBackupServiceActive(NON_SYSTEM_USER));
@@ -754,7 +756,7 @@ public class BackupManagerServiceTest {
private void createBackupManagerServiceAndUnlockSystemUser() {
mService = new BackupManagerServiceTestable(mContextMock);
- mServiceLifecycle = new BackupManagerService.Lifecycle(mContextMock, mService);
+ createBackupServiceLifecycle(mContextMock, mService);
simulateUserUnlocked(UserHandle.USER_SYSTEM);
}
@@ -765,7 +767,15 @@ public class BackupManagerServiceTest {
private void setMockMainUserAndCreateBackupManagerService(int userId) {
when(mUserManagerMock.getMainUser()).thenReturn(UserHandle.of(userId));
mService = new BackupManagerServiceTestable(mContextMock);
- mServiceLifecycle = new BackupManagerService.Lifecycle(mContextMock, mService);
+ createBackupServiceLifecycle(mContextMock, mService);
+ }
+
+ private void createBackupServiceLifecycle(Context context, BackupManagerService service) {
+ // Anytime we manually create the Lifecycle, we need to remove the internal BMS because
+ // it would've been added already at boot time and LocalServices does not allow
+ // overriding an existing service.
+ LocalServices.removeServiceForTest(BackupManagerInternal.class);
+ mServiceLifecycle = new BackupManagerService.Lifecycle(context, service);
}
private void simulateUserUnlocked(int userId) {
diff --git a/services/tests/mockingservicestests/src/com/android/server/power/ThermalManagerServiceMockingTest.java b/services/tests/mockingservicestests/src/com/android/server/power/ThermalManagerServiceMockingTest.java
index f1072da4161f..6d91bee6d3f6 100644
--- a/services/tests/mockingservicestests/src/com/android/server/power/ThermalManagerServiceMockingTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/power/ThermalManagerServiceMockingTest.java
@@ -573,4 +573,14 @@ public class ThermalManagerServiceMockingTest {
assertNotNull(ret);
assertEquals(0, ret.size());
}
+
+ @Test
+ public void forecastSkinTemperature() throws RemoteException {
+ Mockito.when(mAidlHalMock.forecastSkinTemperature(Mockito.anyInt())).thenReturn(
+ 0.55f
+ );
+ float forecast = mAidlWrapper.forecastSkinTemperature(10);
+ Mockito.verify(mAidlHalMock, Mockito.times(1)).forecastSkinTemperature(10);
+ assertEquals(0.55f, forecast, 0.01f);
+ }
}
diff --git a/services/tests/performancehinttests/src/com/android/server/power/hint/HintManagerServiceTest.java b/services/tests/performancehinttests/src/com/android/server/power/hint/HintManagerServiceTest.java
index cd94c0f6e245..e61571288ade 100644
--- a/services/tests/performancehinttests/src/com/android/server/power/hint/HintManagerServiceTest.java
+++ b/services/tests/performancehinttests/src/com/android/server/power/hint/HintManagerServiceTest.java
@@ -70,8 +70,6 @@ import android.os.PerformanceHintManager;
import android.os.Process;
import android.os.RemoteException;
import android.os.SessionCreationConfig;
-import android.platform.test.annotations.DisableFlags;
-import android.platform.test.annotations.EnableFlags;
import android.platform.test.annotations.RequiresFlagsEnabled;
import android.platform.test.flag.junit.CheckFlagsRule;
import android.platform.test.flag.junit.DeviceFlagsValueProvider;
@@ -1388,7 +1386,6 @@ public class HintManagerServiceTest {
@Test
- @EnableFlags({Flags.FLAG_CPU_HEADROOM_AFFINITY_CHECK})
public void testCpuHeadroomCache() throws Exception {
CpuHeadroomParamsInternal params1 = new CpuHeadroomParamsInternal();
CpuHeadroomParams halParams1 = new CpuHeadroomParams();
@@ -1476,8 +1473,7 @@ public class HintManagerServiceTest {
}
@Test
- @EnableFlags({Flags.FLAG_CPU_HEADROOM_AFFINITY_CHECK})
- public void testGetCpuHeadroomDifferentAffinity_flagOn() throws Exception {
+ public void testGetCpuHeadroomDifferentAffinity() throws Exception {
CountDownLatch latch = new CountDownLatch(2);
int[] tids = createThreads(2, latch);
CpuHeadroomParamsInternal params = new CpuHeadroomParamsInternal();
@@ -1497,28 +1493,6 @@ public class HintManagerServiceTest {
verify(mIPowerMock, times(0)).getCpuHeadroom(any());
}
- @Test
- @DisableFlags({Flags.FLAG_CPU_HEADROOM_AFFINITY_CHECK})
- public void testGetCpuHeadroomDifferentAffinity_flagOff() throws Exception {
- CountDownLatch latch = new CountDownLatch(2);
- int[] tids = createThreads(2, latch);
- CpuHeadroomParamsInternal params = new CpuHeadroomParamsInternal();
- params.tids = tids;
- CpuHeadroomParams halParams = new CpuHeadroomParams();
- halParams.tids = tids;
- float headroom = 0.1f;
- CpuHeadroomResult halRet = CpuHeadroomResult.globalHeadroom(headroom);
- String ret1 = runAndWaitForCommand("taskset -p 1 " + tids[0]);
- String ret2 = runAndWaitForCommand("taskset -p 3 " + tids[1]);
-
- HintManagerService service = createService();
- clearInvocations(mIPowerMock);
- when(mIPowerMock.getCpuHeadroom(eq(halParams))).thenReturn(halRet);
- assertEquals("taskset cmd return: " + ret1 + "\n" + ret2, halRet,
- service.getBinderServiceInstance().getCpuHeadroom(params));
- verify(mIPowerMock, times(1)).getCpuHeadroom(any());
- }
-
private String runAndWaitForCommand(String command) throws Exception {
java.lang.Process process = Runtime.getRuntime().exec(command);
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
diff --git a/services/tests/powerservicetests/src/com/android/server/power/PowerManagerServiceTest.java b/services/tests/powerservicetests/src/com/android/server/power/PowerManagerServiceTest.java
index 3cb27451bd57..d9256247b835 100644
--- a/services/tests/powerservicetests/src/com/android/server/power/PowerManagerServiceTest.java
+++ b/services/tests/powerservicetests/src/com/android/server/power/PowerManagerServiceTest.java
@@ -89,6 +89,7 @@ import android.os.PowerManagerInternal;
import android.os.PowerSaveState;
import android.os.UserHandle;
import android.os.test.TestLooper;
+import android.platform.test.annotations.DisableFlags;
import android.platform.test.annotations.EnableFlags;
import android.platform.test.flag.junit.SetFlagsRule;
import android.provider.DeviceConfig;
@@ -119,6 +120,7 @@ import com.android.server.power.PowerManagerService.WakeLock;
import com.android.server.power.batterysaver.BatterySaverController;
import com.android.server.power.batterysaver.BatterySaverPolicy;
import com.android.server.power.batterysaver.BatterySaverStateMachine;
+import com.android.server.power.feature.flags.Flags;
import com.android.server.power.feature.PowerManagerFlags;
import com.android.server.testutils.OffsettableClock;
@@ -3456,6 +3458,25 @@ public class PowerManagerServiceTest {
}
@Test
+ @DisableFlags(Flags.FLAG_ENABLE_SCREEN_TIMEOUT_POLICY_LISTENER_API)
+ public void testAcquireWakelock_screenTimeoutListenersDisabled_noCrashes() {
+ IntArray displayGroupIds = IntArray.wrap(new int[]{Display.DEFAULT_DISPLAY_GROUP});
+ when(mDisplayManagerInternalMock.getDisplayGroupIds()).thenReturn(displayGroupIds);
+
+ final DisplayInfo displayInfo = new DisplayInfo();
+ displayInfo.displayGroupId = Display.DEFAULT_DISPLAY_GROUP;
+ when(mDisplayManagerInternalMock.getDisplayInfo(Display.DEFAULT_DISPLAY))
+ .thenReturn(displayInfo);
+
+ createService();
+ startSystem();
+
+ acquireWakeLock("screenBright", PowerManager.SCREEN_BRIGHT_WAKE_LOCK,
+ Display.DEFAULT_DISPLAY);
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_SCREEN_TIMEOUT_POLICY_LISTENER_API)
public void testAddWakeLockKeepingScreenOn_addsToNotifierAndReportsTimeoutPolicyChange() {
IntArray displayGroupIds = IntArray.wrap(new int[]{Display.DEFAULT_DISPLAY_GROUP});
when(mDisplayManagerInternalMock.getDisplayGroupIds()).thenReturn(displayGroupIds);
@@ -3483,6 +3504,7 @@ public class PowerManagerServiceTest {
}
@Test
+ @EnableFlags(Flags.FLAG_ENABLE_SCREEN_TIMEOUT_POLICY_LISTENER_API)
public void test_addAndRemoveScreenTimeoutListener_propagatesToNotifier()
throws Exception {
IntArray displayGroupIds = IntArray.wrap(new int[]{Display.DEFAULT_DISPLAY_GROUP});
@@ -3509,6 +3531,7 @@ public class PowerManagerServiceTest {
}
@Test
+ @EnableFlags(Flags.FLAG_ENABLE_SCREEN_TIMEOUT_POLICY_LISTENER_API)
public void test_displayIsRemoved_clearsScreenTimeoutListeners()
throws Exception {
IntArray displayGroupIds = IntArray.wrap(new int[]{Display.DEFAULT_DISPLAY_GROUP});
@@ -3531,7 +3554,8 @@ public class PowerManagerServiceTest {
}
@Test
- public void testScreenWakeLockListener_screenHasWakelocks_addsWithHeldTimeoutPolicyToNotifier()
+ @EnableFlags(Flags.FLAG_ENABLE_SCREEN_TIMEOUT_POLICY_LISTENER_API)
+ public void testScreenTimeoutListener_screenHasWakelocks_addsWithHeldTimeoutPolicyToNotifier()
throws Exception {
IntArray displayGroupIds = IntArray.wrap(new int[]{Display.DEFAULT_DISPLAY_GROUP});
when(mDisplayManagerInternalMock.getDisplayGroupIds()).thenReturn(displayGroupIds);
diff --git a/services/tests/powerstatstests/Android.bp b/services/tests/powerstatstests/Android.bp
index d6ca10a23fb9..07b18db59960 100644
--- a/services/tests/powerstatstests/Android.bp
+++ b/services/tests/powerstatstests/Android.bp
@@ -27,6 +27,7 @@ android_test {
"servicestests-utils",
"platform-test-annotations",
"flag-junit",
+ "apct-perftests-utils",
],
libs: [
@@ -64,10 +65,12 @@ android_ravenwood_test {
"ravenwood-junit",
"truth",
"androidx.annotation_annotation",
+ "androidx.test.ext.junit",
"androidx.test.rules",
"androidx.test.uiautomator_uiautomator",
"modules-utils-binary-xml",
"flag-junit",
+ "apct-perftests-utils",
],
srcs: [
"src/com/android/server/power/stats/*.java",
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryTraceTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryTraceTest.java
new file mode 100644
index 000000000000..cc75e9e3114f
--- /dev/null
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryTraceTest.java
@@ -0,0 +1,105 @@
+/*
+ * 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.power.stats;
+
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.os.BatteryStatsManager;
+import android.os.BatteryUsageStats;
+import android.os.BatteryUsageStatsQuery;
+import android.os.ParcelFileDescriptor;
+import android.perftests.utils.TraceMarkParser;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.LargeTest;
+import androidx.test.uiautomator.UiDevice;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.BufferedReader;
+import java.io.InputStreamReader;
+import java.util.HashSet;
+import java.util.Set;
+
+@RunWith(AndroidJUnit4.class)
+@LargeTest
+@android.platform.test.annotations.DisabledOnRavenwood(reason = "Atrace event test")
+public class BatteryStatsHistoryTraceTest {
+ private static final String ATRACE_START = "atrace --async_start -b 1024 -c ss";
+ private static final String ATRACE_STOP = "atrace --async_stop";
+ private static final String ATRACE_DUMP = "atrace --async_dump";
+
+ @Before
+ public void before() throws Exception {
+ runShellCommand(ATRACE_START);
+ }
+
+ @After
+ public void after() throws Exception {
+ runShellCommand(ATRACE_STOP);
+ }
+
+ @Test
+ public void dumpsys() throws Exception {
+ runShellCommand("dumpsys batterystats --history");
+
+ Set<String> slices = readAtraceSlices();
+ assertThat(slices).contains("BatteryStatsHistory.copy");
+ assertThat(slices).contains("BatteryStatsHistory.iterate");
+ }
+
+ @Test
+ public void getBatteryUsageStats() throws Exception {
+ BatteryStatsManager batteryStatsManager =
+ getInstrumentation().getTargetContext().getSystemService(BatteryStatsManager.class);
+ BatteryUsageStatsQuery query = new BatteryUsageStatsQuery.Builder()
+ .includeBatteryHistory().build();
+ BatteryUsageStats batteryUsageStats = batteryStatsManager.getBatteryUsageStats(query);
+ assertThat(batteryUsageStats).isNotNull();
+
+ Set<String> slices = readAtraceSlices();
+ assertThat(slices).contains("BatteryStatsHistory.copy");
+ assertThat(slices).contains("BatteryStatsHistory.iterate");
+ assertThat(slices).contains("BatteryStatsHistory.writeToParcel");
+ }
+
+ private String runShellCommand(String cmd) throws Exception {
+ return UiDevice.getInstance(getInstrumentation()).executeShellCommand(cmd);
+ }
+
+ private Set<String> readAtraceSlices() throws Exception {
+ Set<String> keys = new HashSet<>();
+
+ TraceMarkParser parser = new TraceMarkParser(
+ line -> line.name.startsWith("BatteryStatsHistory."));
+ ParcelFileDescriptor pfd =
+ getInstrumentation().getUiAutomation().executeShellCommand(ATRACE_DUMP);
+ try (BufferedReader reader = new BufferedReader(
+ new InputStreamReader(new ParcelFileDescriptor.AutoCloseInputStream(pfd)))) {
+ String line;
+ while ((line = reader.readLine()) != null) {
+ parser.visit(line);
+ }
+ }
+ parser.forAllSlices((key, slices) -> keys.add(key));
+ return keys;
+ }
+}
diff --git a/services/tests/selinux/src/com/android/server/selinux/SelinuxAuditLogsBuilderTest.java b/services/tests/selinux/src/com/android/server/selinux/SelinuxAuditLogsBuilderTest.java
index e86108d84538..ede61a5a0269 100644
--- a/services/tests/selinux/src/com/android/server/selinux/SelinuxAuditLogsBuilderTest.java
+++ b/services/tests/selinux/src/com/android/server/selinux/SelinuxAuditLogsBuilderTest.java
@@ -15,18 +15,14 @@
*/
package com.android.server.selinux;
-import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity;
import static com.android.server.selinux.SelinuxAuditLogBuilder.toCategories;
import static com.google.common.truth.Truth.assertThat;
-import android.provider.DeviceConfig;
-
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.android.server.selinux.SelinuxAuditLogBuilder.SelinuxAuditLog;
-import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -45,24 +41,12 @@ public class SelinuxAuditLogsBuilderTest {
@Before
public void setUp() {
- runWithShellPermissionIdentity(
- () ->
- DeviceConfig.setLocalOverride(
- DeviceConfig.NAMESPACE_ADSERVICES,
- SelinuxAuditLogBuilder.CONFIG_SELINUX_AUDIT_DOMAIN,
- TEST_DOMAIN));
-
- mAuditLogBuilder = new SelinuxAuditLogBuilder();
+ mAuditLogBuilder = new SelinuxAuditLogBuilder(TEST_DOMAIN);
mScontextMatcher = mAuditLogBuilder.mScontextMatcher;
mTcontextMatcher = mAuditLogBuilder.mTcontextMatcher;
mPathMatcher = mAuditLogBuilder.mPathMatcher;
}
- @After
- public void tearDown() {
- runWithShellPermissionIdentity(() -> DeviceConfig.clearAllLocalOverrides());
- }
-
@Test
public void testMatcher_scontext() {
assertThat(mScontextMatcher.reset("u:r:" + TEST_DOMAIN + ":s0").matches()).isTrue();
@@ -109,13 +93,9 @@ public class SelinuxAuditLogsBuilderTest {
@Test
public void testMatcher_scontextDefaultConfig() {
- runWithShellPermissionIdentity(
- () ->
- DeviceConfig.clearLocalOverride(
- DeviceConfig.NAMESPACE_ADSERVICES,
- SelinuxAuditLogBuilder.CONFIG_SELINUX_AUDIT_DOMAIN));
-
- Matcher scontexMatcher = new SelinuxAuditLogBuilder().mScontextMatcher;
+ Matcher scontexMatcher =
+ new SelinuxAuditLogBuilder(SelinuxAuditLogsCollector.DEFAULT_SELINUX_AUDIT_DOMAIN)
+ .mScontextMatcher;
assertThat(scontexMatcher.reset("u:r:" + TEST_DOMAIN + ":s0").matches()).isFalse();
assertThat(scontexMatcher.reset("u:r:" + TEST_DOMAIN + ":s0:c123,c456").matches())
@@ -221,13 +201,7 @@ public class SelinuxAuditLogsBuilderTest {
@Test
public void testSelinuxAuditLogsBuilder_wrongConfig() {
String notARegexDomain = "not]a[regex";
- runWithShellPermissionIdentity(
- () ->
- DeviceConfig.setLocalOverride(
- DeviceConfig.NAMESPACE_ADSERVICES,
- SelinuxAuditLogBuilder.CONFIG_SELINUX_AUDIT_DOMAIN,
- notARegexDomain));
- SelinuxAuditLogBuilder noOpBuilder = new SelinuxAuditLogBuilder();
+ SelinuxAuditLogBuilder noOpBuilder = new SelinuxAuditLogBuilder(notARegexDomain);
noOpBuilder.reset(
"granted { p } scontext=u:r:"
diff --git a/services/tests/selinux/src/com/android/server/selinux/SelinuxAuditLogsCollectorTest.java b/services/tests/selinux/src/com/android/server/selinux/SelinuxAuditLogsCollectorTest.java
index b6ccf5e0ad80..db58c74e8431 100644
--- a/services/tests/selinux/src/com/android/server/selinux/SelinuxAuditLogsCollectorTest.java
+++ b/services/tests/selinux/src/com/android/server/selinux/SelinuxAuditLogsCollectorTest.java
@@ -15,7 +15,6 @@
*/
package com.android.server.selinux;
-import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
@@ -28,7 +27,6 @@ import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
-import android.provider.DeviceConfig;
import android.util.EventLog;
import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -59,6 +57,7 @@ public class SelinuxAuditLogsCollectorTest {
private final SelinuxAuditLogsCollector mSelinuxAutidLogsCollector =
// Ignore rate limiting for tests
new SelinuxAuditLogsCollector(
+ () -> TEST_DOMAIN,
new RateLimiter(mClock, /* window= */ Duration.ofMillis(0)),
new QuotaLimiter(
mClock, /* windowSize= */ Duration.ofHours(1), /* maxPermits= */ 5));
@@ -67,13 +66,6 @@ public class SelinuxAuditLogsCollectorTest {
@Before
public void setUp() {
- runWithShellPermissionIdentity(
- () ->
- DeviceConfig.setLocalOverride(
- DeviceConfig.NAMESPACE_ADSERVICES,
- SelinuxAuditLogBuilder.CONFIG_SELINUX_AUDIT_DOMAIN,
- TEST_DOMAIN));
-
mSelinuxAutidLogsCollector.setStopRequested(false);
// move the clock forward for the limiters.
mClock.currentTimeMillis += Duration.ofHours(1).toMillis();
@@ -85,7 +77,6 @@ public class SelinuxAuditLogsCollectorTest {
@After
public void tearDown() {
- runWithShellPermissionIdentity(() -> DeviceConfig.clearAllLocalOverrides());
mMockitoSession.finishMocking();
}
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java
index fa78dfce0a17..dafe4827b2fe 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java
@@ -220,6 +220,9 @@ public class AccessibilityManagerServiceTest {
@Mock private ProxyManager mProxyManager;
@Mock private StatusBarManagerInternal mStatusBarManagerInternal;
@Mock private DevicePolicyManager mDevicePolicyManager;
+ @Mock
+ private HearingDevicePhoneCallNotificationController
+ mMockHearingDevicePhoneCallNotificationController;
@Spy private IUserInitializationCompleteCallback mUserInitializationCompleteCallback;
@Captor private ArgumentCaptor<Intent> mIntentArgumentCaptor;
private IAccessibilityManager mA11yManagerServiceOnDevice;
@@ -289,7 +292,8 @@ public class AccessibilityManagerServiceTest {
mMockMagnificationController,
mInputFilter,
mProxyManager,
- mFakePermissionEnforcer);
+ mFakePermissionEnforcer,
+ mMockHearingDevicePhoneCallNotificationController);
mA11yms.switchUser(mTestableContext.getUserId());
mTestableLooper.processAllMessages();
diff --git a/services/tests/servicestests/src/com/android/server/am/ActivityManagerTest.java b/services/tests/servicestests/src/com/android/server/am/ActivityManagerTest.java
index 0bf419ec242c..998c1d1a23b1 100644
--- a/services/tests/servicestests/src/com/android/server/am/ActivityManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/am/ActivityManagerTest.java
@@ -20,6 +20,7 @@ import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
@@ -35,6 +36,7 @@ import android.content.Intent;
import android.content.IntentFilter;
import android.content.ServiceConnection;
import android.content.pm.PackageManager;
+import android.content.pm.UserInfo;
import android.os.Binder;
import android.os.Bundle;
import android.os.DropBoxManager;
@@ -48,6 +50,7 @@ import android.os.Parcel;
import android.os.RemoteException;
import android.os.SystemClock;
import android.os.UserHandle;
+import android.os.UserManager;
import android.platform.test.annotations.Presubmit;
import android.provider.DeviceConfig;
import android.provider.Settings;
@@ -68,6 +71,7 @@ import org.junit.Test;
import java.io.IOException;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
@@ -150,6 +154,25 @@ public class ActivityManagerTest {
}
@Test
+ public void testRemovedUserShouldNotBeRunning() throws Exception {
+ final UserManager userManager = mContext.getSystemService(UserManager.class);
+ assertNotNull("UserManager should not be null", userManager);
+ final UserInfo user = userManager.createUser(
+ "TestUser", UserManager.USER_TYPE_FULL_SECONDARY, 0);
+
+ mService.startUserInBackground(user.id);
+ assertTrue("User should be running", mService.isUserRunning(user.id, 0));
+ assertTrue("User should be in running users",
+ Arrays.stream(mService.getRunningUserIds()).anyMatch(x -> x == user.id));
+
+ userManager.removeUser(user.id);
+ mService.startUserInBackground(user.id);
+ assertFalse("Removed user should not be running", mService.isUserRunning(user.id, 0));
+ assertFalse("Removed user should not be in running users",
+ Arrays.stream(mService.getRunningUserIds()).anyMatch(x -> x == user.id));
+ }
+
+ @Test
public void testServiceUnbindAndKilling() {
for (int i = TEST_LOOPS; i > 0; i--) {
runOnce(i);
diff --git a/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java b/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java
index 6411463fe0d9..06958b81d846 100644
--- a/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java
@@ -490,29 +490,6 @@ public class UserControllerTest {
mInjector.mHandler.clearAllRecordedMessages();
// Verify that continueUserSwitch worked as expected
continueAndCompleteUserSwitch(userState, oldUserId, newUserId);
- verify(mInjector, times(0)).dismissKeyguard(any());
- verify(mInjector, times(1)).dismissUserSwitchingDialog(any());
- continueUserSwitchAssertions(oldUserId, TEST_USER_ID, false, false);
- verifySystemUserVisibilityChangesNeverNotified();
- }
-
- @Test
- public void testContinueUserSwitchDismissKeyguard() {
- when(mInjector.mKeyguardManagerMock.isDeviceSecure(anyInt())).thenReturn(false);
- mUserController.setInitialConfig(/* userSwitchUiEnabled= */ true,
- /* maxRunningUsers= */ 3, /* delayUserDataLocking= */ false,
- /* backgroundUserScheduledStopTimeSecs= */ -1);
- // Start user -- this will update state of mUserController
- mUserController.startUser(TEST_USER_ID, USER_START_MODE_FOREGROUND);
- Message reportMsg = mInjector.mHandler.getMessageForCode(REPORT_USER_SWITCH_MSG);
- assertNotNull(reportMsg);
- UserState userState = (UserState) reportMsg.obj;
- int oldUserId = reportMsg.arg1;
- int newUserId = reportMsg.arg2;
- mInjector.mHandler.clearAllRecordedMessages();
- // Verify that continueUserSwitch worked as expected
- continueAndCompleteUserSwitch(userState, oldUserId, newUserId);
- verify(mInjector, times(1)).dismissKeyguard(any());
verify(mInjector, times(1)).dismissUserSwitchingDialog(any());
continueUserSwitchAssertions(oldUserId, TEST_USER_ID, false, false);
verifySystemUserVisibilityChangesNeverNotified();
@@ -1923,11 +1900,6 @@ public class UserControllerTest {
}
@Override
- protected void dismissKeyguard(Runnable runnable) {
- runnable.run();
- }
-
- @Override
void showUserSwitchingDialog(UserInfo fromUser, UserInfo toUser,
String switchingFromSystemUserMessage, String switchingToSystemUserMessage,
Runnable onShown) {
diff --git a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest1.java b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest1.java
index 0d86d4c3fa28..776f05dfb6ea 100644
--- a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest1.java
+++ b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest1.java
@@ -402,7 +402,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest {
});
}
- public void testPushDynamicShortcut() {
+ public void disabled_testPushDynamicShortcut() {
// Change the max number of shortcuts.
mService.updateConfigurationLocked(ConfigConstants.KEY_MAX_SHORTCUTS + "=5,"
+ ShortcutService.ConfigConstants.KEY_SAVE_DELAY_MILLIS + "=1");
@@ -544,7 +544,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest {
eq(CALLING_PACKAGE_1), eq("s9"), eq(USER_10));
}
- public void testPushDynamicShortcut_CallsToUsageStatsManagerAreThrottled()
+ public void disabled_testPushDynamicShortcut_CallsToUsageStatsManagerAreThrottled()
throws InterruptedException {
mService.updateConfigurationLocked(
ShortcutService.ConfigConstants.KEY_SAVE_DELAY_MILLIS + "=500");
@@ -627,7 +627,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest {
assertEquals(3, mManager.getRemainingCallCount());
}
- public void testPublishWithNoActivity() {
+ public void disbabledTestPublishWithNoActivity() {
// If activity is not explicitly set, use the default one.
mRunningUsers.put(USER_11, true);
@@ -733,7 +733,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest {
});
}
- public void testPublishWithNoActivity_noMainActivityInPackage() {
+ public void disabled_testPublishWithNoActivity_noMainActivityInPackage() {
mRunningUsers.put(USER_11, true);
runWithCaller(CALLING_PACKAGE_2, USER_11, () -> {
@@ -2557,7 +2557,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest {
});
}
- public void testPinShortcutAndGetPinnedShortcuts_multi() {
+ public void disabled_testPinShortcutAndGetPinnedShortcuts_multi() {
// Create some shortcuts.
runWithCaller(CALLING_PACKAGE_1, USER_10, () -> {
assertTrue(mManager.setDynamicShortcuts(list(
@@ -2889,7 +2889,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest {
});
}
- public void testPinShortcutAndGetPinnedShortcuts_crossProfile_plusLaunch() {
+ public void disabled_testPinShortcutAndGetPinnedShortcuts_crossProfile_plusLaunch() {
// Create some shortcuts.
runWithCaller(CALLING_PACKAGE_1, USER_10, () -> {
assertTrue(mManager.setDynamicShortcuts(list(
@@ -3919,7 +3919,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest {
/**
* Try save and load, also stop/start the user.
*/
- public void testSaveAndLoadUser() {
+ public void disabled_testSaveAndLoadUser() {
// First, create some shortcuts and save.
runWithCaller(CALLING_PACKAGE_1, USER_10, () -> {
final Icon icon1 = Icon.createWithResource(getTestContext(), R.drawable.black_64x16);
@@ -8579,7 +8579,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest {
});
}
- public void testReturnedByServer() {
+ public void disabled_testReturnedByServer() {
// Package 1 updated, with manifest shortcuts.
addManifestShortcutResource(
new ComponentName(CALLING_PACKAGE_1, ShortcutActivity.class.getName()),
diff --git a/services/tests/servicestests/src/com/android/server/power/ThermalManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/power/ThermalManagerServiceTest.java
index 2ed71cecd79d..952d8fa47a34 100644
--- a/services/tests/servicestests/src/com/android/server/power/ThermalManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/power/ThermalManagerServiceTest.java
@@ -118,7 +118,6 @@ public class ThermalManagerServiceTest {
private class ThermalHalFake extends ThermalHalWrapper {
private static final int INIT_STATUS = Temperature.THROTTLING_NONE;
private List<Temperature> mTemperatureList = new ArrayList<>();
- private List<Temperature> mOverrideTemperatures = null;
private List<CoolingDevice> mCoolingDeviceList = new ArrayList<>();
private List<TemperatureThreshold> mTemperatureThresholdList = initializeThresholds();
@@ -132,6 +131,9 @@ public class ThermalManagerServiceTest {
INIT_STATUS);
private CoolingDevice mCpu = new CoolingDevice(40, CoolingDevice.TYPE_BATTERY, "cpu");
private CoolingDevice mGpu = new CoolingDevice(43, CoolingDevice.TYPE_BATTERY, "gpu");
+ private Map<Integer, Float> mForecastSkinTemperatures = null;
+ private int mForecastSkinTemperaturesCalled = 0;
+ private boolean mForecastSkinTemperaturesError = false;
private List<TemperatureThreshold> initializeThresholds() {
ArrayList<TemperatureThreshold> thresholds = new ArrayList<>();
@@ -173,12 +175,17 @@ public class ThermalManagerServiceTest {
mCoolingDeviceList.add(mGpu);
}
- void setOverrideTemperatures(List<Temperature> temperatures) {
- mOverrideTemperatures = temperatures;
+ void enableForecastSkinTemperature() {
+ mForecastSkinTemperatures = Map.of(0, 22.0f, 10, 25.0f, 20, 28.0f,
+ 30, 31.0f, 40, 34.0f, 50, 37.0f, 60, 40.0f);
}
- void resetOverrideTemperatures() {
- mOverrideTemperatures = null;
+ void disableForecastSkinTemperature() {
+ mForecastSkinTemperatures = null;
+ }
+
+ void failForecastSkinTemperature() {
+ mForecastSkinTemperaturesError = true;
}
@Override
@@ -219,6 +226,18 @@ public class ThermalManagerServiceTest {
}
@Override
+ protected float forecastSkinTemperature(int forecastSeconds) {
+ mForecastSkinTemperaturesCalled++;
+ if (mForecastSkinTemperaturesError) {
+ throw new RuntimeException();
+ }
+ if (mForecastSkinTemperatures == null) {
+ throw new UnsupportedOperationException();
+ }
+ return mForecastSkinTemperatures.get(forecastSeconds);
+ }
+
+ @Override
protected boolean connectToHal() {
return true;
}
@@ -388,7 +407,7 @@ public class ThermalManagerServiceTest {
Thread.sleep(CALLBACK_TIMEOUT_MILLI_SEC);
resetListenerMock();
int status = Temperature.THROTTLING_SEVERE;
- mFakeHal.setOverrideTemperatures(new ArrayList<>());
+ mFakeHal.mTemperatureList = new ArrayList<>();
// Should not notify on non-skin type
Temperature newBattery = new Temperature(37, Temperature.TYPE_BATTERY, "batt", status);
@@ -518,6 +537,99 @@ public class ThermalManagerServiceTest {
}
@Test
+ @EnableFlags({Flags.FLAG_ALLOW_THERMAL_HAL_SKIN_FORECAST})
+ public void testGetThermalHeadroom_halForecast() throws RemoteException {
+ mFakeHal.mForecastSkinTemperaturesCalled = 0;
+ mFakeHal.enableForecastSkinTemperature();
+ mService = new ThermalManagerService(mContext, mFakeHal);
+ mService.onBootPhase(SystemService.PHASE_ACTIVITY_MANAGER_READY);
+ assertTrue(mService.mIsHalSkinForecastSupported.get());
+ assertEquals(1, mFakeHal.mForecastSkinTemperaturesCalled);
+ mFakeHal.mForecastSkinTemperaturesCalled = 0;
+
+ assertEquals(1.0f, mService.mService.getThermalHeadroom(60), 0.01f);
+ assertEquals(0.9f, mService.mService.getThermalHeadroom(50), 0.01f);
+ assertEquals(0.8f, mService.mService.getThermalHeadroom(40), 0.01f);
+ assertEquals(0.7f, mService.mService.getThermalHeadroom(30), 0.01f);
+ assertEquals(0.6f, mService.mService.getThermalHeadroom(20), 0.01f);
+ assertEquals(0.5f, mService.mService.getThermalHeadroom(10), 0.01f);
+ assertEquals(0.4f, mService.mService.getThermalHeadroom(0), 0.01f);
+ assertEquals(7, mFakeHal.mForecastSkinTemperaturesCalled);
+ }
+
+ @Test
+ @EnableFlags({Flags.FLAG_ALLOW_THERMAL_HAL_SKIN_FORECAST})
+ public void testGetThermalHeadroom_halForecast_disabledOnMultiThresholds()
+ throws RemoteException {
+ mFakeHal.mForecastSkinTemperaturesCalled = 0;
+ List<TemperatureThreshold> thresholds = mFakeHal.initializeThresholds();
+ TemperatureThreshold skinThreshold = new TemperatureThreshold();
+ skinThreshold.type = Temperature.TYPE_SKIN;
+ skinThreshold.name = "skin2";
+ skinThreshold.hotThrottlingThresholds = new float[7 /*ThrottlingSeverity#len*/];
+ skinThreshold.coldThrottlingThresholds = new float[7 /*ThrottlingSeverity#len*/];
+ for (int i = 0; i < skinThreshold.hotThrottlingThresholds.length; ++i) {
+ // Sets NONE to 45.0f, SEVERE to 60.0f, and SHUTDOWN to 75.0f
+ skinThreshold.hotThrottlingThresholds[i] = 45.0f + 5.0f * i;
+ }
+ thresholds.add(skinThreshold);
+ mFakeHal.mTemperatureThresholdList = thresholds;
+ mFakeHal.enableForecastSkinTemperature();
+ mService = new ThermalManagerService(mContext, mFakeHal);
+ mService.onBootPhase(SystemService.PHASE_ACTIVITY_MANAGER_READY);
+ assertFalse("HAL skin forecast should be disabled on multiple SKIN thresholds",
+ mService.mIsHalSkinForecastSupported.get());
+ mService.mService.getThermalHeadroom(10);
+ assertEquals(0, mFakeHal.mForecastSkinTemperaturesCalled);
+ }
+
+ @Test
+ @EnableFlags({Flags.FLAG_ALLOW_THERMAL_HAL_SKIN_FORECAST,
+ Flags.FLAG_ALLOW_THERMAL_THRESHOLDS_CALLBACK})
+ public void testGetThermalHeadroom_halForecast_disabledOnMultiThresholdsCallback()
+ throws RemoteException {
+ mFakeHal.mForecastSkinTemperaturesCalled = 0;
+ mFakeHal.enableForecastSkinTemperature();
+ mService = new ThermalManagerService(mContext, mFakeHal);
+ mService.onBootPhase(SystemService.PHASE_ACTIVITY_MANAGER_READY);
+ assertTrue(mService.mIsHalSkinForecastSupported.get());
+ assertEquals(1, mFakeHal.mForecastSkinTemperaturesCalled);
+ mFakeHal.mForecastSkinTemperaturesCalled = 0;
+
+ TemperatureThreshold newThreshold = new TemperatureThreshold();
+ newThreshold.name = "skin2";
+ newThreshold.type = Temperature.TYPE_SKIN;
+ newThreshold.hotThrottlingThresholds = new float[]{
+ Float.NaN, 43.0f, 46.0f, 49.0f, Float.NaN, Float.NaN, Float.NaN
+ };
+ mFakeHal.mCallback.onThresholdChanged(newThreshold);
+ mService.mService.getThermalHeadroom(10);
+ assertEquals(0, mFakeHal.mForecastSkinTemperaturesCalled);
+ }
+
+ @Test
+ @EnableFlags({Flags.FLAG_ALLOW_THERMAL_HAL_SKIN_FORECAST})
+ public void testGetThermalHeadroom_halForecast_errorOnHal() throws RemoteException {
+ mFakeHal.mForecastSkinTemperaturesCalled = 0;
+ mFakeHal.enableForecastSkinTemperature();
+ mService = new ThermalManagerService(mContext, mFakeHal);
+ mService.onBootPhase(SystemService.PHASE_ACTIVITY_MANAGER_READY);
+ assertTrue(mService.mIsHalSkinForecastSupported.get());
+ assertEquals(1, mFakeHal.mForecastSkinTemperaturesCalled);
+ mFakeHal.mForecastSkinTemperaturesCalled = 0;
+
+ mFakeHal.disableForecastSkinTemperature();
+ assertTrue(Float.isNaN(mService.mService.getThermalHeadroom(10)));
+ assertEquals(1, mFakeHal.mForecastSkinTemperaturesCalled);
+ mFakeHal.enableForecastSkinTemperature();
+ assertFalse(Float.isNaN(mService.mService.getThermalHeadroom(10)));
+ assertEquals(2, mFakeHal.mForecastSkinTemperaturesCalled);
+ mFakeHal.failForecastSkinTemperature();
+ assertTrue(Float.isNaN(mService.mService.getThermalHeadroom(10)));
+ assertEquals(3, mFakeHal.mForecastSkinTemperaturesCalled);
+ }
+
+ @Test
@EnableFlags({Flags.FLAG_ALLOW_THERMAL_THRESHOLDS_CALLBACK,
Flags.FLAG_ALLOW_THERMAL_HEADROOM_THRESHOLDS})
public void testTemperatureWatcherUpdateSevereThresholds() throws Exception {
diff --git a/services/tests/timetests/src/com/android/server/timezonedetector/NotifyingTimeZoneChangeListenerTest.java b/services/tests/timetests/src/com/android/server/timezonedetector/NotifyingTimeZoneChangeListenerTest.java
new file mode 100644
index 000000000000..23c13bd04368
--- /dev/null
+++ b/services/tests/timetests/src/com/android/server/timezonedetector/NotifyingTimeZoneChangeListenerTest.java
@@ -0,0 +1,527 @@
+/*
+ * 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.timezonedetector;
+
+import static com.android.server.timezonedetector.NotifyingTimeZoneChangeListener.AUTO_REVERT_THRESHOLD;
+import static com.android.server.timezonedetector.NotifyingTimeZoneChangeListener.SIGNAL_TYPE_NONE;
+import static com.android.server.timezonedetector.NotifyingTimeZoneChangeListener.SIGNAL_TYPE_UNKNOWN;
+import static com.android.server.timezonedetector.NotifyingTimeZoneChangeListener.STATUS_REJECTED;
+import static com.android.server.timezonedetector.NotifyingTimeZoneChangeListener.STATUS_SUPERSEDED;
+import static com.android.server.timezonedetector.NotifyingTimeZoneChangeListener.STATUS_UNKNOWN;
+import static com.android.server.timezonedetector.NotifyingTimeZoneChangeListener.STATUS_UNTRACKED;
+import static com.android.server.timezonedetector.TimeZoneDetectorStrategy.ORIGIN_LOCATION;
+import static com.android.server.timezonedetector.TimeZoneDetectorStrategy.ORIGIN_MANUAL;
+import static com.android.server.timezonedetector.TimeZoneDetectorStrategy.ORIGIN_TELEPHONY;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.mockito.Mockito.spy;
+
+import android.annotation.Nullable;
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.UiAutomation;
+import android.content.Context;
+import android.os.HandlerThread;
+import android.os.Process;
+import android.os.UserHandle;
+import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.SetFlagsRule;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.server.flags.Flags;
+import com.android.server.timezonedetector.NotifyingTimeZoneChangeListener.TimeZoneChangeRecord;
+import com.android.server.timezonedetector.TimeZoneChangeListener.TimeZoneChangeEvent;
+
+import junitparams.JUnitParamsRunner;
+import junitparams.Parameters;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+import java.time.InstantSource;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * White-box unit tests for {@link NotifyingTimeZoneChangeListener}.
+ */
+@RunWith(JUnitParamsRunner.class)
+@EnableFlags(Flags.FLAG_DATETIME_NOTIFICATIONS)
+public class NotifyingTimeZoneChangeListenerTest {
+
+ @ClassRule
+ public static final SetFlagsRule.ClassRule mClassRule = new SetFlagsRule.ClassRule();
+
+ @Rule(order = 0)
+ public final SetFlagsRule mSetFlagsRule = mClassRule.createSetFlagsRule();
+
+ @Rule(order = 1)
+ public final MockitoRule mockito = MockitoJUnit.rule();
+
+ public static List<@TimeZoneDetectorStrategy.Origin Integer> getDetectionOrigins() {
+ return List.of(ORIGIN_LOCATION, ORIGIN_TELEPHONY);
+ }
+
+ private static final long ARBITRARY_ELAPSED_REALTIME_MILLIS = 1234;
+ /** A time zone used for initialization that does not occur elsewhere in tests. */
+ private static final String ARBITRARY_TIME_ZONE_ID = "Etc/UTC";
+ private static final String INTERACT_ACROSS_USERS_FULL_PERMISSION =
+ "android.permission.INTERACT_ACROSS_USERS_FULL";
+
+ @Mock
+ private Context mContext;
+ private UiAutomation mUiAutomation;
+
+ private FakeNotificationManager mNotificationManager;
+ private HandlerThread mHandlerThread;
+ private TestHandler mHandler;
+ private FakeServiceConfigAccessor mServiceConfigAccessor;
+ private int mUid;
+
+ private NotifyingTimeZoneChangeListener mTimeZoneChangeTracker;
+
+ @Before
+ public void setUp() {
+ mUid = Process.myUid();
+ // Create a thread + handler for processing the work that the service posts.
+ mHandlerThread = new HandlerThread("TimeZoneDetectorInternalTest");
+ mHandlerThread.start();
+ mHandler = new TestHandler(mHandlerThread.getLooper());
+
+ ConfigurationInternal config = new ConfigurationInternal.Builder()
+ .setUserId(mUid)
+ .setTelephonyDetectionFeatureSupported(true)
+ .setGeoDetectionFeatureSupported(true)
+ .setTelephonyFallbackSupported(false)
+ .setGeoDetectionRunInBackgroundEnabled(false)
+ .setEnhancedMetricsCollectionEnabled(false)
+ .setUserConfigAllowed(true)
+ .setAutoDetectionEnabledSetting(false)
+ .setLocationEnabledSetting(true)
+ .setGeoDetectionEnabledSetting(false)
+ .setNotificationsSupported(true)
+ .setNotificationsTrackingSupported(true)
+ .setNotificationsEnabledSetting(false)
+ .setManualChangeTrackingSupported(false)
+ .build();
+
+ mServiceConfigAccessor = spy(new FakeServiceConfigAccessor());
+ mServiceConfigAccessor.initializeCurrentUserConfiguration(config);
+
+ mContext = InstrumentationRegistry.getInstrumentation().getContext();
+ mUiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
+ mUiAutomation.adoptShellPermissionIdentity(INTERACT_ACROSS_USERS_FULL_PERMISSION);
+
+ mNotificationManager = new FakeNotificationManager(mContext, InstantSource.system());
+
+ mTimeZoneChangeTracker = new NotifyingTimeZoneChangeListener(mHandler, mContext,
+ mServiceConfigAccessor, mNotificationManager);
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ mHandlerThread.quit();
+ mHandlerThread.join();
+ }
+
+ @Test
+ public void process_autoDetectionOff_noManualTracking_shouldTrackWithoutNotifying() {
+ enableTimeZoneNotifications();
+
+ TimeZoneChangeRecord expectedChangeEvent = new TimeZoneChangeRecord(
+ /* id= */ 1,
+ new TimeZoneChangeEvent(
+ /* elapsedRealtimeMillis= */ 0,
+ /* unixEpochTimeMillis= */ 1726597800000L,
+ /* origin= */ ORIGIN_MANUAL,
+ /* userId= */ mUid,
+ /* oldZoneId= */ "Europe/Paris",
+ /* newZoneId= */ "Europe/London",
+ /* newConfidence= */ 1,
+ /* cause= */ "NO_REASON"));
+ expectedChangeEvent.setStatus(STATUS_UNTRACKED, SIGNAL_TYPE_NONE);
+
+ assertNull(mTimeZoneChangeTracker.getLastTimeZoneChangeRecord());
+
+ mTimeZoneChangeTracker.process(expectedChangeEvent.getEvent());
+
+ assertEquals(expectedChangeEvent, mTimeZoneChangeTracker.getLastTimeZoneChangeRecord());
+ assertEquals(0, mNotificationManager.getNotifications().size());
+ mHandler.assertTotalMessagesEnqueued(0);
+ }
+
+ @Test
+ public void process_autoDetectionOff_shouldTrackWithoutNotifying() {
+ enableNotificationsWithManualChangeTracking();
+
+ TimeZoneChangeRecord expectedChangeEvent = new TimeZoneChangeRecord(
+ /* id= */ 1,
+ new TimeZoneChangeEvent(
+ /* elapsedRealtimeMillis= */ 0,
+ /* unixEpochTimeMillis= */ 1726597800000L,
+ /* origin= */ ORIGIN_MANUAL,
+ /* userId= */ mUid,
+ /* oldZoneId= */ "Europe/Paris",
+ /* newZoneId= */ "Europe/London",
+ /* newConfidence= */ 1,
+ /* cause= */ "NO_REASON"));
+ expectedChangeEvent.setStatus(STATUS_UNTRACKED, SIGNAL_TYPE_NONE);
+
+ assertNull(mTimeZoneChangeTracker.getLastTimeZoneChangeRecord());
+
+ mTimeZoneChangeTracker.process(expectedChangeEvent.getEvent());
+
+ assertEquals(expectedChangeEvent, mTimeZoneChangeTracker.getLastTimeZoneChangeRecord());
+ assertEquals(0, mNotificationManager.getNotifications().size());
+ mHandler.assertTotalMessagesEnqueued(1);
+ }
+
+ @Test
+ @Parameters(method = "getDetectionOrigins")
+ public void process_automaticDetection_trackingSupported(
+ @TimeZoneDetectorStrategy.Origin int origin) {
+ if (origin == ORIGIN_LOCATION) {
+ enableLocationTimeZoneDetection();
+ } else if (origin == ORIGIN_TELEPHONY) {
+ enableTelephonyTimeZoneDetection();
+ } else {
+ throw new IllegalStateException(
+ "The given origin has not been implemented for this test: " + origin);
+ }
+
+ enableNotificationsWithManualChangeTracking();
+
+ TimeZoneChangeRecord expectedChangeEvent = new TimeZoneChangeRecord(
+ /* id= */ 1,
+ new TimeZoneChangeEvent(
+ /* elapsedRealtimeMillis= */ 0,
+ /* unixEpochTimeMillis= */ 1726597800000L,
+ /* origin= */ origin,
+ /* userId= */ mUid,
+ /* oldZoneId= */ "Europe/Paris",
+ /* newZoneId= */ "Europe/London",
+ /* newConfidence= */ 1,
+ /* cause= */ "NO_REASON"));
+ expectedChangeEvent.setStatus(STATUS_UNKNOWN, SIGNAL_TYPE_UNKNOWN);
+
+ assertNull(mTimeZoneChangeTracker.getLastTimeZoneChangeRecord());
+
+ // lastTrackedChangeEvent == null
+ mTimeZoneChangeTracker.process(expectedChangeEvent.getEvent());
+ TimeZoneChangeRecord trackedEvent1 = mTimeZoneChangeTracker.getLastTimeZoneChangeRecord();
+
+ assertEquals(expectedChangeEvent, trackedEvent1);
+ assertEquals(1, mNotificationManager.getNotifications().size());
+ mHandler.assertTotalMessagesEnqueued(1);
+
+ expectedChangeEvent = new TimeZoneChangeRecord(
+ /* id= */ 2,
+ new TimeZoneChangeEvent(
+ /* elapsedRealtimeMillis= */ 1000L,
+ /* unixEpochTimeMillis= */ 1726597800000L + 1000L,
+ /* origin= */ origin,
+ /* userId= */ mUid,
+ /* oldZoneId= */ "Europe/London",
+ /* newZoneId= */ "Europe/Paris",
+ /* newConfidence= */ 1,
+ /* cause= */ "NO_REASON"));
+ expectedChangeEvent.setStatus(STATUS_UNKNOWN, SIGNAL_TYPE_UNKNOWN);
+
+ // lastTrackedChangeEvent != null
+ mTimeZoneChangeTracker.process(expectedChangeEvent.getEvent());
+ TimeZoneChangeRecord trackedEvent2 = mTimeZoneChangeTracker.getLastTimeZoneChangeRecord();
+
+ assertEquals(STATUS_SUPERSEDED, trackedEvent1.getStatus());
+ assertEquals(expectedChangeEvent, trackedEvent2);
+ assertEquals(2, mNotificationManager.getNotifications().size());
+ mHandler.assertTotalMessagesEnqueued(2);
+
+ disableTimeZoneAutoDetection();
+
+ // Test manual change within revert threshold
+ {
+ expectedChangeEvent = new TimeZoneChangeRecord(
+ /* id= */ 3,
+ new TimeZoneChangeEvent(
+ /* elapsedRealtimeMillis= */ 999L + AUTO_REVERT_THRESHOLD,
+ /* unixEpochTimeMillis= */
+ 1726597800000L + 999L + AUTO_REVERT_THRESHOLD,
+ /* origin= */ ORIGIN_MANUAL,
+ /* userId= */ mUid,
+ /* oldZoneId= */ "Europe/Paris",
+ /* newZoneId= */ "Europe/London",
+ /* newConfidence= */ 1,
+ /* cause= */ "NO_REASON"));
+ expectedChangeEvent.setStatus(STATUS_UNTRACKED, SIGNAL_TYPE_NONE);
+
+ mTimeZoneChangeTracker.process(expectedChangeEvent.getEvent());
+ TimeZoneChangeRecord trackedEvent3 =
+ mTimeZoneChangeTracker.getLastTimeZoneChangeRecord();
+
+ // The user manually changed the time zone within a short period of receiving the
+ // notification, indicating that they rejected the automatic change of time zone
+ assertEquals(STATUS_REJECTED, trackedEvent2.getStatus());
+ assertEquals(expectedChangeEvent, trackedEvent3);
+ assertEquals(2, mNotificationManager.getNotifications().size());
+ mHandler.assertTotalMessagesEnqueued(3);
+ }
+
+ // Test manual change outside of revert threshold
+ {
+ // [START] Reset previous event
+ enableNotificationsWithManualChangeTracking();
+ mTimeZoneChangeTracker.process(trackedEvent2.getEvent());
+ trackedEvent2 = mTimeZoneChangeTracker.getLastTimeZoneChangeRecord();
+ disableTimeZoneAutoDetection();
+ // [END] Reset previous event
+
+ expectedChangeEvent = new TimeZoneChangeRecord(
+ /* id= */ 5,
+ new TimeZoneChangeEvent(
+ /* elapsedRealtimeMillis= */ 1001L + AUTO_REVERT_THRESHOLD,
+ /* unixEpochTimeMillis= */
+ 1726597800000L + 1001L + AUTO_REVERT_THRESHOLD,
+ /* origin= */ ORIGIN_MANUAL,
+ /* userId= */ mUid,
+ /* oldZoneId= */ "Europe/Paris",
+ /* newZoneId= */ "Europe/London",
+ /* newConfidence= */ 1,
+ /* cause= */ "NO_REASON"));
+ expectedChangeEvent.setStatus(STATUS_UNTRACKED, SIGNAL_TYPE_NONE);
+
+ mTimeZoneChangeTracker.process(expectedChangeEvent.getEvent());
+ TimeZoneChangeRecord trackedEvent3 =
+ mTimeZoneChangeTracker.getLastTimeZoneChangeRecord();
+
+ // The user manually changed the time zone outside of the period we consider as a revert
+ assertEquals(STATUS_SUPERSEDED, trackedEvent2.getStatus());
+ assertEquals(expectedChangeEvent, trackedEvent3);
+ assertEquals(3, mNotificationManager.getNotifications().size());
+ mHandler.assertTotalMessagesEnqueued(5);
+ }
+ }
+
+ @Test
+ @Parameters(method = "getDetectionOrigins")
+ public void process_automaticDetection_trackingSupported_missingTransition(
+ @TimeZoneDetectorStrategy.Origin int origin) {
+ if (origin == ORIGIN_LOCATION) {
+ enableLocationTimeZoneDetection();
+ } else if (origin == ORIGIN_TELEPHONY) {
+ enableTelephonyTimeZoneDetection();
+ } else {
+ throw new IllegalStateException(
+ "The given origin has not been implemented for this test: " + origin);
+ }
+
+ enableNotificationsWithManualChangeTracking();
+
+ TimeZoneChangeRecord expectedChangeEvent = new TimeZoneChangeRecord(
+ /* id= */ 1,
+ new TimeZoneChangeEvent(
+ /* elapsedRealtimeMillis= */ 0,
+ /* unixEpochTimeMillis= */ 1726597800000L,
+ /* origin= */ origin,
+ /* userId= */ mUid,
+ /* oldZoneId= */ "Europe/Paris",
+ /* newZoneId= */ "Europe/London",
+ /* newConfidence= */ 1,
+ /* cause= */ "NO_REASON"));
+ expectedChangeEvent.setStatus(STATUS_UNKNOWN, SIGNAL_TYPE_UNKNOWN);
+
+ assertNull(mTimeZoneChangeTracker.getLastTimeZoneChangeRecord());
+
+ // lastTrackedChangeEvent == null
+ mTimeZoneChangeTracker.process(expectedChangeEvent.getEvent());
+ TimeZoneChangeRecord trackedEvent1 = mTimeZoneChangeTracker.getLastTimeZoneChangeRecord();
+
+ assertEquals(expectedChangeEvent, trackedEvent1);
+ assertEquals(1, mNotificationManager.getNotifications().size());
+ mHandler.assertTotalMessagesEnqueued(1);
+
+ expectedChangeEvent = new TimeZoneChangeRecord(
+ /* id= */ 3,
+ new TimeZoneChangeEvent(
+ /* elapsedRealtimeMillis= */ 1000L,
+ /* unixEpochTimeMillis= */ 1726597800000L + 1000L,
+ /* origin= */ origin,
+ /* userId= */ mUid,
+ /* oldZoneId= */ "Europe/Athens",
+ /* newZoneId= */ "Europe/Paris",
+ /* newConfidence= */ 1,
+ /* cause= */ "NO_REASON"));
+ expectedChangeEvent.setStatus(STATUS_UNKNOWN, SIGNAL_TYPE_UNKNOWN);
+
+ // lastTrackedChangeEvent != null
+ mTimeZoneChangeTracker.process(expectedChangeEvent.getEvent());
+ TimeZoneChangeRecord trackedEvent2 = mTimeZoneChangeTracker.getLastTimeZoneChangeRecord();
+
+ assertEquals(STATUS_SUPERSEDED, trackedEvent1.getStatus());
+ assertEquals(expectedChangeEvent, trackedEvent2);
+ assertEquals(2, mNotificationManager.getNotifications().size());
+ mHandler.assertTotalMessagesEnqueued(2);
+ }
+
+ @Test
+ @Parameters(method = "getDetectionOrigins")
+ public void process_automaticDetection_trackingSupported_sameOffset(
+ @TimeZoneDetectorStrategy.Origin int origin) {
+ if (origin == ORIGIN_LOCATION) {
+ enableLocationTimeZoneDetection();
+ } else if (origin == ORIGIN_TELEPHONY) {
+ enableTelephonyTimeZoneDetection();
+ } else {
+ throw new IllegalStateException(
+ "The given origin has not been implemented for this test: " + origin);
+ }
+
+ enableNotificationsWithManualChangeTracking();
+
+ TimeZoneChangeRecord expectedChangeEvent = new TimeZoneChangeRecord(
+ /* id= */ 1,
+ new TimeZoneChangeEvent(
+ /* elapsedRealtimeMillis= */ 0,
+ /* unixEpochTimeMillis= */ 1726597800000L,
+ /* origin= */ origin,
+ /* userId= */ mUid,
+ /* oldZoneId= */ "Europe/Paris",
+ /* newZoneId= */ "Europe/Rome",
+ /* newConfidence= */ 1,
+ /* cause= */ "NO_REASON"));
+ expectedChangeEvent.setStatus(STATUS_UNKNOWN, SIGNAL_TYPE_UNKNOWN);
+
+ assertNull(mTimeZoneChangeTracker.getLastTimeZoneChangeRecord());
+
+ // lastTrackedChangeEvent == null
+ mTimeZoneChangeTracker.process(expectedChangeEvent.getEvent());
+ TimeZoneChangeRecord trackedEvent1 = mTimeZoneChangeTracker.getLastTimeZoneChangeRecord();
+
+ assertEquals(expectedChangeEvent, trackedEvent1);
+ // No notification sent for the same UTC offset
+ assertEquals(0, mNotificationManager.getNotifications().size());
+ mHandler.assertTotalMessagesEnqueued(1);
+ }
+
+ private void enableLocationTimeZoneDetection() {
+ ConfigurationInternal oldConfiguration =
+ mServiceConfigAccessor.getCurrentUserConfigurationInternal();
+ ConfigurationInternal newConfiguration = toBuilder(oldConfiguration)
+ .setAutoDetectionEnabledSetting(true)
+ .setGeoDetectionFeatureSupported(true)
+ .setGeoDetectionEnabledSetting(true)
+ .build();
+
+ mServiceConfigAccessor.simulateCurrentUserConfigurationInternalChange(newConfiguration);
+ }
+
+ private void enableTelephonyTimeZoneDetection() {
+ ConfigurationInternal oldConfiguration =
+ mServiceConfigAccessor.getCurrentUserConfigurationInternal();
+ ConfigurationInternal newConfiguration = toBuilder(oldConfiguration)
+ .setAutoDetectionEnabledSetting(true)
+ .setGeoDetectionEnabledSetting(false)
+ .setTelephonyDetectionFeatureSupported(true)
+ .setTelephonyFallbackSupported(true)
+ .build();
+
+ mServiceConfigAccessor.simulateCurrentUserConfigurationInternalChange(newConfiguration);
+ }
+
+ private void enableTimeZoneNotifications() {
+ ConfigurationInternal oldConfiguration =
+ mServiceConfigAccessor.getCurrentUserConfigurationInternal();
+ ConfigurationInternal newConfiguration = toBuilder(oldConfiguration)
+ .setNotificationsSupported(true)
+ .setNotificationsTrackingSupported(true)
+ .setNotificationsEnabledSetting(true)
+ .setManualChangeTrackingSupported(false)
+ .build();
+
+ mServiceConfigAccessor.simulateCurrentUserConfigurationInternalChange(newConfiguration);
+ }
+
+ private void enableNotificationsWithManualChangeTracking() {
+ ConfigurationInternal oldConfiguration =
+ mServiceConfigAccessor.getCurrentUserConfigurationInternal();
+ ConfigurationInternal newConfiguration = toBuilder(oldConfiguration)
+ .setNotificationsSupported(true)
+ .setNotificationsTrackingSupported(true)
+ .setNotificationsEnabledSetting(true)
+ .setManualChangeTrackingSupported(true)
+ .build();
+
+ mServiceConfigAccessor.simulateCurrentUserConfigurationInternalChange(newConfiguration);
+ }
+
+ private void disableTimeZoneAutoDetection() {
+ ConfigurationInternal oldConfiguration =
+ mServiceConfigAccessor.getCurrentUserConfigurationInternal();
+ ConfigurationInternal newConfiguration = toBuilder(oldConfiguration)
+ .setAutoDetectionEnabledSetting(false)
+ .setGeoDetectionEnabledSetting(false)
+ .build();
+
+ mServiceConfigAccessor.simulateCurrentUserConfigurationInternalChange(newConfiguration);
+ }
+
+ private ConfigurationInternal.Builder toBuilder(ConfigurationInternal config) {
+ return new ConfigurationInternal.Builder()
+ .setUserId(config.getUserId())
+ .setTelephonyDetectionFeatureSupported(config.isTelephonyDetectionSupported())
+ .setGeoDetectionFeatureSupported(config.isGeoDetectionSupported())
+ .setTelephonyFallbackSupported(config.isTelephonyFallbackSupported())
+ .setGeoDetectionRunInBackgroundEnabled(
+ config.getGeoDetectionRunInBackgroundEnabledSetting())
+ .setEnhancedMetricsCollectionEnabled(config.isEnhancedMetricsCollectionEnabled())
+ .setUserConfigAllowed(config.isUserConfigAllowed())
+ .setAutoDetectionEnabledSetting(config.getAutoDetectionEnabledSetting())
+ .setLocationEnabledSetting(config.getLocationEnabledSetting())
+ .setGeoDetectionEnabledSetting(config.getGeoDetectionEnabledSetting())
+ .setNotificationsTrackingSupported(config.isNotificationTrackingSupported())
+ .setNotificationsEnabledSetting(config.getNotificationsEnabledBehavior())
+ .setNotificationsSupported(config.areNotificationsSupported())
+ .setManualChangeTrackingSupported(config.isManualChangeTrackingSupported());
+ }
+
+ private static class FakeNotificationManager extends NotificationManager {
+
+ private final List<Notification> mNotifications = new ArrayList<>();
+
+ FakeNotificationManager(Context context, InstantSource clock) {
+ super(context, clock);
+ }
+
+ @Override
+ public void notifyAsUser(@Nullable String tag, int id, Notification notification,
+ UserHandle user) {
+ mNotifications.add(notification);
+ }
+
+ public List<Notification> getNotifications() {
+ return mNotifications;
+ }
+ }
+}
diff --git a/services/tests/timetests/src/com/android/server/timezonedetector/TimeZoneDetectorStrategyImplTest.java b/services/tests/timetests/src/com/android/server/timezonedetector/TimeZoneDetectorStrategyImplTest.java
index 47a9b2c47173..9a01fa2eb966 100644
--- a/services/tests/timetests/src/com/android/server/timezonedetector/TimeZoneDetectorStrategyImplTest.java
+++ b/services/tests/timetests/src/com/android/server/timezonedetector/TimeZoneDetectorStrategyImplTest.java
@@ -41,6 +41,9 @@ import static com.android.server.timezonedetector.ConfigInternalForTests.CONFIG_
import static com.android.server.timezonedetector.ConfigInternalForTests.CONFIG_AUTO_ENABLED_GEO_ENABLED;
import static com.android.server.timezonedetector.ConfigInternalForTests.CONFIG_USER_RESTRICTED_AUTO_ENABLED;
import static com.android.server.timezonedetector.ConfigInternalForTests.USER_ID;
+import static com.android.server.timezonedetector.TimeZoneDetectorStrategy.ORIGIN_LOCATION;
+import static com.android.server.timezonedetector.TimeZoneDetectorStrategy.ORIGIN_MANUAL;
+import static com.android.server.timezonedetector.TimeZoneDetectorStrategy.ORIGIN_TELEPHONY;
import static com.android.server.timezonedetector.TimeZoneDetectorStrategyImpl.TELEPHONY_SCORE_HIGH;
import static com.android.server.timezonedetector.TimeZoneDetectorStrategyImpl.TELEPHONY_SCORE_HIGHEST;
import static com.android.server.timezonedetector.TimeZoneDetectorStrategyImpl.TELEPHONY_SCORE_LOW;
@@ -73,16 +76,21 @@ import android.app.timezonedetector.ManualTimeZoneSuggestion;
import android.app.timezonedetector.TelephonyTimeZoneSuggestion;
import android.app.timezonedetector.TelephonyTimeZoneSuggestion.MatchType;
import android.app.timezonedetector.TelephonyTimeZoneSuggestion.Quality;
+import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.SetFlagsRule;
import android.service.timezone.TimeZoneProviderStatus;
import android.util.IndentingPrintWriter;
import com.android.server.SystemTimeZone.TimeZoneConfidence;
+import com.android.server.flags.Flags;
import com.android.server.timezonedetector.TimeZoneDetectorStrategyImpl.QualifiedTelephonyTimeZoneSuggestion;
import junitparams.JUnitParamsRunner;
import junitparams.Parameters;
import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -98,8 +106,14 @@ import java.util.function.Function;
* White-box unit tests for {@link TimeZoneDetectorStrategyImpl}.
*/
@RunWith(JUnitParamsRunner.class)
+@EnableFlags(Flags.FLAG_DATETIME_NOTIFICATIONS)
public class TimeZoneDetectorStrategyImplTest {
+ @ClassRule
+ public static final SetFlagsRule.ClassRule mClassRule = new SetFlagsRule.ClassRule();
+ @Rule
+ public final SetFlagsRule mSetFlagsRule = mClassRule.createSetFlagsRule();
+
private static final long ARBITRARY_ELAPSED_REALTIME_MILLIS = 1234;
/** A time zone used for initialization that does not occur elsewhere in tests. */
private static final String ARBITRARY_TIME_ZONE_ID = "Etc/UTC";
@@ -130,6 +144,7 @@ public class TimeZoneDetectorStrategyImplTest {
private FakeServiceConfigAccessor mFakeServiceConfigAccessorSpy;
private FakeEnvironment mFakeEnvironment;
+ private FakeTimeZoneChangeEventListener mFakeTimeZoneChangeEventTracker;
private TimeZoneDetectorStrategyImpl mTimeZoneDetectorStrategy;
@@ -139,9 +154,10 @@ public class TimeZoneDetectorStrategyImplTest {
mFakeServiceConfigAccessorSpy = spy(new FakeServiceConfigAccessor());
mFakeServiceConfigAccessorSpy.initializeCurrentUserConfiguration(
CONFIG_AUTO_DISABLED_GEO_DISABLED);
+ mFakeTimeZoneChangeEventTracker = new FakeTimeZoneChangeEventListener();
mTimeZoneDetectorStrategy = new TimeZoneDetectorStrategyImpl(
- mFakeServiceConfigAccessorSpy, mFakeEnvironment);
+ mFakeServiceConfigAccessorSpy, mFakeEnvironment, mFakeTimeZoneChangeEventTracker);
}
@Test
@@ -363,6 +379,10 @@ public class TimeZoneDetectorStrategyImplTest {
// SlotIndex1 should always beat slotIndex2, all other things being equal.
assertEquals(expectedSlotIndex1ScoredSuggestion,
mTimeZoneDetectorStrategy.findBestTelephonySuggestionForTests());
+
+ if (Flags.datetimeNotifications()) {
+ assertEmpty(mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents());
+ }
}
/**
@@ -398,6 +418,10 @@ public class TimeZoneDetectorStrategyImplTest {
SLOT_INDEX1, expectedScoredSuggestion);
assertEquals(expectedScoredSuggestion,
mTimeZoneDetectorStrategy.findBestTelephonySuggestionForTests());
+
+ if (Flags.datetimeNotifications()) {
+ assertEmpty(mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents());
+ }
}
// A good quality suggestion will be used.
@@ -415,6 +439,13 @@ public class TimeZoneDetectorStrategyImplTest {
SLOT_INDEX1, expectedScoredSuggestion);
assertEquals(expectedScoredSuggestion,
mTimeZoneDetectorStrategy.findBestTelephonySuggestionForTests());
+
+ if (Flags.datetimeNotifications()) {
+ assertEquals(1, mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents().size());
+ assertEquals(ORIGIN_TELEPHONY,
+ mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents().getFirst()
+ .getOrigin());
+ }
}
// A low quality suggestion will be accepted, but not used to set the device time zone.
@@ -432,6 +463,11 @@ public class TimeZoneDetectorStrategyImplTest {
SLOT_INDEX1, expectedScoredSuggestion);
assertEquals(expectedScoredSuggestion,
mTimeZoneDetectorStrategy.findBestTelephonySuggestionForTests());
+
+ if (Flags.datetimeNotifications()) {
+ // Still 1 from last good quality suggestion but not recorded as quality is too low
+ assertEquals(1, mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents().size());
+ }
}
}
@@ -492,6 +528,17 @@ public class TimeZoneDetectorStrategyImplTest {
assertEquals(expectedScoredSuggestion,
mTimeZoneDetectorStrategy.findBestTelephonySuggestionForTests());
}
+
+ if (Flags.datetimeNotifications()) {
+ /*
+ * Only 6 out of 7 tests have a quality good enough to trigger an event and the
+ * configuration is reset at every loop.
+ */
+ assertEquals(6, mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents().size());
+ assertEquals(ORIGIN_TELEPHONY,
+ mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents().getFirst()
+ .getOrigin());
+ }
}
@Test
@@ -518,6 +565,18 @@ public class TimeZoneDetectorStrategyImplTest {
for (TelephonyTestCase testCase : descendingCasesByScore) {
makeSlotIndex1SuggestionAndCheckState(script, testCase);
}
+
+ if (Flags.datetimeNotifications()) {
+ /*
+ * Only 6 out of 7 tests have a quality good enough to trigger an event and the
+ * set of tests is run twice.
+ */
+ List<TimeZoneChangeListener.TimeZoneChangeEvent> timeZoneChangeEvents =
+ mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents();
+
+ assertEquals(12, timeZoneChangeEvents.size());
+ assertEquals(ORIGIN_TELEPHONY, timeZoneChangeEvents.getFirst().getOrigin());
+ }
}
private void makeSlotIndex1SuggestionAndCheckState(Script script, TelephonyTestCase testCase) {
@@ -641,6 +700,18 @@ public class TimeZoneDetectorStrategyImplTest {
.verifyLatestQualifiedTelephonySuggestionReceived(
SLOT_INDEX2, expectedEmptySlotIndex2ScoredSuggestion);
}
+
+ if (Flags.datetimeNotifications()) {
+ /*
+ * Only 6 out of 7 tests have a quality good enough to trigger an event and the
+ * simulation runs twice per loop with a different time zone (i.e. London and Paris).
+ */
+ List<TimeZoneChangeListener.TimeZoneChangeEvent> timeZoneChangeEvents =
+ mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents();
+
+ assertEquals(12, timeZoneChangeEvents.size());
+ assertEquals(ORIGIN_TELEPHONY, timeZoneChangeEvents.getFirst().getOrigin());
+ }
}
/**
@@ -683,6 +754,14 @@ public class TimeZoneDetectorStrategyImplTest {
// Latest suggestion should be used.
script.simulateSetAutoMode(true)
.verifyTimeZoneChangedAndReset(newYorkSuggestion);
+
+ if (Flags.datetimeNotifications()) {
+ List<TimeZoneChangeListener.TimeZoneChangeEvent> timeZoneChangeEvents =
+ mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents();
+ assertEquals(2, timeZoneChangeEvents.size());
+ assertTrue(timeZoneChangeEvents.stream()
+ .allMatch(x -> x.getOrigin() == ORIGIN_TELEPHONY));
+ }
}
@Test
@@ -714,6 +793,10 @@ public class TimeZoneDetectorStrategyImplTest {
.verifyTimeZoneNotChanged();
assertNull(mTimeZoneDetectorStrategy.getLatestManualSuggestion());
+
+ if (Flags.datetimeNotifications()) {
+ assertEmpty(mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents());
+ }
}
@Test
@@ -732,6 +815,15 @@ public class TimeZoneDetectorStrategyImplTest {
.verifyTimeZoneChangedAndReset(manualSuggestion);
assertEquals(manualSuggestion, mTimeZoneDetectorStrategy.getLatestManualSuggestion());
+
+ if (Flags.datetimeNotifications()) {
+ List<TimeZoneChangeListener.TimeZoneChangeEvent> timeZoneChangeEvents =
+ mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents();
+
+ assertEquals(1, timeZoneChangeEvents.size());
+ assertEquals(ORIGIN_MANUAL, timeZoneChangeEvents.getFirst().getOrigin()
+ );
+ }
}
@Test
@@ -754,6 +846,10 @@ public class TimeZoneDetectorStrategyImplTest {
.verifyTimeZoneNotChanged();
assertNull(mTimeZoneDetectorStrategy.getLatestManualSuggestion());
+
+ if (Flags.datetimeNotifications()) {
+ assertEmpty(mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents());
+ }
}
@Test
@@ -780,6 +876,16 @@ public class TimeZoneDetectorStrategyImplTest {
script.verifyTimeZoneNotChanged();
assertNull(mTimeZoneDetectorStrategy.getLatestManualSuggestion());
}
+
+ List<TimeZoneChangeListener.TimeZoneChangeEvent> timeZoneChangeEvents =
+ mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents();
+
+ if (Flags.datetimeNotifications() && expectedResult) {
+ assertEquals(1, timeZoneChangeEvents.size());
+ assertEquals(ORIGIN_MANUAL, timeZoneChangeEvents.getFirst().getOrigin());
+ } else {
+ assertEmpty(timeZoneChangeEvents);
+ }
}
@Test
@@ -830,6 +936,10 @@ public class TimeZoneDetectorStrategyImplTest {
// Assert internal service state.
script.verifyCachedDetectorStatus(expectedDetectorStatus)
.verifyLatestLocationAlgorithmEventReceived(locationAlgorithmEvent);
+
+ if (Flags.datetimeNotifications()) {
+ assertEmpty(mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents());
+ }
}
{
@@ -857,6 +967,10 @@ public class TimeZoneDetectorStrategyImplTest {
// Assert internal service state.
script.verifyCachedDetectorStatus(expectedDetectorStatus)
.verifyLatestLocationAlgorithmEventReceived(locationAlgorithmEvent);
+
+ if (Flags.datetimeNotifications()) {
+ assertEmpty(mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents());
+ }
}
}
@@ -893,6 +1007,10 @@ public class TimeZoneDetectorStrategyImplTest {
// Assert internal service state.
script.verifyCachedDetectorStatus(expectedDetectorStatus)
.verifyLatestLocationAlgorithmEventReceived(locationAlgorithmEvent);
+
+ if (Flags.datetimeNotifications()) {
+ assertEmpty(mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents());
+ }
}
@Test
@@ -927,6 +1045,10 @@ public class TimeZoneDetectorStrategyImplTest {
// Assert internal service state.
script.verifyCachedDetectorStatus(expectedDetectorStatus)
.verifyLatestLocationAlgorithmEventReceived(locationAlgorithmEvent);
+
+ if (Flags.datetimeNotifications()) {
+ assertEmpty(mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents());
+ }
}
@Test
@@ -962,6 +1084,14 @@ public class TimeZoneDetectorStrategyImplTest {
// Assert internal service state.
script.verifyCachedDetectorStatus(expectedDetectorStatus)
.verifyLatestLocationAlgorithmEventReceived(locationAlgorithmEvent);
+
+ if (Flags.datetimeNotifications()) {
+ List<TimeZoneChangeListener.TimeZoneChangeEvent> timeZoneChangeEvents =
+ mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents();
+
+ assertEquals(1, timeZoneChangeEvents.size());
+ assertEquals(ORIGIN_LOCATION, timeZoneChangeEvents.getFirst().getOrigin());
+ }
}
/**
@@ -999,6 +1129,17 @@ public class TimeZoneDetectorStrategyImplTest {
script.simulateLocationAlgorithmEvent(londonOrParisEvent)
.verifyTimeZoneNotChanged()
.verifyLatestLocationAlgorithmEventReceived(londonOrParisEvent);
+
+ if (Flags.datetimeNotifications()) {
+ // we do not record events if the time zone does not change (i.e. 2 / 4 of the
+ // simulated cases)
+ List<TimeZoneChangeListener.TimeZoneChangeEvent> timeZoneChangeEvents =
+ mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents();
+
+ assertEquals(2, timeZoneChangeEvents.size());
+ assertTrue(timeZoneChangeEvents.stream()
+ .allMatch(x -> x.getOrigin() == ORIGIN_LOCATION));
+ }
}
/**
@@ -1059,6 +1200,16 @@ public class TimeZoneDetectorStrategyImplTest {
// A configuration change is considered a "state change".
assertStateChangeNotificationsSent(stateChangeListener, 1);
+
+ if (Flags.datetimeNotifications()) {
+ List<TimeZoneChangeListener.TimeZoneChangeEvent> timeZoneChangeEvents =
+ mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents();
+
+ assertEquals(3, timeZoneChangeEvents.size());
+ assertEquals(ORIGIN_TELEPHONY, timeZoneChangeEvents.get(0).getOrigin());
+ assertEquals(ORIGIN_LOCATION, timeZoneChangeEvents.get(1).getOrigin());
+ assertEquals(ORIGIN_TELEPHONY, timeZoneChangeEvents.get(2).getOrigin());
+ }
}
@Test
@@ -1088,6 +1239,14 @@ public class TimeZoneDetectorStrategyImplTest {
.simulateTelephonyTimeZoneSuggestion(telephonySuggestion)
.verifyTimeZoneChangedAndReset(telephonySuggestion)
.verifyTelephonyFallbackIsEnabled(true);
+
+ if (Flags.datetimeNotifications()) {
+ List<TimeZoneChangeListener.TimeZoneChangeEvent> timeZoneChangeEvents =
+ mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents();
+
+ assertEquals(1, timeZoneChangeEvents.size());
+ assertEquals(ORIGIN_TELEPHONY, timeZoneChangeEvents.get(0).getOrigin());
+ }
}
// Receiving an "uncertain" geolocation suggestion should have no effect.
@@ -1098,6 +1257,11 @@ public class TimeZoneDetectorStrategyImplTest {
script.simulateLocationAlgorithmEvent(locationAlgorithmEvent)
.verifyTimeZoneNotChanged()
.verifyTelephonyFallbackIsEnabled(true);
+
+ if (Flags.datetimeNotifications()) {
+ // unchanged
+ assertEquals(1, mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents().size());
+ }
}
// Receiving a "certain" geolocation suggestion should disable telephony fallback mode.
@@ -1109,6 +1273,14 @@ public class TimeZoneDetectorStrategyImplTest {
script.simulateLocationAlgorithmEvent(locationAlgorithmEvent)
.verifyTimeZoneChangedAndReset(locationAlgorithmEvent)
.verifyTelephonyFallbackIsEnabled(false);
+
+ if (Flags.datetimeNotifications()) {
+ List<TimeZoneChangeListener.TimeZoneChangeEvent> timeZoneChangeEvents =
+ mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents();
+
+ assertEquals(2, timeZoneChangeEvents.size());
+ assertEquals(ORIGIN_LOCATION, timeZoneChangeEvents.get(1).getOrigin());
+ }
}
// Used to record the last telephony suggestion received, which will be used when fallback
@@ -1125,6 +1297,11 @@ public class TimeZoneDetectorStrategyImplTest {
.verifyTimeZoneNotChanged()
.verifyTelephonyFallbackIsEnabled(false);
lastTelephonySuggestion = telephonySuggestion;
+
+ if (Flags.datetimeNotifications()) {
+ // unchanged
+ assertEquals(2, mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents().size());
+ }
}
// Geolocation suggestions should continue to be used as normal (previous telephony
@@ -1151,6 +1328,14 @@ public class TimeZoneDetectorStrategyImplTest {
// No change needed, device will already be set to Europe/Rome.
.verifyTimeZoneNotChanged()
.verifyTelephonyFallbackIsEnabled(false);
+
+ if (Flags.datetimeNotifications()) {
+ List<TimeZoneChangeListener.TimeZoneChangeEvent> timeZoneChangeEvents =
+ mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents();
+
+ assertEquals(3, timeZoneChangeEvents.size());
+ assertEquals(ORIGIN_LOCATION, timeZoneChangeEvents.get(2).getOrigin());
+ }
}
// Enable telephony fallback. Nothing will change, because the geolocation is still certain,
@@ -1160,6 +1345,11 @@ public class TimeZoneDetectorStrategyImplTest {
.simulateEnableTelephonyFallback()
.verifyTimeZoneNotChanged()
.verifyTelephonyFallbackIsEnabled(true);
+
+ if (Flags.datetimeNotifications()) {
+ // unchanged
+ assertEquals(3, mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents().size());
+ }
}
// Make the geolocation algorithm uncertain.
@@ -1170,6 +1360,14 @@ public class TimeZoneDetectorStrategyImplTest {
script.simulateLocationAlgorithmEvent(locationAlgorithmEvent)
.verifyTimeZoneChangedAndReset(lastTelephonySuggestion)
.verifyTelephonyFallbackIsEnabled(true);
+
+ if (Flags.datetimeNotifications()) {
+ List<TimeZoneChangeListener.TimeZoneChangeEvent> timeZoneChangeEvents =
+ mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents();
+
+ assertEquals(4, timeZoneChangeEvents.size());
+ assertEquals(ORIGIN_TELEPHONY, timeZoneChangeEvents.get(3).getOrigin());
+ }
}
// Make the geolocation algorithm certain, disabling telephony fallback.
@@ -1181,6 +1379,14 @@ public class TimeZoneDetectorStrategyImplTest {
script.simulateLocationAlgorithmEvent(locationAlgorithmEvent)
.verifyTimeZoneChangedAndReset(locationAlgorithmEvent)
.verifyTelephonyFallbackIsEnabled(false);
+
+ if (Flags.datetimeNotifications()) {
+ List<TimeZoneChangeListener.TimeZoneChangeEvent> timeZoneChangeEvents =
+ mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents();
+
+ assertEquals(5, timeZoneChangeEvents.size());
+ assertEquals(ORIGIN_LOCATION, timeZoneChangeEvents.get(4).getOrigin());
+ }
}
// Demonstrate what happens when geolocation is uncertain when telephony fallback is
@@ -1195,6 +1401,14 @@ public class TimeZoneDetectorStrategyImplTest {
.simulateEnableTelephonyFallback()
.verifyTimeZoneChangedAndReset(lastTelephonySuggestion)
.verifyTelephonyFallbackIsEnabled(true);
+
+ if (Flags.datetimeNotifications()) {
+ List<TimeZoneChangeListener.TimeZoneChangeEvent> timeZoneChangeEvents =
+ mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents();
+
+ assertEquals(6, timeZoneChangeEvents.size());
+ assertEquals(ORIGIN_TELEPHONY, timeZoneChangeEvents.get(5).getOrigin());
+ }
}
}
@@ -1225,6 +1439,13 @@ public class TimeZoneDetectorStrategyImplTest {
.simulateTelephonyTimeZoneSuggestion(telephonySuggestion)
.verifyTimeZoneChangedAndReset(telephonySuggestion)
.verifyTelephonyFallbackIsEnabled(true);
+
+ if (Flags.datetimeNotifications()) {
+ List<TimeZoneChangeListener.TimeZoneChangeEvent> timeZoneChangeEvents =
+ mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents();
+ assertEquals(1, timeZoneChangeEvents.size());
+ assertEquals(ORIGIN_TELEPHONY, timeZoneChangeEvents.get(0).getOrigin());
+ }
}
// Receiving an "uncertain" geolocation suggestion without a status should have no effect.
@@ -1235,6 +1456,11 @@ public class TimeZoneDetectorStrategyImplTest {
script.simulateLocationAlgorithmEvent(locationAlgorithmEvent)
.verifyTimeZoneNotChanged()
.verifyTelephonyFallbackIsEnabled(true);
+
+ if (Flags.datetimeNotifications()) {
+ // unchanged
+ assertEquals(1, mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents().size());
+ }
}
// Receiving a "certain" geolocation suggestion should disable telephony fallback mode.
@@ -1246,6 +1472,13 @@ public class TimeZoneDetectorStrategyImplTest {
script.simulateLocationAlgorithmEvent(locationAlgorithmEvent)
.verifyTimeZoneChangedAndReset(locationAlgorithmEvent)
.verifyTelephonyFallbackIsEnabled(false);
+
+ if (Flags.datetimeNotifications()) {
+ List<TimeZoneChangeListener.TimeZoneChangeEvent> timeZoneChangeEvents =
+ mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents();
+ assertEquals(2, timeZoneChangeEvents.size());
+ assertEquals(ORIGIN_LOCATION, timeZoneChangeEvents.get(1).getOrigin());
+ }
}
// Used to record the last telephony suggestion received, which will be used when fallback
@@ -1262,6 +1495,11 @@ public class TimeZoneDetectorStrategyImplTest {
.verifyTimeZoneNotChanged()
.verifyTelephonyFallbackIsEnabled(false);
lastTelephonySuggestion = telephonySuggestion;
+
+ if (Flags.datetimeNotifications()) {
+ // unchanged
+ assertEquals(2, mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents().size());
+ }
}
// Geolocation suggestions should continue to be used as normal (previous telephony
@@ -1291,6 +1529,13 @@ public class TimeZoneDetectorStrategyImplTest {
// No change needed, device will already be set to Europe/Rome.
.verifyTimeZoneNotChanged()
.verifyTelephonyFallbackIsEnabled(false);
+
+ if (Flags.datetimeNotifications()) {
+ List<TimeZoneChangeListener.TimeZoneChangeEvent> timeZoneChangeEvents =
+ mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents();
+ assertEquals(3, timeZoneChangeEvents.size());
+ assertEquals(ORIGIN_LOCATION, timeZoneChangeEvents.get(2).getOrigin());
+ }
}
// Enable telephony fallback via a LocationAlgorithmEvent containing an "uncertain"
@@ -1310,6 +1555,13 @@ public class TimeZoneDetectorStrategyImplTest {
script.simulateLocationAlgorithmEvent(uncertainEventBlockedBySettings)
.verifyTimeZoneChangedAndReset(lastTelephonySuggestion)
.verifyTelephonyFallbackIsEnabled(true);
+
+ if (Flags.datetimeNotifications()) {
+ List<TimeZoneChangeListener.TimeZoneChangeEvent> timeZoneChangeEvents =
+ mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents();
+ assertEquals(4, timeZoneChangeEvents.size());
+ assertEquals(ORIGIN_TELEPHONY, timeZoneChangeEvents.get(3).getOrigin());
+ }
}
// Make the geolocation algorithm certain, disabling telephony fallback.
@@ -1321,6 +1573,13 @@ public class TimeZoneDetectorStrategyImplTest {
script.simulateLocationAlgorithmEvent(locationAlgorithmEvent)
.verifyTimeZoneChangedAndReset(locationAlgorithmEvent)
.verifyTelephonyFallbackIsEnabled(false);
+
+ if (Flags.datetimeNotifications()) {
+ List<TimeZoneChangeListener.TimeZoneChangeEvent> timeZoneChangeEvents =
+ mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents();
+ assertEquals(5, timeZoneChangeEvents.size());
+ assertEquals(ORIGIN_LOCATION, timeZoneChangeEvents.get(4).getOrigin());
+ }
}
}
@@ -1349,6 +1608,10 @@ public class TimeZoneDetectorStrategyImplTest {
script.simulateLocationAlgorithmEvent(locationAlgorithmEvent)
.verifyTimeZoneNotChanged()
.verifyTelephonyFallbackIsEnabled(true);
+
+ if (Flags.datetimeNotifications()) {
+ assertEmpty(mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents());
+ }
}
// Make an uncertain geolocation suggestion, there is no telephony suggestion to fall back
@@ -1360,6 +1623,10 @@ public class TimeZoneDetectorStrategyImplTest {
script.simulateLocationAlgorithmEvent(locationAlgorithmEvent)
.verifyTimeZoneNotChanged()
.verifyTelephonyFallbackIsEnabled(true);
+
+ if (Flags.datetimeNotifications()) {
+ assertEmpty(mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents());
+ }
}
// Similar to the case above, but force a fallback attempt after making a "certain"
@@ -1386,6 +1653,13 @@ public class TimeZoneDetectorStrategyImplTest {
.simulateEnableTelephonyFallback()
.verifyTimeZoneNotChanged()
.verifyTelephonyFallbackIsEnabled(true);
+
+ if (Flags.datetimeNotifications()) {
+ List<TimeZoneChangeListener.TimeZoneChangeEvent> timeZoneChangeEvents =
+ mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents();
+ assertEquals(1, timeZoneChangeEvents.size());
+ assertEquals(ORIGIN_LOCATION, timeZoneChangeEvents.get(0).getOrigin());
+ }
}
}
@@ -1803,6 +2077,16 @@ public class TimeZoneDetectorStrategyImplTest {
userId, manualTimeZoneSuggestion, bypassUserPolicyChecks);
assertEquals(expectedResult, actualResult);
+ List<TimeZoneChangeListener.TimeZoneChangeEvent> timeZoneChangeEvents =
+ mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents();
+
+ if (actualResult && Flags.datetimeNotifications()) {
+ assertEquals(1, timeZoneChangeEvents.size());
+ assertEquals(ORIGIN_MANUAL, timeZoneChangeEvents.getFirst().getOrigin());
+ } else {
+ assertEmpty(timeZoneChangeEvents);
+ }
+
return this;
}
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/GroupHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/GroupHelperTest.java
index 6cb24293a7d5..fa733e85c89c 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/GroupHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/GroupHelperTest.java
@@ -2509,6 +2509,134 @@ public class GroupHelperTest extends UiServiceTestCase {
}
@Test
+ @EnableFlags({FLAG_NOTIFICATION_FORCE_GROUPING, FLAG_NOTIFICATION_FORCE_GROUP_SINGLETONS,
+ android.app.Flags.FLAG_CHECK_AUTOGROUP_BEFORE_POST})
+ public void testRepostWithNewChannel_afterAutogrouping_isRegrouped() {
+ final String pkg = "package";
+ final List<NotificationRecord> notificationList = new ArrayList<>();
+ // Post ungrouped notifications => will be autogrouped
+ for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) {
+ NotificationRecord notification = getNotificationRecord(pkg, i + 42,
+ String.valueOf(i + 42), UserHandle.SYSTEM, null, false);
+ notificationList.add(notification);
+ mGroupHelper.onNotificationPosted(notification, false);
+ }
+
+ final String expectedGroupKey = GroupHelper.getFullAggregateGroupKey(pkg,
+ AGGREGATE_GROUP_KEY + "AlertingSection", UserHandle.SYSTEM.getIdentifier());
+ verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(),
+ eq(expectedGroupKey), anyInt(), any());
+ verify(mCallback, times(AUTOGROUP_AT_COUNT - 1)).addAutoGroup(anyString(),
+ eq(expectedGroupKey), eq(true));
+
+ // Post ungrouped notifications to a different section, below autogroup limit
+ Mockito.reset(mCallback);
+ // Post ungrouped notifications => will be autogrouped
+ final NotificationChannel silentChannel = new NotificationChannel("TEST_CHANNEL_ID1",
+ "TEST_CHANNEL_ID1", IMPORTANCE_LOW);
+ for (int i = 0; i < AUTOGROUP_AT_COUNT - 1; i++) {
+ NotificationRecord notification = getNotificationRecord(pkg, i + 4242,
+ String.valueOf(i + 4242), UserHandle.SYSTEM, null, false, silentChannel);
+ notificationList.add(notification);
+ mGroupHelper.onNotificationPosted(notification, false);
+ }
+
+ verify(mCallback, never()).addAutoGroupSummary(anyInt(), anyString(), anyString(),
+ anyString(), anyInt(), any());
+ verify(mCallback, never()).addAutoGroup(anyString(), anyString(), anyBoolean());
+
+ // Update a notification to a different channel that moves it to a different section
+ Mockito.reset(mCallback);
+ final NotificationRecord notifToInvalidate = notificationList.get(0);
+ final NotificationSectioner initialSection = GroupHelper.getSection(notifToInvalidate);
+ final NotificationChannel updatedChannel = new NotificationChannel("TEST_CHANNEL_ID2",
+ "TEST_CHANNEL_ID2", IMPORTANCE_LOW);
+ notifToInvalidate.updateNotificationChannel(updatedChannel);
+ assertThat(GroupHelper.getSection(notifToInvalidate)).isNotEqualTo(initialSection);
+ boolean needsAutogrouping = mGroupHelper.onNotificationPosted(notifToInvalidate, false);
+ assertThat(needsAutogrouping).isTrue();
+
+ // Check that the silent section was autogrouped
+ final String silentSectionGroupKey = GroupHelper.getFullAggregateGroupKey(pkg,
+ AGGREGATE_GROUP_KEY + "SilentSection", UserHandle.SYSTEM.getIdentifier());
+ verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(),
+ eq(silentSectionGroupKey), anyInt(), any());
+ verify(mCallback, times(AUTOGROUP_AT_COUNT - 1)).addAutoGroup(anyString(),
+ eq(silentSectionGroupKey), eq(true));
+ verify(mCallback, times(1)).removeAutoGroup(eq(notifToInvalidate.getKey()));
+ verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString());
+ verify(mCallback, times(1)).updateAutogroupSummary(anyInt(), anyString(),
+ eq(expectedGroupKey), any());
+ }
+
+ @Test
+ @EnableFlags({FLAG_NOTIFICATION_FORCE_GROUPING, FLAG_NOTIFICATION_FORCE_GROUP_SINGLETONS,
+ android.app.Flags.FLAG_CHECK_AUTOGROUP_BEFORE_POST})
+ public void testRepostWithNewChannel_afterForceGrouping_isRegrouped() {
+ final String pkg = "package";
+ final String groupName = "testGroup";
+ final List<NotificationRecord> notificationList = new ArrayList<>();
+ final ArrayMap<String, NotificationRecord> summaryByGroup = new ArrayMap<>();
+ // Post valid section summary notifications without children => force group
+ for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) {
+ NotificationRecord notification = getNotificationRecord(pkg, i + 42,
+ String.valueOf(i + 42), UserHandle.SYSTEM, groupName, false);
+ notificationList.add(notification);
+ mGroupHelper.onNotificationPostedWithDelay(notification, notificationList,
+ summaryByGroup);
+ }
+
+ final String expectedGroupKey = GroupHelper.getFullAggregateGroupKey(pkg,
+ AGGREGATE_GROUP_KEY + "AlertingSection", UserHandle.SYSTEM.getIdentifier());
+ verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(),
+ eq(expectedGroupKey), anyInt(), any());
+ verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(),
+ eq(expectedGroupKey), eq(true));
+
+ // Update a notification to a different channel that moves it to a different section
+ Mockito.reset(mCallback);
+ final NotificationRecord notifToInvalidate = notificationList.get(0);
+ final NotificationSectioner initialSection = GroupHelper.getSection(notifToInvalidate);
+ final NotificationChannel updatedChannel = new NotificationChannel("TEST_CHANNEL_ID2",
+ "TEST_CHANNEL_ID2", IMPORTANCE_LOW);
+ notifToInvalidate.updateNotificationChannel(updatedChannel);
+ assertThat(GroupHelper.getSection(notifToInvalidate)).isNotEqualTo(initialSection);
+ boolean needsAutogrouping = mGroupHelper.onNotificationPosted(notifToInvalidate, false);
+
+ mGroupHelper.onNotificationPostedWithDelay(notifToInvalidate, notificationList,
+ summaryByGroup);
+
+ // Check that the updated notification is removed from the autogroup
+ assertThat(needsAutogrouping).isFalse();
+ verify(mCallback, times(1)).removeAutoGroup(eq(notifToInvalidate.getKey()));
+ verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString());
+ verify(mCallback, times(1)).updateAutogroupSummary(anyInt(), anyString(),
+ eq(expectedGroupKey), any());
+
+ // Post child notifications for the silent sectin => will be autogrouped
+ Mockito.reset(mCallback);
+ final NotificationChannel silentChannel = new NotificationChannel("TEST_CHANNEL_ID1",
+ "TEST_CHANNEL_ID1", IMPORTANCE_LOW);
+ for (int i = 0; i < AUTOGROUP_AT_COUNT - 1; i++) {
+ NotificationRecord notification = getNotificationRecord(pkg, i + 4242,
+ String.valueOf(i + 4242), UserHandle.SYSTEM, "aGroup", false, silentChannel);
+ notificationList.add(notification);
+ needsAutogrouping = mGroupHelper.onNotificationPosted(notification, false);
+ assertThat(needsAutogrouping).isFalse();
+ mGroupHelper.onNotificationPostedWithDelay(notification, notificationList,
+ summaryByGroup);
+ }
+
+ // Check that the silent section was autogrouped
+ final String silentSectionGroupKey = GroupHelper.getFullAggregateGroupKey(pkg,
+ AGGREGATE_GROUP_KEY + "SilentSection", UserHandle.SYSTEM.getIdentifier());
+ verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(),
+ eq(silentSectionGroupKey), anyInt(), any());
+ verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(),
+ eq(silentSectionGroupKey), eq(true));
+ }
+
+ @Test
@EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING)
public void testMoveAggregateGroups_updateChannel() {
final String pkg = "package";
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
index 7885c9b902e2..e43b28bb9404 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -6341,6 +6341,26 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
}
@Test
+ public void testOnlyAutogroupIfNeeded_channelChanged_ghUpdate() {
+ NotificationRecord r = generateNotificationRecord(mTestNotificationChannel, 0,
+ "testOnlyAutogroupIfNeeded_channelChanged_ghUpdate", null, false);
+ mService.addNotification(r);
+
+ NotificationRecord update = generateNotificationRecord(mSilentChannel, 0,
+ "testOnlyAutogroupIfNeeded_channelChanged_ghUpdate", null, false);
+ mService.addEnqueuedNotification(update);
+
+ NotificationManagerService.PostNotificationRunnable runnable =
+ mService.new PostNotificationRunnable(update.getKey(),
+ update.getSbn().getPackageName(), update.getUid(),
+ mPostNotificationTrackerFactory.newTracker(null));
+ runnable.run();
+ waitForIdle();
+
+ verify(mGroupHelper, times(1)).onNotificationPosted(any(), anyBoolean());
+ }
+
+ @Test
public void testOnlyAutogroupIfGroupChanged_noValidChange_noGhUpdate() {
NotificationRecord r = generateNotificationRecord(mTestNotificationChannel, 0,
"testOnlyAutogroupIfGroupChanged_noValidChange_noGhUpdate", null, false);
@@ -17901,4 +17921,63 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
verify(mGroupHelper, times(1)).onNotificationUnbundled(eq(r1), eq(hasOriginalSummary));
}
+ @Test
+ @EnableFlags({FLAG_NOTIFICATION_CLASSIFICATION,
+ FLAG_NOTIFICATION_FORCE_GROUPING,
+ FLAG_NOTIFICATION_REGROUP_ON_CLASSIFICATION})
+ public void testRebundleNotification_restoresBundleChannel() throws Exception {
+ NotificationManagerService.WorkerHandler handler = mock(
+ NotificationManagerService.WorkerHandler.class);
+ mService.setHandler(handler);
+ when(mAssistants.isSameUser(any(), anyInt())).thenReturn(true);
+ when(mAssistants.isServiceTokenValidLocked(any())).thenReturn(true);
+ when(mAssistants.isAdjustmentKeyTypeAllowed(anyInt())).thenReturn(true);
+ when(mAssistants.isTypeAdjustmentAllowedForPackage(anyString(), anyInt())).thenReturn(true);
+
+ // Post a single notification
+ final boolean hasOriginalSummary = false;
+ final NotificationRecord r = generateNotificationRecord(mTestNotificationChannel);
+ final String keyToUnbundle = r.getKey();
+ mService.addNotification(r);
+
+ // Classify notification into the NEWS bundle
+ Bundle signals = new Bundle();
+ signals.putInt(Adjustment.KEY_TYPE, Adjustment.TYPE_NEWS);
+ Adjustment adjustment = new Adjustment(
+ r.getSbn().getPackageName(), r.getKey(), signals, "", r.getUser().getIdentifier());
+ mBinderService.applyAdjustmentFromAssistant(null, adjustment);
+ waitForIdle();
+ r.applyAdjustments();
+ // Check that the NotificationRecord channel is updated
+ assertThat(r.getChannel().getId()).isEqualTo(NEWS_ID);
+ assertThat(r.getBundleType()).isEqualTo(Adjustment.TYPE_NEWS);
+
+ // Unbundle the notification
+ mService.mNotificationDelegate.unbundleNotification(keyToUnbundle);
+
+ // Check that the original channel was restored
+ assertThat(r.getChannel().getId()).isEqualTo(TEST_CHANNEL_ID);
+ assertThat(r.getBundleType()).isEqualTo(Adjustment.TYPE_NEWS);
+ verify(mGroupHelper, times(1)).onNotificationUnbundled(eq(r), eq(hasOriginalSummary));
+
+ Mockito.reset(mRankingHandler);
+ Mockito.reset(mGroupHelper);
+
+ // Rebundle the notification
+ mService.mNotificationDelegate.rebundleNotification(keyToUnbundle);
+
+ // Actually apply the adjustments
+ doAnswer(invocationOnMock -> {
+ ((NotificationRecord) invocationOnMock.getArguments()[0]).applyAdjustments();
+ ((NotificationRecord) invocationOnMock.getArguments()[0]).calculateImportance();
+ return null;
+ }).when(mRankingHelper).extractSignals(any(NotificationRecord.class));
+ mService.handleRankingSort();
+ verify(handler, times(1)).scheduleSendRankingUpdate();
+
+ // Check that the bundle channel was restored
+ verify(mRankingHandler, times(1)).requestSort();
+ assertThat(r.getChannel().getId()).isEqualTo(NEWS_ID);
+ }
+
}
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
index 8e79514c875e..f41805d40b0d 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
@@ -66,7 +66,6 @@ import static com.android.internal.config.sysui.SystemUiSystemPropertiesFlags.No
import static com.android.internal.util.FrameworkStatsLog.PACKAGE_NOTIFICATION_PREFERENCES__FSI_STATE__DENIED;
import static com.android.internal.util.FrameworkStatsLog.PACKAGE_NOTIFICATION_PREFERENCES__FSI_STATE__GRANTED;
import static com.android.internal.util.FrameworkStatsLog.PACKAGE_NOTIFICATION_PREFERENCES__FSI_STATE__NOT_REQUESTED;
-import static com.android.server.notification.Flags.FLAG_NOTIFICATION_VERIFY_CHANNEL_SOUND_URI;
import static com.android.server.notification.Flags.FLAG_PERSIST_INCOMPLETE_RESTORE_DATA;
import static com.android.server.notification.NotificationChannelLogger.NotificationChannelEvent.NOTIFICATION_CHANNEL_UPDATED_BY_USER;
import static com.android.server.notification.PreferencesHelper.DEFAULT_BUBBLE_PREFERENCE;
@@ -164,6 +163,7 @@ import com.android.os.AtomsProto.PackageNotificationChannelPreferences;
import com.android.os.AtomsProto.PackageNotificationPreferences;
import com.android.server.UiServiceTestCase;
import com.android.server.notification.PermissionHelper.PackagePermission;
+import com.android.server.uri.UriGrantsManagerInternal;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
@@ -179,6 +179,9 @@ import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4;
+import platform.test.runner.parameterized.Parameters;
+
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.ByteArrayInputStream;
@@ -199,9 +202,6 @@ import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ThreadLocalRandom;
-import platform.test.runner.parameterized.ParameterizedAndroidJunit4;
-import platform.test.runner.parameterized.Parameters;
-
@SmallTest
@RunWith(ParameterizedAndroidJunit4.class)
@EnableFlags(FLAG_PERSIST_INCOMPLETE_RESTORE_DATA)
@@ -239,9 +239,10 @@ public class PreferencesHelperTest extends UiServiceTestCase {
private NotificationManager.Policy mTestNotificationPolicy;
- private PreferencesHelper mHelper;
- // fresh object for testing xml reading
- private PreferencesHelper mXmlHelper;
+ private TestPreferencesHelper mHelper;
+ // fresh object for testing xml reading; also TestPreferenceHelper in order to avoid interacting
+ // with real IpcDataCaches
+ private TestPreferencesHelper mXmlHelper;
private AudioAttributes mAudioAttributes;
private NotificationChannelLoggerFake mLogger = new NotificationChannelLoggerFake();
@@ -378,10 +379,10 @@ public class PreferencesHelperTest extends UiServiceTestCase {
when(mUserProfiles.getCurrentProfileIds()).thenReturn(currentProfileIds);
when(mClock.millis()).thenReturn(System.currentTimeMillis());
- mHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
+ mHelper = new TestPreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles,
mUgmInternal, false, mClock);
- mXmlHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
+ mXmlHelper = new TestPreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles,
mUgmInternal, false, mClock);
resetZenModeHelper();
@@ -793,7 +794,7 @@ public class PreferencesHelperTest extends UiServiceTestCase {
@Test
public void testReadXml_oldXml_migrates() throws Exception {
- mXmlHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
+ mXmlHelper = new TestPreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles,
mUgmInternal, /* showReviewPermissionsNotification= */ true, mClock);
@@ -929,7 +930,7 @@ public class PreferencesHelperTest extends UiServiceTestCase {
@Test
public void testReadXml_newXml_noMigration_showPermissionNotification() throws Exception {
- mXmlHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
+ mXmlHelper = new TestPreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles,
mUgmInternal, /* showReviewPermissionsNotification= */ true, mClock);
@@ -988,7 +989,7 @@ public class PreferencesHelperTest extends UiServiceTestCase {
@Test
public void testReadXml_newXml_permissionNotificationOff() throws Exception {
- mHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
+ mHelper = new TestPreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles,
mUgmInternal, /* showReviewPermissionsNotification= */ false, mClock);
@@ -1047,7 +1048,7 @@ public class PreferencesHelperTest extends UiServiceTestCase {
@Test
public void testReadXml_newXml_noMigration_noPermissionNotification() throws Exception {
- mHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
+ mHelper = new TestPreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles,
mUgmInternal, /* showReviewPermissionsNotification= */ true, mClock);
@@ -1641,7 +1642,7 @@ public class PreferencesHelperTest extends UiServiceTestCase {
serializer.flush();
// simulate load after reboot
- mXmlHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
+ mXmlHelper = new TestPreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles,
mUgmInternal, false, mClock);
loadByteArrayXml(baos.toByteArray(), false, USER_ALL);
@@ -1696,7 +1697,7 @@ public class PreferencesHelperTest extends UiServiceTestCase {
Duration.ofDays(2).toMillis() + System.currentTimeMillis());
// simulate load after reboot
- mXmlHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
+ mXmlHelper = new TestPreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles,
mUgmInternal, false, mClock);
loadByteArrayXml(xml.getBytes(), false, USER_ALL);
@@ -1774,10 +1775,10 @@ public class PreferencesHelperTest extends UiServiceTestCase {
when(contentResolver.getResourceId(ANDROID_RES_SOUND_URI)).thenReturn(resId).thenThrow(
new FileNotFoundException("")).thenReturn(resId);
- mHelper = new PreferencesHelper(mContext, mPm, mHandler, mMockZenModeHelper,
+ mHelper = new TestPreferencesHelper(mContext, mPm, mHandler, mMockZenModeHelper,
mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles,
mUgmInternal, false, mClock);
- mXmlHelper = new PreferencesHelper(mContext, mPm, mHandler, mMockZenModeHelper,
+ mXmlHelper = new TestPreferencesHelper(mContext, mPm, mHandler, mMockZenModeHelper,
mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles,
mUgmInternal, false, mClock);
@@ -3190,7 +3191,6 @@ public class PreferencesHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags(FLAG_NOTIFICATION_VERIFY_CHANNEL_SOUND_URI)
public void testCreateChannel_noSoundUriPermission_contentSchemeVerified() {
final Uri sound = Uri.parse(SCHEME_CONTENT + "://media/test/sound/uri");
@@ -3210,7 +3210,6 @@ public class PreferencesHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags(FLAG_NOTIFICATION_VERIFY_CHANNEL_SOUND_URI)
public void testCreateChannel_noSoundUriPermission_fileSchemaIgnored() {
final Uri sound = Uri.parse(SCHEME_FILE + "://path/sound");
@@ -3229,7 +3228,6 @@ public class PreferencesHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags(FLAG_NOTIFICATION_VERIFY_CHANNEL_SOUND_URI)
public void testCreateChannel_noSoundUriPermission_resourceSchemaIgnored() {
final Uri sound = Uri.parse(SCHEME_ANDROID_RESOURCE + "://resId/sound");
@@ -6573,4 +6571,223 @@ public class PreferencesHelperTest extends UiServiceTestCase {
mHelper.setCanBePromoted(PKG_P, UID_P, false, false);
assertThat(mHelper.canBePromoted(PKG_P, UID_P)).isTrue();
}
+
+ @Test
+ @EnableFlags(android.app.Flags.FLAG_NM_BINDER_PERF_CACHE_CHANNELS)
+ public void testInvalidateChannelCache_invalidateOnCreationAndChange() {
+ mHelper.resetCacheInvalidation();
+ NotificationChannel channel = new NotificationChannel("id", "name", IMPORTANCE_DEFAULT);
+ mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel, true, false, UID_N_MR1,
+ false);
+
+ // new channel should invalidate the cache.
+ assertThat(mHelper.hasCacheBeenInvalidated()).isTrue();
+
+ // when the channel data is updated, should invalidate the cache again after that.
+ mHelper.resetCacheInvalidation();
+ NotificationChannel newChannel = channel.copy();
+ newChannel.setName("new name");
+ newChannel.setImportance(IMPORTANCE_HIGH);
+ mHelper.updateNotificationChannel(PKG_N_MR1, UID_N_MR1, newChannel, true, UID_N_MR1, false);
+ assertThat(mHelper.hasCacheBeenInvalidated()).isTrue();
+
+ // also for conversations
+ mHelper.resetCacheInvalidation();
+ String parentId = "id";
+ String convId = "conversation";
+ NotificationChannel conv = new NotificationChannel(
+ String.format(CONVERSATION_CHANNEL_ID_FORMAT, parentId, convId), "conversation",
+ IMPORTANCE_DEFAULT);
+ conv.setConversationId(parentId, convId);
+ mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, conv, true, false, UID_N_MR1,
+ false);
+ assertThat(mHelper.hasCacheBeenInvalidated()).isTrue();
+
+ mHelper.resetCacheInvalidation();
+ NotificationChannel newConv = conv.copy();
+ newConv.setName("changed");
+ mHelper.updateNotificationChannel(PKG_N_MR1, UID_N_MR1, newConv, true, UID_N_MR1, false);
+ assertThat(mHelper.hasCacheBeenInvalidated()).isTrue();
+ }
+
+ @Test
+ @EnableFlags(android.app.Flags.FLAG_NM_BINDER_PERF_CACHE_CHANNELS)
+ public void testInvalidateChannelCache_invalidateOnDelete() {
+ NotificationChannel channel = new NotificationChannel("id", "name", IMPORTANCE_DEFAULT);
+ mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel, true, false, UID_N_MR1,
+ false);
+
+ // ignore any invalidations up until now
+ mHelper.resetCacheInvalidation();
+
+ mHelper.deleteNotificationChannel(PKG_N_MR1, UID_N_MR1, "id", UID_N_MR1, false);
+ assertThat(mHelper.hasCacheBeenInvalidated()).isTrue();
+
+ // recreate channel and now permanently delete
+ mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel, true, false, UID_N_MR1,
+ false);
+ mHelper.resetCacheInvalidation();
+ mHelper.permanentlyDeleteNotificationChannel(PKG_N_MR1, UID_N_MR1, "id");
+ assertThat(mHelper.hasCacheBeenInvalidated()).isTrue();
+ }
+
+ @Test
+ @EnableFlags(android.app.Flags.FLAG_NM_BINDER_PERF_CACHE_CHANNELS)
+ public void testInvalidateChannelCache_noInvalidationWhenNoChange() {
+ NotificationChannel channel = new NotificationChannel("id", "name", IMPORTANCE_DEFAULT);
+ mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel, true, false, UID_N_MR1,
+ false);
+
+ // ignore any invalidations up until now
+ mHelper.resetCacheInvalidation();
+
+ // newChannel, same as the old channel
+ NotificationChannel newChannel = channel.copy();
+ mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, newChannel, true, false, UID_N_MR1,
+ false);
+ mHelper.updateNotificationChannel(PKG_N_MR1, UID_N_MR1, newChannel, true, UID_N_MR1, false);
+
+ // because there were no effective changes, we should not see any cache invalidations
+ assertThat(mHelper.hasCacheBeenInvalidated()).isFalse();
+
+ // deletions of a nonexistent channel also don't change anything
+ mHelper.resetCacheInvalidation();
+ mHelper.deleteNotificationChannel(PKG_N_MR1, UID_N_MR1, "nonexistent", UID_N_MR1, false);
+ assertThat(mHelper.hasCacheBeenInvalidated()).isFalse();
+ }
+
+ @Test
+ @EnableFlags(android.app.Flags.FLAG_NM_BINDER_PERF_CACHE_CHANNELS)
+ public void testInvalidateCache_multipleUsersAndPackages() {
+ // Setup: create channels for:
+ // pkg O, user
+ // pkg O, work (same channel ID, different user)
+ // pkg N_MR1, user
+ // pkg N_MR1, user, conversation child of above
+ String p2u1ConvId = String.format(CONVERSATION_CHANNEL_ID_FORMAT, "p2", "conv");
+ NotificationChannel p1u1 = new NotificationChannel("p1", "p1u1", IMPORTANCE_DEFAULT);
+ NotificationChannel p1u2 = new NotificationChannel("p1", "p1u2", IMPORTANCE_DEFAULT);
+ NotificationChannel p2u1 = new NotificationChannel("p2", "p2u1", IMPORTANCE_DEFAULT);
+ NotificationChannel p2u1Conv = new NotificationChannel(p2u1ConvId, "p2u1 conv",
+ IMPORTANCE_DEFAULT);
+ p2u1Conv.setConversationId("p2", "conv");
+
+ mHelper.createNotificationChannel(PKG_O, UID_O, p1u1, true,
+ false, UID_O, false);
+ mHelper.createNotificationChannel(PKG_O, UID_O + UserHandle.PER_USER_RANGE, p1u2, true,
+ false, UID_O + UserHandle.PER_USER_RANGE, false);
+ mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, p2u1, true,
+ false, UID_N_MR1, false);
+ mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, p2u1Conv, true,
+ false, UID_N_MR1, false);
+ mHelper.resetCacheInvalidation();
+
+ // Update to an existent channel, with a change: should invalidate
+ NotificationChannel p1u1New = p1u1.copy();
+ p1u1New.setName("p1u1 new");
+ mHelper.updateNotificationChannel(PKG_O, UID_O, p1u1New, true, UID_O, false);
+ assertThat(mHelper.hasCacheBeenInvalidated()).isTrue();
+
+ // Do it again, but no change for this user
+ mHelper.resetCacheInvalidation();
+ mHelper.updateNotificationChannel(PKG_O, UID_O, p1u1New.copy(), true, UID_O, false);
+ assertThat(mHelper.hasCacheBeenInvalidated()).isFalse();
+
+ // Delete conversations, but for a package without those conversations
+ mHelper.resetCacheInvalidation();
+ mHelper.deleteConversations(PKG_O, UID_O, Set.of(p2u1Conv.getConversationId()), UID_O,
+ false);
+ assertThat(mHelper.hasCacheBeenInvalidated()).isFalse();
+
+ // Now delete conversations for the right package
+ mHelper.resetCacheInvalidation();
+ mHelper.deleteConversations(PKG_N_MR1, UID_N_MR1, Set.of(p2u1Conv.getConversationId()),
+ UID_N_MR1, false);
+ assertThat(mHelper.hasCacheBeenInvalidated()).isTrue();
+ }
+
+ @Test
+ @EnableFlags(android.app.Flags.FLAG_NM_BINDER_PERF_CACHE_CHANNELS)
+ public void testInvalidateCache_userRemoved() throws Exception {
+ NotificationChannel c1 = new NotificationChannel("id1", "name1", IMPORTANCE_DEFAULT);
+ int uid1 = UserHandle.getUid(1, 1);
+ setUpPackageWithUid("pkg1", uid1);
+ mHelper.createNotificationChannel("pkg1", uid1, c1, true, false, uid1, false);
+ mHelper.resetCacheInvalidation();
+
+ // delete user 1; should invalidate cache
+ mHelper.onUserRemoved(1);
+ assertThat(mHelper.hasCacheBeenInvalidated()).isTrue();
+ }
+
+ @Test
+ @EnableFlags(android.app.Flags.FLAG_NM_BINDER_PERF_CACHE_CHANNELS)
+ public void testInvalidateCache_packagesChanged() {
+ NotificationChannel channel1 =
+ new NotificationChannel("id1", "name1", NotificationManager.IMPORTANCE_HIGH);
+ mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel1, true, false,
+ UID_N_MR1, false);
+
+ // package deleted: expect cache invalidation
+ mHelper.resetCacheInvalidation();
+ mHelper.onPackagesChanged(true, USER_SYSTEM, new String[]{PKG_N_MR1},
+ new int[]{UID_N_MR1});
+ assertThat(mHelper.hasCacheBeenInvalidated()).isTrue();
+
+ // re-created: expect cache invalidation again
+ mHelper.resetCacheInvalidation();
+ mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel1, true, false,
+ UID_N_MR1, false);
+ mHelper.onPackagesChanged(false, USER_SYSTEM, new String[]{PKG_N_MR1},
+ new int[]{UID_N_MR1});
+ assertThat(mHelper.hasCacheBeenInvalidated()).isTrue();
+ }
+
+ @Test
+ @DisableFlags(android.app.Flags.FLAG_NM_BINDER_PERF_CACHE_CHANNELS)
+ public void testInvalidateCache_flagOff_neverTouchesCache() {
+ // Do a bunch of channel-changing operations.
+ NotificationChannel channel =
+ new NotificationChannel("id", "name1", NotificationManager.IMPORTANCE_HIGH);
+ mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel, true, false,
+ UID_N_MR1, false);
+
+ NotificationChannel copy = channel.copy();
+ copy.setName("name2");
+ mHelper.updateNotificationChannel(PKG_N_MR1, UID_N_MR1, copy, true, UID_N_MR1, false);
+ mHelper.deleteNotificationChannel(PKG_N_MR1, UID_N_MR1, "id", UID_N_MR1, false);
+
+ assertThat(mHelper.hasCacheBeenInvalidated()).isFalse();
+ }
+
+ // Test version of PreferencesHelper whose only functional difference is that it does not
+ // interact with the real IpcDataCache, and instead tracks whether or not the cache has been
+ // invalidated since creation or the last reset.
+ private static class TestPreferencesHelper extends PreferencesHelper {
+ private boolean mCacheInvalidated = false;
+
+ TestPreferencesHelper(Context context, PackageManager pm, RankingHandler rankingHandler,
+ ZenModeHelper zenHelper, PermissionHelper permHelper, PermissionManager permManager,
+ NotificationChannelLogger notificationChannelLogger,
+ AppOpsManager appOpsManager, ManagedServices.UserProfiles userProfiles,
+ UriGrantsManagerInternal ugmInternal,
+ boolean showReviewPermissionsNotification, Clock clock) {
+ super(context, pm, rankingHandler, zenHelper, permHelper, permManager,
+ notificationChannelLogger, appOpsManager, userProfiles, ugmInternal,
+ showReviewPermissionsNotification, clock);
+ }
+
+ @Override
+ protected void invalidateNotificationChannelCache() {
+ mCacheInvalidated = true;
+ }
+
+ boolean hasCacheBeenInvalidated() {
+ return mCacheInvalidated;
+ }
+
+ void resetCacheInvalidation() {
+ mCacheInvalidated = false;
+ }
+ }
}
diff --git a/services/tests/vibrator/src/com/android/server/vibrator/BasicToPwleSegmentAdapterTest.java b/services/tests/vibrator/src/com/android/server/vibrator/BasicToPwleSegmentAdapterTest.java
new file mode 100644
index 000000000000..09f573cd1ee0
--- /dev/null
+++ b/services/tests/vibrator/src/com/android/server/vibrator/BasicToPwleSegmentAdapterTest.java
@@ -0,0 +1,158 @@
+/*
+ * 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.vibrator;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.hardware.vibrator.IVibrator;
+import android.os.VibratorInfo;
+import android.os.vibrator.BasicPwleSegment;
+import android.os.vibrator.Flags;
+import android.os.vibrator.PwleSegment;
+import android.os.vibrator.StepSegment;
+import android.os.vibrator.VibrationEffectSegment;
+import android.platform.test.annotations.DisableFlags;
+import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.SetFlagsRule;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.IntStream;
+
+public class BasicToPwleSegmentAdapterTest {
+
+ private static final float TEST_RESONANT_FREQUENCY = 150;
+ private static final float[] TEST_FREQUENCIES =
+ new float[]{90f, 120f, 150f, 60f, 30f, 210f, 270f, 300f, 240f, 180f};
+ private static final float[] TEST_OUTPUT_ACCELERATIONS =
+ new float[]{1.2f, 1.8f, 2.4f, 0.6f, 0.1f, 2.2f, 1.0f, 0.5f, 1.9f, 3.0f};
+
+ private static final VibratorInfo.FrequencyProfile TEST_FREQUENCY_PROFILE =
+ new VibratorInfo.FrequencyProfile(TEST_RESONANT_FREQUENCY, TEST_FREQUENCIES,
+ TEST_OUTPUT_ACCELERATIONS);
+
+ private static final VibratorInfo.FrequencyProfile EMPTY_FREQUENCY_PROFILE =
+ new VibratorInfo.FrequencyProfile(TEST_RESONANT_FREQUENCY, null, null);
+
+ private BasicToPwleSegmentAdapter mAdapter;
+
+ @Rule
+ public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
+ @Before
+ public void setUp() throws Exception {
+ mAdapter = new BasicToPwleSegmentAdapter();
+ }
+
+ @Test
+ @DisableFlags(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
+ public void testBasicPwleSegments_withFeatureFlagDisabled_returnsOriginalSegments() {
+ List<VibrationEffectSegment> segments = new ArrayList<>(Arrays.asList(
+ // startIntensity, endIntensity, startSharpness, endSharpness, duration
+ new BasicPwleSegment(0.2f, 0.8f, 0.2f, 0.4f, 20),
+ new BasicPwleSegment(0.8f, 0.2f, 0.4f, 0.5f, 100),
+ new BasicPwleSegment(0.2f, 0.65f, 0.5f, 0.5f, 50)));
+ List<VibrationEffectSegment> originalSegments = new ArrayList<>(segments);
+
+ VibratorInfo vibratorInfo = createVibratorInfo(
+ TEST_FREQUENCY_PROFILE, IVibrator.CAP_COMPOSE_PWLE_EFFECTS_V2);
+
+ assertThat(mAdapter.adaptToVibrator(vibratorInfo, segments, /*repeatIndex= */ -1))
+ .isEqualTo(-1);
+ assertThat(mAdapter.adaptToVibrator(vibratorInfo, segments, /*repeatIndex= */ 1))
+ .isEqualTo(1);
+
+ assertThat(segments).isEqualTo(originalSegments);
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
+ public void testBasicPwleSegments_noPwleCapability_returnsOriginalSegments() {
+ List<VibrationEffectSegment> segments = new ArrayList<>(Arrays.asList(
+ // startIntensity, endIntensity, startSharpness, endSharpness, duration
+ new BasicPwleSegment(0.2f, 0.8f, 0.2f, 0.4f, 20),
+ new BasicPwleSegment(0.8f, 0.2f, 0.4f, 0.5f, 100),
+ new BasicPwleSegment(0.2f, 0.65f, 0.5f, 0.5f, 50)));
+ List<VibrationEffectSegment> originalSegments = new ArrayList<>(segments);
+
+ VibratorInfo vibratorInfo = createVibratorInfo(TEST_FREQUENCY_PROFILE);
+
+ assertThat(mAdapter.adaptToVibrator(vibratorInfo, segments, /*repeatIndex= */ -1))
+ .isEqualTo(-1);
+ assertThat(mAdapter.adaptToVibrator(vibratorInfo, segments, /*repeatIndex= */ 1))
+ .isEqualTo(1);
+
+ assertThat(segments).isEqualTo(originalSegments);
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
+ public void testBasicPwleSegments_invalidFrequencyProfile_returnsOriginalSegments() {
+ List<VibrationEffectSegment> segments = new ArrayList<>(Arrays.asList(
+ // startIntensity, endIntensity, startSharpness, endSharpness, duration
+ new BasicPwleSegment(0.2f, 0.8f, 0.2f, 0.4f, 20),
+ new BasicPwleSegment(0.8f, 0.2f, 0.4f, 0.5f, 100),
+ new BasicPwleSegment(0.2f, 0.65f, 0.5f, 0.5f, 50)));
+ List<VibrationEffectSegment> originalSegments = new ArrayList<>(segments);
+ VibratorInfo vibratorInfo = createVibratorInfo(
+ EMPTY_FREQUENCY_PROFILE, IVibrator.CAP_COMPOSE_PWLE_EFFECTS_V2);
+
+ assertThat(mAdapter.adaptToVibrator(vibratorInfo, segments, /*repeatIndex= */ -1))
+ .isEqualTo(-1);
+ assertThat(mAdapter.adaptToVibrator(vibratorInfo, segments, /*repeatIndex= */ 1))
+ .isEqualTo(1);
+
+ assertThat(segments).isEqualTo(originalSegments);
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
+ public void testBasicPwleSegments_withPwleCapability_adaptSegmentsCorrectly() {
+ List<VibrationEffectSegment> segments = new ArrayList<>(Arrays.asList(
+ new StepSegment(/* amplitude= */ 1, /* frequencyHz= */ 40f, /* duration= */ 100),
+ // startIntensity, endIntensity, startSharpness, endSharpness, duration
+ new BasicPwleSegment(0.0f, 1.0f, 0.0f, 1.0f, 100),
+ new BasicPwleSegment(0.0f, 1.0f, 0.0f, 1.0f, 100),
+ new BasicPwleSegment(0.0f, 1.0f, 0.0f, 1.0f, 100)));
+ List<VibrationEffectSegment> expectedSegments = Arrays.asList(
+ new StepSegment(/* amplitude= */ 1, /* frequencyHz= */ 40f, /* duration= */ 100),
+ // startAmplitude, endAmplitude, startFrequencyHz, endFrequencyHz, duration
+ new PwleSegment(0.0f, 1.0f, 30.0f, 300.0f, 100),
+ new PwleSegment(0.0f, 1.0f, 30.0f, 300.0f, 100),
+ new PwleSegment(0.0f, 1.0f, 30.0f, 300.0f, 100));
+ VibratorInfo vibratorInfo = createVibratorInfo(
+ TEST_FREQUENCY_PROFILE, IVibrator.CAP_COMPOSE_PWLE_EFFECTS_V2);
+
+ assertThat(mAdapter.adaptToVibrator(vibratorInfo, segments, /*repeatIndex= */ 1))
+ .isEqualTo(1);
+
+ assertThat(segments).isEqualTo(expectedSegments);
+ }
+
+ private static VibratorInfo createVibratorInfo(VibratorInfo.FrequencyProfile frequencyProfile,
+ int... capabilities) {
+ return new VibratorInfo.Builder(0)
+ .setCapabilities(IntStream.of(capabilities).reduce((a, b) -> a | b).orElse(0))
+ .setFrequencyProfile(frequencyProfile)
+ .build();
+ }
+}
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatOrientationOverridesTest.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatOrientationOverridesTest.java
index 9d191cea8acb..a0727a7af87b 100644
--- a/services/tests/wmtests/src/com/android/server/wm/AppCompatOrientationOverridesTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatOrientationOverridesTest.java
@@ -335,7 +335,7 @@ public class AppCompatOrientationOverridesTest extends WindowTestsBase {
}
private AppCompatOrientationOverrides getTopOrientationOverrides() {
- return activity().top().mAppCompatController.getAppCompatOrientationOverrides();
+ return activity().top().mAppCompatController.getOrientationOverrides();
}
}
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatOrientationPolicyTest.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatOrientationPolicyTest.java
index a21ab5de5de2..4faa71451a4d 100644
--- a/services/tests/wmtests/src/com/android/server/wm/AppCompatOrientationPolicyTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatOrientationPolicyTest.java
@@ -601,7 +601,7 @@ public class AppCompatOrientationPolicyTest extends WindowTestsBase {
}
private AppCompatOrientationOverrides getTopOrientationOverrides() {
- return activity().top().mAppCompatController.getAppCompatOrientationOverrides();
+ return activity().top().mAppCompatController.getOrientationOverrides();
}
private AppCompatOrientationPolicy getTopAppCompatOrientationPolicy() {
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
index 5486aa34b5fa..dfd10ec86a20 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
@@ -1183,6 +1183,18 @@ public class DisplayContentTests extends WindowTestsBase {
assertEquals(prev, mDisplayContent.getLastOrientationSource());
// The top will use the rotation from "prev" with fixed rotation.
assertTrue(top.hasFixedRotationTransform());
+
+ mDisplayContent.continueUpdateOrientationForDiffOrienLaunchingApp();
+ assertFalse(top.hasFixedRotationTransform());
+
+ // Assume that the requested orientation of "prev" is landscape. And the display is also
+ // rotated to landscape. The activities from bottom to top are TaskB{"prev, "behindTop"},
+ // TaskB{"top"}. Then "behindTop" should also get landscape according to ORIENTATION_BEHIND
+ // instead of resolving as undefined which causes to unexpected fixed portrait rotation.
+ final ActivityRecord behindTop = new ActivityBuilder(mAtm).setTask(prev.getTask())
+ .setOnTop(false).setScreenOrientation(SCREEN_ORIENTATION_BEHIND).build();
+ mDisplayContent.applyFixedRotationForNonTopVisibleActivityIfNeeded(behindTop);
+ assertFalse(behindTop.hasFixedRotationTransform());
}
@Test
diff --git a/services/tests/wmtests/src/com/android/server/wm/DragDropControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/DragDropControllerTests.java
index de4b6fac7abf..dc16de1aab5e 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DragDropControllerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DragDropControllerTests.java
@@ -34,6 +34,7 @@ import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.when;
import static com.android.server.wm.DragDropController.MSG_UNHANDLED_DROP_LISTENER_TIMEOUT;
+import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
@@ -42,16 +43,19 @@ import static org.junit.Assert.fail;
import static org.junit.Assume.assumeTrue;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
+import android.annotation.Nullable;
import android.app.PendingIntent;
import android.content.ClipData;
import android.content.ClipDescription;
import android.content.Intent;
import android.content.pm.ShortcutServiceInternal;
import android.graphics.PixelFormat;
+import android.graphics.Rect;
import android.os.Binder;
import android.os.Handler;
import android.os.IBinder;
@@ -60,6 +64,7 @@ import android.os.Message;
import android.os.Parcelable;
import android.os.RemoteException;
import android.os.UserHandle;
+import android.platform.test.annotations.EnableFlags;
import android.platform.test.annotations.Presubmit;
import android.view.DragEvent;
import android.view.InputChannel;
@@ -74,6 +79,7 @@ import androidx.test.filters.SmallTest;
import com.android.server.LocalServices;
import com.android.server.pm.UserManagerInternal;
+import com.android.window.flags.Flags;
import org.junit.After;
import org.junit.AfterClass;
@@ -87,6 +93,7 @@ import org.mockito.Mockito;
import java.util.ArrayList;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
+import java.util.function.Consumer;
/**
* Tests for the {@link DragDropController} class.
@@ -141,17 +148,28 @@ public class DragDropControllerTests extends WindowTestsBase {
}
}
+ private WindowState createDropTargetWindow(String name) {
+ return createDropTargetWindow(name, null /* targetDisplay */);
+ }
+
/**
* Creates a window state which can be used as a drop target.
*/
- private WindowState createDropTargetWindow(String name, int ownerId) {
- final Task task = new TaskBuilder(mSupervisor).setUserId(ownerId).build();
- final ActivityRecord activity = new ActivityBuilder(mAtm).setTask(task).setUseProcess(
- mProcess).build();
+ private WindowState createDropTargetWindow(String name,
+ @Nullable DisplayContent targetDisplay) {
+ final WindowState window;
+ if (targetDisplay == null) {
+ final Task task = new TaskBuilder(mSupervisor).build();
+ final ActivityRecord activity = new ActivityBuilder(mAtm).setTask(task).setUseProcess(
+ mProcess).build();
+ window = newWindowBuilder(name, TYPE_BASE_APPLICATION).setWindowToken(
+ activity).setClientWindow(new TestIWindow()).build();
+ } else {
+ window = newWindowBuilder(name, TYPE_BASE_APPLICATION).setDisplay(
+ targetDisplay).setClientWindow(new TestIWindow()).build();
+ }
// Use a new TestIWindow so we don't collect events for other windows
- final WindowState window = newWindowBuilder(name, TYPE_BASE_APPLICATION).setWindowToken(
- activity).setOwnerId(ownerId).setClientWindow(new TestIWindow()).build();
InputChannel channel = new InputChannel();
window.openInputChannel(channel);
window.mHasSurface = true;
@@ -174,7 +192,7 @@ public class DragDropControllerTests extends WindowTestsBase {
public void setUp() throws Exception {
mTarget = new TestDragDropController(mWm, mWm.mH.getLooper());
mProcess = mSystemServicesTestRule.addProcess(TEST_PACKAGE, "testProc", TEST_PID, TEST_UID);
- mWindow = createDropTargetWindow("Drag test window", 0);
+ mWindow = createDropTargetWindow("Drag test window");
doReturn(mWindow).when(mDisplayContent).getTouchableWinAtPointLocked(0, 0);
when(mWm.mInputManager.startDragAndDrop(any(IBinder.class), any(IBinder.class))).thenReturn(
true);
@@ -239,7 +257,7 @@ public class DragDropControllerTests extends WindowTestsBase {
iwindow.setDragEventJournal(dragEvents);
startDrag(View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_GLOBAL_URI_READ,
- ClipData.newPlainText("label", "text"), () -> {
+ ClipData.newPlainText("label", "text"), (unused) -> {
// Verify the start-drag event is sent for invisible windows
final DragEvent dragEvent = dragEvents.get(0);
assertTrue(dragEvent.getAction() == ACTION_DRAG_STARTED);
@@ -263,8 +281,8 @@ public class DragDropControllerTests extends WindowTestsBase {
@Test
public void testPrivateInterceptGlobalDragDropIgnoresNonLocalWindows() {
- WindowState nonLocalWindow = createDropTargetWindow("App drag test window", 0);
- WindowState globalInterceptWindow = createDropTargetWindow("Global drag test window", 0);
+ WindowState nonLocalWindow = createDropTargetWindow("App drag test window");
+ WindowState globalInterceptWindow = createDropTargetWindow("Global drag test window");
globalInterceptWindow.mAttrs.privateFlags |= PRIVATE_FLAG_INTERCEPT_GLOBAL_DRAG_AND_DROP;
// Necessary for now since DragState.sendDragStartedLocked() will recycle drag events
@@ -281,7 +299,7 @@ public class DragDropControllerTests extends WindowTestsBase {
globalInterceptIWindow.setDragEventJournal(globalInterceptWindowDragEvents);
startDrag(View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_GLOBAL_URI_READ,
- createClipDataForActivity(null, mock(UserHandle.class)), () -> {
+ createClipDataForActivity(null, mock(UserHandle.class)), (unused) -> {
// Verify the start-drag event is sent for the local and global intercept window
// but not the other window
assertTrue(nonLocalWindowDragEvents.isEmpty());
@@ -324,7 +342,7 @@ public class DragDropControllerTests extends WindowTestsBase {
iwindow.setDragEventJournal(dragEvents);
startDrag(View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_START_INTENT_SENDER_ON_UNHANDLED_DRAG,
- ClipData.newPlainText("label", "text"), () -> {
+ ClipData.newPlainText("label", "text"), (unused) -> {
// Verify the start-drag event has the drag flags
final DragEvent dragEvent = dragEvents.get(0);
assertTrue(dragEvent.getAction() == ACTION_DRAG_STARTED);
@@ -347,6 +365,184 @@ public class DragDropControllerTests extends WindowTestsBase {
});
}
+ @Test
+ public void testDragEventCoordinates() {
+ int dragStartX = mWindow.getBounds().centerX();
+ int dragStartY = mWindow.getBounds().centerY();
+ int startOffsetPx = 10;
+ int dropCoordsPx = 15;
+ WindowState window2 = createDropTargetWindow("App drag test window");
+ Rect bounds = new Rect(dragStartX + startOffsetPx, dragStartY + startOffsetPx,
+ mWindow.getBounds().right, mWindow.getBounds().bottom);
+ window2.setBounds(bounds);
+ window2.getFrame().set(bounds);
+
+ // Necessary for now since DragState.sendDragStartedLocked() will recycle drag events
+ // immediately after dispatching, which is a problem when using mockito arguments captor
+ // because it returns and modifies the same drag event.
+ TestIWindow iwindow = (TestIWindow) mWindow.mClient;
+ final ArrayList<DragEvent> dragEvents = new ArrayList<>();
+ iwindow.setDragEventJournal(dragEvents);
+ TestIWindow iwindow2 = (TestIWindow) window2.mClient;
+ final ArrayList<DragEvent> dragEvents2 = new ArrayList<>();
+ iwindow2.setDragEventJournal(dragEvents2);
+
+ startDrag(dragStartX, dragStartY, View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_GLOBAL_URI_READ,
+ ClipData.newPlainText("label", "text"), (unused) -> {
+ // Verify the start-drag event is sent as-is for the drag origin window.
+ final DragEvent dragEvent = dragEvents.get(0);
+ assertEquals(ACTION_DRAG_STARTED, dragEvent.getAction());
+ assertEquals(dragStartX, dragEvent.getX(), 0.0 /* delta */);
+ assertEquals(dragStartY, dragEvent.getY(), 0.0 /* delta */);
+ // Verify the start-drag event is sent relative to the window top-left.
+ final DragEvent dragEvent2 = dragEvents2.get(0);
+ assertEquals(ACTION_DRAG_STARTED, dragEvent2.getAction());
+ assertEquals(-startOffsetPx, dragEvent2.getX(), 0.0 /* delta */);
+ assertEquals(-startOffsetPx, dragEvent2.getY(), 0.0 /* delta */);
+
+ try {
+ mTarget.mDeferDragStateClosed = true;
+ // x, y is window-local coordinate.
+ mTarget.reportDropWindow(window2.mInputChannelToken, dropCoordsPx,
+ dropCoordsPx);
+ mTarget.handleMotionEvent(false, window2.getDisplayId(), dropCoordsPx,
+ dropCoordsPx);
+ mToken = window2.mClient.asBinder();
+ // Verify only window2 received the DROP event and coords are sent as-is.
+ assertEquals(1, dragEvents.size());
+ assertEquals(2, dragEvents2.size());
+ final DragEvent dropEvent = last(dragEvents2);
+ assertEquals(ACTION_DROP, dropEvent.getAction());
+ assertEquals(dropCoordsPx, dropEvent.getX(), 0.0 /* delta */);
+ assertEquals(dropCoordsPx, dropEvent.getY(), 0.0 /* delta */);
+ assertEquals(window2.getDisplayId(), dropEvent.getDisplayId());
+
+ mTarget.reportDropResult(iwindow2, true);
+ // Verify both windows received ACTION_DRAG_ENDED event.
+ assertEquals(ACTION_DRAG_ENDED, last(dragEvents).getAction());
+ assertEquals(window2.getDisplayId(), last(dragEvents).getDisplayId());
+ assertEquals(ACTION_DRAG_ENDED, last(dragEvents2).getAction());
+ assertEquals(window2.getDisplayId(), last(dragEvents2).getDisplayId());
+ } finally {
+ mTarget.mDeferDragStateClosed = false;
+ }
+ });
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_CONNECTED_DISPLAYS_DND)
+ public void testDragEventConnectedDisplaysCoordinates() {
+ final DisplayContent testDisplay = createMockSimulatedDisplay();
+ int dragStartX = mWindow.getBounds().centerX();
+ int dragStartY = mWindow.getBounds().centerY();
+ int dropCoordsPx = 15;
+ WindowState window2 = createDropTargetWindow("App drag test window", testDisplay);
+
+ // Necessary for now since DragState.sendDragStartedLocked() will recycle drag events
+ // immediately after dispatching, which is a problem when using mockito arguments captor
+ // because it returns and modifies the same drag event.
+ TestIWindow iwindow = (TestIWindow) mWindow.mClient;
+ final ArrayList<DragEvent> dragEvents = new ArrayList<>();
+ iwindow.setDragEventJournal(dragEvents);
+ TestIWindow iwindow2 = (TestIWindow) window2.mClient;
+ final ArrayList<DragEvent> dragEvents2 = new ArrayList<>();
+ iwindow2.setDragEventJournal(dragEvents2);
+
+ startDrag(dragStartX, dragStartY, View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_GLOBAL_URI_READ,
+ ClipData.newPlainText("label", "text"), (unused) -> {
+ // Verify the start-drag event is sent as-is for the drag origin window.
+ final DragEvent dragEvent = dragEvents.get(0);
+ assertEquals(ACTION_DRAG_STARTED, dragEvent.getAction());
+ assertEquals(dragStartX, dragEvent.getX(), 0.0 /* delta */);
+ assertEquals(dragStartY, dragEvent.getY(), 0.0 /* delta */);
+ // Verify the start-drag event from different display is sent out of display
+ // bounds.
+ final DragEvent dragEvent2 = dragEvents2.get(0);
+ assertEquals(ACTION_DRAG_STARTED, dragEvent2.getAction());
+ assertEquals(-window2.getBounds().left - 1, dragEvent2.getX(), 0.0 /* delta */);
+ assertEquals(-window2.getBounds().top - 1, dragEvent2.getY(), 0.0 /* delta */);
+
+ try {
+ mTarget.mDeferDragStateClosed = true;
+ mTarget.handleMotionEvent(true, testDisplay.getDisplayId(), dropCoordsPx,
+ dropCoordsPx);
+ // x, y is window-local coordinate.
+ mTarget.reportDropWindow(window2.mInputChannelToken, dropCoordsPx,
+ dropCoordsPx);
+ mTarget.handleMotionEvent(false, testDisplay.getDisplayId(), dropCoordsPx,
+ dropCoordsPx);
+ mToken = window2.mClient.asBinder();
+ // Verify only window2 received the DROP event and coords are sent as-is
+ assertEquals(1, dragEvents.size());
+ assertEquals(2, dragEvents2.size());
+ final DragEvent dropEvent = last(dragEvents2);
+ assertEquals(ACTION_DROP, dropEvent.getAction());
+ assertEquals(dropCoordsPx, dropEvent.getX(), 0.0 /* delta */);
+ assertEquals(dropCoordsPx, dropEvent.getY(), 0.0 /* delta */);
+ assertEquals(testDisplay.getDisplayId(), dropEvent.getDisplayId());
+
+ mTarget.reportDropResult(iwindow2, true);
+ // Verify both windows received ACTION_DRAG_ENDED event.
+ assertEquals(ACTION_DRAG_ENDED, last(dragEvents).getAction());
+ assertEquals(testDisplay.getDisplayId(), last(dragEvents).getDisplayId());
+ assertEquals(ACTION_DRAG_ENDED, last(dragEvents2).getAction());
+ assertEquals(testDisplay.getDisplayId(), last(dragEvents2).getDisplayId());
+ } finally {
+ mTarget.mDeferDragStateClosed = false;
+ }
+ });
+ }
+
+ @Test
+ public void testDragMove() {
+ startDrag(0, 0, View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_GLOBAL_URI_READ,
+ ClipData.newPlainText("label", "text"), (surface) -> {
+ int dragMoveX = mWindow.getBounds().centerX();
+ int dragMoveY = mWindow.getBounds().centerY();
+ final SurfaceControl.Transaction transaction =
+ mSystemServicesTestRule.mTransaction;
+ clearInvocations(transaction);
+
+ mTarget.handleMotionEvent(true, mWindow.getDisplayId(), dragMoveX, dragMoveY);
+ verify(transaction).setPosition(surface, dragMoveX, dragMoveY);
+
+ // Clean-up.
+ mTarget.reportDropWindow(mWindow.mInputChannelToken, 0, 0);
+ mTarget.handleMotionEvent(false /* keepHandling */, mWindow.getDisplayId(), 0,
+ 0);
+ mToken = mWindow.mClient.asBinder();
+ });
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_CONNECTED_DISPLAYS_DND)
+ public void testConnectedDisplaysDragMoveToOtherDisplay() {
+ final float testDensityMultiplier = 1.5f;
+ final DisplayContent testDisplay = createMockSimulatedDisplay();
+ testDisplay.mBaseDisplayDensity =
+ (int) (mDisplayContent.mBaseDisplayDensity * testDensityMultiplier);
+ WindowState testWindow = createDropTargetWindow("App drag test window", testDisplay);
+
+ // Test starts from mWindow which is on default display.
+ startDrag(0, 0, View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_GLOBAL_URI_READ,
+ ClipData.newPlainText("label", "text"), (surface) -> {
+ final SurfaceControl.Transaction transaction =
+ mSystemServicesTestRule.mTransaction;
+ clearInvocations(transaction);
+ mTarget.handleMotionEvent(true, testWindow.getDisplayId(), 0, 0);
+
+ verify(transaction).reparent(surface, testDisplay.getSurfaceControl());
+ verify(transaction).setScale(surface, testDensityMultiplier,
+ testDensityMultiplier);
+
+ // Clean-up.
+ mTarget.reportDropWindow(mWindow.mInputChannelToken, 0, 0);
+ mTarget.handleMotionEvent(false /* keepHandling */, mWindow.getDisplayId(), 0,
+ 0);
+ mToken = mWindow.mClient.asBinder();
+ });
+ }
+
private DragEvent last(ArrayList<DragEvent> list) {
return list.get(list.size() - 1);
}
@@ -503,7 +699,7 @@ public class DragDropControllerTests extends WindowTestsBase {
@Test
public void testRequestSurfaceForReturnAnimationFlag_dropSuccessful() {
- WindowState otherWindow = createDropTargetWindow("App drag test window", 0);
+ WindowState otherWindow = createDropTargetWindow("App drag test window");
TestIWindow otherIWindow = (TestIWindow) otherWindow.mClient;
// Necessary for now since DragState.sendDragStartedLocked() will recycle drag events
@@ -515,7 +711,7 @@ public class DragDropControllerTests extends WindowTestsBase {
startDrag(View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_GLOBAL_URI_READ
| View.DRAG_FLAG_REQUEST_SURFACE_FOR_RETURN_ANIMATION,
- ClipData.newPlainText("label", "text"), () -> {
+ ClipData.newPlainText("label", "text"), (unused) -> {
assertTrue(dragEvents.get(0).getAction() == ACTION_DRAG_STARTED);
// Verify after consuming that the drag surface is relinquished
@@ -534,7 +730,7 @@ public class DragDropControllerTests extends WindowTestsBase {
@Test
public void testRequestSurfaceForReturnAnimationFlag_dropUnsuccessful() {
- WindowState otherWindow = createDropTargetWindow("App drag test window", 0);
+ WindowState otherWindow = createDropTargetWindow("App drag test window");
TestIWindow otherIWindow = (TestIWindow) otherWindow.mClient;
// Necessary for now since DragState.sendDragStartedLocked() will recycle drag events
@@ -546,7 +742,7 @@ public class DragDropControllerTests extends WindowTestsBase {
startDrag(View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_GLOBAL_URI_READ
| View.DRAG_FLAG_REQUEST_SURFACE_FOR_RETURN_ANIMATION,
- ClipData.newPlainText("label", "text"), () -> {
+ ClipData.newPlainText("label", "text"), (unused) -> {
assertTrue(dragEvents.get(0).getAction() == ACTION_DRAG_STARTED);
// Verify after consuming that the drag surface is relinquished
@@ -583,7 +779,7 @@ public class DragDropControllerTests extends WindowTestsBase {
mTarget.setGlobalDragListener(listener);
final int invalidXY = 100_000;
startDrag(View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_START_INTENT_SENDER_ON_UNHANDLED_DRAG,
- ClipData.newPlainText("label", "Test"), () -> {
+ ClipData.newPlainText("label", "Test"), (unused) -> {
// Trigger an unhandled drop and verify the global drag listener was called
mTarget.reportDropWindow(mWindow.mInputChannelToken, invalidXY, invalidXY);
mTarget.handleMotionEvent(false /* keepHandling */, mWindow.getDisplayId(),
@@ -608,7 +804,7 @@ public class DragDropControllerTests extends WindowTestsBase {
mTarget.setGlobalDragListener(listener);
final int invalidXY = 100_000;
startDrag(View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_START_INTENT_SENDER_ON_UNHANDLED_DRAG,
- ClipData.newPlainText("label", "Test"), () -> {
+ ClipData.newPlainText("label", "Test"), (unused) -> {
// Trigger an unhandled drop and verify the global drag listener was called
mTarget.reportDropWindow(mock(IBinder.class), invalidXY, invalidXY);
mTarget.handleMotionEvent(false /* keepHandling */, mWindow.getDisplayId(),
@@ -631,7 +827,7 @@ public class DragDropControllerTests extends WindowTestsBase {
doReturn(mock(Binder.class)).when(listener).asBinder();
mTarget.setGlobalDragListener(listener);
final int invalidXY = 100_000;
- startDrag(View.DRAG_FLAG_GLOBAL, ClipData.newPlainText("label", "Test"), () -> {
+ startDrag(View.DRAG_FLAG_GLOBAL, ClipData.newPlainText("label", "Test"), (unused) -> {
// Trigger an unhandled drop and verify the global drag listener was not called
mTarget.reportDropWindow(mock(IBinder.class), invalidXY, invalidXY);
mTarget.handleMotionEvent(false /* keepHandling */, mDisplayContent.getDisplayId(),
@@ -654,7 +850,7 @@ public class DragDropControllerTests extends WindowTestsBase {
mTarget.setGlobalDragListener(listener);
final int invalidXY = 100_000;
startDrag(View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_START_INTENT_SENDER_ON_UNHANDLED_DRAG,
- ClipData.newPlainText("label", "Test"), () -> {
+ ClipData.newPlainText("label", "Test"), (unused) -> {
// Trigger an unhandled drop and verify the global drag listener was called
mTarget.reportDropWindow(mock(IBinder.class), invalidXY, invalidXY);
mTarget.handleMotionEvent(false /* keepHandling */,
@@ -675,7 +871,7 @@ public class DragDropControllerTests extends WindowTestsBase {
}
private void doDragAndDrop(int flags, ClipData data, float dropX, float dropY) {
- startDrag(flags, data, () -> {
+ startDrag(flags, data, (unused) -> {
mTarget.reportDropWindow(mWindow.mInputChannelToken, dropX, dropY);
mTarget.handleMotionEvent(false /* keepHandling */, mWindow.getDisplayId(), dropX,
dropY);
@@ -686,19 +882,26 @@ public class DragDropControllerTests extends WindowTestsBase {
/**
* Starts a drag with the given parameters, calls Runnable `r` after drag is started.
*/
- private void startDrag(int flag, ClipData data, Runnable r) {
+ private void startDrag(int flag, ClipData data, Consumer<SurfaceControl> c) {
+ startDrag(0, 0, flag, data, c);
+ }
+
+ /**
+ * Starts a drag with the given parameters, calls Runnable `r` after drag is started.
+ */
+ private void startDrag(float startInWindowX, float startInWindowY, int flag, ClipData data,
+ Consumer<SurfaceControl> c) {
final SurfaceSession appSession = new SurfaceSession();
try {
final SurfaceControl surface = new SurfaceControl.Builder(appSession).setName(
"drag surface").setBufferSize(100, 100).setFormat(
PixelFormat.TRANSLUCENT).build();
-
assertTrue(mWm.mInputManager.startDragAndDrop(new Binder(), new Binder()));
- mToken = mTarget.performDrag(TEST_PID, 0, mWindow.mClient, flag, surface, 0, 0, 0, 0, 0,
- 0, 0, data);
+ mToken = mTarget.performDrag(TEST_PID, 0, mWindow.mClient, flag, surface, 0, 0, 0,
+ startInWindowX, startInWindowY, 0, 0, data);
assertNotNull(mToken);
- r.run();
+ c.accept(surface);
} finally {
appSession.kill();
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/PersisterQueueTests.java b/services/tests/wmtests/src/com/android/server/wm/PersisterQueueTests.java
index 3e87f1f96fcd..ee9673f5ee77 100644
--- a/services/tests/wmtests/src/com/android/server/wm/PersisterQueueTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/PersisterQueueTests.java
@@ -177,15 +177,16 @@ public class PersisterQueueTests {
assertTrue("Target didn't call callback enough times.",
mListener.waitForAllExpectedCallbackDone(TIMEOUT_ALLOWANCE));
+ // Wait until writing thread is waiting, which indicates the thread is waiting for new tasks
+ // to appear.
+ assertTrue("Failed to wait until the writing thread is waiting.",
+ mTarget.waitUntilWritingThreadIsWaiting(TIMEOUT_ALLOWANCE));
+
// Second item
mFactory.setExpectedProcessedItemNumber(1);
mListener.setExpectedOnPreProcessItemCallbackTimes(1);
dispatchTime = SystemClock.uptimeMillis();
- // Synchronize on the instance to make sure we schedule the item after it starts to wait for
- // task indefinitely.
- synchronized (mTarget) {
- mTarget.addItem(mFactory.createItem(), false);
- }
+ mTarget.addItem(mFactory.createItem(), false);
assertTrue("Target didn't process item enough times.",
mFactory.waitForAllExpectedItemsProcessed(PRE_TASK_DELAY_MS + TIMEOUT_ALLOWANCE));
assertEquals("Target didn't process all items.", 2, mFactory.getTotalProcessedItemCount());
diff --git a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
index d6080e08774e..07ee09a350d9 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
@@ -185,31 +185,46 @@ public class SizeCompatTests extends WindowTestsBase {
}
private ActivityRecord setUpApp(DisplayContent display) {
- return setUpApp(display, null /* appBuilder */);
+ return setUpApp(display, null /* appBuilder */, null /* taskBuilder */);
}
private ActivityRecord setUpApp(DisplayContent display, ActivityBuilder appBuilder) {
+ return setUpApp(display, appBuilder, null /* taskBuilder */);
+ }
+
+ private ActivityRecord setUpApp(DisplayContent display, ActivityBuilder aBuilder,
+ TaskBuilder tBuilder) {
// Use the real package name (com.android.frameworks.wmtests) so that
// EnableCompatChanges/DisableCompatChanges can take effect.
// Otherwise the fake WindowTestsBase.DEFAULT_COMPONENT_PACKAGE_NAME will make
// PlatformCompat#isChangeEnabledByPackageName always return default value.
final ComponentName componentName = ComponentName.createRelative(
mContext, SizeCompatTests.class.getName());
- mTask = new TaskBuilder(mSupervisor).setDisplay(display).setComponent(componentName)
+ final TaskBuilder taskBuilder = tBuilder != null ? tBuilder : new TaskBuilder(mSupervisor);
+ mTask = taskBuilder.setDisplay(display).setComponent(componentName)
.build();
- final ActivityBuilder builder = appBuilder != null ? appBuilder : new ActivityBuilder(mAtm);
- mActivity = builder.setTask(mTask).setComponent(componentName).build();
+ final ActivityBuilder appBuilder = aBuilder != null ? aBuilder : new ActivityBuilder(mAtm);
+ mActivity = appBuilder.setTask(mTask).setComponent(componentName).build();
doReturn(false).when(mActivity).isImmersiveMode(any());
return mActivity;
}
private ActivityRecord setUpDisplaySizeWithApp(int dw, int dh) {
- return setUpDisplaySizeWithApp(dw, dh, null /* appBuilder */);
+ return setUpDisplaySizeWithApp(dw, dh, null /* appBuilder */, null /* taskBuilder */);
}
private ActivityRecord setUpDisplaySizeWithApp(int dw, int dh, ActivityBuilder appBuilder) {
+ return setUpDisplaySizeWithApp(dw, dh, appBuilder, null /* taskBuilder */);
+ }
+
+ private ActivityRecord setUpDisplaySizeWithApp(int dw, int dh, TaskBuilder taskBuilder) {
+ return setUpDisplaySizeWithApp(dw, dh, null /* appBuilder */, taskBuilder);
+ }
+
+ private ActivityRecord setUpDisplaySizeWithApp(int dw, int dh, ActivityBuilder appBuilder,
+ TaskBuilder taskBuilder) {
final TestDisplayContent.Builder builder = new TestDisplayContent.Builder(mAtm, dw, dh);
- return setUpApp(builder.build(), appBuilder);
+ return setUpApp(builder.build(), appBuilder, taskBuilder);
}
private void setUpLargeScreenDisplayWithApp(int dw, int dh) {
@@ -834,7 +849,7 @@ public class SizeCompatTests extends WindowTestsBase {
// Change the fixed orientation.
mActivity.setRequestedOrientation(SCREEN_ORIENTATION_LANDSCAPE);
assertTrue(mActivity.isRelaunching());
- assertTrue(mActivity.mAppCompatController.getAppCompatOrientationOverrides()
+ assertTrue(mActivity.mAppCompatController.getOrientationOverrides()
.getIsRelaunchingAfterRequestedOrientationChanged());
assertFitted();
@@ -4469,6 +4484,80 @@ public class SizeCompatTests extends WindowTestsBase {
assertEquals(new Rect(0, 0, 1000, 2800), bounds);
}
+ @Test
+ @EnableFlags(Flags.FLAG_IGNORE_ASPECT_RATIO_RESTRICTIONS_FOR_RESIZEABLE_FREEFORM_ACTIVITIES)
+ public void testUserAspectRatioOverridesNotAppliedToResizeableFreeformActivity() {
+ final TaskBuilder taskBuilder =
+ new TaskBuilder(mSupervisor).setWindowingMode(WINDOWING_MODE_FREEFORM);
+ setUpDisplaySizeWithApp(2500, 1600, taskBuilder);
+
+ mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
+ spyOn(mActivity.mWmService.mAppCompatConfiguration);
+ doReturn(true).when(mActivity.mWmService.mAppCompatConfiguration)
+ .isUserAppAspectRatioSettingsEnabled();
+ final AppCompatController appCompatController = mActivity.mAppCompatController;
+ final AppCompatAspectRatioOverrides aspectRatioOverrides =
+ appCompatController.getAppCompatAspectRatioOverrides();
+ spyOn(aspectRatioOverrides);
+ // Set user aspect ratio override.
+ doReturn(USER_MIN_ASPECT_RATIO_16_9).when(aspectRatioOverrides)
+ .getUserMinAspectRatioOverrideCode();
+
+ prepareLimitedBounds(mActivity, SCREEN_ORIENTATION_PORTRAIT, /* isUnresizable= */ false);
+ assertFalse(appCompatController.getAppCompatAspectRatioPolicy().isAspectRatioApplied());
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_IGNORE_ASPECT_RATIO_RESTRICTIONS_FOR_RESIZEABLE_FREEFORM_ACTIVITIES)
+ public void testUserAspectRatioOverridesAppliedToNonResizeableFreeformActivity() {
+ final TaskBuilder taskBuilder =
+ new TaskBuilder(mSupervisor).setWindowingMode(WINDOWING_MODE_FREEFORM);
+ setUpDisplaySizeWithApp(2500, 1600, taskBuilder);
+
+ mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
+ spyOn(mActivity.mWmService.mAppCompatConfiguration);
+ doReturn(true).when(mActivity.mWmService.mAppCompatConfiguration)
+ .isUserAppAspectRatioSettingsEnabled();
+ final AppCompatController appCompatController = mActivity.mAppCompatController;
+ final AppCompatAspectRatioOverrides aspectRatioOverrides =
+ appCompatController.getAppCompatAspectRatioOverrides();
+ spyOn(aspectRatioOverrides);
+ // Set user aspect ratio override.
+ doReturn(USER_MIN_ASPECT_RATIO_16_9).when(aspectRatioOverrides)
+ .getUserMinAspectRatioOverrideCode();
+
+ prepareLimitedBounds(mActivity, SCREEN_ORIENTATION_PORTRAIT, /* isUnresizable= */ true);
+ assertTrue(appCompatController.getAppCompatAspectRatioPolicy().isAspectRatioApplied());
+ }
+
+ @Test
+ @EnableCompatChanges({ActivityInfo.OVERRIDE_MIN_ASPECT_RATIO,
+ ActivityInfo.OVERRIDE_MIN_ASPECT_RATIO_LARGE})
+ @EnableFlags(Flags.FLAG_IGNORE_ASPECT_RATIO_RESTRICTIONS_FOR_RESIZEABLE_FREEFORM_ACTIVITIES)
+ public void testSystemAspectRatioOverridesNotAppliedToResizeableFreeformActivity() {
+ final TaskBuilder taskBuilder =
+ new TaskBuilder(mSupervisor).setWindowingMode(WINDOWING_MODE_FREEFORM);
+ setUpDisplaySizeWithApp(2500, 1600, taskBuilder);
+ prepareLimitedBounds(mActivity, SCREEN_ORIENTATION_PORTRAIT, /* isUnresizable= */ false);
+
+ assertFalse(mActivity.mAppCompatController.getAppCompatAspectRatioPolicy()
+ .isAspectRatioApplied());
+ }
+
+ @Test
+ @EnableCompatChanges({ActivityInfo.OVERRIDE_MIN_ASPECT_RATIO,
+ ActivityInfo.OVERRIDE_MIN_ASPECT_RATIO_LARGE})
+ @EnableFlags(Flags.FLAG_IGNORE_ASPECT_RATIO_RESTRICTIONS_FOR_RESIZEABLE_FREEFORM_ACTIVITIES)
+ public void testSystemAspectRatioOverridesAppliedToNonResizeableFreeformActivity() {
+ final TaskBuilder taskBuilder =
+ new TaskBuilder(mSupervisor).setWindowingMode(WINDOWING_MODE_FREEFORM);
+ setUpDisplaySizeWithApp(2500, 1600, taskBuilder);
+ prepareLimitedBounds(mActivity, SCREEN_ORIENTATION_PORTRAIT, /* isUnresizable= */ true);
+
+ assertTrue(mActivity.mAppCompatController.getAppCompatAspectRatioPolicy()
+ .isAspectRatioApplied());
+ }
+
private void assertVerticalPositionForDifferentDisplayConfigsForLandscapeActivity(
float letterboxVerticalPositionMultiplier, Rect fixedOrientationLetterbox,
Rect sizeCompatUnscaled, Rect sizeCompatScaled) {
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java
index 5ed2df30518b..cc447a18758c 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java
@@ -1269,6 +1269,7 @@ public class WindowContainerTests extends WindowTestsBase {
final SurfaceControl.Transaction t = mock(SurfaceControl.Transaction.class);
spyOn(container);
spyOn(surfaceAnimator);
+ doReturn(t).when(container).getSyncTransaction();
// Trigger for first relative layer call.
container.assignRelativeLayer(t, relativeParent, 1 /* layer */);
@@ -1295,6 +1296,7 @@ public class WindowContainerTests extends WindowTestsBase {
spyOn(container);
spyOn(surfaceAnimator);
spyOn(surfaceFreezer);
+ doReturn(t).when(container).getSyncTransaction();
container.setLayer(t, 1);
container.setRelativeLayer(t, relativeParent, 2);
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 d1f5d157560b..be79160c3a09 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java
@@ -76,6 +76,7 @@ import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
+import android.app.ActivityManager;
import android.app.ActivityThread;
import android.app.IApplicationThread;
import android.content.pm.ActivityInfo;
@@ -1522,7 +1523,7 @@ public class WindowManagerServiceTests extends WindowTestsBase {
@EnableFlags(Flags.FLAG_CONDENSE_CONFIGURATION_CHANGE_FOR_SIMPLE_MODE)
public void setConfigurationChangeSettingsForUser_createsFromParcel_callsSettingImpl()
throws Settings.SettingNotFoundException {
- final int userId = 0;
+ final int currentUserId = ActivityManager.getCurrentUser();
final int forcedDensity = 400;
final float forcedFontScaleFactor = 1.15f;
final Parcelable.Creator<ConfigurationChangeSetting> creator =
@@ -1536,10 +1537,10 @@ public class WindowManagerServiceTests extends WindowTestsBase {
mWm.setConfigurationChangeSettingsForUser(settings, UserHandle.USER_CURRENT);
- verify(mDisplayContent).setForcedDensity(forcedDensity, userId);
+ verify(mDisplayContent).setForcedDensity(forcedDensity, currentUserId);
assertEquals(forcedFontScaleFactor, Settings.System.getFloat(
mContext.getContentResolver(), Settings.System.FONT_SCALE), 0.1f /* delta */);
- verify(mAtm).updateFontScaleIfNeeded(userId);
+ verify(mAtm).updateFontScaleIfNeeded(currentUserId);
}
@Test
diff --git a/telecomm/java/android/telecom/TelecomManager.java b/telecomm/java/android/telecom/TelecomManager.java
index 7082f0028a5e..e65e4b05ef98 100644
--- a/telecomm/java/android/telecom/TelecomManager.java
+++ b/telecomm/java/android/telecom/TelecomManager.java
@@ -29,6 +29,7 @@ import android.annotation.SuppressAutoDoc;
import android.annotation.SuppressLint;
import android.annotation.SystemApi;
import android.annotation.SystemService;
+import android.annotation.TestApi;
import android.compat.annotation.ChangeId;
import android.compat.annotation.EnabledSince;
import android.compat.annotation.UnsupportedAppUsage;
@@ -1886,6 +1887,34 @@ public class TelecomManager {
}
/**
+ * This test API determines the foreground service delegation state for a VoIP app that adds
+ * calls via {@link TelecomManager#addCall(CallAttributes, Executor, OutcomeReceiver,
+ * CallControlCallback, CallEventCallback)}. Foreground Service Delegation allows applications
+ * to operate in the background starting in Android 14 and is granted by Telecom via a request
+ * to the ActivityManager.
+ *
+ * @param handle of the voip app that is being checked
+ * @return true if the app has foreground service delegation. Otherwise, false.
+ *
+ * @hide
+ */
+ @FlaggedApi(Flags.FLAG_VOIP_CALL_MONITOR_REFACTOR)
+ @TestApi
+ public boolean hasForegroundServiceDelegation(@Nullable PhoneAccountHandle handle) {
+ ITelecomService service = getTelecomService();
+ if (service != null) {
+ try {
+ return service.hasForegroundServiceDelegation(handle, mContext.getOpPackageName());
+ } catch (RemoteException e) {
+ Log.e(TAG,
+ "RemoteException calling ITelecomService#hasForegroundServiceDelegation.",
+ e);
+ }
+ }
+ return false;
+ }
+
+ /**
* Return the line 1 phone number for given phone account.
*
* <p>Requires Permission:
diff --git a/telecomm/java/com/android/internal/telecom/ITelecomService.aidl b/telecomm/java/com/android/internal/telecom/ITelecomService.aidl
index c85374e0b660..b32379ae4b1e 100644
--- a/telecomm/java/com/android/internal/telecom/ITelecomService.aidl
+++ b/telecomm/java/com/android/internal/telecom/ITelecomService.aidl
@@ -409,4 +409,10 @@ interface ITelecomService {
*/
void addCall(in CallAttributes callAttributes, in ICallEventCallback callback, String callId,
String callingPackage);
+
+ /**
+ * @see TelecomServiceImpl#hasForegroundServiceDelegation
+ */
+ boolean hasForegroundServiceDelegation(in PhoneAccountHandle phoneAccountHandle,
+ String callingPackage);
}
diff --git a/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/open/MainActivityStartsSecondaryWithAlwaysExpandTest.kt b/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/open/MainActivityStartsSecondaryWithAlwaysExpandTest.kt
index 08b5f38a4655..75bd5d157bb2 100644
--- a/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/open/MainActivityStartsSecondaryWithAlwaysExpandTest.kt
+++ b/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/open/MainActivityStartsSecondaryWithAlwaysExpandTest.kt
@@ -17,7 +17,6 @@
package com.android.server.wm.flicker.activityembedding.open
import android.graphics.Rect
-import android.platform.test.annotations.Presubmit
import android.tools.flicker.junit.FlickerParametersRunnerFactory
import android.tools.flicker.legacy.FlickerBuilder
import android.tools.flicker.legacy.LegacyFlickerTest
@@ -68,13 +67,21 @@ class MainActivityStartsSecondaryWithAlwaysExpandTest(flicker: LegacyFlickerTest
}
}
- @Ignore("Not applicable to this CUJ.") override fun navBarWindowIsVisibleAtStartAndEnd() {}
+ @Ignore("Not applicable to this CUJ.")
+ @Test
+ override fun navBarWindowIsVisibleAtStartAndEnd() {}
- @FlakyTest(bugId = 291575593) override fun entireScreenCovered() {}
+ @FlakyTest(bugId = 291575593)
+ @Test
+ override fun entireScreenCovered() {}
- @Ignore("Not applicable to this CUJ.") override fun statusBarWindowIsAlwaysVisible() {}
+ @Ignore("Not applicable to this CUJ.")
+ @Test
+ override fun statusBarWindowIsAlwaysVisible() {}
- @Ignore("Not applicable to this CUJ.") override fun statusBarLayerPositionAtStartAndEnd() {}
+ @Ignore("Not applicable to this CUJ.")
+ @Test
+ override fun statusBarLayerPositionAtStartAndEnd() {}
/** Transition begins with a split. */
@FlakyTest(bugId = 286952194)
@@ -122,7 +129,6 @@ class MainActivityStartsSecondaryWithAlwaysExpandTest(flicker: LegacyFlickerTest
/** Always expand activity is on top of the split. */
@FlakyTest(bugId = 286952194)
- @Presubmit
@Test
fun endsWithAlwaysExpandActivityOnTop() {
flicker.assertWmEnd {
diff --git a/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/splitscreen/EnterSystemSplitTest.kt b/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/splitscreen/EnterSystemSplitTest.kt
index 0ca8f37b239b..e41364595648 100644
--- a/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/splitscreen/EnterSystemSplitTest.kt
+++ b/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/splitscreen/EnterSystemSplitTest.kt
@@ -176,12 +176,15 @@ class EnterSystemSplitTest(flicker: LegacyFlickerTest) : ActivityEmbeddingTestBa
}
@Ignore("Not applicable to this CUJ.")
+ @Test
override fun visibleLayersShownMoreThanOneConsecutiveEntry() {}
@FlakyTest(bugId = 342596801)
+ @Test
override fun entireScreenCovered() = super.entireScreenCovered()
@FlakyTest(bugId = 342596801)
+ @Test
override fun visibleWindowsShownMoreThanOneConsecutiveEntry() =
super.visibleWindowsShownMoreThanOneConsecutiveEntry()
diff --git a/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromFixedOrientationTest.kt b/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromFixedOrientationTest.kt
index b8f11dcf8970..ad083fa428a9 100644
--- a/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromFixedOrientationTest.kt
+++ b/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromFixedOrientationTest.kt
@@ -16,7 +16,6 @@
package com.android.server.wm.flicker.ime
-import android.platform.test.annotations.Postsubmit
import android.platform.test.annotations.Presubmit
import android.tools.Rotation
import android.tools.flicker.junit.FlickerParametersRunnerFactory
@@ -81,7 +80,6 @@ class ShowImeOnAppStartWhenLaunchingAppFromFixedOrientationTest(flicker: LegacyF
}
@FlakyTest(bugId = 290767483)
- @Postsubmit
@Test
fun imeLayerAlphaOneAfterSnapshotStartingWindowRemoval() {
val layerTrace = flicker.reader.readLayersTrace() ?: error("Unable to read layers trace")
diff --git a/tests/Input/AndroidManifest.xml b/tests/Input/AndroidManifest.xml
index 914adc40194d..8d380f0d72a6 100644
--- a/tests/Input/AndroidManifest.xml
+++ b/tests/Input/AndroidManifest.xml
@@ -32,7 +32,7 @@
android:process=":externalProcess">
</activity>
- <activity android:name="com.android.test.input.CaptureEventActivity"
+ <activity android:name="com.android.cts.input.CaptureEventActivity"
android:label="Capture events"
android:configChanges="touchscreen|uiMode|orientation|screenSize|screenLayout|keyboardHidden|uiMode|navigation|keyboard|density|fontScale|layoutDirection|locale|mcc|mnc|smallestScreenSize"
android:enableOnBackInvokedCallback="false"
diff --git a/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt b/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt
index d35c9008e8cb..8c04f647fb2f 100644
--- a/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt
+++ b/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt
@@ -59,6 +59,7 @@ import junitparams.Parameters
import org.junit.After
import org.junit.Assert.assertArrayEquals
import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Rule
@@ -1466,20 +1467,32 @@ class KeyGestureControllerTests {
@Parameters(method = "customInputGesturesTestArguments")
fun testCustomKeyGestures(test: TestData) {
setupKeyGestureController()
+ val trigger = InputGestureData.createKeyTrigger(
+ test.expectedKeys[0],
+ test.expectedModifierState
+ )
val builder = InputGestureData.Builder()
.setKeyGestureType(test.expectedKeyGestureType)
- .setTrigger(
- InputGestureData.createKeyTrigger(
- test.expectedKeys[0],
- test.expectedModifierState
- )
- )
+ .setTrigger(trigger)
if (test.expectedAppLaunchData != null) {
builder.setAppLaunchData(test.expectedAppLaunchData)
}
val inputGestureData = builder.build()
- keyGestureController.addCustomInputGesture(0, inputGestureData.aidlData)
+ assertNull(
+ test.toString(),
+ keyGestureController.getInputGesture(0, trigger.aidlTrigger)
+ )
+ assertEquals(
+ test.toString(),
+ InputManager.CUSTOM_INPUT_GESTURE_RESULT_SUCCESS,
+ keyGestureController.addCustomInputGesture(0, builder.build().aidlData)
+ )
+ assertEquals(
+ test.toString(),
+ inputGestureData.aidlData,
+ keyGestureController.getInputGesture(0, trigger.aidlTrigger)
+ )
testKeyGestureInternal(test)
}
diff --git a/tests/Input/src/com/android/test/input/CaptureEventActivity.kt b/tests/Input/src/com/android/test/input/CaptureEventActivity.kt
deleted file mode 100644
index d54e3470d9c4..000000000000
--- a/tests/Input/src/com/android/test/input/CaptureEventActivity.kt
+++ /dev/null
@@ -1,85 +0,0 @@
-/*
- * 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.test.input
-
-import android.app.Activity
-import android.os.Bundle
-import android.view.InputEvent
-import android.view.KeyEvent
-import android.view.MotionEvent
-import java.util.concurrent.LinkedBlockingQueue
-import java.util.concurrent.TimeUnit
-import org.junit.Assert.assertNull
-
-class CaptureEventActivity : Activity() {
- private val events = LinkedBlockingQueue<InputEvent>()
- var shouldHandleKeyEvents = true
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
-
- // Set the fixed orientation if requested
- if (intent.hasExtra(EXTRA_FIXED_ORIENTATION)) {
- val orientation = intent.getIntExtra(EXTRA_FIXED_ORIENTATION, 0)
- setRequestedOrientation(orientation)
- }
-
- // Set the flag if requested
- if (intent.hasExtra(EXTRA_WINDOW_FLAGS)) {
- val flags = intent.getIntExtra(EXTRA_WINDOW_FLAGS, 0)
- window.addFlags(flags)
- }
- }
-
- override fun dispatchGenericMotionEvent(ev: MotionEvent?): Boolean {
- events.add(MotionEvent.obtain(ev))
- return true
- }
-
- override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
- events.add(MotionEvent.obtain(ev))
- return true
- }
-
- override fun dispatchKeyEvent(event: KeyEvent?): Boolean {
- events.add(KeyEvent(event))
- return shouldHandleKeyEvents
- }
-
- override fun dispatchTrackballEvent(ev: MotionEvent?): Boolean {
- events.add(MotionEvent.obtain(ev))
- return true
- }
-
- fun getInputEvent(): InputEvent? {
- return events.poll(5, TimeUnit.SECONDS)
- }
-
- fun hasReceivedEvents(): Boolean {
- return !events.isEmpty()
- }
-
- fun assertNoEvents() {
- val event = events.poll(100, TimeUnit.MILLISECONDS)
- assertNull("Expected no events, but received $event", event)
- }
-
- companion object {
- const val EXTRA_FIXED_ORIENTATION = "fixed_orientation"
- const val EXTRA_WINDOW_FLAGS = "window_flags"
- }
-}
diff --git a/tests/Input/src/com/android/test/input/UinputRecordingIntegrationTests.kt b/tests/Input/src/com/android/test/input/UinputRecordingIntegrationTests.kt
index 0b281d8d39e2..9e0f7347943d 100644
--- a/tests/Input/src/com/android/test/input/UinputRecordingIntegrationTests.kt
+++ b/tests/Input/src/com/android/test/input/UinputRecordingIntegrationTests.kt
@@ -28,6 +28,7 @@ import android.view.InputEvent
import android.view.MotionEvent
import androidx.test.platform.app.InstrumentationRegistry
import com.android.cts.input.BatchedEventSplitter
+import com.android.cts.input.CaptureEventActivity
import com.android.cts.input.InputJsonParser
import com.android.cts.input.VirtualDisplayActivityScenario
import com.android.cts.input.inputeventmatchers.isResampled
diff --git a/tests/PlatformCompatGating/src/com/android/tests/gating/PlatformCompatPermissionsTest.java b/tests/PlatformCompatGating/src/com/android/tests/gating/PlatformCompatPermissionsTest.java
index 060133df0a40..e7e3d10c958b 100644
--- a/tests/PlatformCompatGating/src/com/android/tests/gating/PlatformCompatPermissionsTest.java
+++ b/tests/PlatformCompatGating/src/com/android/tests/gating/PlatformCompatPermissionsTest.java
@@ -81,7 +81,8 @@ public final class PlatformCompatPermissionsTest {
thrown.expect(SecurityException.class);
final String packageName = mContext.getPackageName();
- mPlatformCompat.reportChange(1, mPackageManager.getApplicationInfo(packageName, 0));
+ mPlatformCompat.reportChange(1,
+ mPackageManager.getApplicationInfo(packageName, Process.myUid()));
}
@Test
@@ -90,7 +91,8 @@ public final class PlatformCompatPermissionsTest {
mUiAutomation.adoptShellPermissionIdentity(LOG_COMPAT_CHANGE);
final String packageName = mContext.getPackageName();
- mPlatformCompat.reportChange(1, mPackageManager.getApplicationInfo(packageName, 0));
+ mPlatformCompat.reportChange(1,
+ mPackageManager.getApplicationInfo(packageName, Process.myUid()));
}
@Test
@@ -99,7 +101,7 @@ public final class PlatformCompatPermissionsTest {
thrown.expect(SecurityException.class);
final String packageName = mContext.getPackageName();
- mPlatformCompat.reportChangeByPackageName(1, packageName, 0);
+ mPlatformCompat.reportChangeByPackageName(1, packageName, Process.myUid());
}
@Test
@@ -108,7 +110,7 @@ public final class PlatformCompatPermissionsTest {
mUiAutomation.adoptShellPermissionIdentity(LOG_COMPAT_CHANGE);
final String packageName = mContext.getPackageName();
- mPlatformCompat.reportChangeByPackageName(1, packageName, 0);
+ mPlatformCompat.reportChangeByPackageName(1, packageName, Process.myUid());
}
@Test
@@ -133,7 +135,8 @@ public final class PlatformCompatPermissionsTest {
thrown.expect(SecurityException.class);
final String packageName = mContext.getPackageName();
- mPlatformCompat.isChangeEnabled(1, mPackageManager.getApplicationInfo(packageName, 0));
+ mPlatformCompat.isChangeEnabled(1,
+ mPackageManager.getApplicationInfo(packageName, Process.myUid()));
}
@Test
@@ -143,7 +146,8 @@ public final class PlatformCompatPermissionsTest {
mUiAutomation.adoptShellPermissionIdentity(READ_COMPAT_CHANGE_CONFIG);
final String packageName = mContext.getPackageName();
- mPlatformCompat.isChangeEnabled(1, mPackageManager.getApplicationInfo(packageName, 0));
+ mPlatformCompat.isChangeEnabled(1,
+ mPackageManager.getApplicationInfo(packageName, Process.myUid()));
}
@Test
@@ -152,7 +156,8 @@ public final class PlatformCompatPermissionsTest {
mUiAutomation.adoptShellPermissionIdentity(READ_COMPAT_CHANGE_CONFIG, LOG_COMPAT_CHANGE);
final String packageName = mContext.getPackageName();
- mPlatformCompat.isChangeEnabled(1, mPackageManager.getApplicationInfo(packageName, 0));
+ mPlatformCompat.isChangeEnabled(1,
+ mPackageManager.getApplicationInfo(packageName, Process.myUid()));
}
@Test
@@ -161,7 +166,7 @@ public final class PlatformCompatPermissionsTest {
thrown.expect(SecurityException.class);
final String packageName = mContext.getPackageName();
- mPlatformCompat.isChangeEnabledByPackageName(1, packageName, 0);
+ mPlatformCompat.isChangeEnabledByPackageName(1, packageName, Process.myUid());
}
@Test
@@ -171,7 +176,7 @@ public final class PlatformCompatPermissionsTest {
mUiAutomation.adoptShellPermissionIdentity(READ_COMPAT_CHANGE_CONFIG);
final String packageName = mContext.getPackageName();
- mPlatformCompat.isChangeEnabledByPackageName(1, packageName, 0);
+ mPlatformCompat.isChangeEnabledByPackageName(1, packageName, Process.myUid());
}
@Test
@@ -180,7 +185,7 @@ public final class PlatformCompatPermissionsTest {
mUiAutomation.adoptShellPermissionIdentity(READ_COMPAT_CHANGE_CONFIG, LOG_COMPAT_CHANGE);
final String packageName = mContext.getPackageName();
- mPlatformCompat.isChangeEnabledByPackageName(1, packageName, 0);
+ mPlatformCompat.isChangeEnabledByPackageName(1, packageName, Process.myUid());
}
@Test
diff --git a/tools/aapt2/cmd/Command.cpp b/tools/aapt2/cmd/Command.cpp
index 20315561cceb..f00a6cad6b46 100644
--- a/tools/aapt2/cmd/Command.cpp
+++ b/tools/aapt2/cmd/Command.cpp
@@ -54,7 +54,9 @@ 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 {
- *value = (flags & Command::kPath) ? GetSafePath(arg) : std::string(arg);
+ if (value) {
+ *value = (flags & Command::kPath) ? GetSafePath(arg) : std::string(arg);
+ }
return true;
};
@@ -65,7 +67,9 @@ 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 {
- value->push_back((flags & Command::kPath) ? GetSafePath(arg) : std::string(arg));
+ if (value) {
+ value->push_back((flags & Command::kPath) ? GetSafePath(arg) : std::string(arg));
+ }
return true;
};
@@ -76,7 +80,9 @@ 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 {
- *value = (flags & Command::kPath) ? GetSafePath(arg) : std::string(arg);
+ if (value) {
+ *value = (flags & Command::kPath) ? GetSafePath(arg) : std::string(arg);
+ }
return true;
};
@@ -87,7 +93,9 @@ 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 {
- value->push_back((flags & Command::kPath) ? GetSafePath(arg) : std::string(arg));
+ if (value) {
+ value->push_back((flags & Command::kPath) ? GetSafePath(arg) : std::string(arg));
+ }
return true;
};
@@ -98,7 +106,9 @@ 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 {
- value->emplace(arg);
+ if (value) {
+ value->emplace(arg);
+ }
return true;
};
@@ -108,7 +118,9 @@ 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 {
- *value = true;
+ if (value) {
+ *value = true;
+ }
return true;
};
diff --git a/tools/aapt2/cmd/Command_test.cpp b/tools/aapt2/cmd/Command_test.cpp
index 2a3cb2a0c65d..ad167c979662 100644
--- a/tools/aapt2/cmd/Command_test.cpp
+++ b/tools/aapt2/cmd/Command_test.cpp
@@ -159,4 +159,22 @@ 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 6c3eae11eab9..060bc5fa2242 100644
--- a/tools/aapt2/cmd/Convert.cpp
+++ b/tools/aapt2/cmd/Convert.cpp
@@ -425,9 +425,6 @@ 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 9452e588953e..98c8f5ff89c0 100644
--- a/tools/aapt2/cmd/Convert.h
+++ b/tools/aapt2/cmd/Convert.h
@@ -36,11 +36,9 @@ class ConvertCommand : public Command {
kOutputFormatProto, kOutputFormatBinary, kOutputFormatBinary), &output_format_);
AddOptionalSwitch(
"--enable-sparse-encoding",
- "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_);
+ "[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("--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"
@@ -87,7 +85,6 @@ 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 232b4024abd2..eb71189ffc46 100644
--- a/tools/aapt2/cmd/Link.cpp
+++ b/tools/aapt2/cmd/Link.cpp
@@ -2504,9 +2504,6 @@ 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 2f17853718ec..b5bd905c02be 100644
--- a/tools/aapt2/cmd/Link.h
+++ b/tools/aapt2/cmd/Link.h
@@ -75,7 +75,6 @@ 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;
@@ -163,9 +162,11 @@ 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",
- "This decreases APK size at the cost of resource retrieval performance.",
- &options_.use_sparse_encoding);
+ 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-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 762441ee1872..f218307af578 100644
--- a/tools/aapt2/cmd/Optimize.cpp
+++ b/tools/aapt2/cmd/Optimize.cpp
@@ -406,9 +406,6 @@ 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 012b0f230ca2..e3af584cbbd9 100644
--- a/tools/aapt2/cmd/Optimize.h
+++ b/tools/aapt2/cmd/Optimize.h
@@ -61,9 +61,6 @@ 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;
@@ -106,11 +103,9 @@ class OptimizeCommand : public Command {
&kept_artifacts_);
AddOptionalSwitch(
"--enable-sparse-encoding",
- "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);
+ "[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("--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 1a82021bce71..b8ac7925d44e 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+.
+ // Otherwise, only sparse encode if the entries will be read on platforms S_V2+ (32).
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 0633bc81cb25..f1c4c3512ed3 100644
--- a/tools/aapt2/format/binary/TableFlattener.h
+++ b/tools/aapt2/format/binary/TableFlattener.h
@@ -37,8 +37,7 @@ constexpr const size_t kSparseEncodingThreshold = 60;
enum class SparseEntriesMode {
// Disables sparse encoding for entries.
Disabled,
- // 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+.
+ // Enables sparse encoding for all entries for APKs with minSdk >= 32 (S_V2).
Enabled,
// Enables sparse encoding for all entries regardless of minSdk.
Forced,
@@ -47,7 +46,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::Disabled;
+ SparseEntriesMode sparse_entries = SparseEntriesMode::Enabled;
// 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 0f1168514c4a..e3d589eb078b 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::Enabled;
+ options.sparse_entries = SparseEntriesMode::Disabled;
std::string no_sparse_contents;
- ASSERT_TRUE(Flatten(context.get(), {}, table_in.get(), &no_sparse_contents));
+ ASSERT_TRUE(Flatten(context.get(), options, table_in.get(), &no_sparse_contents));
std::string sparse_contents;
- ASSERT_TRUE(Flatten(context.get(), options, table_in.get(), &sparse_contents));
+ ASSERT_TRUE(Flatten(context.get(), {}, 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::Enabled;
+ options.sparse_entries = SparseEntriesMode::Disabled;
std::string no_sparse_contents;
- ASSERT_TRUE(Flatten(context.get(), {}, table_in.get(), &no_sparse_contents));
+ ASSERT_TRUE(Flatten(context.get(), options, table_in.get(), &no_sparse_contents));
std::string sparse_contents;
- ASSERT_TRUE(Flatten(context.get(), options, table_in.get(), &sparse_contents));
+ ASSERT_TRUE(Flatten(context.get(), {}, table_in.get(), &sparse_contents));
EXPECT_GT(no_sparse_contents.size(), sparse_contents.size());
diff --git a/tools/aapt2/integration-tests/DumpTest/components_full_proto.txt b/tools/aapt2/integration-tests/DumpTest/components_full_proto.txt
index 6da6fc6f12c3..d0807f2ecd34 100644
--- a/tools/aapt2/integration-tests/DumpTest/components_full_proto.txt
+++ b/tools/aapt2/integration-tests/DumpTest/components_full_proto.txt
@@ -877,7 +877,7 @@ resource_table {
}
tool_fingerprint {
tool: "Android Asset Packaging Tool (aapt)"
- version: "2.19-SOONG BUILD NUMBER PLACEHOLDER"
+ version: "2.20-SOONG BUILD NUMBER PLACEHOLDER"
}
}
xml_files {
diff --git a/tools/aapt2/readme.md b/tools/aapt2/readme.md
index 8368f9d16af8..664d8412a3be 100644
--- a/tools/aapt2/readme.md
+++ b/tools/aapt2/readme.md
@@ -1,5 +1,11 @@
# Android Asset Packaging Tool 2.0 (AAPT2) release notes
+## 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.
- Fixed issue with resource deduplication. (bug 64397629)
diff --git a/tools/aapt2/util/Util.cpp b/tools/aapt2/util/Util.cpp
index 3d83caf29bba..6a4dfa629394 100644
--- a/tools/aapt2/util/Util.cpp
+++ b/tools/aapt2/util/Util.cpp
@@ -227,7 +227,7 @@ std::string GetToolFingerprint() {
static const char* const sMajorVersion = "2";
// Update minor version whenever a feature or flag is added.
- static const char* const sMinorVersion = "19";
+ static const char* const sMinorVersion = "20";
// The build id of aapt2 binary.
static const std::string sBuildId = [] {
diff --git a/tools/systemfeatures/src/com/android/systemfeatures/SystemFeaturesGenerator.kt b/tools/systemfeatures/src/com/android/systemfeatures/SystemFeaturesGenerator.kt
index ea660b013893..22d364ec3212 100644
--- a/tools/systemfeatures/src/com/android/systemfeatures/SystemFeaturesGenerator.kt
+++ b/tools/systemfeatures/src/com/android/systemfeatures/SystemFeaturesGenerator.kt
@@ -263,7 +263,7 @@ object SystemFeaturesGenerator {
.returns(Boolean::class.java)
.addParameter(CONTEXT_CLASS, "context")
.addParameter(String::class.java, "featureName")
- .addStatement("return context.getPackageManager().hasSystemFeature(featureName, 0)")
+ .addStatement("return context.getPackageManager().hasSystemFeature(featureName)")
.build()
)
}
diff --git a/tools/systemfeatures/tests/golden/RoFeatures.java.gen b/tools/systemfeatures/tests/golden/RoFeatures.java.gen
index ee97b26159de..730dacbbf995 100644
--- a/tools/systemfeatures/tests/golden/RoFeatures.java.gen
+++ b/tools/systemfeatures/tests/golden/RoFeatures.java.gen
@@ -70,7 +70,7 @@ public final class RoFeatures {
}
private static boolean hasFeatureFallback(Context context, String featureName) {
- return context.getPackageManager().hasSystemFeature(featureName, 0);
+ return context.getPackageManager().hasSystemFeature(featureName);
}
/**
diff --git a/tools/systemfeatures/tests/golden/RoNoFeatures.java.gen b/tools/systemfeatures/tests/golden/RoNoFeatures.java.gen
index 40c7db7ff1df..fe268c70708e 100644
--- a/tools/systemfeatures/tests/golden/RoNoFeatures.java.gen
+++ b/tools/systemfeatures/tests/golden/RoNoFeatures.java.gen
@@ -25,7 +25,7 @@ public final class RoNoFeatures {
}
private static boolean hasFeatureFallback(Context context, String featureName) {
- return context.getPackageManager().hasSystemFeature(featureName, 0);
+ return context.getPackageManager().hasSystemFeature(featureName);
}
/**
diff --git a/tools/systemfeatures/tests/golden/RwFeatures.java.gen b/tools/systemfeatures/tests/golden/RwFeatures.java.gen
index 7bf89614b92d..bcf978de3c1f 100644
--- a/tools/systemfeatures/tests/golden/RwFeatures.java.gen
+++ b/tools/systemfeatures/tests/golden/RwFeatures.java.gen
@@ -55,7 +55,7 @@ public final class RwFeatures {
}
private static boolean hasFeatureFallback(Context context, String featureName) {
- return context.getPackageManager().hasSystemFeature(featureName, 0);
+ return context.getPackageManager().hasSystemFeature(featureName);
}
/**
diff --git a/tools/systemfeatures/tests/golden/RwNoFeatures.java.gen b/tools/systemfeatures/tests/golden/RwNoFeatures.java.gen
index eb7ec63f1d7d..7bad5a2bae2a 100644
--- a/tools/systemfeatures/tests/golden/RwNoFeatures.java.gen
+++ b/tools/systemfeatures/tests/golden/RwNoFeatures.java.gen
@@ -14,7 +14,7 @@ import android.util.ArrayMap;
*/
public final class RwNoFeatures {
private static boolean hasFeatureFallback(Context context, String featureName) {
- return context.getPackageManager().hasSystemFeature(featureName, 0);
+ return context.getPackageManager().hasSystemFeature(featureName);
}
/**
diff --git a/tools/systemfeatures/tests/src/SystemFeaturesGeneratorTest.java b/tools/systemfeatures/tests/src/SystemFeaturesGeneratorTest.java
index ed3f5c94ba79..491b55e7992c 100644
--- a/tools/systemfeatures/tests/src/SystemFeaturesGeneratorTest.java
+++ b/tools/systemfeatures/tests/src/SystemFeaturesGeneratorTest.java
@@ -76,28 +76,28 @@ public class SystemFeaturesGeneratorTest {
// Also ensure we fall back to the PackageManager for feature APIs without an accompanying
// versioned feature definition.
- when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WATCH, 0)).thenReturn(true);
+ when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WATCH)).thenReturn(true);
assertThat(RwFeatures.hasFeatureWatch(mContext)).isTrue();
- when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WATCH, 0)).thenReturn(false);
+ when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WATCH)).thenReturn(false);
assertThat(RwFeatures.hasFeatureWatch(mContext)).isFalse();
}
@Test
public void testReadonlyDisabledWithDefinedFeatures() {
// Always fall back to the PackageManager for defined, explicit features queries.
- when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WATCH, 0)).thenReturn(true);
+ when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WATCH)).thenReturn(true);
assertThat(RwFeatures.hasFeatureWatch(mContext)).isTrue();
- when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WATCH, 0)).thenReturn(false);
+ when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WATCH)).thenReturn(false);
assertThat(RwFeatures.hasFeatureWatch(mContext)).isFalse();
- when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WIFI, 0)).thenReturn(true);
+ when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WIFI)).thenReturn(true);
assertThat(RwFeatures.hasFeatureWifi(mContext)).isTrue();
- when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_VULKAN, 0)).thenReturn(false);
+ when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_VULKAN)).thenReturn(false);
assertThat(RwFeatures.hasFeatureVulkan(mContext)).isFalse();
- when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_AUTO, 0)).thenReturn(false);
+ when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_AUTO)).thenReturn(false);
assertThat(RwFeatures.hasFeatureAuto(mContext)).isFalse();
// For defined and undefined features, conditional queries should report null (unknown).
@@ -139,9 +139,9 @@ public class SystemFeaturesGeneratorTest {
assertThat(RoFeatures.maybeHasFeature(PackageManager.FEATURE_VULKAN, 100)).isFalse();
// VERSION=
- when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_AUTO, 0)).thenReturn(false);
+ when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_AUTO)).thenReturn(false);
assertThat(RoFeatures.hasFeatureAuto(mContext)).isFalse();
- when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_AUTO, 0)).thenReturn(true);
+ when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_AUTO)).thenReturn(true);
assertThat(RoFeatures.hasFeatureAuto(mContext)).isTrue();
assertThat(RoFeatures.maybeHasFeature(PackageManager.FEATURE_AUTO, -1)).isNull();
assertThat(RoFeatures.maybeHasFeature(PackageManager.FEATURE_AUTO, 0)).isNull();
@@ -149,9 +149,9 @@ public class SystemFeaturesGeneratorTest {
// For feature APIs without an associated feature definition, conditional queries should
// report null, and explicit queries should report runtime-defined versions.
- when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_PC, 0)).thenReturn(true);
+ when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_PC)).thenReturn(true);
assertThat(RoFeatures.hasFeaturePc(mContext)).isTrue();
- when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_PC, 0)).thenReturn(false);
+ when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_PC)).thenReturn(false);
assertThat(RoFeatures.hasFeaturePc(mContext)).isFalse();
assertThat(RoFeatures.maybeHasFeature(PackageManager.FEATURE_PC, -1)).isNull();
assertThat(RoFeatures.maybeHasFeature(PackageManager.FEATURE_PC, 0)).isNull();