summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--apex/jobscheduler/service/aconfig/job.aconfig12
-rw-r--r--apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java6
-rw-r--r--core/api/current.txt125
-rw-r--r--core/api/system-current.txt18
-rw-r--r--core/api/test-current.txt40
-rw-r--r--core/java/android/app/Activity.java18
-rw-r--r--core/java/android/app/ActivityThread.java2
-rw-r--r--core/java/android/app/ApplicationStartInfo.java89
-rw-r--r--core/java/android/app/AutomaticZenRule.java134
-rw-r--r--core/java/android/app/INotificationManager.aidl2
-rw-r--r--core/java/android/app/Notification.java19
-rw-r--r--core/java/android/app/NotificationManager.java70
-rw-r--r--core/java/android/app/UiAutomation.java29
-rw-r--r--core/java/android/app/UiModeManager.java6
-rw-r--r--core/java/android/app/contextualsearch/ContextualSearchManager.java121
-rw-r--r--core/java/android/app/contextualsearch/IContextualSearchManager.aidl7
-rw-r--r--core/java/android/app/contextualsearch/flags.aconfig8
-rw-r--r--core/java/android/app/jank/JankTracker.java136
-rw-r--r--core/java/android/app/notification.aconfig10
-rw-r--r--core/java/android/app/supervision/ISupervisionManager.aidl3
-rw-r--r--core/java/android/app/supervision/SupervisionManager.java71
-rw-r--r--core/java/android/app/supervision/flags.aconfig8
-rw-r--r--core/java/android/companion/virtual/flags/flags.aconfig10
-rw-r--r--core/java/android/content/Context.java4
-rw-r--r--core/java/android/content/pm/multiuser.aconfig7
-rw-r--r--core/java/android/content/res/ApkAssets.java56
-rw-r--r--core/java/android/content/res/ResourceTimer.java56
-rw-r--r--core/java/android/content/res/XmlBlock.java14
-rw-r--r--core/java/android/hardware/display/ColorDisplayManager.java2
-rw-r--r--core/java/android/hardware/display/DisplayManager.java15
-rw-r--r--core/java/android/hardware/display/DisplayManagerGlobal.java23
-rw-r--r--core/java/android/hardware/fingerprint/FingerprintSensorConfigurations.java4
-rw-r--r--core/java/android/hardware/input/InputSettings.java4
-rw-r--r--core/java/android/os/UserManager.java7
-rw-r--r--core/java/android/os/vibrator/VibratorFrequencyProfile.java22
-rw-r--r--core/java/android/provider/Settings.java2
-rw-r--r--core/java/android/security/FileIntegrityManager.java8
-rw-r--r--core/java/android/security/IFileIntegrityService.aidl2
-rw-r--r--core/java/android/security/intrusiondetection/IntrusionDetectionEvent.java8
-rw-r--r--core/java/android/service/notification/Condition.java52
-rw-r--r--core/java/android/service/notification/SystemZenRules.java3
-rw-r--r--core/java/android/service/notification/ZenAdapters.java8
-rw-r--r--core/java/android/service/notification/ZenDeviceEffects.java5
-rw-r--r--core/java/android/service/notification/ZenModeConfig.java537
-rw-r--r--core/java/android/service/notification/ZenModeDiff.java93
-rw-r--r--core/java/android/service/notification/ZenPolicy.java118
-rw-r--r--core/java/android/service/wallpaper/WallpaperService.java13
-rw-r--r--core/java/android/view/Display.java1
-rw-r--r--core/java/android/view/DisplayInfo.java27
-rw-r--r--core/java/android/view/NotificationHeaderView.java19
-rw-r--r--core/java/android/view/ViewRootImpl.java36
-rw-r--r--core/java/android/view/WindowManager.java10
-rw-r--r--core/java/android/view/accessibility/AccessibilityNodeInfo.java21
-rw-r--r--core/java/android/view/flags/refresh_rate_flags.aconfig7
-rw-r--r--core/java/android/widget/RemoteViews.java16
-rw-r--r--core/java/android/window/DesktopModeFlags.java1
-rw-r--r--core/java/android/window/TransitionInfo.java4
-rw-r--r--core/java/android/window/flags/lse_desktop_experience.aconfig20
-rw-r--r--core/java/com/android/internal/pm/pkg/component/AconfigFlags.java15
-rw-r--r--core/java/com/android/internal/security/VerityUtils.java3
-rw-r--r--core/java/com/android/internal/widget/LockPatternView.java38
-rw-r--r--core/java/com/android/internal/widget/remotecompose/core/CoreDocument.java3
-rw-r--r--core/java/com/android/internal/widget/remotecompose/core/Operations.java9
-rw-r--r--core/java/com/android/internal/widget/remotecompose/core/RemoteComposeBuffer.java14
-rw-r--r--core/java/com/android/internal/widget/remotecompose/core/operations/DrawContent.java119
-rw-r--r--core/java/com/android/internal/widget/remotecompose/core/operations/layout/CanvasOperations.java175
-rw-r--r--core/java/com/android/internal/widget/remotecompose/core/operations/layout/Component.java20
-rw-r--r--core/java/com/android/internal/widget/remotecompose/core/operations/layout/LayoutComponent.java34
-rw-r--r--core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/DrawContentOperation.java125
-rw-r--r--core/jni/android_content_res_ApkAssets.cpp108
-rw-r--r--core/jni/android_media_AudioSystem.cpp12
-rw-r--r--core/jni/com_android_internal_os_ZygoteCommandBuffer.cpp18
-rw-r--r--core/res/Android.bp1
-rw-r--r--core/res/AndroidManifest.xml186
-rw-r--r--core/res/res/drawable-w192dp/loader_horizontal_watch.xml97
-rw-r--r--core/res/res/drawable-w204dp/loader_horizontal_watch.xml104
-rw-r--r--core/res/res/drawable-w216dp/loader_horizontal_watch.xml105
-rw-r--r--core/res/res/drawable-w228dp/loader_horizontal_watch.xml103
-rw-r--r--core/res/res/drawable-w240dp/loader_horizontal_watch.xml104
-rw-r--r--core/res/res/drawable/ic_notification_summarization.xml3
-rw-r--r--core/res/res/drawable/loader_horizontal_watch.xml103
-rw-r--r--core/res/res/layout/notification_2025_conversation_header.xml2
-rw-r--r--core/res/res/layout/notification_2025_template_collapsed_base.xml1
-rw-r--r--core/res/res/layout/notification_2025_template_collapsed_media.xml1
-rw-r--r--core/res/res/layout/notification_2025_template_collapsed_messaging.xml1
-rw-r--r--core/res/res/layout/notification_2025_template_compact_heads_up_base.xml1
-rw-r--r--core/res/res/layout/notification_2025_template_compact_heads_up_messaging.xml1
-rw-r--r--core/res/res/values-w192dp/dimens_watch.xml28
-rw-r--r--core/res/res/values-w204dp-round-watch/dimens_watch.xml21
-rw-r--r--core/res/res/values-w216dp/dimens_watch.xml21
-rw-r--r--core/res/res/values-w228dp/dimens_watch.xml21
-rw-r--r--core/res/res/values-w240dp/dimens_material.xml4
-rw-r--r--core/res/res/values-watch/styles_device_defaults.xml3
-rw-r--r--core/res/res/values/config.xml9
-rw-r--r--core/res/res/values/dimens.xml3
-rw-r--r--core/res/res/values/dimens_watch.xml4
-rw-r--r--core/res/res/values/strings.xml49
-rw-r--r--core/res/res/values/symbols.xml4
-rw-r--r--core/tests/coretests/src/android/app/AutomaticZenRuleTest.java11
-rw-r--r--core/tests/coretests/src/android/app/NotificationManagerTest.java8
-rw-r--r--core/tests/coretests/src/android/hardware/display/DisplayManagerGlobalTest.java21
-rw-r--r--core/tests/coretests/src/android/service/notification/ConditionTest.java13
-rw-r--r--core/tests/coretests/src/com/android/internal/app/ChooserActivityTest.java7
-rw-r--r--core/tests/coretests/src/com/android/internal/app/ChooserWrapperActivity.java10
-rw-r--r--graphics/java/android/graphics/FrameInfo.java7
-rw-r--r--graphics/java/android/graphics/HardwareRenderer.java12
-rw-r--r--graphics/java/android/graphics/RuntimeShader.java38
-rw-r--r--keystore/java/android/security/GateKeeper.java2
-rw-r--r--keystore/java/android/security/keystore/ArrayUtils.java2
-rw-r--r--keystore/java/android/security/keystore/Utils.java2
-rw-r--r--keystore/java/android/security/keystore2/KeyStore2ParameterUtils.java4
-rw-r--r--keystore/java/android/security/keystore2/KeymasterUtils.java46
-rw-r--r--libs/WindowManager/Shell/OWNERS2
-rw-r--r--libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml54
-rw-r--r--libs/WindowManager/Shell/res/values/dimen.xml8
-rw-r--r--libs/WindowManager/Shell/shared/res/color/bubble_drop_target_background_color.xml20
-rw-r--r--libs/WindowManager/Shell/shared/res/drawable/bubble_drop_target_background.xml25
-rw-r--r--libs/WindowManager/Shell/shared/res/values/dimen.xml9
-rw-r--r--libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/TransitionUtil.java45
-rw-r--r--libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/animation/Interpolators.java11
-rw-r--r--libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/BubbleBarLocation.kt6
-rw-r--r--libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DragZoneFactory.kt90
-rw-r--r--libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java44
-rw-r--r--libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/split/SplitScreenConstants.java1
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java19
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleLogger.java6
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java2
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTransitions.java13
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayController.java3
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/WindowContainerTransactionSupplier.kt35
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/split/CenterParallaxSpec.java39
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DismissingParallaxSpec.java75
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerSnapAlgorithm.java4
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/split/FlexParallaxSpec.java172
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/split/NoParallaxSpec.java34
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/split/ParallaxSpec.java62
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/split/ResizingEffectPolicy.java177
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java8
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitState.java5
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/events/ReachabilityGestureListener.kt66
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/events/ReachabilityGestureListenerFactory.kt43
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java16
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java8
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandler.kt3
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandler.kt38
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeShellCommandHandler.kt26
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java7
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt256
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTaskChangeListener.kt6
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt360
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt26
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandler.java3
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/desktopwallpaperactivity/DesktopWallpaperActivityTokenProvider.kt12
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DeskTransition.kt3
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksOrganizer.kt6
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksTransitionObserver.kt45
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizer.kt17
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializerImpl.kt4
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionHandler.java3
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionStarter.java7
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitionHandler.java4
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java2
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipDismissTargetHandler.java1
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java28
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java197
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransitionState.java3
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/transition/PipExpandHandler.java331
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/transition/PipTransitionUtils.java133
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java3
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java62
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java4
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java65
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java33
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt63
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskOperations.java12
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java5
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/viewhost/ReusableWindowDecorViewHost.kt9
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/SnapEventHandler.kt43
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleTransitionsTest.java2
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayControllerTests.java186
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/WindowContainerTransactionSupplierTest.kt42
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PhonePipKeepClearAlgorithmTest.java (renamed from libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PhonePipKeepClearAlgorithmTest.java)7
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PhoneSizeSpecSourceTest.java (renamed from libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PhoneSizeSpecSourceTest.java)7
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PipBoundsAlgorithmTest.java (renamed from libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsAlgorithmTest.java)11
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PipBoundsStateTest.java (renamed from libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsStateTest.java)8
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PipDoubleTapHelperTest.java (renamed from libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipDoubleTapHelperTest.java)6
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PipSnapAlgorithmTest.java (renamed from libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipSnapAlgorithmTest.java)6
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/FlexParallaxSpecTests.java401
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/events/ReachabilityGestureListenerFactoryTest.kt131
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/events/ReachabilityGestureListenerTest.kt146
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt137
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt170
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt446
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt37
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/desktopwallpaperactivity/DesktopWallpaperActivityTokenProviderTest.kt128
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/DesksTransitionObserverTest.kt67
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizerTest.kt102
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipSchedulerTest.java51
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipTouchStateTest.java148
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/transition/PipExpandHandlerTest.java188
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatusTest.kt46
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt6
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTestsBase.kt3
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java25
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java44
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/common/viewhost/ReusableWindowDecorViewHostTest.kt61
-rw-r--r--libs/androidfw/ApkAssets.cpp9
-rw-r--r--libs/androidfw/AssetManager2.cpp5
-rw-r--r--libs/androidfw/AssetsProvider.cpp91
-rw-r--r--libs/androidfw/Idmap.cpp21
-rw-r--r--libs/androidfw/LocaleDataLookup.cpp10
-rw-r--r--libs/androidfw/ResourceTypes.cpp78
-rw-r--r--libs/androidfw/Util.cpp25
-rw-r--r--libs/androidfw/include/androidfw/ApkAssets.h2
-rw-r--r--libs/androidfw/include/androidfw/AssetsProvider.h29
-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/hwui/aconfig/hwui_flags.aconfig18
-rw-r--r--libs/hwui/jni/Shader.cpp15
-rw-r--r--libs/hwui/jni/android_graphics_HardwareRenderer.cpp16
-rw-r--r--media/java/android/media/AudioDeviceVolumeManager.java55
-rw-r--r--media/java/android/media/AudioSystem.java15
-rw-r--r--media/java/android/media/MediaCodec.java2
-rw-r--r--media/java/android/media/MediaRoute2ProviderService.java167
-rw-r--r--media/java/android/media/RingtoneManager.java20
-rw-r--r--media/java/android/media/Utils.java6
-rw-r--r--packages/SettingsLib/CollapsingToolbarBaseActivity/Android.bp1
-rw-r--r--packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceBindings.kt6
-rw-r--r--packages/SettingsLib/SettingsTheme/src/com/android/settingslib/widget/SettingsBasePreferenceFragment.kt11
-rw-r--r--packages/SettingsLib/SettingsTransition/Android.bp1
-rw-r--r--packages/SettingsLib/TopIntroPreference/Android.bp1
-rw-r--r--packages/SettingsLib/res/drawable/ic_mobile_0_4_bar.xml26
-rw-r--r--packages/SettingsLib/res/drawable/ic_mobile_0_4_bar_error.xml71
-rw-r--r--packages/SettingsLib/res/drawable/ic_mobile_0_5_bar.xml28
-rw-r--r--packages/SettingsLib/res/drawable/ic_mobile_0_5_bar_error.xml80
-rw-r--r--packages/SettingsLib/res/drawable/ic_mobile_1_4_bar.xml22
-rw-r--r--packages/SettingsLib/res/drawable/ic_mobile_1_4_bar_error.xml54
-rw-r--r--packages/SettingsLib/res/drawable/ic_mobile_1_5_bar.xml26
-rw-r--r--packages/SettingsLib/res/drawable/ic_mobile_1_5_bar_error.xml78
-rw-r--r--packages/SettingsLib/res/drawable/ic_mobile_2_4_bar.xml22
-rw-r--r--packages/SettingsLib/res/drawable/ic_mobile_2_4_bar_error.xml67
-rw-r--r--packages/SettingsLib/res/drawable/ic_mobile_2_5_bar.xml26
-rw-r--r--packages/SettingsLib/res/drawable/ic_mobile_2_5_bar_error.xml76
-rw-r--r--packages/SettingsLib/res/drawable/ic_mobile_3_4_bar.xml20
-rw-r--r--packages/SettingsLib/res/drawable/ic_mobile_3_4_bar_error.xml65
-rw-r--r--packages/SettingsLib/res/drawable/ic_mobile_3_5_bar.xml24
-rw-r--r--packages/SettingsLib/res/drawable/ic_mobile_3_5_bar_error.xml74
-rw-r--r--packages/SettingsLib/res/drawable/ic_mobile_4_4_bar.xml18
-rw-r--r--packages/SettingsLib/res/drawable/ic_mobile_4_4_bar_error.xml63
-rw-r--r--packages/SettingsLib/res/drawable/ic_mobile_4_5_bar.xml22
-rw-r--r--packages/SettingsLib/res/drawable/ic_mobile_4_5_bar_error.xml72
-rw-r--r--packages/SettingsLib/res/drawable/ic_mobile_5_5_bar.xml20
-rw-r--r--packages/SettingsLib/res/drawable/ic_mobile_5_5_bar_error.xml70
-rw-r--r--packages/SettingsLib/res/values/strings.xml3
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java108
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/graph/SignalDrawable.java12
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/notification/data/repository/ZenModeRepository.kt24
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/notification/modes/EnableDndDialogFactory.java18
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/utils/CustomDialogHelper.java47
-rw-r--r--packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceTest.java86
-rw-r--r--packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/data/repository/ZenModeRepositoryTest.kt2
-rw-r--r--packages/SystemUI/OWNERS12
-rw-r--r--packages/SystemUI/aconfig/Android.bp1
-rw-r--r--packages/SystemUI/aconfig/systemui.aconfig67
-rw-r--r--packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewTransitionAnimatorController.kt57
-rw-r--r--packages/SystemUI/animation/src/com/android/systemui/animation/IViewTransitionRegistry.kt56
-rw-r--r--packages/SystemUI/animation/src/com/android/systemui/animation/TextAnimator.kt297
-rw-r--r--packages/SystemUI/animation/src/com/android/systemui/animation/ViewTransitionRegistry.kt38
-rw-r--r--packages/SystemUI/animation/src/com/android/systemui/animation/ViewTransitionToken.kt10
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenContent.kt10
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprint.kt41
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/MediaCarouselSection.kt7
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/TopAreaSection.kt3
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeOverlay.kt36
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneTransitionLayoutDataSource.kt5
-rw-r--r--packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt27
-rw-r--r--packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleDigitalHandLayerController.kt3
-rw-r--r--packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt186
-rw-r--r--packages/SystemUI/docs/qs-tiles.md167
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardMessageAreaControllerTest.java17
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/activity/data/repository/ActivityManagerRepositoryTest.kt113
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/animation/GhostedViewTransitionAnimatorControllerTest.kt60
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/common/ui/view/SeekBarWithIconButtonsViewTest.java25
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/viewmodel/CommunalTransitionViewModelTest.kt4
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorTest.kt19
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/source/TestShortcuts.kt2
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromAlternateBouncerTransitionInteractorTest.kt33
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractorTest.kt42
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt20
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/scenetransition/LockscreenSceneTransitionInteractorTest.kt39
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/view/layout/blueprints/DefaultKeyguardBlueprintTest.kt2
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/DozingToDreamingTransitionViewModelTest.kt54
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModelTest.kt49
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardMediaViewModelTest.kt17
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelTest.kt162
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/media/dialog/MediaOutputAdapterLegacyTest.java (renamed from packages/SystemUI/multivalentTests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java)96
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModelTest.kt37
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/GridLayoutTypeInteractorTest.kt2
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/QSColumnsInteractorTest.kt2
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/MediaInRowInLandscapeViewModelTest.kt4
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/QSColumnsViewModelTest.kt4
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt42
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractorImplTest.kt4
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/startable/ShadeStartableTest.kt3
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/shared/plugins/PluginInstanceTest.java2
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractorTest.kt67
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractorTest.kt260
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt183
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/TimeRemainingStateTest.kt237
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/featurepods/media/domain/interactor/MediaControlChipInteractorTest.kt117
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/featurepods/media/ui/viewmodel/MediaControlChipViewModelTest.kt65
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/AssistantFeedbackControllerTest.java2
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/DynamicChildBindControllerTest.java2
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/BundleEntryTest.kt75
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/HighPriorityProviderTest.java22
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/ColorizedFgsCoordinatorTest.kt54
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerTest.kt82
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManagerTest.kt118
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewmodel/EmptyShadeViewModelTest.kt14
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImplTest.kt90
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/icon/domain/interactor/NotificationIconsInteractorTest.kt30
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/people/PeopleNotificationIdentifierTest.kt162
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/PromotedNotificationsInteractorTest.kt156
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java11
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerTest.kt12
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationInfoTest.kt8
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImplTest.kt9
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java2
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/PromotedNotificationInfoTest.java11
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/shared/TestActiveNotificationModel.kt7
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModelTest.kt5
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationShelfTest.kt7
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallbackTest.java37
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt2
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/theme/HardwareColorRule.java39
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/theme/HardwareColors.java30
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/theme/ThemeOverlayControllerTest.java194
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioSharingStreamSliderViewModelTest.kt63
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/wallpapers/data/repository/WallpaperRepositoryImplTest.kt8
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/wallpapers/domain/interactor/WallpaperFocalAreaInteractorTest.kt116
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/wallpapers/ui/viewmodel/WallpaperFocalAreaViewModelTest.kt148
-rw-r--r--packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockLogger.kt15
-rw-r--r--packages/SystemUI/res-keyguard/drawable/pin_bouncer_confirm.xml31
-rw-r--r--packages/SystemUI/res-keyguard/drawable/pin_bouncer_delete_filled.xml25
-rw-r--r--packages/SystemUI/res-keyguard/drawable/pin_bouncer_delete_outline.xml31
-rw-r--r--packages/SystemUI/res-keyguard/layout/keyguard_bouncer_message_area.xml2
-rw-r--r--packages/SystemUI/res-keyguard/layout/keyguard_password_motion_layout.xml1
-rw-r--r--packages/SystemUI/res-keyguard/layout/keyguard_password_view.xml1
-rw-r--r--packages/SystemUI/res-keyguard/layout/keyguard_pattern_motion_layout.xml1
-rw-r--r--packages/SystemUI/res-keyguard/layout/keyguard_pattern_view.xml1
-rw-r--r--packages/SystemUI/res-keyguard/layout/keyguard_pin_motion_layout.xml1
-rw-r--r--packages/SystemUI/res-keyguard/layout/keyguard_pin_view.xml1
-rw-r--r--packages/SystemUI/res-keyguard/values/dimens.xml4
-rw-r--r--packages/SystemUI/res/layout/media_session_view.xml2
-rw-r--r--packages/SystemUI/res/layout/notification_2025_hybrid.xml1
-rw-r--r--packages/SystemUI/res/layout/notification_2025_hybrid_conversation.xml1
-rw-r--r--packages/SystemUI/res/values/config.xml5
-rw-r--r--packages/SystemUI/res/values/dimens.xml5
-rw-r--r--packages/SystemUI/res/values/strings.xml4
-rw-r--r--packages/SystemUI/res/values/tiles_states_strings.xml10
-rw-r--r--packages/SystemUI/res/xml/media_session_collapsed.xml7
-rw-r--r--packages/SystemUI/src/com/android/keyguard/KeyguardMessageAreaController.java66
-rw-r--r--packages/SystemUI/src/com/android/keyguard/KeyguardPatternView.java14
-rw-r--r--packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputView.java9
-rw-r--r--packages/SystemUI/src/com/android/keyguard/NumPadAnimator.java45
-rw-r--r--packages/SystemUI/src/com/android/keyguard/NumPadButton.java28
-rw-r--r--packages/SystemUI/src/com/android/keyguard/NumPadKey.java2
-rw-r--r--packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationSettings.java15
-rw-r--r--packages/SystemUI/src/com/android/systemui/accessibility/fontscaling/FontScalingDialogDelegate.kt14
-rw-r--r--packages/SystemUI/src/com/android/systemui/activity/data/model/AppVisibilityModel.kt29
-rw-r--r--packages/SystemUI/src/com/android/systemui/activity/data/repository/ActivityManagerRepository.kt46
-rw-r--r--packages/SystemUI/src/com/android/systemui/bouncer/shared/constants/KeyguardBouncerConstants.kt11
-rw-r--r--packages/SystemUI/src/com/android/systemui/bouncer/ui/binder/KeyguardBouncerViewBinder.kt1
-rw-r--r--packages/SystemUI/src/com/android/systemui/common/ui/view/SeekBarWithIconButtonsView.java13
-rw-r--r--packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSceneRepository.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java2
-rw-r--r--packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/source/MultitaskingShortcutsSource.kt10
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/data/repository/LockscreenSceneTransitionRepository.kt14
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAlternateBouncerTransitionInteractor.kt21
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt8
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractor.kt67
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/scenetransition/LockscreenSceneTransitionInteractor.kt9
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt8
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModel.kt10
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DozingToDreamingTransitionViewModel.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt26
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardMediaViewModel.kt8
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModel.kt156
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataManager.kt6
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt3
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt27
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaViewController.kt10
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/controls/ui/util/MediaViewModelListUpdateCallback.kt3
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapterBase.java (renamed from packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java)312
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapterLegacy.java (renamed from packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java)311
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java4
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialog.java2
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java2
-rw-r--r--packages/SystemUI/src/com/android/systemui/modes/shared/ModesUi.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModel.kt22
-rw-r--r--packages/SystemUI/src/com/android/systemui/privacy/PrivacyDialogV2.kt113
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tileimpl/SubtitleArrayMapping.kt1
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateLegacy.java8
-rw-r--r--packages/SystemUI/src/com/android/systemui/scene/data/repository/SceneContainerRepository.kt8
-rw-r--r--packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt84
-rw-r--r--packages/SystemUI/src/com/android/systemui/scene/domain/resolver/HomeSceneFamilyResolver.kt15
-rw-r--r--packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt37
-rw-r--r--packages/SystemUI/src/com/android/systemui/scene/shared/logger/SceneLogger.kt45
-rw-r--r--packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneDataSource.kt6
-rw-r--r--packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneDataSourceDelegator.kt6
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayAwareModule.kt37
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayChangeLatencyTracker.kt13
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt7
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractor.kt28
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImpl.kt36
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractor.kt14
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/shared/flag/ShadeWindowGoesAround.kt19
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractor.kt44
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractor.kt60
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/model/NotificationChipModel.kt7
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModel.kt48
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/ChipContent.kt23
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/TimeRemainingState.kt122
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/featurepods/media/MediaControlChipStartable.kt43
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/featurepods/media/domain/interactor/MediaControlChipInteractor.kt75
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/featurepods/media/ui/viewmodel/MediaControlChipViewModel.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/BundleEntry.java63
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/EntryAdapter.java44
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifInflaterImpl.java14
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java42
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/PipelineEntry.java2
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/ColorizedFgsCoordinator.java54
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/HighPriorityProvider.java44
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManager.java15
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerImpl.java104
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManager.java28
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManagerImpl.java29
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationStackOptionalModule.kt41
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java1
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImpl.java38
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/domain/interactor/NotificationIconsInteractor.kt23
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/people/PeopleNotificationIdentifier.kt25
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/AODPromotedNotification.kt6
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/AODPromotedNotificationInteractor.kt8
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/PromotedNotificationsInteractor.kt143
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/AsyncRowInflater.kt90
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/BasicRowInflater.kt51
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java84
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java5
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java21
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationInfo.java48
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinder.java21
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt8
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/PromotedNotificationInfo.java14
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindStage.java36
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowInflaterTask.java72
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/icon/AppIconProvider.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java11
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java8
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLogger.kt40
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt8
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt3
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallback.java13
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt43
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernStatusBarMobileView.kt12
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeControllerImpl.java7
-rw-r--r--packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java122
-rw-r--r--packages/SystemUI/src/com/android/systemui/util/kotlin/SysUICoroutinesModule.kt16
-rw-r--r--packages/SystemUI/src/com/android/systemui/util/settings/repository/SecureSettingsForUserRepository.kt35
-rw-r--r--packages/SystemUI/src/com/android/systemui/util/settings/repository/SettingsForUserRepository.kt66
-rw-r--r--packages/SystemUI/src/com/android/systemui/util/settings/repository/UserAwareSettingsRepository.kt3
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioSharingStreamSliderViewModel.kt23
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModel.kt11
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/panel/shared/VolumePanelLogger.kt52
-rw-r--r--packages/SystemUI/src/com/android/systemui/wallpapers/data/repository/NoopWallpaperRepository.kt6
-rw-r--r--packages/SystemUI/src/com/android/systemui/wallpapers/data/repository/WallpaperFocalAreaRepository.kt18
-rw-r--r--packages/SystemUI/src/com/android/systemui/wallpapers/data/repository/WallpaperRepository.kt125
-rw-r--r--packages/SystemUI/src/com/android/systemui/wallpapers/domain/interactor/WallpaperFocalAreaInteractor.kt62
-rw-r--r--packages/SystemUI/src/com/android/systemui/wallpapers/ui/viewmodel/WallpaperFocalAreaViewModel.kt30
-rw-r--r--packages/SystemUI/src/com/android/systemui/window/ui/WindowRootViewBinder.kt13
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/OnTeardownRuleTest.kt (renamed from packages/SystemUI/multivalentTests/src/com/android/systemui/OnTeardownRuleTest.kt)0
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/accessibility/IMagnificationConnectionTest.java (renamed from packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/IMagnificationConnectionTest.java)0
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationSettingsTest.java13
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/animation/TextAnimatorTest.kt28
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt (renamed from packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt)0
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDialogDelegateTest.kt (renamed from packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDialogDelegateTest.kt)0
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayAnimationsControllerTest.kt (renamed from packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayAnimationsControllerTest.kt)0
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/dump/LogEulogizerTest.kt (renamed from packages/SystemUI/multivalentTests/src/com/android/systemui/dump/LogEulogizerTest.kt)0
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/education/domain/ui/view/ContextualEduUiCoordinatorTest.kt (renamed from packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/ui/view/ContextualEduUiCoordinatorTest.kt)0
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModelTest.kt (renamed from packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModelTest.kt)0
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/lifecycle/HydratorTest.kt (renamed from packages/SystemUI/multivalentTests/src/com/android/systemui/lifecycle/HydratorTest.kt)0
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerTest.kt2
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java3
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/QSFragmentComposeTest.kt (renamed from packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/QSFragmentComposeTest.kt)0
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/sensorprivacy/SensorUseStartedActivityTest.kt (renamed from packages/SystemUI/multivalentTests/src/com/android/systemui/sensorprivacy/SensorUseStartedActivityTest.kt)0
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerImplTest.java (renamed from packages/SystemUI/multivalentTests/src/com/android/systemui/shade/QuickSettingsControllerImplTest.java)0
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/shadow/DoubleShadowTextClockTest.kt (renamed from packages/SystemUI/multivalentTests/src/com/android/systemui/shadow/DoubleShadowTextClockTest.kt)0
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/shared/clocks/AnimatableClockViewTest.kt (renamed from packages/SystemUI/multivalentTests/src/com/android/systemui/shared/clocks/AnimatableClockViewTest.kt)55
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/shared/condition/CombinedConditionTest.kt (renamed from packages/SystemUI/multivalentTests/src/com/android/systemui/shared/condition/CombinedConditionTest.kt)0
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/shared/system/UncaughtExceptionPreHandlerTest.kt (renamed from packages/SystemUI/multivalentTests/src/com/android/systemui/shared/system/UncaughtExceptionPreHandlerTest.kt)0
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/featurepods/popups/ui/viewmodel/StatusBarPopupChipsViewModelTest.kt (renamed from packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/featurepods/popups/ui/viewmodel/StatusBarPopupChipsViewModelTest.kt)0
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/layout/StatusBarBoundsProviderTest.kt (renamed from packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/layout/StatusBarBoundsProviderTest.kt)0
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryTest.java148
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorTest.java (renamed from packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorTest.java)0
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/BigPictureIconManagerTest.kt (renamed from packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/BigPictureIconManagerTest.kt)0
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java51
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.kt38
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerWithScenesTest.kt16
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/RowImageInflaterTest.kt (renamed from packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/RowImageInflaterTest.kt)0
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.kt (renamed from packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.kt)0
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java107
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java (renamed from packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java)0
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TouchableRegionViewControllerTest.kt (renamed from packages/SystemUI/multivalentTests/src/com/android/systemui/temporarydisplay/TouchableRegionViewControllerTest.kt)0
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/unfold/FoldAodAnimationControllerTest.kt (renamed from packages/SystemUI/multivalentTests/src/com/android/systemui/unfold/FoldAodAnimationControllerTest.kt)0
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/unfold/UnfoldLatencyTrackerTest.kt (renamed from packages/SystemUI/multivalentTests/src/com/android/systemui/unfold/UnfoldLatencyTrackerTest.kt)0
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/unfold/progress/PhysicsBasedUnfoldTransitionProgressProviderTest.kt (renamed from packages/SystemUI/multivalentTests/src/com/android/systemui/unfold/progress/PhysicsBasedUnfoldTransitionProgressProviderTest.kt)0
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/unfold/progress/UnfoldRemoteFilterTest.kt (renamed from packages/SystemUI/multivalentTests/src/com/android/systemui/unfold/progress/UnfoldRemoteFilterTest.kt)0
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/usb/UsbPermissionActivityTest.kt (renamed from packages/SystemUI/multivalentTests/src/com/android/systemui/usb/UsbPermissionActivityTest.kt)0
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/user/CreateUserActivityTest.kt (renamed from packages/SystemUI/multivalentTests/src/com/android/systemui/user/CreateUserActivityTest.kt)0
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/util/kotlin/FlowUtilTests.kt (renamed from packages/SystemUI/multivalentTests/src/com/android/systemui/util/kotlin/FlowUtilTests.kt)0
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/util/kotlin/PackageManagerExtComponentEnabledTest.kt (renamed from packages/SystemUI/multivalentTests/src/com/android/systemui/util/kotlin/PackageManagerExtComponentEnabledTest.kt)0
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/util/sensors/AsyncSensorManagerTest.java (renamed from packages/SystemUI/multivalentTests/src/com/android/systemui/util/sensors/AsyncSensorManagerTest.java)0
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/wallpapers/GradientColorWallpaperTest.kt (renamed from packages/SystemUI/multivalentTests/src/com/android/systemui/wallpapers/GradientColorWallpaperTest.kt)0
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/activity/data/repository/ActivityManagerRepositoryKosmos.kt56
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractorKosmos.kt4
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModelKosmos.kt4
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardMediaViewModelFactoryKosmos.kt7
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelKosmos.kt4
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/FakeSceneDataSource.kt6
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeDisplayChangeLatencyTrackerKosmos.kt2
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractorKosmos.kt5
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeOverlayContentViewModelKosmos.kt4
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelKosmos.kt2
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/NotificationEntryBuilderKosmos.kt90
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/NotifPipelineKosmos.kt5
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/icon/domain/interactor/NotificationIconsInteractorKosmos.kt2
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractorKosmos.kt14
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/AODPromotedNotificationInteractorKosmos.kt3
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/PromotedNotificationsInteractorKosmos.kt33
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowBuilder.kt6
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinderKosmos.kt2
-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/util/settings/data/repository/SecureSettingsForUserRepositoryKosmos.kt28
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/util/time/FakeSystemClockKosmos.kt3
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioSharingStreamSliderViewModelKosmos.kt4
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModelKosmos.kt2
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/wallpapers/data/repository/FakeWallpaperFocalAreaRepository.kt9
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/wallpapers/data/repository/FakeWallpaperRepository.kt8
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/wallpapers/data/repository/WallpaperRepositoryKosmos.kt3
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/wallpapers/domain/interactor/WallpaperFocalAreaInteractor.kt8
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/wallpapers/ui/viewmodel/WallpaperFocalAreaViewModel.kt6
-rw-r--r--ravenwood/runtime-jni/ravenwood_initializer.cpp77
-rw-r--r--services/accessibility/accessibility.aconfig7
-rw-r--r--services/contextualsearch/java/com/android/server/contextualsearch/ContextualSearchManagerService.java77
-rw-r--r--services/core/java/com/android/server/UiModeManagerService.java15
-rw-r--r--services/core/java/com/android/server/am/AppStartInfoTracker.java40
-rw-r--r--services/core/java/com/android/server/appop/DiscreteOpsSqlRegistry.java4
-rw-r--r--services/core/java/com/android/server/audio/AudioDeviceBroker.java39
-rw-r--r--services/core/java/com/android/server/audio/AudioDeviceInventory.java73
-rw-r--r--services/core/java/com/android/server/audio/AudioService.java3
-rw-r--r--services/core/java/com/android/server/audio/AudioSystemAdapter.java4
-rw-r--r--services/core/java/com/android/server/audio/BtHelper.java29
-rw-r--r--services/core/java/com/android/server/backup/SystemBackupAgent.java8
-rw-r--r--services/core/java/com/android/server/backup/WearBackupHelper.java49
-rw-r--r--services/core/java/com/android/server/backup/WearBackupInternal.java32
-rw-r--r--services/core/java/com/android/server/display/DisplayManagerService.java18
-rw-r--r--services/core/java/com/android/server/display/LogicalDisplayMapper.java12
-rw-r--r--services/core/java/com/android/server/display/VirtualDisplayAdapter.java33
-rw-r--r--services/core/java/com/android/server/display/feature/DisplayManagerFlags.java14
-rw-r--r--services/core/java/com/android/server/display/feature/display_flags.aconfig14
-rw-r--r--services/core/java/com/android/server/dreams/DreamManagerService.java3
-rw-r--r--services/core/java/com/android/server/hdmi/AudioDeviceVolumeManagerWrapper.java8
-rw-r--r--services/core/java/com/android/server/hdmi/DefaultAudioDeviceVolumeManagerWrapper.java14
-rw-r--r--services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java3
-rw-r--r--services/core/java/com/android/server/hdmi/HdmiControlService.java8
-rw-r--r--services/core/java/com/android/server/input/InputManagerInternal.java38
-rw-r--r--services/core/java/com/android/server/input/InputManagerService.java60
-rw-r--r--services/core/java/com/android/server/input/NativeInputManagerService.java13
-rw-r--r--services/core/java/com/android/server/inputmethod/InputMethodManagerService.java29
-rw-r--r--services/core/java/com/android/server/media/MediaRoute2ProviderWatcher.java3
-rw-r--r--services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java26
-rw-r--r--services/core/java/com/android/server/media/quality/MediaQualityService.java140
-rw-r--r--services/core/java/com/android/server/notification/ManagedServices.java293
-rw-r--r--services/core/java/com/android/server/notification/NotificationManagerService.java230
-rw-r--r--services/core/java/com/android/server/notification/NotificationShellCmd.java9
-rw-r--r--services/core/java/com/android/server/notification/PreferencesHelper.java65
-rw-r--r--services/core/java/com/android/server/notification/ZenModeEventLogger.java117
-rw-r--r--services/core/java/com/android/server/notification/ZenModeFiltering.java11
-rw-r--r--services/core/java/com/android/server/notification/ZenModeHelper.java622
-rw-r--r--services/core/java/com/android/server/notification/flags.aconfig7
-rw-r--r--services/core/java/com/android/server/pm/BackgroundInstallControlService.java35
-rw-r--r--services/core/java/com/android/server/policy/PhoneWindowManager.java35
-rw-r--r--services/core/java/com/android/server/power/PowerManagerService.java12
-rw-r--r--services/core/java/com/android/server/resources/ResourcesManagerShellCommand.java1
-rw-r--r--services/core/java/com/android/server/security/AttestationVerificationPeerDeviceVerifier.java6
-rw-r--r--services/core/java/com/android/server/security/CertificateRevocationStatusManager.java45
-rw-r--r--services/core/java/com/android/server/security/FileIntegrityService.java5
-rw-r--r--services/core/java/com/android/server/security/intrusiondetection/DataAggregator.java2
-rw-r--r--services/core/java/com/android/server/security/intrusiondetection/NetworkLogSource.java4
-rw-r--r--services/core/java/com/android/server/security/intrusiondetection/SecurityLogSource.java15
-rw-r--r--services/core/java/com/android/server/wm/ActivityRecord.java4
-rw-r--r--services/core/java/com/android/server/wm/ActivityTaskSupervisor.java16
-rw-r--r--services/core/java/com/android/server/wm/AppCompatController.java8
-rw-r--r--services/core/java/com/android/server/wm/AppCompatSandboxingPolicy.java65
-rw-r--r--services/core/java/com/android/server/wm/AppCompatSizeCompatModePolicy.java8
-rw-r--r--services/core/java/com/android/server/wm/AppCompatUtils.java12
-rw-r--r--services/core/java/com/android/server/wm/BackNavigationController.java4
-rw-r--r--services/core/java/com/android/server/wm/DeferredDisplayUpdater.java8
-rw-r--r--services/core/java/com/android/server/wm/DesktopModeHelper.java38
-rw-r--r--services/core/java/com/android/server/wm/DisplayContent.java54
-rw-r--r--services/core/java/com/android/server/wm/DragState.java55
-rw-r--r--services/core/java/com/android/server/wm/KeyguardController.java24
-rw-r--r--services/core/java/com/android/server/wm/Task.java3
-rw-r--r--services/core/java/com/android/server/wm/Transition.java5
-rw-r--r--services/core/java/com/android/server/wm/TransitionController.java13
-rw-r--r--services/core/java/com/android/server/wm/WallpaperController.java25
-rw-r--r--services/core/jni/com_android_server_input_InputManagerService.cpp34
-rw-r--r--services/supervision/java/com/android/server/supervision/SupervisionService.java40
-rw-r--r--services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java500
-rw-r--r--services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/src/com/android/apps/inputmethod/simpleime/ims/InputMethodServiceWrapper.java6
-rw-r--r--services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageParserTest.java43
-rw-r--r--services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayMapperTest.java11
-rw-r--r--services/tests/mockingservicestests/src/com/android/server/job/controllers/JobStatusTest.java86
-rw-r--r--services/tests/powerservicetests/src/com/android/server/power/NotifierTest.java36
-rw-r--r--services/tests/powerservicetests/src/com/android/server/power/PowerManagerServiceTest.java43
-rw-r--r--services/tests/powerstatstests/res/raw/battery-history.zipbin0 -> 272063 bytes
-rw-r--r--services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsProviderPerfTest.java179
-rw-r--r--services/tests/security/intrusiondetection/src/com/android/server/security/intrusiondetection/IntrusionDetectionServiceTest.java178
-rw-r--r--services/tests/servicestests/src/com/android/server/audio/AudioDeviceInventoryTest.java4
-rw-r--r--services/tests/servicestests/src/com/android/server/audio/NoOpAudioSystemAdapter.java6
-rw-r--r--services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java6
-rw-r--r--services/tests/servicestests/src/com/android/server/hdmi/BaseAbsoluteVolumeBehaviorTest.java2
-rw-r--r--services/tests/servicestests/src/com/android/server/hdmi/BaseTvToAudioSystemAvbTest.java6
-rw-r--r--services/tests/servicestests/src/com/android/server/hdmi/FakeAudioFramework.java8
-rw-r--r--services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java2
-rw-r--r--services/tests/uiservicestests/Android.bp1
-rw-r--r--services/tests/uiservicestests/src/android/app/NotificationManagerZenTest.java12
-rw-r--r--services/tests/uiservicestests/src/com/android/server/UiModeManagerServiceTest.java4
-rw-r--r--services/tests/uiservicestests/src/com/android/server/UiServiceTestCase.java4
-rw-r--r--services/tests/uiservicestests/src/com/android/server/notification/DefaultDeviceEffectsApplierTest.java24
-rw-r--r--services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java496
-rw-r--r--services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java221
-rw-r--r--services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java66
-rw-r--r--services/tests/uiservicestests/src/com/android/server/notification/RankingHelperTest.java8
-rw-r--r--services/tests/uiservicestests/src/com/android/server/notification/ZenAdaptersTest.java22
-rw-r--r--services/tests/uiservicestests/src/com/android/server/notification/ZenDeviceEffectsTest.java7
-rw-r--r--services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java57
-rw-r--r--services/tests/uiservicestests/src/com/android/server/notification/ZenModeDiffTest.java88
-rw-r--r--services/tests/uiservicestests/src/com/android/server/notification/ZenModeFilteringTest.java3
-rw-r--r--services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java795
-rw-r--r--services/tests/uiservicestests/src/com/android/server/notification/ZenPolicyTest.java35
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/ActivityTaskSupervisorTests.java26
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/DesktopModeHelperTest.java50
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java68
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java43
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/WindowTracingPerfettoTest.java39
-rw-r--r--telephony/java/android/telephony/CarrierConfigManager.java15
-rw-r--r--telephony/java/android/telephony/satellite/SatelliteManager.java48
-rw-r--r--telephony/java/com/android/internal/telephony/ITelephony.aidl11
-rw-r--r--tests/AppJankTest/res/values/strings.xml3
-rw-r--r--tests/AppJankTest/src/android/app/jank/tests/IntegrationTests.java3
-rw-r--r--tests/AppJankTest/src/android/app/jank/tests/JankTrackerActivity.java26
-rw-r--r--tests/AttestationVerificationTest/src/com/android/server/security/CertificateRevocationStatusManagerTest.java66
-rw-r--r--tests/Input/src/com/android/server/input/InputManagerServiceTests.kt2
-rw-r--r--tests/utils/testutils/java/android/os/test/TestLooper.java168
-rw-r--r--tests/vcn/Android.bp10
-rw-r--r--tests/vcn/AndroidManifest.xml5
-rw-r--r--tests/vcn/AndroidTest.xml10
-rw-r--r--tests/vcn/java/android/net/vcn/VcnCellUnderlyingNetworkTemplateTest.java13
-rw-r--r--tests/vcn/java/android/net/vcn/VcnConfigTest.java10
-rw-r--r--tests/vcn/java/android/net/vcn/VcnGatewayConnectionConfigTest.java10
-rw-r--r--tests/vcn/java/android/net/vcn/VcnManagerTest.java12
-rw-r--r--tests/vcn/java/android/net/vcn/VcnTransportInfoTest.java12
-rw-r--r--tests/vcn/java/android/net/vcn/VcnUnderlyingNetworkPolicyTest.java12
-rw-r--r--tests/vcn/java/android/net/vcn/VcnUnderlyingNetworkSpecifierTest.java10
-rw-r--r--tests/vcn/java/android/net/vcn/VcnUtilsTest.java12
-rw-r--r--tests/vcn/java/android/net/vcn/VcnWifiUnderlyingNetworkTemplateTest.java13
-rw-r--r--tests/vcn/java/android/net/vcn/persistablebundleutils/EapSessionConfigUtilsTest.java10
-rw-r--r--tests/vcn/java/android/net/vcn/persistablebundleutils/IkeIdentificationUtilsTest.java10
-rw-r--r--tests/vcn/java/android/net/vcn/persistablebundleutils/IkeSessionParamsUtilsTest.java9
-rw-r--r--tests/vcn/java/android/net/vcn/persistablebundleutils/IkeTrafficSelectorUtilsTest.java10
-rw-r--r--tests/vcn/java/android/net/vcn/persistablebundleutils/SaProposalUtilsTest.java10
-rw-r--r--tests/vcn/java/android/net/vcn/persistablebundleutils/TunnelConnectionParamsUtilsTest.java10
-rw-r--r--tests/vcn/java/android/net/vcn/persistablebundleutils/TunnelModeChildSessionParamsUtilsTest.java10
-rw-r--r--tests/vcn/java/android/net/vcn/util/MtuUtilsTest.java10
-rw-r--r--tests/vcn/java/android/net/vcn/util/PersistableBundleUtilsTest.java10
-rw-r--r--tests/vcn/java/com/android/server/VcnManagementServiceTest.java10
-rw-r--r--tests/vcn/java/com/android/server/vcn/TelephonySubscriptionTrackerTest.java10
-rw-r--r--tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionConnectedStateTest.java10
-rw-r--r--tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionConnectingStateTest.java11
-rw-r--r--tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionDisconnectedStateTest.java11
-rw-r--r--tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionDisconnectingStateTest.java12
-rw-r--r--tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionRetryTimeoutStateTest.java12
-rw-r--r--tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTest.java10
-rw-r--r--tests/vcn/java/com/android/server/vcn/VcnNetworkProviderTest.java10
-rw-r--r--tests/vcn/java/com/android/server/vcn/VcnTest.java11
-rw-r--r--tests/vcn/java/com/android/server/vcn/routeselection/IpSecPacketLossDetectorTest.java11
-rw-r--r--tests/vcn/java/com/android/server/vcn/routeselection/NetworkPriorityClassifierTest.java12
-rw-r--r--tests/vcn/java/com/android/server/vcn/routeselection/UnderlyingNetworkControllerTest.java11
-rw-r--r--tests/vcn/java/com/android/server/vcn/routeselection/UnderlyingNetworkEvaluatorTest.java11
-rw-r--r--tools/aapt2/Debug.cpp28
705 files changed, 20478 insertions, 7343 deletions
diff --git a/apex/jobscheduler/service/aconfig/job.aconfig b/apex/jobscheduler/service/aconfig/job.aconfig
index 86ed06bf4e3d..29df80fda33d 100644
--- a/apex/jobscheduler/service/aconfig/job.aconfig
+++ b/apex/jobscheduler/service/aconfig/job.aconfig
@@ -105,4 +105,14 @@ flag {
metadata {
purpose: PURPOSE_BUGFIX
}
-} \ No newline at end of file
+}
+
+flag {
+ name: "include_trace_tag_in_job_name"
+ namespace: "backstage_power"
+ description: "Add the trace tag to the job name"
+ bug: "354795473"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java
index aaf69864fe97..2d069f934d0d 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java
@@ -674,6 +674,12 @@ public final class JobStatus {
this.job = job;
StringBuilder batteryName = new StringBuilder();
+ if (com.android.server.job.Flags.includeTraceTagInJobName()) {
+ final String filteredTraceTag = this.getFilteredTraceTag();
+ if (filteredTraceTag != null) {
+ batteryName.append("#").append(filteredTraceTag).append("#");
+ }
+ }
if (namespace != null) {
batteryName.append("@").append(namespace).append("@");
}
diff --git a/core/api/current.txt b/core/api/current.txt
index d4ed533cad9e..5c0ecf72030f 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -100,6 +100,9 @@ package android {
field public static final String EXECUTE_APP_ACTION = "android.permission.EXECUTE_APP_ACTION";
field @FlaggedApi("android.app.appfunctions.flags.enable_app_function_manager") public static final String EXECUTE_APP_FUNCTIONS = "android.permission.EXECUTE_APP_FUNCTIONS";
field public static final String EXPAND_STATUS_BAR = "android.permission.EXPAND_STATUS_BAR";
+ field @FlaggedApi("android.xr.xr_manifest_entries") public static final String EYE_TRACKING_COARSE = "android.permission.EYE_TRACKING_COARSE";
+ field @FlaggedApi("android.xr.xr_manifest_entries") public static final String EYE_TRACKING_FINE = "android.permission.EYE_TRACKING_FINE";
+ field @FlaggedApi("android.xr.xr_manifest_entries") public static final String FACE_TRACKING = "android.permission.FACE_TRACKING";
field public static final String FACTORY_TEST = "android.permission.FACTORY_TEST";
field public static final String FOREGROUND_SERVICE = "android.permission.FOREGROUND_SERVICE";
field public static final String FOREGROUND_SERVICE_CAMERA = "android.permission.FOREGROUND_SERVICE_CAMERA";
@@ -120,6 +123,8 @@ package android {
field public static final String GET_PACKAGE_SIZE = "android.permission.GET_PACKAGE_SIZE";
field @Deprecated public static final String GET_TASKS = "android.permission.GET_TASKS";
field public static final String GLOBAL_SEARCH = "android.permission.GLOBAL_SEARCH";
+ field @FlaggedApi("android.xr.xr_manifest_entries") public static final String HAND_TRACKING = "android.permission.HAND_TRACKING";
+ field @FlaggedApi("android.xr.xr_manifest_entries") public static final String HEAD_TRACKING = "android.permission.HEAD_TRACKING";
field public static final String HIDE_OVERLAY_WINDOWS = "android.permission.HIDE_OVERLAY_WINDOWS";
field public static final String HIGH_SAMPLING_RATE_SENSORS = "android.permission.HIGH_SAMPLING_RATE_SENSORS";
field public static final String INSTALL_LOCATION_PROVIDER = "android.permission.INSTALL_LOCATION_PROVIDER";
@@ -295,6 +300,8 @@ package android {
field public static final String REQUEST_PASSWORD_COMPLEXITY = "android.permission.REQUEST_PASSWORD_COMPLEXITY";
field @Deprecated public static final String RESTART_PACKAGES = "android.permission.RESTART_PACKAGES";
field public static final String RUN_USER_INITIATED_JOBS = "android.permission.RUN_USER_INITIATED_JOBS";
+ field @FlaggedApi("android.xr.xr_manifest_entries") public static final String SCENE_UNDERSTANDING_COARSE = "android.permission.SCENE_UNDERSTANDING_COARSE";
+ field @FlaggedApi("android.xr.xr_manifest_entries") public static final String SCENE_UNDERSTANDING_FINE = "android.permission.SCENE_UNDERSTANDING_FINE";
field public static final String SCHEDULE_EXACT_ALARM = "android.permission.SCHEDULE_EXACT_ALARM";
field public static final String SEND_RESPOND_VIA_MESSAGE = "android.permission.SEND_RESPOND_VIA_MESSAGE";
field public static final String SEND_SMS = "android.permission.SEND_SMS";
@@ -362,6 +369,8 @@ package android {
field public static final String SENSORS = "android.permission-group.SENSORS";
field public static final String SMS = "android.permission-group.SMS";
field public static final String STORAGE = "android.permission-group.STORAGE";
+ field @FlaggedApi("android.xr.xr_manifest_entries") public static final String XR_TRACKING = "android.permission-group.XR_TRACKING";
+ field @FlaggedApi("android.xr.xr_manifest_entries") public static final String XR_TRACKING_SENSITIVE = "android.permission-group.XR_TRACKING_SENSITIVE";
}
public final class R {
@@ -5479,37 +5488,37 @@ package android.app {
method public android.net.Uri getConditionId();
method @Nullable public android.content.ComponentName getConfigurationActivity();
method public long getCreationTime();
- method @FlaggedApi("android.app.modes_api") @Nullable public android.service.notification.ZenDeviceEffects getDeviceEffects();
- method @FlaggedApi("android.app.modes_api") @DrawableRes public int getIconResId();
+ method @Nullable public android.service.notification.ZenDeviceEffects getDeviceEffects();
+ method @DrawableRes public int getIconResId();
method public int getInterruptionFilter();
method public String getName();
method public android.content.ComponentName getOwner();
- method @FlaggedApi("android.app.modes_api") @Nullable public String getTriggerDescription();
- method @FlaggedApi("android.app.modes_api") public int getType();
+ method @Nullable public String getTriggerDescription();
+ method public int getType();
method @Nullable public android.service.notification.ZenPolicy getZenPolicy();
method public boolean isEnabled();
- method @FlaggedApi("android.app.modes_api") public boolean isManualInvocationAllowed();
+ method public boolean isManualInvocationAllowed();
method public void setConditionId(android.net.Uri);
method public void setConfigurationActivity(@Nullable android.content.ComponentName);
- method @FlaggedApi("android.app.modes_api") public void setDeviceEffects(@Nullable android.service.notification.ZenDeviceEffects);
+ method public void setDeviceEffects(@Nullable android.service.notification.ZenDeviceEffects);
method public void setEnabled(boolean);
method public void setInterruptionFilter(int);
method public void setName(String);
method public void setZenPolicy(@Nullable android.service.notification.ZenPolicy);
method public void writeToParcel(android.os.Parcel, int);
field @NonNull public static final android.os.Parcelable.Creator<android.app.AutomaticZenRule> CREATOR;
- field @FlaggedApi("android.app.modes_api") public static final int TYPE_BEDTIME = 3; // 0x3
- field @FlaggedApi("android.app.modes_api") public static final int TYPE_DRIVING = 4; // 0x4
- field @FlaggedApi("android.app.modes_api") public static final int TYPE_IMMERSIVE = 5; // 0x5
- field @FlaggedApi("android.app.modes_api") public static final int TYPE_MANAGED = 7; // 0x7
- field @FlaggedApi("android.app.modes_api") public static final int TYPE_OTHER = 0; // 0x0
- field @FlaggedApi("android.app.modes_api") public static final int TYPE_SCHEDULE_CALENDAR = 2; // 0x2
- field @FlaggedApi("android.app.modes_api") public static final int TYPE_SCHEDULE_TIME = 1; // 0x1
- field @FlaggedApi("android.app.modes_api") public static final int TYPE_THEATER = 6; // 0x6
- field @FlaggedApi("android.app.modes_api") public static final int TYPE_UNKNOWN = -1; // 0xffffffff
- }
-
- @FlaggedApi("android.app.modes_api") public static final class AutomaticZenRule.Builder {
+ field public static final int TYPE_BEDTIME = 3; // 0x3
+ field public static final int TYPE_DRIVING = 4; // 0x4
+ field public static final int TYPE_IMMERSIVE = 5; // 0x5
+ field public static final int TYPE_MANAGED = 7; // 0x7
+ field public static final int TYPE_OTHER = 0; // 0x0
+ field public static final int TYPE_SCHEDULE_CALENDAR = 2; // 0x2
+ field public static final int TYPE_SCHEDULE_TIME = 1; // 0x1
+ field public static final int TYPE_THEATER = 6; // 0x6
+ field public static final int TYPE_UNKNOWN = -1; // 0xffffffff
+ }
+
+ public static final class AutomaticZenRule.Builder {
ctor public AutomaticZenRule.Builder(@NonNull android.app.AutomaticZenRule);
ctor public AutomaticZenRule.Builder(@NonNull String, @NonNull android.net.Uri);
method @NonNull public android.app.AutomaticZenRule build();
@@ -7127,7 +7136,7 @@ package android.app {
public class NotificationManager {
method public String addAutomaticZenRule(android.app.AutomaticZenRule);
- method @FlaggedApi("android.app.modes_api") public boolean areAutomaticZenRulesUserManaged();
+ method public boolean areAutomaticZenRulesUserManaged();
method @Deprecated public boolean areBubblesAllowed();
method public boolean areBubblesEnabled();
method public boolean areNotificationsEnabled();
@@ -7147,7 +7156,7 @@ package android.app {
method public void deleteNotificationChannelGroup(String);
method public android.service.notification.StatusBarNotification[] getActiveNotifications();
method public android.app.AutomaticZenRule getAutomaticZenRule(String);
- method @FlaggedApi("android.app.modes_api") public int getAutomaticZenRuleState(@NonNull String);
+ method public int getAutomaticZenRuleState(@NonNull String);
method public java.util.Map<java.lang.String,android.app.AutomaticZenRule> getAutomaticZenRules();
method public int getBubblePreference();
method @NonNull public android.app.NotificationManager.Policy getConsolidatedNotificationPolicy();
@@ -7176,14 +7185,14 @@ package android.app {
field public static final String ACTION_APP_BLOCK_STATE_CHANGED = "android.app.action.APP_BLOCK_STATE_CHANGED";
field public static final String ACTION_AUTOMATIC_ZEN_RULE = "android.app.action.AUTOMATIC_ZEN_RULE";
field public static final String ACTION_AUTOMATIC_ZEN_RULE_STATUS_CHANGED = "android.app.action.AUTOMATIC_ZEN_RULE_STATUS_CHANGED";
- field @FlaggedApi("android.app.modes_api") public static final String ACTION_CONSOLIDATED_NOTIFICATION_POLICY_CHANGED = "android.app.action.CONSOLIDATED_NOTIFICATION_POLICY_CHANGED";
+ field public static final String ACTION_CONSOLIDATED_NOTIFICATION_POLICY_CHANGED = "android.app.action.CONSOLIDATED_NOTIFICATION_POLICY_CHANGED";
field public static final String ACTION_INTERRUPTION_FILTER_CHANGED = "android.app.action.INTERRUPTION_FILTER_CHANGED";
field public static final String ACTION_NOTIFICATION_CHANNEL_BLOCK_STATE_CHANGED = "android.app.action.NOTIFICATION_CHANNEL_BLOCK_STATE_CHANGED";
field public static final String ACTION_NOTIFICATION_CHANNEL_GROUP_BLOCK_STATE_CHANGED = "android.app.action.NOTIFICATION_CHANNEL_GROUP_BLOCK_STATE_CHANGED";
field public static final String ACTION_NOTIFICATION_POLICY_ACCESS_GRANTED_CHANGED = "android.app.action.NOTIFICATION_POLICY_ACCESS_GRANTED_CHANGED";
field public static final String ACTION_NOTIFICATION_POLICY_CHANGED = "android.app.action.NOTIFICATION_POLICY_CHANGED";
- field @FlaggedApi("android.app.modes_api") public static final int AUTOMATIC_RULE_STATUS_ACTIVATED = 4; // 0x4
- field @FlaggedApi("android.app.modes_api") public static final int AUTOMATIC_RULE_STATUS_DEACTIVATED = 5; // 0x5
+ field public static final int AUTOMATIC_RULE_STATUS_ACTIVATED = 4; // 0x4
+ field public static final int AUTOMATIC_RULE_STATUS_DEACTIVATED = 5; // 0x5
field public static final int AUTOMATIC_RULE_STATUS_DISABLED = 2; // 0x2
field public static final int AUTOMATIC_RULE_STATUS_ENABLED = 1; // 0x1
field public static final int AUTOMATIC_RULE_STATUS_REMOVED = 3; // 0x3
@@ -7197,7 +7206,7 @@ package android.app {
field public static final String EXTRA_BLOCKED_STATE = "android.app.extra.BLOCKED_STATE";
field public static final String EXTRA_NOTIFICATION_CHANNEL_GROUP_ID = "android.app.extra.NOTIFICATION_CHANNEL_GROUP_ID";
field public static final String EXTRA_NOTIFICATION_CHANNEL_ID = "android.app.extra.NOTIFICATION_CHANNEL_ID";
- field @FlaggedApi("android.app.modes_api") public static final String EXTRA_NOTIFICATION_POLICY = "android.app.extra.NOTIFICATION_POLICY";
+ field public static final String EXTRA_NOTIFICATION_POLICY = "android.app.extra.NOTIFICATION_POLICY";
field public static final int IMPORTANCE_DEFAULT = 3; // 0x3
field public static final int IMPORTANCE_HIGH = 4; // 0x4
field public static final int IMPORTANCE_LOW = 2; // 0x2
@@ -9188,6 +9197,14 @@ package android.app.blob {
}
+package android.app.contextualsearch {
+
+ @FlaggedApi("android.app.contextualsearch.flags.self_invocation") public final class ContextualSearchManager {
+ method @FlaggedApi("android.app.contextualsearch.flags.self_invocation") public void startContextualSearch();
+ }
+
+}
+
package android.app.jank {
@FlaggedApi("android.app.jank.detailed_app_jank_metrics_api") public final class AppJankStats {
@@ -17604,6 +17621,7 @@ package android.graphics {
method public void setIntUniform(@NonNull String, int, int, int);
method public void setIntUniform(@NonNull String, int, int, int, int);
method public void setIntUniform(@NonNull String, @NonNull int[]);
+ method @FlaggedApi("com.android.graphics.hwui.flags.shader_color_space") public void setWorkingColorSpace(@Nullable android.graphics.ColorSpace);
}
@FlaggedApi("com.android.graphics.hwui.flags.runtime_color_filters_blenders") public class RuntimeXfermode extends android.graphics.Xfermode {
@@ -25039,8 +25057,10 @@ package android.media {
method public final void notifySessionCreated(long, @NonNull android.media.RoutingSessionInfo);
method public final void notifySessionReleased(@NonNull String);
method public final void notifySessionUpdated(@NonNull android.media.RoutingSessionInfo);
+ method @FlaggedApi("com.android.media.flags.enable_mirroring_in_media_router_2") @Nullable @RequiresPermission("android.permission.MODIFY_AUDIO_ROUTING") public final android.media.MediaRoute2ProviderService.MediaStreams notifySystemRoutingSessionCreated(long, @NonNull android.media.RoutingSessionInfo, @NonNull android.media.MediaRoute2ProviderService.MediaStreamsFormats);
method @CallSuper @Nullable public android.os.IBinder onBind(@NonNull android.content.Intent);
method public abstract void onCreateSession(long, @NonNull String, @NonNull String, @Nullable android.os.Bundle);
+ method @FlaggedApi("com.android.media.flags.enable_mirroring_in_media_router_2") public void onCreateSystemRoutingSession(long, @NonNull String, @NonNull android.media.MediaRoute2ProviderService.SystemRoutingSessionParams);
method public abstract void onDeselectRoute(long, @NonNull String, @NonNull String);
method public void onDiscoveryPreferenceChanged(@NonNull android.media.RouteDiscoveryPreference);
method public abstract void onReleaseSession(long, @NonNull String);
@@ -25048,15 +25068,44 @@ package android.media {
method public abstract void onSetRouteVolume(long, @NonNull String, int);
method public abstract void onSetSessionVolume(long, @NonNull String, int);
method public abstract void onTransferToRoute(long, @NonNull String, @NonNull String);
+ field @FlaggedApi("com.android.media.flags.enable_mirroring_in_media_router_2") public static final String CATEGORY_SYSTEM_MEDIA = "android.media.MediaRoute2ProviderService.SYSTEM_MEDIA";
+ field @FlaggedApi("com.android.media.flags.enable_mirroring_in_media_router_2") public static final int REASON_FAILED_TO_REROUTE_SYSTEM_MEDIA = 6; // 0x6
field public static final int REASON_INVALID_COMMAND = 4; // 0x4
field public static final int REASON_NETWORK_ERROR = 2; // 0x2
field public static final int REASON_REJECTED = 1; // 0x1
field public static final int REASON_ROUTE_NOT_AVAILABLE = 3; // 0x3
+ field @FlaggedApi("com.android.media.flags.enable_mirroring_in_media_router_2") public static final int REASON_UNIMPLEMENTED = 5; // 0x5
field public static final int REASON_UNKNOWN_ERROR = 0; // 0x0
field public static final long REQUEST_ID_NONE = 0L; // 0x0L
field public static final String SERVICE_INTERFACE = "android.media.MediaRoute2ProviderService";
}
+ @FlaggedApi("com.android.media.flags.enable_mirroring_in_media_router_2") public static final class MediaRoute2ProviderService.MediaStreams {
+ method @Nullable public android.media.AudioRecord getAudioRecord();
+ }
+
+ @FlaggedApi("com.android.media.flags.enable_mirroring_in_media_router_2") public static final class MediaRoute2ProviderService.MediaStreamsFormats {
+ method @FlaggedApi("com.android.media.flags.enable_mirroring_in_media_router_2") @Nullable public android.media.AudioFormat getAudioFormat();
+ }
+
+ @FlaggedApi("com.android.media.flags.enable_mirroring_in_media_router_2") public static final class MediaRoute2ProviderService.MediaStreamsFormats.Builder {
+ ctor public MediaRoute2ProviderService.MediaStreamsFormats.Builder();
+ method @NonNull public android.media.MediaRoute2ProviderService.MediaStreamsFormats build();
+ method @NonNull public android.media.MediaRoute2ProviderService.MediaStreamsFormats.Builder setAudioFormat(@NonNull android.media.AudioFormat);
+ }
+
+ @FlaggedApi("com.android.media.flags.enable_mirroring_in_media_router_2") public static final class MediaRoute2ProviderService.SystemRoutingSessionParams {
+ method @FlaggedApi("com.android.media.flags.enable_mirroring_in_media_router_2") @NonNull public android.os.Bundle getExtras();
+ method @FlaggedApi("com.android.media.flags.enable_mirroring_in_media_router_2") @NonNull public String getPackageName();
+ }
+
+ public static final class MediaRoute2ProviderService.SystemRoutingSessionParams.Builder {
+ ctor public MediaRoute2ProviderService.SystemRoutingSessionParams.Builder();
+ method @NonNull public android.media.MediaRoute2ProviderService.SystemRoutingSessionParams build();
+ method @NonNull public android.media.MediaRoute2ProviderService.SystemRoutingSessionParams.Builder setExtras(@NonNull android.os.Bundle);
+ method @NonNull public android.media.MediaRoute2ProviderService.SystemRoutingSessionParams.Builder setPackageName(@NonNull String);
+ }
+
public class MediaRouter {
method public void addCallback(int, android.media.MediaRouter.Callback);
method public void addCallback(int, android.media.MediaRouter.Callback, int);
@@ -38232,7 +38281,7 @@ package android.provider {
field public static final String ACTION_APP_OPEN_BY_DEFAULT_SETTINGS = "android.settings.APP_OPEN_BY_DEFAULT_SETTINGS";
field public static final String ACTION_APP_SEARCH_SETTINGS = "android.settings.APP_SEARCH_SETTINGS";
field public static final String ACTION_APP_USAGE_SETTINGS = "android.settings.action.APP_USAGE_SETTINGS";
- field @FlaggedApi("android.app.modes_api") public static final String ACTION_AUTOMATIC_ZEN_RULE_SETTINGS = "android.settings.AUTOMATIC_ZEN_RULE_SETTINGS";
+ field public static final String ACTION_AUTOMATIC_ZEN_RULE_SETTINGS = "android.settings.AUTOMATIC_ZEN_RULE_SETTINGS";
field public static final String ACTION_AUTO_ROTATE_SETTINGS = "android.settings.AUTO_ROTATE_SETTINGS";
field public static final String ACTION_BATTERY_SAVER_SETTINGS = "android.settings.BATTERY_SAVER_SETTINGS";
field public static final String ACTION_BIOMETRIC_ENROLL = "android.settings.BIOMETRIC_ENROLL";
@@ -38326,7 +38375,7 @@ package android.provider {
field public static final String EXTRA_AIRPLANE_MODE_ENABLED = "airplane_mode_enabled";
field public static final String EXTRA_APP_PACKAGE = "android.provider.extra.APP_PACKAGE";
field public static final String EXTRA_AUTHORITIES = "authorities";
- field @FlaggedApi("android.app.modes_api") public static final String EXTRA_AUTOMATIC_ZEN_RULE_ID = "android.provider.extra.AUTOMATIC_ZEN_RULE_ID";
+ field public static final String EXTRA_AUTOMATIC_ZEN_RULE_ID = "android.provider.extra.AUTOMATIC_ZEN_RULE_ID";
field public static final String EXTRA_BATTERY_SAVER_MODE_ENABLED = "android.settings.extra.battery_saver_mode_enabled";
field public static final String EXTRA_BIOMETRIC_AUTHENTICATORS_ALLOWED = "android.provider.extra.BIOMETRIC_AUTHENTICATORS_ALLOWED";
field public static final String EXTRA_CHANNEL_FILTER_LIST = "android.provider.extra.CHANNEL_FILTER_LIST";
@@ -42118,9 +42167,9 @@ package android.service.notification {
public final class Condition implements android.os.Parcelable {
ctor public Condition(android.net.Uri, String, int);
- ctor @FlaggedApi("android.app.modes_api") public Condition(@Nullable android.net.Uri, @Nullable String, int, int);
+ ctor public Condition(@Nullable android.net.Uri, @Nullable String, int, int);
ctor public Condition(android.net.Uri, String, String, String, int, int, int);
- ctor @FlaggedApi("android.app.modes_api") public Condition(@Nullable android.net.Uri, @Nullable String, @Nullable String, @Nullable String, int, int, int, int);
+ ctor public Condition(@Nullable android.net.Uri, @Nullable String, @Nullable String, @Nullable String, int, int, int, int);
ctor public Condition(android.os.Parcel);
method public android.service.notification.Condition copy();
method public int describeContents();
@@ -42133,10 +42182,10 @@ package android.service.notification {
field public static final int FLAG_RELEVANT_ALWAYS = 2; // 0x2
field public static final int FLAG_RELEVANT_NOW = 1; // 0x1
field public static final String SCHEME = "condition";
- field @FlaggedApi("android.app.modes_api") public static final int SOURCE_CONTEXT = 3; // 0x3
- field @FlaggedApi("android.app.modes_api") public static final int SOURCE_SCHEDULE = 2; // 0x2
- field @FlaggedApi("android.app.modes_api") public static final int SOURCE_UNKNOWN = 0; // 0x0
- field @FlaggedApi("android.app.modes_api") public static final int SOURCE_USER_ACTION = 1; // 0x1
+ field public static final int SOURCE_CONTEXT = 3; // 0x3
+ field public static final int SOURCE_SCHEDULE = 2; // 0x2
+ field public static final int SOURCE_UNKNOWN = 0; // 0x0
+ field public static final int SOURCE_USER_ACTION = 1; // 0x1
field public static final int STATE_ERROR = 3; // 0x3
field public static final int STATE_FALSE = 0; // 0x0
field public static final int STATE_TRUE = 1; // 0x1
@@ -42146,7 +42195,7 @@ package android.service.notification {
field public final android.net.Uri id;
field public final String line1;
field public final String line2;
- field @FlaggedApi("android.app.modes_api") public final int source;
+ field public final int source;
field public final int state;
field public final String summary;
}
@@ -42317,7 +42366,7 @@ package android.service.notification {
field @NonNull public static final android.os.Parcelable.Creator<android.service.notification.StatusBarNotification> CREATOR;
}
- @FlaggedApi("android.app.modes_api") public final class ZenDeviceEffects implements android.os.Parcelable {
+ public final class ZenDeviceEffects implements android.os.Parcelable {
method public int describeContents();
method public boolean shouldDimWallpaper();
method public boolean shouldDisplayGrayscale();
@@ -42327,7 +42376,7 @@ package android.service.notification {
field @NonNull public static final android.os.Parcelable.Creator<android.service.notification.ZenDeviceEffects> CREATOR;
}
- @FlaggedApi("android.app.modes_api") public static final class ZenDeviceEffects.Builder {
+ public static final class ZenDeviceEffects.Builder {
ctor public ZenDeviceEffects.Builder();
ctor public ZenDeviceEffects.Builder(@NonNull android.service.notification.ZenDeviceEffects);
method @NonNull public android.service.notification.ZenDeviceEffects build();
@@ -42349,7 +42398,7 @@ package android.service.notification {
method public int getPriorityCategoryReminders();
method public int getPriorityCategoryRepeatCallers();
method public int getPriorityCategorySystem();
- method @FlaggedApi("android.app.modes_api") public int getPriorityChannelsAllowed();
+ method public int getPriorityChannelsAllowed();
method public int getPriorityConversationSenders();
method public int getPriorityMessageSenders();
method public int getVisualEffectAmbient();
@@ -42384,7 +42433,7 @@ package android.service.notification {
method @NonNull public android.service.notification.ZenPolicy.Builder allowEvents(boolean);
method @NonNull public android.service.notification.ZenPolicy.Builder allowMedia(boolean);
method @NonNull public android.service.notification.ZenPolicy.Builder allowMessages(int);
- method @FlaggedApi("android.app.modes_api") @NonNull public android.service.notification.ZenPolicy.Builder allowPriorityChannels(boolean);
+ method @NonNull public android.service.notification.ZenPolicy.Builder allowPriorityChannels(boolean);
method @NonNull public android.service.notification.ZenPolicy.Builder allowReminders(boolean);
method @NonNull public android.service.notification.ZenPolicy.Builder allowRepeatCallers(boolean);
method @NonNull public android.service.notification.ZenPolicy.Builder allowSystem(boolean);
@@ -45122,6 +45171,7 @@ package android.telephony {
field public static final String KEY_RTT_UPGRADE_SUPPORTED_BOOL = "rtt_upgrade_supported_bool";
field public static final String KEY_RTT_UPGRADE_SUPPORTED_FOR_DOWNGRADED_VT_CALL_BOOL = "rtt_upgrade_supported_for_downgraded_vt_call";
field @FlaggedApi("com.android.internal.telephony.flags.carrier_enabled_satellite_flag") public static final String KEY_SATELLITE_ATTACH_SUPPORTED_BOOL = "satellite_attach_supported_bool";
+ field @FlaggedApi("com.android.internal.telephony.flags.satellite_25q4_apis") public static final String KEY_SATELLITE_CONNECTED_NOTIFICATION_THROTTLE_MILLIS_INT = "satellite_connected_notification_throttle_millis_int";
field @FlaggedApi("com.android.internal.telephony.flags.carrier_enabled_satellite_flag") public static final String KEY_SATELLITE_CONNECTION_HYSTERESIS_SEC_INT = "satellite_connection_hysteresis_sec_int";
field @FlaggedApi("com.android.internal.telephony.flags.satellite_system_apis") public static final String KEY_SATELLITE_DATA_SUPPORT_MODE_INT = "satellite_data_support_mode_int";
field @FlaggedApi("com.android.internal.telephony.flags.satellite_system_apis") public static final String KEY_SATELLITE_DISPLAY_NAME_STRING = "satellite_display_name_string";
@@ -48694,6 +48744,7 @@ package android.telephony.satellite {
@FlaggedApi("com.android.internal.telephony.flags.satellite_state_change_listener") public final class SatelliteManager {
method @FlaggedApi("com.android.internal.telephony.flags.satellite_state_change_listener") @RequiresPermission(anyOf={android.Manifest.permission.READ_BASIC_PHONE_STATE, "android.permission.READ_PRIVILEGED_PHONE_STATE", android.Manifest.permission.READ_PHONE_STATE, "carrier privileges"}) public void registerStateChangeListener(@NonNull java.util.concurrent.Executor, @NonNull android.telephony.satellite.SatelliteStateChangeListener);
method @FlaggedApi("com.android.internal.telephony.flags.satellite_state_change_listener") @RequiresPermission(anyOf={android.Manifest.permission.READ_BASIC_PHONE_STATE, "android.permission.READ_PRIVILEGED_PHONE_STATE", android.Manifest.permission.READ_PHONE_STATE, "carrier privileges"}) public void unregisterStateChangeListener(@NonNull android.telephony.satellite.SatelliteStateChangeListener);
+ field @FlaggedApi("com.android.internal.telephony.flags.satellite_25q4_apis") public static final String PROPERTY_SATELLITE_DATA_OPTIMIZED = "android.telephony.PROPERTY_SATELLITE_DATA_OPTIMIZED";
}
@FlaggedApi("com.android.internal.telephony.flags.satellite_state_change_listener") public interface SatelliteStateChangeListener {
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index 76cce7439454..0d5ec199b953 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -7,7 +7,7 @@ package android {
field public static final String ACCESS_BROADCAST_RADIO = "android.permission.ACCESS_BROADCAST_RADIO";
field public static final String ACCESS_BROADCAST_RESPONSE_STATS = "android.permission.ACCESS_BROADCAST_RESPONSE_STATS";
field public static final String ACCESS_CACHE_FILESYSTEM = "android.permission.ACCESS_CACHE_FILESYSTEM";
- field @FlaggedApi("android.app.contextualsearch.flags.enable_service") public static final String ACCESS_CONTEXTUAL_SEARCH = "android.permission.ACCESS_CONTEXTUAL_SEARCH";
+ field public static final String ACCESS_CONTEXTUAL_SEARCH = "android.permission.ACCESS_CONTEXTUAL_SEARCH";
field public static final String ACCESS_CONTEXT_HUB = "android.permission.ACCESS_CONTEXT_HUB";
field public static final String ACCESS_DRM_CERTIFICATES = "android.permission.ACCESS_DRM_CERTIFICATES";
field @FlaggedApi("android.permission.flags.fine_power_monitor_permission") public static final String ACCESS_FINE_POWER_MONITORS = "android.permission.ACCESS_FINE_POWER_MONITORS";
@@ -151,6 +151,8 @@ package android {
field @FlaggedApi("android.content.pm.emergency_install_permission") public static final String EMERGENCY_INSTALL_PACKAGES = "android.permission.EMERGENCY_INSTALL_PACKAGES";
field public static final String ENTER_CAR_MODE_PRIORITIZED = "android.permission.ENTER_CAR_MODE_PRIORITIZED";
field public static final String EXEMPT_FROM_AUDIO_RECORD_RESTRICTIONS = "android.permission.EXEMPT_FROM_AUDIO_RECORD_RESTRICTIONS";
+ field @FlaggedApi("android.xr.xr_manifest_entries") public static final String EYE_CALIBRATION = "android.permission.EYE_CALIBRATION";
+ field @FlaggedApi("android.xr.xr_manifest_entries") public static final String FACE_TRACKING_CALIBRATION = "android.permission.FACE_TRACKING_CALIBRATION";
field public static final String FORCE_BACK = "android.permission.FORCE_BACK";
field public static final String FORCE_STOP_PACKAGES = "android.permission.FORCE_STOP_PACKAGES";
field public static final String GET_APP_METADATA = "android.permission.GET_APP_METADATA";
@@ -168,6 +170,7 @@ package android {
field public static final String HARDWARE_TEST = "android.permission.HARDWARE_TEST";
field public static final String HDMI_CEC = "android.permission.HDMI_CEC";
field @Deprecated public static final String HIDE_NON_SYSTEM_OVERLAY_WINDOWS = "android.permission.HIDE_NON_SYSTEM_OVERLAY_WINDOWS";
+ field @FlaggedApi("android.xr.xr_manifest_entries") public static final String IMPORT_XR_ANCHOR = "android.permission.IMPORT_XR_ANCHOR";
field public static final String INJECT_EVENTS = "android.permission.INJECT_EVENTS";
field @FlaggedApi("android.content.pm.sdk_dependency_installer") public static final String INSTALL_DEPENDENCY_SHARED_LIBRARIES = "android.permission.INSTALL_DEPENDENCY_SHARED_LIBRARIES";
field public static final String INSTALL_DPC_PACKAGES = "android.permission.INSTALL_DPC_PACKAGES";
@@ -450,6 +453,7 @@ package android {
field public static final String WRITE_SECURITY_LOG = "android.permission.WRITE_SECURITY_LOG";
field public static final String WRITE_SMS = "android.permission.WRITE_SMS";
field @FlaggedApi("android.provider.user_keys") public static final String WRITE_VERIFICATION_STATE_E2EE_CONTACT_KEYS = "android.permission.WRITE_VERIFICATION_STATE_E2EE_CONTACT_KEYS";
+ field @FlaggedApi("android.xr.xr_manifest_entries") public static final String XR_TRACKING_IN_BACKGROUND = "android.permission.XR_TRACKING_IN_BACKGROUND";
}
public static final class Manifest.permission_group {
@@ -2231,7 +2235,7 @@ package android.app.contextualsearch {
field @NonNull public static final android.os.Parcelable.Creator<android.app.contextualsearch.CallbackToken> CREATOR;
}
- public final class ContextualSearchManager {
+ @FlaggedApi("android.app.contextualsearch.flags.self_invocation") public final class ContextualSearchManager {
method @RequiresPermission(android.Manifest.permission.ACCESS_CONTEXTUAL_SEARCH) public void startContextualSearch(int);
field public static final String ACTION_LAUNCH_CONTEXTUAL_SEARCH = "android.app.contextualsearch.action.LAUNCH_CONTEXTUAL_SEARCH";
field public static final int ENTRYPOINT_LONG_PRESS_HOME = 2; // 0x2
@@ -2934,6 +2938,14 @@ package android.app.smartspace.uitemplatedata {
}
+package android.app.supervision {
+
+ @FlaggedApi("android.app.supervision.flags.supervision_manager_apis") public class SupervisionManager {
+ method @FlaggedApi("android.app.supervision.flags.supervision_manager_apis") @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.QUERY_USERS}) public boolean isSupervisionEnabled();
+ }
+
+}
+
package android.app.time {
public final class Capabilities {
@@ -3797,6 +3809,7 @@ package android.content {
field public static final String SHARED_CONNECTIVITY_SERVICE = "shared_connectivity";
field public static final String SMARTSPACE_SERVICE = "smartspace";
field public static final String STATS_MANAGER = "stats";
+ field @FlaggedApi("android.app.supervision.flags.supervision_manager_apis") public static final String SUPERVISION_SERVICE = "supervision";
field public static final String SYSTEM_CONFIG_SERVICE = "system_config";
field public static final String SYSTEM_UPDATE_SERVICE = "system_update";
field @FlaggedApi("com.android.net.thread.platform.flags.thread_enabled_platform") public static final String THREAD_NETWORK_SERVICE = "thread_network";
@@ -18580,6 +18593,7 @@ package android.telephony.satellite {
method @FlaggedApi("com.android.internal.telephony.flags.satellite_system_apis") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void deprovisionSatellite(@NonNull java.util.List<android.telephony.satellite.SatelliteSubscriberInfo>, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,android.telephony.satellite.SatelliteManager.SatelliteException>);
method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void deprovisionService(@NonNull String, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>);
method @FlaggedApi("com.android.internal.telephony.flags.carrier_enabled_satellite_flag") @NonNull @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public java.util.Set<java.lang.Integer> getAttachRestrictionReasonsForCarrier(int);
+ method @FlaggedApi("com.android.internal.telephony.flags.satellite_25q4_apis") @NonNull @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public java.util.List<java.lang.String> getSatelliteDataOptimizedApps();
method @FlaggedApi("com.android.internal.telephony.flags.satellite_system_apis") @NonNull @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public int[] getSatelliteDisallowedReasons();
method @FlaggedApi("com.android.internal.telephony.flags.carrier_enabled_satellite_flag") @NonNull @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public java.util.List<java.lang.String> getSatellitePlmnsForCarrier(int);
method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void pollPendingDatagrams(@NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>);
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index 9e9e3c2f13c1..975c2c27cb22 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -393,25 +393,25 @@ package android.app {
}
public class NotificationManager {
- method @FlaggedApi("android.app.modes_api") @NonNull public String addAutomaticZenRule(@NonNull android.app.AutomaticZenRule, boolean);
+ method @NonNull public String addAutomaticZenRule(@NonNull android.app.AutomaticZenRule, boolean);
method @FlaggedApi("android.service.notification.notification_classification") public void allowAssistantAdjustment(@NonNull String);
method public void cleanUpCallersAfter(long);
method @FlaggedApi("android.service.notification.notification_classification") public void disallowAssistantAdjustment(@NonNull String);
- method @FlaggedApi("android.app.modes_api") @NonNull public android.service.notification.ZenPolicy getDefaultZenPolicy();
+ method @NonNull public android.service.notification.ZenPolicy getDefaultZenPolicy();
method public android.content.ComponentName getEffectsSuppressor();
method @FlaggedApi("android.service.notification.notification_classification") @NonNull public java.util.Set<java.lang.String> getUnsupportedAdjustmentTypes();
method public boolean isNotificationPolicyAccessGrantedForPackage(@NonNull String);
- method @FlaggedApi("android.app.modes_api") public boolean removeAutomaticZenRule(@NonNull String, boolean);
+ method public boolean removeAutomaticZenRule(@NonNull String, boolean);
method @FlaggedApi("android.service.notification.notification_classification") public void setAssistantAdjustmentKeyTypeState(int, boolean);
method @FlaggedApi("android.app.api_rich_ongoing") public void setCanPostPromotedNotifications(@NonNull String, int, boolean);
method @RequiresPermission(android.Manifest.permission.MANAGE_NOTIFICATION_LISTENERS) public void setNotificationListenerAccessGranted(@NonNull android.content.ComponentName, boolean, boolean);
method @RequiresPermission(android.Manifest.permission.MANAGE_TOAST_RATE_LIMITING) public void setToastRateLimitingEnabled(boolean);
- method @FlaggedApi("android.app.modes_api") public boolean updateAutomaticZenRule(@NonNull String, @NonNull android.app.AutomaticZenRule, boolean);
+ method public boolean updateAutomaticZenRule(@NonNull String, @NonNull android.app.AutomaticZenRule, boolean);
method public void updateNotificationChannel(@NonNull String, int, @NonNull android.app.NotificationChannel);
}
public static class NotificationManager.Policy implements android.os.Parcelable {
- method @FlaggedApi("android.app.modes_api") public boolean allowPriorityChannels();
+ method public boolean allowPriorityChannels();
}
public final class PendingIntent implements android.os.Parcelable {
@@ -478,8 +478,8 @@ package android.app {
method public void destroy();
method @NonNull public java.util.Set<java.lang.String> getAdoptedShellPermissions();
method @Deprecated public boolean grantRuntimePermission(String, String, android.os.UserHandle);
- method public boolean injectInputEvent(@NonNull android.view.InputEvent, boolean, boolean);
- method public void injectInputEventToInputFilter(@NonNull android.view.InputEvent);
+ method @Deprecated public boolean injectInputEvent(@NonNull android.view.InputEvent, boolean, boolean);
+ method @Deprecated public void injectInputEventToInputFilter(@NonNull android.view.InputEvent);
method public boolean isNodeInCache(@NonNull android.view.accessibility.AccessibilityNodeInfo);
method public void removeOverridePermissionState(int, @NonNull String);
method @Deprecated public boolean revokeRuntimePermission(String, String, android.os.UserHandle);
@@ -490,15 +490,15 @@ package android.app {
}
public class UiModeManager {
- method @FlaggedApi("android.app.modes_api") @RequiresPermission(android.Manifest.permission.MODIFY_DAY_NIGHT_MODE) public int getAttentionModeThemeOverlay();
+ method @RequiresPermission(android.Manifest.permission.MODIFY_DAY_NIGHT_MODE) public int getAttentionModeThemeOverlay();
method public boolean isNightModeLocked();
method public boolean isUiModeLocked();
method @RequiresPermission(value=android.Manifest.permission.TOGGLE_AUTOMOTIVE_PROJECTION, conditional=true) public boolean releaseProjection(int);
method @RequiresPermission(value=android.Manifest.permission.TOGGLE_AUTOMOTIVE_PROJECTION, conditional=true) public boolean requestProjection(int);
- field @FlaggedApi("android.app.modes_api") public static final int MODE_ATTENTION_THEME_OVERLAY_DAY = 1002; // 0x3ea
- field @FlaggedApi("android.app.modes_api") public static final int MODE_ATTENTION_THEME_OVERLAY_NIGHT = 1001; // 0x3e9
- field @FlaggedApi("android.app.modes_api") public static final int MODE_ATTENTION_THEME_OVERLAY_OFF = 1000; // 0x3e8
- field @FlaggedApi("android.app.modes_api") public static final int MODE_ATTENTION_THEME_OVERLAY_UNKNOWN = -1; // 0xffffffff
+ field public static final int MODE_ATTENTION_THEME_OVERLAY_DAY = 1002; // 0x3ea
+ field public static final int MODE_ATTENTION_THEME_OVERLAY_NIGHT = 1001; // 0x3e9
+ field public static final int MODE_ATTENTION_THEME_OVERLAY_OFF = 1000; // 0x3e8
+ field public static final int MODE_ATTENTION_THEME_OVERLAY_UNKNOWN = -1; // 0xffffffff
field public static final int PROJECTION_TYPE_ALL = -1; // 0xffffffff
field public static final int PROJECTION_TYPE_AUTOMOTIVE = 1; // 0x1
field public static final int PROJECTION_TYPE_NONE = 0; // 0x0
@@ -850,6 +850,14 @@ package android.app.prediction {
}
+package android.app.supervision {
+
+ @FlaggedApi("android.app.supervision.flags.supervision_manager_apis") public class SupervisionManager {
+ method public void setSupervisionEnabled(boolean);
+ }
+
+}
+
package android.app.usage {
public class StorageStatsManager {
@@ -1716,7 +1724,7 @@ package android.hardware.display {
}
public final class ColorDisplayManager {
- method @FlaggedApi("android.app.modes_api") @RequiresPermission(android.Manifest.permission.CONTROL_DISPLAY_COLOR_TRANSFORMS) public boolean isSaturationActivated();
+ method @RequiresPermission(android.Manifest.permission.CONTROL_DISPLAY_COLOR_TRANSFORMS) public boolean isSaturationActivated();
}
public final class DisplayManager {
@@ -3237,16 +3245,16 @@ package android.service.notification {
method @Deprecated public boolean isBound();
}
- @FlaggedApi("android.app.modes_api") public final class ZenDeviceEffects implements android.os.Parcelable {
+ public final class ZenDeviceEffects implements android.os.Parcelable {
method @NonNull public java.util.Set<java.lang.String> getExtraEffects();
}
- @FlaggedApi("android.app.modes_api") public static final class ZenDeviceEffects.Builder {
+ public static final class ZenDeviceEffects.Builder {
method @NonNull public android.service.notification.ZenDeviceEffects.Builder setExtraEffects(@NonNull java.util.Set<java.lang.String>);
}
public final class ZenPolicy implements android.os.Parcelable {
- method @FlaggedApi("android.app.modes_api") @NonNull public android.service.notification.ZenPolicy overwrittenWith(@Nullable android.service.notification.ZenPolicy);
+ method @NonNull public android.service.notification.ZenPolicy overwrittenWith(@Nullable android.service.notification.ZenPolicy);
}
public static final class ZenPolicy.Builder {
diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java
index 252d23f69400..ee9c64f97382 100644
--- a/core/java/android/app/Activity.java
+++ b/core/java/android/app/Activity.java
@@ -292,13 +292,15 @@ import java.util.function.Consumer;
* to the user, it must be completely restarted and restored to its previous state.</li>
* </ul>
*
- * <p>The following diagram shows the important state paths of an Activity.
+ * <p>The following diagram shows the important state paths of an activity.
* The square rectangles represent callback methods you can implement to
- * perform operations when the Activity moves between states. The colored
- * ovals are major states the Activity can be in.</p>
+ * perform operations when the activity moves between states. The colored
+ * ovals are major states the activity can be in.</p>
*
- * <p><img src="../../../images/activity_lifecycle.png"
- * alt="State diagram for an Android Activity Lifecycle." border="0" /></p>
+ * <p><img class="invert"
+ * style="display: block; margin: auto;"
+ * src="../../../images/activity_lifecycle.png"
+ * alt="State diagram for the Android activity lifecycle." /></p>
*
* <p>There are three key loops you may be interested in monitoring within your
* activity:
@@ -505,7 +507,7 @@ import java.util.function.Consumer;
* changes.</p>
*
* <p>Unless you specify otherwise, a configuration change (such as a change
- * in screen orientation, language, input devices, etc) will cause your
+ * in screen orientation, language, input devices, etc.) will cause your
* current activity to be <em>destroyed</em>, going through the normal activity
* lifecycle process of {@link #onPause},
* {@link #onStop}, and {@link #onDestroy} as appropriate. If the activity
@@ -1838,7 +1840,7 @@ public class Activity extends ContextThemeWrapper
*
* <p>You can call {@link #finish} from within this function, in
* which case onDestroy() will be immediately called after {@link #onCreate} without any of the
- * rest of the activity lifecycle ({@link #onStart}, {@link #onResume}, {@link #onPause}, etc)
+ * rest of the activity lifecycle ({@link #onStart}, {@link #onResume}, {@link #onPause}, etc.)
* executing.
*
* <p><em>Derived classes must call through to the super class's
@@ -2132,7 +2134,7 @@ public class Activity extends ContextThemeWrapper
*
* <p>You can call {@link #finish} from within this function, in
* which case {@link #onStop} will be immediately called after {@link #onStart} without the
- * lifecycle transitions in-between ({@link #onResume}, {@link #onPause}, etc) executing.
+ * lifecycle transitions in-between ({@link #onResume}, {@link #onPause}, etc.) executing.
*
* <p><em>Derived classes must call through to the super class's
* implementation of this method. If they do not, an exception will be
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java
index 2d7ed46fe64a..f63170aa159d 100644
--- a/core/java/android/app/ActivityThread.java
+++ b/core/java/android/app/ActivityThread.java
@@ -104,6 +104,7 @@ 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;
@@ -5283,6 +5284,7 @@ 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/ApplicationStartInfo.java b/core/java/android/app/ApplicationStartInfo.java
index 3214bd8f01fc..2e8031dd22fe 100644
--- a/core/java/android/app/ApplicationStartInfo.java
+++ b/core/java/android/app/ApplicationStartInfo.java
@@ -840,7 +840,9 @@ public final class ApplicationStartInfo implements Parcelable {
* @hide
*/
// LINT.IfChange(write_proto)
- public void writeToProto(ProtoOutputStream proto, long fieldId) throws IOException {
+ public void writeToProto(ProtoOutputStream proto, long fieldId,
+ ByteArrayOutputStream byteArrayOutputStream, ObjectOutputStream objectOutputStream,
+ TypedXmlSerializer typedXmlSerializer) throws IOException {
final long token = proto.start(fieldId);
proto.write(ApplicationStartInfoProto.PID, mPid);
proto.write(ApplicationStartInfoProto.REAL_UID, mRealUid);
@@ -850,38 +852,38 @@ public final class ApplicationStartInfo implements Parcelable {
proto.write(ApplicationStartInfoProto.STARTUP_STATE, mStartupState);
proto.write(ApplicationStartInfoProto.REASON, mReason);
if (mStartupTimestampsNs != null && mStartupTimestampsNs.size() > 0) {
- ByteArrayOutputStream timestampsBytes = new ByteArrayOutputStream();
- ObjectOutputStream timestampsOut = new ObjectOutputStream(timestampsBytes);
- TypedXmlSerializer serializer = Xml.resolveSerializer(timestampsOut);
- serializer.startDocument(null, true);
- serializer.startTag(null, PROTO_SERIALIZER_ATTRIBUTE_TIMESTAMPS);
+ byteArrayOutputStream = new ByteArrayOutputStream();
+ objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
+ typedXmlSerializer = Xml.resolveSerializer(objectOutputStream);
+ typedXmlSerializer.startDocument(null, true);
+ typedXmlSerializer.startTag(null, PROTO_SERIALIZER_ATTRIBUTE_TIMESTAMPS);
for (int i = 0; i < mStartupTimestampsNs.size(); i++) {
- serializer.startTag(null, PROTO_SERIALIZER_ATTRIBUTE_TIMESTAMP);
- serializer.attributeInt(null, PROTO_SERIALIZER_ATTRIBUTE_KEY,
+ typedXmlSerializer.startTag(null, PROTO_SERIALIZER_ATTRIBUTE_TIMESTAMP);
+ typedXmlSerializer.attributeInt(null, PROTO_SERIALIZER_ATTRIBUTE_KEY,
mStartupTimestampsNs.keyAt(i));
- serializer.attributeLong(null, PROTO_SERIALIZER_ATTRIBUTE_TS,
+ typedXmlSerializer.attributeLong(null, PROTO_SERIALIZER_ATTRIBUTE_TS,
mStartupTimestampsNs.valueAt(i));
- serializer.endTag(null, PROTO_SERIALIZER_ATTRIBUTE_TIMESTAMP);
+ typedXmlSerializer.endTag(null, PROTO_SERIALIZER_ATTRIBUTE_TIMESTAMP);
}
- serializer.endTag(null, PROTO_SERIALIZER_ATTRIBUTE_TIMESTAMPS);
- serializer.endDocument();
+ typedXmlSerializer.endTag(null, PROTO_SERIALIZER_ATTRIBUTE_TIMESTAMPS);
+ typedXmlSerializer.endDocument();
proto.write(ApplicationStartInfoProto.STARTUP_TIMESTAMPS,
- timestampsBytes.toByteArray());
- timestampsOut.close();
+ byteArrayOutputStream.toByteArray());
+ objectOutputStream.close();
}
proto.write(ApplicationStartInfoProto.START_TYPE, mStartType);
if (mStartIntent != null) {
- ByteArrayOutputStream intentBytes = new ByteArrayOutputStream();
- ObjectOutputStream intentOut = new ObjectOutputStream(intentBytes);
- TypedXmlSerializer serializer = Xml.resolveSerializer(intentOut);
- serializer.startDocument(null, true);
- serializer.startTag(null, PROTO_SERIALIZER_ATTRIBUTE_INTENT);
- mStartIntent.saveToXml(serializer);
- serializer.endTag(null, PROTO_SERIALIZER_ATTRIBUTE_INTENT);
- serializer.endDocument();
+ byteArrayOutputStream = new ByteArrayOutputStream();
+ objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
+ typedXmlSerializer = Xml.resolveSerializer(objectOutputStream);
+ typedXmlSerializer.startDocument(null, true);
+ typedXmlSerializer.startTag(null, PROTO_SERIALIZER_ATTRIBUTE_INTENT);
+ mStartIntent.saveToXml(typedXmlSerializer);
+ typedXmlSerializer.endTag(null, PROTO_SERIALIZER_ATTRIBUTE_INTENT);
+ typedXmlSerializer.endDocument();
proto.write(ApplicationStartInfoProto.START_INTENT,
- intentBytes.toByteArray());
- intentOut.close();
+ byteArrayOutputStream.toByteArray());
+ objectOutputStream.close();
}
proto.write(ApplicationStartInfoProto.LAUNCH_MODE, mLaunchMode);
proto.write(ApplicationStartInfoProto.WAS_FORCE_STOPPED, mWasForceStopped);
@@ -900,7 +902,9 @@ public final class ApplicationStartInfo implements Parcelable {
* @hide
*/
// LINT.IfChange(read_proto)
- public void readFromProto(ProtoInputStream proto, long fieldId)
+ public void readFromProto(ProtoInputStream proto, long fieldId,
+ ByteArrayInputStream byteArrayInputStream, ObjectInputStream objectInputStream,
+ TypedXmlPullParser typedXmlPullParser)
throws IOException, WireTypeMismatchException, ClassNotFoundException {
final long token = proto.start(fieldId);
while (proto.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
@@ -927,19 +931,21 @@ public final class ApplicationStartInfo implements Parcelable {
mReason = proto.readInt(ApplicationStartInfoProto.REASON);
break;
case (int) ApplicationStartInfoProto.STARTUP_TIMESTAMPS:
- ByteArrayInputStream timestampsBytes = new ByteArrayInputStream(proto.readBytes(
+ byteArrayInputStream = new ByteArrayInputStream(proto.readBytes(
ApplicationStartInfoProto.STARTUP_TIMESTAMPS));
- ObjectInputStream timestampsIn = new ObjectInputStream(timestampsBytes);
+ objectInputStream = new ObjectInputStream(byteArrayInputStream);
mStartupTimestampsNs = new ArrayMap<Integer, Long>();
try {
- TypedXmlPullParser parser = Xml.resolvePullParser(timestampsIn);
- XmlUtils.beginDocument(parser, PROTO_SERIALIZER_ATTRIBUTE_TIMESTAMPS);
- int depth = parser.getDepth();
- while (XmlUtils.nextElementWithin(parser, depth)) {
- if (PROTO_SERIALIZER_ATTRIBUTE_TIMESTAMP.equals(parser.getName())) {
- int key = parser.getAttributeInt(null,
+ typedXmlPullParser = Xml.resolvePullParser(objectInputStream);
+ XmlUtils.beginDocument(typedXmlPullParser,
+ PROTO_SERIALIZER_ATTRIBUTE_TIMESTAMPS);
+ int depth = typedXmlPullParser.getDepth();
+ while (XmlUtils.nextElementWithin(typedXmlPullParser, depth)) {
+ if (PROTO_SERIALIZER_ATTRIBUTE_TIMESTAMP.equals(
+ typedXmlPullParser.getName())) {
+ int key = typedXmlPullParser.getAttributeInt(null,
PROTO_SERIALIZER_ATTRIBUTE_KEY);
- long ts = parser.getAttributeLong(null,
+ long ts = typedXmlPullParser.getAttributeLong(null,
PROTO_SERIALIZER_ATTRIBUTE_TS);
mStartupTimestampsNs.put(key, ts);
}
@@ -947,23 +953,24 @@ public final class ApplicationStartInfo implements Parcelable {
} catch (XmlPullParserException e) {
// Timestamps lost
}
- timestampsIn.close();
+ objectInputStream.close();
break;
case (int) ApplicationStartInfoProto.START_TYPE:
mStartType = proto.readInt(ApplicationStartInfoProto.START_TYPE);
break;
case (int) ApplicationStartInfoProto.START_INTENT:
- ByteArrayInputStream intentBytes = new ByteArrayInputStream(proto.readBytes(
+ byteArrayInputStream = new ByteArrayInputStream(proto.readBytes(
ApplicationStartInfoProto.START_INTENT));
- ObjectInputStream intentIn = new ObjectInputStream(intentBytes);
+ objectInputStream = new ObjectInputStream(byteArrayInputStream);
try {
- TypedXmlPullParser parser = Xml.resolvePullParser(intentIn);
- XmlUtils.beginDocument(parser, PROTO_SERIALIZER_ATTRIBUTE_INTENT);
- mStartIntent = Intent.restoreFromXml(parser);
+ typedXmlPullParser = Xml.resolvePullParser(objectInputStream);
+ XmlUtils.beginDocument(typedXmlPullParser,
+ PROTO_SERIALIZER_ATTRIBUTE_INTENT);
+ mStartIntent = Intent.restoreFromXml(typedXmlPullParser);
} catch (XmlPullParserException e) {
// Intent lost
}
- intentIn.close();
+ objectInputStream.close();
break;
case (int) ApplicationStartInfoProto.LAUNCH_MODE:
mLaunchMode = proto.readInt(ApplicationStartInfoProto.LAUNCH_MODE);
diff --git a/core/java/android/app/AutomaticZenRule.java b/core/java/android/app/AutomaticZenRule.java
index 9d1d9c7b69de..fa977c93113a 100644
--- a/core/java/android/app/AutomaticZenRule.java
+++ b/core/java/android/app/AutomaticZenRule.java
@@ -19,7 +19,6 @@ package android.app;
import static com.android.internal.util.Preconditions.checkArgument;
import android.annotation.DrawableRes;
-import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -52,48 +51,40 @@ public final class AutomaticZenRule implements Parcelable {
* and the value returned if the true type was added in an API level higher than the calling
* app's targetSdk.
*/
- @FlaggedApi(Flags.FLAG_MODES_API)
public static final int TYPE_UNKNOWN = -1;
/**
* Rule is of a known type, but not one of the specific types.
*/
- @FlaggedApi(Flags.FLAG_MODES_API)
public static final int TYPE_OTHER = 0;
/**
* The type for rules triggered according to a time-based schedule.
*/
- @FlaggedApi(Flags.FLAG_MODES_API)
public static final int TYPE_SCHEDULE_TIME = 1;
/**
* The type for rules triggered by calendar events.
*/
- @FlaggedApi(Flags.FLAG_MODES_API)
public static final int TYPE_SCHEDULE_CALENDAR = 2;
/**
* The type for rules triggered by bedtime/sleeping, like time of day, or snore detection.
*
* <p>Only the 'Wellbeing' app may own rules of this type.
*/
- @FlaggedApi(Flags.FLAG_MODES_API)
public static final int TYPE_BEDTIME = 3;
/**
* The type for rules triggered by driving detection, like Bluetooth connections or vehicle
* sounds.
*/
- @FlaggedApi(Flags.FLAG_MODES_API)
public static final int TYPE_DRIVING = 4;
/**
* The type for rules triggered by the user entering an immersive activity, like opening an app
* using {@link WindowInsetsController#hide(int)}.
*/
- @FlaggedApi(Flags.FLAG_MODES_API)
public static final int TYPE_IMMERSIVE = 5;
/**
* The type for rules that have a {@link ZenPolicy} that implies that the
* device should not make sound and potentially hide some visual effects; may be triggered
* when entering a location where silence is requested, like a theater.
*/
- @FlaggedApi(Flags.FLAG_MODES_API)
public static final int TYPE_THEATER = 6;
/**
* The type for rules created and managed by a device owner. These rules may not be fully
@@ -101,7 +92,6 @@ public final class AutomaticZenRule implements Parcelable {
*
* <p>Only a 'Device Owner' app may own rules of this type.
*/
- @FlaggedApi(Flags.FLAG_MODES_API)
public static final int TYPE_MANAGED = 7;
/** @hide */
@@ -127,17 +117,14 @@ public final class AutomaticZenRule implements Parcelable {
/**
* @hide
*/
- @FlaggedApi(Flags.FLAG_MODES_API)
public static final int FIELD_NAME = 1 << 0;
/**
* @hide
*/
- @FlaggedApi(Flags.FLAG_MODES_API)
public static final int FIELD_INTERRUPTION_FILTER = 1 << 1;
/**
* @hide
*/
- @FlaggedApi(Flags.FLAG_MODES_API)
public static final int FIELD_ICON = 1 << 2;
private boolean enabled;
@@ -149,10 +136,8 @@ public final class AutomaticZenRule implements Parcelable {
private long creationTime;
private ZenPolicy mZenPolicy;
private ZenDeviceEffects mDeviceEffects;
- // TODO: b/310620812 - Remove this once FLAG_MODES_API is inlined.
- private boolean mModified = false;
private String mPkg;
- private int mType = Flags.modesApi() ? TYPE_UNKNOWN : 0;
+ private int mType = TYPE_UNKNOWN;
private int mIconResId;
private String mTriggerDescription;
private boolean mAllowManualInvocation;
@@ -229,8 +214,10 @@ public final class AutomaticZenRule implements Parcelable {
/**
* @hide
+ * @deprecated Do not add new usages; will be removed soon.
*/
- // TODO: b/310620812 - Remove when the flag is inlined (all system callers should use Builder).
+ // TODO: b/368247671 - Remove when modes_ui is inlined (remaining usages are in obsolete tests)
+ @Deprecated
public AutomaticZenRule(String name, ComponentName owner, ComponentName configurationActivity,
Uri conditionId, ZenPolicy policy, int interruptionFilter, boolean enabled,
long creationTime) {
@@ -251,15 +238,12 @@ public final class AutomaticZenRule implements Parcelable {
source.readParcelable(null, android.content.ComponentName.class));
creationTime = source.readLong();
mZenPolicy = source.readParcelable(null, ZenPolicy.class);
- mModified = source.readInt() == ENABLED;
mPkg = source.readString();
- if (Flags.modesApi()) {
- mDeviceEffects = source.readParcelable(null, ZenDeviceEffects.class);
- mAllowManualInvocation = source.readBoolean();
- mIconResId = source.readInt();
- mTriggerDescription = getTrimmedString(source.readString(), MAX_DESC_LENGTH);
- mType = source.readInt();
- }
+ mDeviceEffects = source.readParcelable(null, ZenDeviceEffects.class);
+ mAllowManualInvocation = source.readBoolean();
+ mIconResId = source.readInt();
+ mTriggerDescription = getTrimmedString(source.readString(), MAX_DESC_LENGTH);
+ mType = source.readInt();
}
/**
@@ -306,15 +290,6 @@ public final class AutomaticZenRule implements Parcelable {
}
/**
- * Returns whether this rule's name has been modified by the user.
- * @hide
- */
- // TODO: b/310620812 - Consider removing completely. Seems not be used anywhere except tests.
- public boolean isModified() {
- return mModified;
- }
-
- /**
* Gets the {@link ZenPolicy} applied if {@link #getInterruptionFilter()} is
* {@link NotificationManager#INTERRUPTION_FILTER_PRIORITY}.
*/
@@ -325,7 +300,6 @@ public final class AutomaticZenRule implements Parcelable {
/** Gets the {@link ZenDeviceEffects} of this rule. */
@Nullable
- @FlaggedApi(Flags.FLAG_MODES_API)
public ZenDeviceEffects getDeviceEffects() {
return mDeviceEffects;
}
@@ -378,14 +352,6 @@ public final class AutomaticZenRule implements Parcelable {
}
/**
- * Sets modified state of this rule.
- * @hide
- */
- public void setModified(boolean modified) {
- this.mModified = modified;
- }
-
- /**
* Sets the {@link ZenPolicy} applied if {@link #getInterruptionFilter()} is
* {@link NotificationManager#INTERRUPTION_FILTER_PRIORITY}.
*
@@ -404,7 +370,6 @@ public final class AutomaticZenRule implements Parcelable {
* <p>When updating an existing rule via {@link NotificationManager#updateAutomaticZenRule},
* a {@code null} value here means the previous set of effects is retained.
*/
- @FlaggedApi(Flags.FLAG_MODES_API)
public void setDeviceEffects(@Nullable ZenDeviceEffects deviceEffects) {
mDeviceEffects = deviceEffects;
}
@@ -458,7 +423,6 @@ public final class AutomaticZenRule implements Parcelable {
/**
* Gets the type of the rule.
*/
- @FlaggedApi(Flags.FLAG_MODES_API)
public @Type int getType() {
return mType;
}
@@ -467,7 +431,6 @@ public final class AutomaticZenRule implements Parcelable {
* Sets the type of the rule.
* @hide
*/
- @FlaggedApi(Flags.FLAG_MODES_API)
public void setType(@Type int type) {
mType = checkValidType(type);
}
@@ -476,7 +439,6 @@ public final class AutomaticZenRule implements Parcelable {
* Gets the user visible description of when this rule is active
* (see {@link Condition#STATE_TRUE}).
*/
- @FlaggedApi(Flags.FLAG_MODES_API)
public @Nullable String getTriggerDescription() {
return mTriggerDescription;
}
@@ -489,7 +451,6 @@ public final class AutomaticZenRule implements Parcelable {
* "When connected to [Car Name]".
* @hide
*/
- @FlaggedApi(Flags.FLAG_MODES_API)
public void setTriggerDescription(@Nullable String triggerDescription) {
mTriggerDescription = triggerDescription;
}
@@ -497,7 +458,6 @@ public final class AutomaticZenRule implements Parcelable {
/**
* Gets the resource id of the drawable icon for this rule.
*/
- @FlaggedApi(Flags.FLAG_MODES_API)
public @DrawableRes int getIconResId() {
return mIconResId;
}
@@ -506,7 +466,6 @@ public final class AutomaticZenRule implements Parcelable {
* Sets a resource id of a tintable vector drawable representing the rule in image form.
* @hide
*/
- @FlaggedApi(Flags.FLAG_MODES_API)
public void setIconResId(int iconResId) {
mIconResId = iconResId;
}
@@ -515,7 +474,6 @@ public final class AutomaticZenRule implements Parcelable {
* Gets whether this rule can be manually activated by the user even when the triggering
* condition for the rule is not met.
*/
- @FlaggedApi(Flags.FLAG_MODES_API)
public boolean isManualInvocationAllowed() {
return mAllowManualInvocation;
}
@@ -525,23 +483,18 @@ public final class AutomaticZenRule implements Parcelable {
* condition for the rule is not met.
* @hide
*/
- @FlaggedApi(Flags.FLAG_MODES_API)
public void setManualInvocationAllowed(boolean allowManualInvocation) {
mAllowManualInvocation = allowManualInvocation;
}
/** @hide */
- @FlaggedApi(Flags.FLAG_MODES_API)
public void validate() {
- if (Flags.modesApi()) {
- checkValidType(mType);
- if (mDeviceEffects != null) {
- mDeviceEffects.validate();
- }
+ checkValidType(mType);
+ if (mDeviceEffects != null) {
+ mDeviceEffects.validate();
}
}
- @FlaggedApi(Flags.FLAG_MODES_API)
@Type
private static int checkValidType(@Type int type) {
checkArgument(type >= TYPE_UNKNOWN && type <= TYPE_MANAGED,
@@ -571,39 +524,34 @@ public final class AutomaticZenRule implements Parcelable {
dest.writeParcelable(configurationActivity, 0);
dest.writeLong(creationTime);
dest.writeParcelable(mZenPolicy, 0);
- dest.writeInt(mModified ? ENABLED : DISABLED);
dest.writeString(mPkg);
- if (Flags.modesApi()) {
- dest.writeParcelable(mDeviceEffects, 0);
- dest.writeBoolean(mAllowManualInvocation);
- dest.writeInt(mIconResId);
- dest.writeString(mTriggerDescription);
- dest.writeInt(mType);
- }
+ dest.writeParcelable(mDeviceEffects, 0);
+ dest.writeBoolean(mAllowManualInvocation);
+ dest.writeInt(mIconResId);
+ dest.writeString(mTriggerDescription);
+ dest.writeInt(mType);
}
@Override
public String toString() {
- StringBuilder sb = new StringBuilder(AutomaticZenRule.class.getSimpleName()).append('[')
+ return new StringBuilder(AutomaticZenRule.class.getSimpleName())
+ .append('[')
.append("enabled=").append(enabled)
.append(",name=").append(name)
+ .append(",type=").append(mType)
.append(",interruptionFilter=").append(interruptionFilter)
.append(",pkg=").append(mPkg)
.append(",conditionId=").append(conditionId)
.append(",owner=").append(owner)
.append(",configActivity=").append(configurationActivity)
.append(",creationTime=").append(creationTime)
- .append(",mZenPolicy=").append(mZenPolicy);
-
- if (Flags.modesApi()) {
- sb.append(",deviceEffects=").append(mDeviceEffects)
- .append(",allowManualInvocation=").append(mAllowManualInvocation)
- .append(",iconResId=").append(mIconResId)
- .append(",triggerDescription=").append(mTriggerDescription)
- .append(",type=").append(mType);
- }
-
- return sb.append(']').toString();
+ .append(",mZenPolicy=").append(mZenPolicy)
+ .append(",deviceEffects=").append(mDeviceEffects)
+ .append(",allowManualInvocation=").append(mAllowManualInvocation)
+ .append(",iconResId=").append(mIconResId)
+ .append(",triggerDescription=").append(mTriggerDescription)
+ .append(']')
+ .toString();
}
/** @hide */
@@ -626,8 +574,7 @@ public final class AutomaticZenRule implements Parcelable {
if (!(o instanceof AutomaticZenRule)) return false;
if (o == this) return true;
final AutomaticZenRule other = (AutomaticZenRule) o;
- boolean finalEquals = other.enabled == enabled
- && other.mModified == mModified
+ return other.enabled == enabled
&& Objects.equals(other.name, name)
&& other.interruptionFilter == interruptionFilter
&& Objects.equals(other.conditionId, conditionId)
@@ -635,27 +582,19 @@ public final class AutomaticZenRule implements Parcelable {
&& Objects.equals(other.mZenPolicy, mZenPolicy)
&& Objects.equals(other.configurationActivity, configurationActivity)
&& Objects.equals(other.mPkg, mPkg)
- && other.creationTime == creationTime;
- if (Flags.modesApi()) {
- return finalEquals
- && Objects.equals(other.mDeviceEffects, mDeviceEffects)
- && other.mAllowManualInvocation == mAllowManualInvocation
- && other.mIconResId == mIconResId
- && Objects.equals(other.mTriggerDescription, mTriggerDescription)
- && other.mType == mType;
- }
- return finalEquals;
+ && other.creationTime == creationTime
+ && Objects.equals(other.mDeviceEffects, mDeviceEffects)
+ && other.mAllowManualInvocation == mAllowManualInvocation
+ && other.mIconResId == mIconResId
+ && Objects.equals(other.mTriggerDescription, mTriggerDescription)
+ && other.mType == mType;
}
@Override
public int hashCode() {
- if (Flags.modesApi()) {
- return Objects.hash(enabled, name, interruptionFilter, conditionId, owner,
- configurationActivity, mZenPolicy, mDeviceEffects, mModified, creationTime,
- mPkg, mAllowManualInvocation, mIconResId, mTriggerDescription, mType);
- }
return Objects.hash(enabled, name, interruptionFilter, conditionId, owner,
- configurationActivity, mZenPolicy, mModified, creationTime, mPkg);
+ configurationActivity, mZenPolicy, mDeviceEffects, creationTime,
+ mPkg, mAllowManualInvocation, mIconResId, mTriggerDescription, mType);
}
public static final @android.annotation.NonNull Parcelable.Creator<AutomaticZenRule> CREATOR
@@ -705,7 +644,6 @@ public final class AutomaticZenRule implements Parcelable {
return input;
}
- @FlaggedApi(Flags.FLAG_MODES_API)
public static final class Builder {
private String mName;
private ComponentName mOwner;
diff --git a/core/java/android/app/INotificationManager.aidl b/core/java/android/app/INotificationManager.aidl
index b9255ecaf1b6..00df7246a300 100644
--- a/core/java/android/app/INotificationManager.aidl
+++ b/core/java/android/app/INotificationManager.aidl
@@ -225,8 +225,6 @@ interface INotificationManager
ZenPolicy getDefaultZenPolicy();
AutomaticZenRule getAutomaticZenRule(String id);
Map<String, AutomaticZenRule> getAutomaticZenRules();
- // TODO: b/310620812 - Remove getZenRules() when MODES_API is inlined.
- List<ZenModeConfig.ZenRule> getZenRules();
String addAutomaticZenRule(in AutomaticZenRule automaticZenRule, String pkg, boolean fromUser);
boolean updateAutomaticZenRule(String id, in AutomaticZenRule automaticZenRule, boolean fromUser);
boolean removeAutomaticZenRule(String id, boolean fromUser);
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java
index 0ca4a329fd5a..5dca1c70a2e6 100644
--- a/core/java/android/app/Notification.java
+++ b/core/java/android/app/Notification.java
@@ -6002,7 +6002,7 @@ public class Notification implements Parcelable
// HUNS, which use a different layout that already accounts for that). Templates that
// have content that will be displayed under the small icon also use a different margin.
if (Flags.notificationsRedesignTemplates()
- && !p.mHeaderless && !p.mHasContentInLeftMargin) {
+ && !p.mHeaderless && !p.mSkipTopLineAlignment) {
int margin = getContentMarginTop(mContext,
R.dimen.notification_2025_content_margin_top);
contentView.setViewLayoutMargin(R.id.notification_main_column,
@@ -6594,13 +6594,8 @@ public class Notification implements Parcelable
int notifMargin = resources.getDimensionPixelSize(R.dimen.notification_2025_margin);
// Spacing between the text lines, scaling with the font size (originally in sp)
int spacing = resources.getDimensionPixelSize(spacingRes);
-
// Size of the text in the notification top line (originally in sp)
- int[] textSizeAttr = new int[] { android.R.attr.textSize };
- TypedArray typedArray = context.obtainStyledAttributes(
- R.style.TextAppearance_DeviceDefault_Notification_Info, textSizeAttr);
- int textSize = typedArray.getDimensionPixelSize(0 /* index */, -1 /* default */);
- typedArray.recycle();
+ int textSize = resources.getDimensionPixelSize(R.dimen.notification_subtext_size);
// Adding up all the values as pixels
return notifMargin + spacing + textSize;
@@ -9503,7 +9498,7 @@ public class Notification implements Parcelable
.hideLeftIcon(isOneToOne)
.hideRightIcon(hideRightIcons || isOneToOne)
.headerTextSecondary(isHeaderless ? null : conversationTitle)
- .hasContentInLeftMargin(true);
+ .skipTopLineAlignment(true);
RemoteViews contentView = mBuilder.applyStandardTemplateWithActions(
isConversationLayout
? mBuilder.getConversationLayoutResource()
@@ -14681,7 +14676,7 @@ public class Notification implements Parcelable
Icon mPromotedPicture;
boolean mCallStyleActions;
boolean mAllowTextWithProgress;
- boolean mHasContentInLeftMargin;
+ boolean mSkipTopLineAlignment;
int mTitleViewId;
int mTextViewId;
@Nullable CharSequence mTitle;
@@ -14707,7 +14702,7 @@ public class Notification implements Parcelable
mPromotedPicture = null;
mCallStyleActions = false;
mAllowTextWithProgress = false;
- mHasContentInLeftMargin = false;
+ mSkipTopLineAlignment = false;
mTitleViewId = R.id.title;
mTextViewId = R.id.text;
mTitle = null;
@@ -14774,8 +14769,8 @@ public class Notification implements Parcelable
return this;
}
- public StandardTemplateParams hasContentInLeftMargin(boolean hasContentInLeftMargin) {
- mHasContentInLeftMargin = hasContentInLeftMargin;
+ public StandardTemplateParams skipTopLineAlignment(boolean skipTopLineAlignment) {
+ mSkipTopLineAlignment = skipTopLineAlignment;
return this;
}
diff --git a/core/java/android/app/NotificationManager.java b/core/java/android/app/NotificationManager.java
index 00f896deae4b..726999a08322 100644
--- a/core/java/android/app/NotificationManager.java
+++ b/core/java/android/app/NotificationManager.java
@@ -356,7 +356,6 @@ public class NotificationManager {
* a DND component, the rule owner should activate any extra behavior that's part of that mode
* in response to this broadcast.
*/
- @FlaggedApi(Flags.FLAG_MODES_API)
public static final int AUTOMATIC_RULE_STATUS_ACTIVATED = 4;
/**
@@ -367,7 +366,6 @@ public class NotificationManager {
* longer met) and then {@link Condition#STATE_TRUE} when the trigger criteria is freshly met,
* or when the user re-activates it.
*/
- @FlaggedApi(Flags.FLAG_MODES_API)
public static final int AUTOMATIC_RULE_STATUS_DEACTIVATED = 5;
/**
@@ -415,7 +413,6 @@ public class NotificationManager {
* <p>This broadcast is only sent to registered receivers and receivers in packages that have
* been granted Notification Policy access (see {@link #isNotificationPolicyAccessGranted()}).
*/
- @FlaggedApi(Flags.FLAG_MODES_API)
@SdkConstant(SdkConstant.SdkConstantType.BROADCAST_INTENT_ACTION)
public static final String ACTION_CONSOLIDATED_NOTIFICATION_POLICY_CHANGED =
"android.app.action.CONSOLIDATED_NOTIFICATION_POLICY_CHANGED";
@@ -425,7 +422,6 @@ public class NotificationManager {
* {@link #ACTION_CONSOLIDATED_NOTIFICATION_POLICY_CHANGED} containing the new
* {@link Policy} value.
*/
- @FlaggedApi(Flags.FLAG_MODES_API)
public static final String EXTRA_NOTIFICATION_POLICY =
"android.app.extra.NOTIFICATION_POLICY";
@@ -1726,9 +1722,8 @@ public class NotificationManager {
* rule management to system settings/uis via
* {@link Settings#ACTION_AUTOMATIC_ZEN_RULE_SETTINGS}.
*/
- @FlaggedApi(Flags.FLAG_MODES_API)
public boolean areAutomaticZenRulesUserManaged() {
- if (Flags.modesApi() && Flags.modesUi()) {
+ if (Flags.modesUi()) {
PackageManager pm = mContext.getPackageManager();
return !pm.hasSystemFeature(PackageManager.FEATURE_WATCH)
&& !pm.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE)
@@ -1748,21 +1743,7 @@ public class NotificationManager {
public Map<String, AutomaticZenRule> getAutomaticZenRules() {
INotificationManager service = service();
try {
- if (Flags.modesApi()) {
- return service.getAutomaticZenRules();
- } else {
- List<ZenModeConfig.ZenRule> rules = service.getZenRules();
- Map<String, AutomaticZenRule> ruleMap = new HashMap<>();
- for (ZenModeConfig.ZenRule rule : rules) {
- AutomaticZenRule azr = new AutomaticZenRule(rule.name, rule.component,
- rule.configurationActivity, rule.conditionId, rule.zenPolicy,
- zenModeToInterruptionFilter(rule.zenMode), rule.enabled,
- rule.creationTime);
- azr.setPackageName(rule.pkg);
- ruleMap.put(rule.id, azr);
- }
- return ruleMap;
- }
+ return service.getAutomaticZenRules();
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -1804,7 +1785,6 @@ public class NotificationManager {
/** @hide */
@TestApi
- @FlaggedApi(Flags.FLAG_MODES_API)
@NonNull
public String addAutomaticZenRule(@NonNull AutomaticZenRule automaticZenRule,
boolean fromUser) {
@@ -1840,7 +1820,6 @@ public class NotificationManager {
/** @hide */
@TestApi
- @FlaggedApi(Flags.FLAG_MODES_API)
public boolean updateAutomaticZenRule(@NonNull String id,
@NonNull AutomaticZenRule automaticZenRule, boolean fromUser) {
INotificationManager service = service();
@@ -1860,7 +1839,6 @@ public class NotificationManager {
* @param id The id of the rule
* @return the state of the rule.
*/
- @FlaggedApi(Flags.FLAG_MODES_API)
@Condition.State
public int getAutomaticZenRuleState(@NonNull String id) {
INotificationManager service = service();
@@ -1935,7 +1913,6 @@ public class NotificationManager {
/** @hide */
@TestApi
- @FlaggedApi(Flags.FLAG_MODES_API)
public boolean removeAutomaticZenRule(@NonNull String id, boolean fromUser) {
INotificationManager service = service();
try {
@@ -2326,7 +2303,6 @@ public class NotificationManager {
* @hide
*/
@TestApi
- @FlaggedApi(Flags.FLAG_MODES_API)
public @NonNull ZenPolicy getDefaultZenPolicy() {
INotificationManager service = service();
try {
@@ -2693,7 +2669,7 @@ public class NotificationManager {
/**
* @hide
*/
- public static final int STATE_CHANNELS_BYPASSING_DND = 1 << 0;
+ public static final int STATE_HAS_PRIORITY_CHANNELS = 1 << 0;
/**
* Whether the policy indicates that even priority channels are NOT permitted to bypass DND.
@@ -2918,7 +2894,7 @@ public class NotificationManager {
@Override
public String toString() {
- StringBuilder sb = new StringBuilder().append("NotificationManager.Policy[")
+ return new StringBuilder().append("NotificationManager.Policy[")
.append("priorityCategories=")
.append(priorityCategoriesToString(priorityCategories))
.append(",priorityCallSenders=")
@@ -2928,24 +2904,19 @@ public class NotificationManager {
.append(",priorityConvSenders=")
.append(conversationSendersToString(priorityConversationSenders))
.append(",suppressedVisualEffects=")
- .append(suppressedEffectsToString(suppressedVisualEffects));
- if (Flags.modesApi()) {
- sb.append(",hasPriorityChannels=");
- } else {
- sb.append(",areChannelsBypassingDnd=");
- }
- sb.append((state == STATE_UNSET
- ? "unset"
- : ((state & STATE_CHANNELS_BYPASSING_DND) != 0)
- ? "true"
- : "false"));
- if (Flags.modesApi()) {
- sb.append(",allowPriorityChannels=")
- .append((state == STATE_UNSET
- ? "unset"
- : (allowPriorityChannels() ? "true" : "false")));
- }
- return sb.append("]").toString();
+ .append(suppressedEffectsToString(suppressedVisualEffects))
+ .append(",hasPriorityChannels=")
+ .append((state == STATE_UNSET
+ ? "unset"
+ : ((state & STATE_HAS_PRIORITY_CHANNELS) != 0)
+ ? "true"
+ : "false"))
+ .append(",allowPriorityChannels=")
+ .append((state == STATE_UNSET
+ ? "unset"
+ : (allowPriorityChannels() ? "true" : "false")))
+ .append("]")
+ .toString();
}
/** @hide */
@@ -3220,7 +3191,6 @@ public class NotificationManager {
}
/** @hide **/
- @FlaggedApi(Flags.FLAG_MODES_API)
@TestApi // so CTS tests can read this state without having to use implementation detail
public boolean allowPriorityChannels() {
if (state == STATE_UNSET) {
@@ -3230,17 +3200,15 @@ public class NotificationManager {
}
/** @hide */
- @FlaggedApi(Flags.FLAG_MODES_API)
public boolean hasPriorityChannels() {
- return (state & STATE_CHANNELS_BYPASSING_DND) != 0;
+ return (state & STATE_HAS_PRIORITY_CHANNELS) != 0;
}
/** @hide **/
- @FlaggedApi(Flags.FLAG_MODES_API)
public static int policyState(boolean hasPriorityChannels, boolean allowPriorityChannels) {
int state = 0;
if (hasPriorityChannels) {
- state |= STATE_CHANNELS_BYPASSING_DND;
+ state |= STATE_HAS_PRIORITY_CHANNELS;
}
if (!allowPriorityChannels) {
state |= STATE_PRIORITY_CHANNELS_BLOCKED;
diff --git a/core/java/android/app/UiAutomation.java b/core/java/android/app/UiAutomation.java
index 7b63ab80964d..464bcc025d92 100644
--- a/core/java/android/app/UiAutomation.java
+++ b/core/java/android/app/UiAutomation.java
@@ -956,10 +956,9 @@ public final class UiAutomation {
* <p>
* <strong>Note:</strong> It is caller's responsibility to recycle the event.
* </p>
- *
- * @param event The event to inject.
- * @param sync Whether to inject the event synchronously.
- * @return Whether event injection succeeded.
+ * @param event the event to inject
+ * @param sync whether to inject the event synchronously
+ * @return {@code true} if event injection succeeded
*/
public boolean injectInputEvent(InputEvent event, boolean sync) {
return injectInputEvent(event, sync, true /* waitForAnimations */);
@@ -972,15 +971,21 @@ public final class UiAutomation {
* <strong>Note:</strong> It is caller's responsibility to recycle the event.
* </p>
*
- * @param event The event to inject.
- * @param sync Whether to inject the event synchronously.
- * @param waitForAnimations Whether to wait for all window container animations and surface
- * operations to complete.
- * @return Whether event injection succeeded.
+ * @param event the event to inject
+ * @param sync whether to inject the event synchronously.
+ * @param waitForAnimations whether to wait for all window container animations and surface
+ * operations to complete
+ * @return {@code true} if event injection succeeded
*
+ * @deprecated for CTS tests prefer inject input events using uinput
+ * (com.android.cts.input.UinputDevice) or hid devices (com.android.cts.input.HidDevice).
+ * Alternatively, InjectInputInProcess (com.android.cts.input.InjectInputProcess) can be used
+ * for in-process injection.
* @hide
*/
@TestApi
+ @Deprecated // Deprecated for CTS tests
+ @SuppressLint("UnflaggedApi") // @FlaggedApi breaks previously released @TestApi, b/395889250
public boolean injectInputEvent(@NonNull InputEvent event, boolean sync,
boolean waitForAnimations) {
try {
@@ -1003,9 +1008,15 @@ public final class UiAutomation {
* Events injected to the input subsystem using the standard {@link #injectInputEvent} method
* skip the accessibility input filter to avoid feedback loops.
*
+ * @deprecated for CTS tests prefer inject input events using uinput
+ * (com.android.cts.input.UinputDevice) or hid devices (com.android.cts.input.HidDevice).
+ * Alternatively, InjectInputInProcess (com.android.cts.input.InjectInputProcess) can be used
+ * for in-process injection.
* @hide
*/
@TestApi
+ @Deprecated
+ @SuppressLint("UnflaggedApi") // @FlaggedApi breaks previously released @TestApi, b/395889250
public void injectInputEventToInputFilter(@NonNull InputEvent event) {
try {
mUiAutomationConnection.injectInputEventToInputFilter(event);
diff --git a/core/java/android/app/UiModeManager.java b/core/java/android/app/UiModeManager.java
index f6c789d51aee..33466dd79be1 100644
--- a/core/java/android/app/UiModeManager.java
+++ b/core/java/android/app/UiModeManager.java
@@ -312,7 +312,6 @@ public class UiModeManager {
* #getAttentionModeThemeOverlay()}: Keeps night mode as set by {@link #setNightMode(int)}.
* @hide
*/
- @FlaggedApi(Flags.FLAG_MODES_API)
@TestApi
public static final int MODE_ATTENTION_THEME_OVERLAY_OFF = 1000;
@@ -321,7 +320,6 @@ public class UiModeManager {
* #getAttentionModeThemeOverlay()}: Maintains night mode always on.
* @hide
*/
- @FlaggedApi(Flags.FLAG_MODES_API)
@TestApi
public static final int MODE_ATTENTION_THEME_OVERLAY_NIGHT = 1001;
@@ -330,7 +328,6 @@ public class UiModeManager {
* #getAttentionModeThemeOverlay()}: Maintains night mode always off (Light).
* @hide
*/
- @FlaggedApi(Flags.FLAG_MODES_API)
@TestApi
public static final int MODE_ATTENTION_THEME_OVERLAY_DAY = 1002;
@@ -338,7 +335,6 @@ public class UiModeManager {
* Constant for {@link #getAttentionModeThemeOverlay()}: Error communication with server.
* @hide
*/
- @FlaggedApi(Flags.FLAG_MODES_API)
@TestApi
public static final int MODE_ATTENTION_THEME_OVERLAY_UNKNOWN = -1;
@@ -940,7 +936,6 @@ public class UiModeManager {
* {@code AttentionModeThemeOverlayType}.
* @hide
*/
- @FlaggedApi(Flags.FLAG_MODES_API)
@RequiresPermission(android.Manifest.permission.MODIFY_DAY_NIGHT_MODE)
public void setAttentionModeThemeOverlay(
@AttentionModeThemeOverlayType int attentionModeThemeOverlayType) {
@@ -967,7 +962,6 @@ public class UiModeManager {
*
* @hide
*/
- @FlaggedApi(Flags.FLAG_MODES_API)
@TestApi
@RequiresPermission(android.Manifest.permission.MODIFY_DAY_NIGHT_MODE)
public @AttentionModeThemeOverlayReturnType int getAttentionModeThemeOverlay() {
diff --git a/core/java/android/app/contextualsearch/ContextualSearchManager.java b/core/java/android/app/contextualsearch/ContextualSearchManager.java
index 2ce431dcb32d..4e5fa6bac951 100644
--- a/core/java/android/app/contextualsearch/ContextualSearchManager.java
+++ b/core/java/android/app/contextualsearch/ContextualSearchManager.java
@@ -32,6 +32,9 @@ import android.util.Log;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
/**
* {@link ContextualSearchManager} is a system service to facilitate contextual search experience on
@@ -39,10 +42,8 @@ import java.lang.annotation.RetentionPolicy;
* <p>
* This class lets a caller start contextual search by calling {@link #startContextualSearch}
* method.
- *
- * @hide
*/
-@SystemApi
+@FlaggedApi(Flags.FLAG_SELF_INVOCATION)
public final class ContextualSearchManager {
/**
@@ -50,7 +51,9 @@ public final class ContextualSearchManager {
* Only supposed to be used with ACTION_LAUNCH_CONTEXTUAL_SEARCH.
*
* @see #ACTION_LAUNCH_CONTEXTUAL_SEARCH
+ * @hide
*/
+ @SystemApi
public static final String EXTRA_ENTRYPOINT =
"android.app.contextualsearch.extra.ENTRYPOINT";
@@ -60,7 +63,9 @@ public final class ContextualSearchManager {
* Only supposed to be used with ACTION_LAUNCH_CONTEXTUAL_SEARCH.
*
* @see #ACTION_LAUNCH_CONTEXTUAL_SEARCH
+ * @hide
*/
+ @SystemApi
public static final String EXTRA_FLAG_SECURE_FOUND =
"android.app.contextualsearch.extra.FLAG_SECURE_FOUND";
@@ -69,7 +74,9 @@ public final class ContextualSearchManager {
* Only supposed to be used with ACTION_LAUNCH_CONTEXTUAL_SEARCH.
*
* @see #ACTION_LAUNCH_CONTEXTUAL_SEARCH
+ * @hide
*/
+ @SystemApi
public static final String EXTRA_SCREENSHOT =
"android.app.contextualsearch.extra.SCREENSHOT";
@@ -79,7 +86,9 @@ public final class ContextualSearchManager {
* Only supposed to be used with ACTION_LAUNCH_CONTEXTUAL_SEARCH.
*
* @see #ACTION_LAUNCH_CONTEXTUAL_SEARCH
+ * @hide
*/
+ @SystemApi
public static final String EXTRA_IS_MANAGED_PROFILE_VISIBLE =
"android.app.contextualsearch.extra.IS_MANAGED_PROFILE_VISIBLE";
@@ -89,7 +98,9 @@ public final class ContextualSearchManager {
* Only supposed to be used with ACTION_LAUNCH_CONTEXTUAL_SEARCH.
*
* @see #ACTION_LAUNCH_CONTEXTUAL_SEARCH
+ * @hide
*/
+ @SystemApi
public static final String EXTRA_VISIBLE_PACKAGE_NAMES =
"android.app.contextualsearch.extra.VISIBLE_PACKAGE_NAMES";
@@ -98,10 +109,9 @@ public final class ContextualSearchManager {
* {@link SystemClock#uptimeMillis()}.
* Only supposed to be used with ACTION_LAUNCH_CONTEXTUAL_SEARCH.
*
- * @see #ACTION_LAUNCH_CONTEXTUAL_SEARCH
- *
* TODO: un-hide in W
*
+ * @see #ACTION_LAUNCH_CONTEXTUAL_SEARCH
* @hide
*/
public static final String EXTRA_INVOCATION_TIME_MS =
@@ -113,7 +123,9 @@ public final class ContextualSearchManager {
* Only supposed to be used with ACTION_LAUNCH_CONTEXTUAL_SEARCH.
*
* @see #ACTION_LAUNCH_CONTEXTUAL_SEARCH
+ * @hide
*/
+ @SystemApi
public static final String EXTRA_TOKEN = "android.app.contextualsearch.extra.TOKEN";
/**
@@ -132,7 +144,10 @@ public final class ContextualSearchManager {
* experience must add this intent filter action to the activity it wants to be launched.
* <br>
* <b>Note</b> This activity must not be exported.
+ *
+ * @hide
*/
+ @SystemApi
public static final String ACTION_LAUNCH_CONTEXTUAL_SEARCH =
"android.app.contextualsearch.action.LAUNCH_CONTEXTUAL_SEARCH";
@@ -144,23 +159,63 @@ public final class ContextualSearchManager {
public static final String FEATURE_CONTEXTUAL_SEARCH =
"com.google.android.feature.CONTEXTUAL_SEARCH";
- /** Entrypoint to be used when a user long presses on the nav handle. */
+ /**
+ * Entrypoint to be used when a user long presses on the nav handle.
+ *
+ * @hide
+ */
+ @SystemApi
public static final int ENTRYPOINT_LONG_PRESS_NAV_HANDLE = 1;
- /** Entrypoint to be used when a user long presses on the home button. */
+
+ /** Entrypoint to be used when a user long presses on the home button.
+ *
+ * @hide
+ */
+ @SystemApi
public static final int ENTRYPOINT_LONG_PRESS_HOME = 2;
- /** Entrypoint to be used when a user long presses on the overview button. */
+
+ /** Entrypoint to be used when a user long presses on the overview button.
+ *
+ * @hide
+ */
+ @SystemApi
public static final int ENTRYPOINT_LONG_PRESS_OVERVIEW = 3;
- /** Entrypoint to be used when a user presses the action button in overview. */
+
+ /**
+ * Entrypoint to be used when a user presses the action button in overview.
+ *
+ * @hide
+ */
+ @SystemApi
public static final int ENTRYPOINT_OVERVIEW_ACTION = 4;
- /** Entrypoint to be used when a user presses the context menu button in overview. */
+
+ /**
+ * Entrypoint to be used when a user presses the context menu button in overview.
+ *
+ * @hide
+ */
+ @SystemApi
public static final int ENTRYPOINT_OVERVIEW_MENU = 5;
- /** Entrypoint to be used by system actions like TalkBack, Accessibility etc. */
+
+ /**
+ * Entrypoint to be used by system actions like TalkBack, Accessibility etc.
+ *
+ * @hide
+ */
+ @SystemApi
public static final int ENTRYPOINT_SYSTEM_ACTION = 9;
- /** Entrypoint to be used when a user long presses on the meta key. */
+
+ /**
+ * Entrypoint to be used when a user long presses on the meta key.
+ *
+ * @hide
+ */
+ @SystemApi
public static final int ENTRYPOINT_LONG_PRESS_META = 10;
+
/**
* The {@link Entrypoint} annotation is used to standardize the entrypoints supported by
- * {@link #startContextualSearch} method.
+ * {@link #startContextualSearch(int entrypoint)} method.
*
* @hide
*/
@@ -174,8 +229,18 @@ public final class ContextualSearchManager {
ENTRYPOINT_LONG_PRESS_META
})
@Retention(RetentionPolicy.SOURCE)
- public @interface Entrypoint {
- }
+ public @interface Entrypoint {}
+
+ private static final Set<Integer> VALID_ENTRYPOINT_VALUES = new HashSet<>(Arrays.asList(
+ ENTRYPOINT_LONG_PRESS_NAV_HANDLE,
+ ENTRYPOINT_LONG_PRESS_HOME,
+ ENTRYPOINT_LONG_PRESS_OVERVIEW,
+ ENTRYPOINT_OVERVIEW_ACTION,
+ ENTRYPOINT_OVERVIEW_MENU,
+ ENTRYPOINT_SYSTEM_ACTION,
+ ENTRYPOINT_LONG_PRESS_META
+ ));
+
private static final String TAG = ContextualSearchManager.class.getSimpleName();
private static final boolean DEBUG = false;
@@ -189,7 +254,7 @@ public final class ContextualSearchManager {
}
/**
- * Used to start contextual search.
+ * Used to start contextual search for a given system entrypoint.
* <p>
* When {@link #startContextualSearch} is called, the system server does the following:
* <ul>
@@ -202,9 +267,15 @@ public final class ContextualSearchManager {
* </p>
*
* @param entrypoint the invocation entrypoint
+ *
+ * @hide
*/
@RequiresPermission(ACCESS_CONTEXTUAL_SEARCH)
+ @SystemApi
public void startContextualSearch(@Entrypoint int entrypoint) {
+ if (!VALID_ENTRYPOINT_VALUES.contains(entrypoint)) {
+ throw new IllegalArgumentException("Invalid entrypoint: " + entrypoint);
+ }
if (DEBUG) Log.d(TAG, "startContextualSearch for entrypoint: " + entrypoint);
try {
mService.startContextualSearch(entrypoint);
@@ -213,4 +284,22 @@ public final class ContextualSearchManager {
e.rethrowFromSystemServer();
}
}
+
+ /**
+ * Used to start contextual search from within an app.
+ *
+ * <p>System apps should use the available System APIs rather than this method.
+ *
+ * @throws SecurityException if the caller does not have a foreground Activity.
+ */
+ @FlaggedApi(Flags.FLAG_SELF_INVOCATION)
+ public void startContextualSearch() {
+ if (DEBUG) Log.d(TAG, "startContextualSearch from app");
+ try {
+ mService.startContextualSearchForForegroundApp();
+ } catch (RemoteException e) {
+ if (DEBUG) Log.d(TAG, "Failed to startContextualSearch", e);
+ e.rethrowFromSystemServer();
+ }
+ }
}
diff --git a/core/java/android/app/contextualsearch/IContextualSearchManager.aidl b/core/java/android/app/contextualsearch/IContextualSearchManager.aidl
index 9b0b8b775971..8789daab3afe 100644
--- a/core/java/android/app/contextualsearch/IContextualSearchManager.aidl
+++ b/core/java/android/app/contextualsearch/IContextualSearchManager.aidl
@@ -4,7 +4,8 @@ import android.app.contextualsearch.IContextualSearchCallback;
/**
* @hide
*/
-oneway interface IContextualSearchManager {
- void startContextualSearch(int entrypoint);
- void getContextualSearchState(in IBinder token, in IContextualSearchCallback callback);
+interface IContextualSearchManager {
+ void startContextualSearchForForegroundApp();
+ oneway void startContextualSearch(int entrypoint);
+ oneway void getContextualSearchState(in IBinder token, in IContextualSearchCallback callback);
}
diff --git a/core/java/android/app/contextualsearch/flags.aconfig b/core/java/android/app/contextualsearch/flags.aconfig
index d81ec1e8b883..bc1f7cea7fce 100644
--- a/core/java/android/app/contextualsearch/flags.aconfig
+++ b/core/java/android/app/contextualsearch/flags.aconfig
@@ -39,3 +39,11 @@ flag {
description: "Add audio playing status to the contextual search invocation intent."
bug: "372935419"
}
+
+flag {
+ name: "self_invocation"
+ namespace: "sysui_integrations"
+ description: "Enable apps to self-invoke Contextual Search."
+ bug: "368653769"
+ is_exported: true
+}
diff --git a/core/java/android/app/jank/JankTracker.java b/core/java/android/app/jank/JankTracker.java
index a04f96a9f6e3..9c85b09f6be3 100644
--- a/core/java/android/app/jank/JankTracker.java
+++ b/core/java/android/app/jank/JankTracker.java
@@ -20,6 +20,7 @@ import android.annotation.FlaggedApi;
import android.annotation.NonNull;
import android.os.Handler;
import android.os.HandlerThread;
+import android.util.Log;
import android.view.AttachedSurfaceControl;
import android.view.Choreographer;
import android.view.SurfaceControl;
@@ -30,16 +31,22 @@ import com.android.internal.annotations.VisibleForTesting;
import java.util.ArrayList;
import java.util.HashMap;
+import java.util.List;
/**
* This class is responsible for registering callbacks that will receive JankData batches.
* It handles managing the background thread that JankData will be processed on. As well as acting
* as an intermediary between widgets and the state tracker, routing state changes to the tracker.
+ *
* @hide
*/
@FlaggedApi(Flags.FLAG_DETAILED_APP_JANK_METRICS_API)
public class JankTracker {
-
+ private static final boolean DEBUG = false;
+ private static final String DEBUG_KEY = "JANKTRACKER";
+ // How long to delay the JankData listener registration.
+ //TODO b/394956095 see if this can be reduced or eliminated.
+ private static final int REGISTRATION_DELAY_MS = 1000;
// Tracks states reported by widgets.
private StateTracker mStateTracker;
// Processes JankData batches and associates frames to widget states.
@@ -49,9 +56,6 @@ public class JankTracker {
private HandlerThread mHandlerThread = new HandlerThread("AppJankTracker");
private Handler mHandler = null;
- // Needed so we know when the view is attached to a window.
- private ViewTreeObserver mViewTreeObserver;
-
// Handle to a registered OnJankData listener.
private SurfaceControl.OnJankDataListenerRegistration mJankDataListenerRegistration;
@@ -76,6 +80,40 @@ public class JankTracker {
*/
private boolean mListenersRegistered = false;
+ @FlaggedApi(com.android.window.flags.Flags.FLAG_JANK_API)
+ private final SurfaceControl.OnJankDataListener mJankDataListener =
+ new SurfaceControl.OnJankDataListener() {
+ @Override
+ public void onJankDataAvailable(
+ @androidx.annotation.NonNull List<SurfaceControl.JankData> jankData) {
+ if (mJankDataProcessor == null) return;
+ mJankDataProcessor.processJankData(jankData, mActivityName, mAppUid);
+ }
+ };
+
+ private final ViewTreeObserver.OnWindowAttachListener mOnWindowAttachListener =
+ new ViewTreeObserver.OnWindowAttachListener() {
+ @Override
+ public void onWindowAttached() {
+ getHandler().postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ mDecorView.getViewTreeObserver()
+ .removeOnWindowAttachListener(mOnWindowAttachListener);
+ registerForJankData();
+ }
+ }, REGISTRATION_DELAY_MS);
+ }
+
+ // Leave this empty. Only need to know when the DecorView is attached to the Window
+ // in order to get a handle to AttachedSurfaceControl. There is no need to tie
+ // anything to when the view is detached as all un-registration code is tied to
+ // the lifecycle of the enclosing activity.
+ @Override
+ public void onWindowDetached() {
+
+ }
+ };
public JankTracker(Choreographer choreographer, View decorView) {
mStateTracker = new StateTracker(choreographer);
@@ -108,9 +146,10 @@ public class JankTracker {
/**
* Will add the widget category, id and state as a UI state to associate frames to it.
+ *
* @param widgetCategory preselected general widget category
- * @param widgetId developer defined widget id if available.
- * @param widgetState the current active widget state.
+ * @param widgetId developer defined widget id if available.
+ * @param widgetState the current active widget state.
*/
public void addUiState(String widgetCategory, String widgetId, String widgetState) {
if (!shouldTrack()) return;
@@ -121,9 +160,10 @@ public class JankTracker {
/**
* Will remove the widget category, id and state as a ui state and no longer attribute frames
* to it.
+ *
* @param widgetCategory preselected general widget category
- * @param widgetId developer defined widget id if available.
- * @param widgetState no longer active widget state.
+ * @param widgetId developer defined widget id if available.
+ * @param widgetState no longer active widget state.
*/
public void removeUiState(String widgetCategory, String widgetId, String widgetState) {
if (!shouldTrack()) return;
@@ -133,10 +173,11 @@ public class JankTracker {
/**
* Call to update a jank state to a different state.
+ *
* @param widgetCategory preselected general widget category.
- * @param widgetId developer defined widget id if available.
- * @param currentState current state of the widget.
- * @param nextState the state the widget will be in.
+ * @param widgetId developer defined widget id if available.
+ * @param currentState current state of the widget.
+ * @param nextState the state the widget will be in.
*/
public void updateUiState(String widgetCategory, String widgetId, String currentState,
String nextState) {
@@ -150,10 +191,11 @@ public class JankTracker {
*/
public void enableAppJankTracking() {
// Add the activity as a state, this will ensure we track frames to the activity without the
- // need of a decorated widget to be used.
+ // need for a decorated widget to be used.
// TODO b/376116199 replace "NONE" with UNSPECIFIED once the API changes are merged.
mStateTracker.putState("NONE", mActivityName, "NONE");
mTrackingEnabled = true;
+ registerForJankData();
}
/**
@@ -163,10 +205,12 @@ public class JankTracker {
mTrackingEnabled = false;
// TODO b/376116199 replace "NONE" with UNSPECIFIED once the API changes are merged.
mStateTracker.removeState("NONE", mActivityName, "NONE");
+ unregisterForJankData();
}
/**
* Retrieve all pending widget states, this is intended for testing purposes only.
+ *
* @param stateDataList the ArrayList that will be populated with the pending states.
*/
@VisibleForTesting
@@ -190,16 +234,35 @@ public class JankTracker {
@VisibleForTesting
public void forceListenerRegistration() {
mSurfaceControl = mDecorView.getRootSurfaceControl();
- registerForJankData();
- // TODO b/376116199 Check if registration is good.
- mListenersRegistered = true;
+ registerJankDataListener();
+ }
+
+ private void unregisterForJankData() {
+ if (mJankDataListenerRegistration == null) return;
+
+ if (com.android.window.flags.Flags.jankApi()) {
+ mJankDataListenerRegistration.release();
+ }
+ mJankDataListenerRegistration = null;
+ mListenersRegistered = false;
}
private void registerForJankData() {
- if (mSurfaceControl == null) return;
- /*
- TODO b/376115668 Register for JankData batches from new JankTracking API
- */
+ if (mDecorView == null) return;
+
+ mSurfaceControl = mDecorView.getRootSurfaceControl();
+
+ if (mSurfaceControl == null || mListenersRegistered) return;
+
+ // Wait a short time before registering the listener. During development it was observed
+ // that if a listener is registered too quickly after a hot or warm start no data is
+ // received b/394956095.
+ getHandler().postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ registerJankDataListener();
+ }
+ }, REGISTRATION_DELAY_MS);
}
/**
@@ -218,23 +281,30 @@ public class JankTracker {
*/
private void registerWindowListeners() {
if (mDecorView == null) return;
- mViewTreeObserver = mDecorView.getViewTreeObserver();
- mViewTreeObserver.addOnWindowAttachListener(new ViewTreeObserver.OnWindowAttachListener() {
- @Override
- public void onWindowAttached() {
- getHandler().postDelayed(new Runnable() {
- @Override
- public void run() {
- forceListenerRegistration();
- }
- }, 1000);
+ mDecorView.getViewTreeObserver().addOnWindowAttachListener(mOnWindowAttachListener);
+ }
+
+ private void registerJankDataListener() {
+ if (mSurfaceControl == null) {
+ if (DEBUG) {
+ Log.d(DEBUG_KEY, "SurfaceControl is Null");
}
+ return;
+ }
- @Override
- public void onWindowDetached() {
- // TODO b/376116199 do we un-register the callback or just not process the data.
+ if (com.android.window.flags.Flags.jankApi()) {
+ mJankDataListenerRegistration = mSurfaceControl.registerOnJankDataListener(
+ mHandlerThread.getThreadExecutor(), mJankDataListener);
+
+ if (mJankDataListenerRegistration
+ == SurfaceControl.OnJankDataListenerRegistration.NONE) {
+ if (DEBUG) {
+ Log.d(DEBUG_KEY, "OnJankDataListenerRegistration is assigned NONE");
+ }
+ return;
}
- });
+ mListenersRegistered = true;
+ }
}
private Handler getHandler() {
diff --git a/core/java/android/app/notification.aconfig b/core/java/android/app/notification.aconfig
index 9d8ab03982e6..8e6b88c66408 100644
--- a/core/java/android/app/notification.aconfig
+++ b/core/java/android/app/notification.aconfig
@@ -26,6 +26,7 @@ flag {
bug: "378660052"
}
+# Flag for finalized API: In Nextfood but exported (and therefore must stay).
flag {
name: "modes_api"
is_exported: true
@@ -82,6 +83,13 @@ flag {
}
flag {
+ name: "modes_cleanup_implicit"
+ namespace: "systemui"
+ description: "Deletes implicit modes if never customized and not used for some time. Depends on MODES_UI"
+ bug: "394087495"
+}
+
+flag {
name: "api_tvextender"
is_exported: true
namespace: "systemui"
@@ -321,7 +329,7 @@ flag {
name: "no_sbnholder"
namespace: "systemui"
description: "removes sbnholder from NLS"
- bug: "362981561"
+ bug: "378128805"
}
flag {
diff --git a/core/java/android/app/supervision/ISupervisionManager.aidl b/core/java/android/app/supervision/ISupervisionManager.aidl
index e583302e4d3b..2f67a8abcd17 100644
--- a/core/java/android/app/supervision/ISupervisionManager.aidl
+++ b/core/java/android/app/supervision/ISupervisionManager.aidl
@@ -16,11 +16,14 @@
package android.app.supervision;
+import android.content.Intent;
+
/**
* Internal IPC interface to the supervision service.
* {@hide}
*/
interface ISupervisionManager {
+ Intent createConfirmSupervisionCredentialsIntent();
boolean isSupervisionEnabledForUser(int userId);
void setSupervisionEnabledForUser(int userId, boolean enabled);
String getActiveSupervisionAppPackage(int userId);
diff --git a/core/java/android/app/supervision/SupervisionManager.java b/core/java/android/app/supervision/SupervisionManager.java
index d30705536045..0270edf080a9 100644
--- a/core/java/android/app/supervision/SupervisionManager.java
+++ b/core/java/android/app/supervision/SupervisionManager.java
@@ -16,13 +16,22 @@
package android.app.supervision;
+import static android.Manifest.permission.INTERACT_ACROSS_USERS;
+import static android.Manifest.permission.MANAGE_USERS;
+import static android.Manifest.permission.QUERY_USERS;
+
+import android.annotation.FlaggedApi;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
+import android.annotation.SystemApi;
import android.annotation.SystemService;
+import android.annotation.TestApi;
import android.annotation.UserHandleAware;
import android.annotation.UserIdInt;
+import android.app.supervision.flags.Flags;
import android.compat.annotation.UnsupportedAppUsage;
import android.content.Context;
+import android.content.Intent;
import android.os.RemoteException;
/**
@@ -31,6 +40,8 @@ import android.os.RemoteException;
* @hide
*/
@SystemService(Context.SUPERVISION_SERVICE)
+@SystemApi
+@FlaggedApi(Flags.FLAG_SUPERVISION_MANAGER_APIS)
public class SupervisionManager {
private final Context mContext;
@Nullable private final ISupervisionManager mService;
@@ -47,7 +58,8 @@ public class SupervisionManager {
*
* @hide
*/
- public static final String ACTION_ENABLE_SUPERVISION = "android.app.action.ENABLE_SUPERVISION";
+ public static final String ACTION_ENABLE_SUPERVISION =
+ "android.app.supervision.action.ENABLE_SUPERVISION";
/**
* Activity action: ask the human user to disable supervision for this user. Only the app that
@@ -62,7 +74,7 @@ public class SupervisionManager {
* @hide
*/
public static final String ACTION_DISABLE_SUPERVISION =
- "android.app.action.DISABLE_SUPERVISION";
+ "android.app.supervision.action.DISABLE_SUPERVISION";
/** @hide */
@UnsupportedAppUsage
@@ -72,11 +84,46 @@ public class SupervisionManager {
}
/**
+ * Creates an {@link Intent} that can be used with {@link Context#startActivity(Intent)} to
+ * launch the activity to verify supervision credentials.
+ *
+ * <p>A valid {@link Intent} is always returned if supervision is enabled at the time this API
+ * is called, the launched activity still need to perform validity checks as the supervision
+ * state can change when the activity is launched. A null intent is returned if supervision is
+ * disabled at the time of this API call.
+ *
+ * <p>A result code of {@link android.app.Activity#RESULT_OK} indicates successful verification
+ * of the supervision credentials.
+ *
+ * @hide
+ */
+ @RequiresPermission(value = android.Manifest.permission.QUERY_USERS)
+ @Nullable
+ public Intent createConfirmSupervisionCredentialsIntent() {
+ if (mService != null) {
+ try {
+ Intent result = mService.createConfirmSupervisionCredentialsIntent();
+ if (result != null) {
+ result.prepareToEnterProcess(
+ Intent.LOCAL_FLAG_FROM_SYSTEM, mContext.getAttributionSource());
+ }
+ return result;
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return null;
+ }
+
+ /**
* Returns whether the device is supervised.
*
* @hide
*/
- @UserHandleAware
+ @SystemApi
+ @FlaggedApi(Flags.FLAG_SUPERVISION_MANAGER_APIS)
+ @RequiresPermission(anyOf = {MANAGE_USERS, QUERY_USERS})
+ @UserHandleAware(requiresPermissionIfNotCaller = INTERACT_ACROSS_USERS)
public boolean isSupervisionEnabled() {
return isSupervisionEnabledForUser(mContext.getUserId());
}
@@ -84,14 +131,10 @@ public class SupervisionManager {
/**
* Returns whether the device is supervised.
*
- * <p>The caller must be from the same user as the target or hold the {@link
- * android.Manifest.permission#INTERACT_ACROSS_USERS} permission.
- *
* @hide
*/
- @RequiresPermission(
- value = android.Manifest.permission.INTERACT_ACROSS_USERS,
- conditional = true)
+ @RequiresPermission(anyOf = {MANAGE_USERS, QUERY_USERS})
+ @UserHandleAware(requiresPermissionIfNotCaller = INTERACT_ACROSS_USERS)
public boolean isSupervisionEnabledForUser(@UserIdInt int userId) {
if (mService != null) {
try {
@@ -108,7 +151,8 @@ public class SupervisionManager {
*
* @hide
*/
- @UserHandleAware
+ @TestApi
+ @UserHandleAware(requiresPermissionIfNotCaller = INTERACT_ACROSS_USERS)
public void setSupervisionEnabled(boolean enabled) {
setSupervisionEnabledForUser(mContext.getUserId(), enabled);
}
@@ -116,14 +160,9 @@ public class SupervisionManager {
/**
* Sets whether the device is supervised for a given user.
*
- * <p>The caller must be from the same user as the target or hold the {@link
- * android.Manifest.permission#INTERACT_ACROSS_USERS} permission.
- *
* @hide
*/
- @RequiresPermission(
- value = android.Manifest.permission.INTERACT_ACROSS_USERS,
- conditional = true)
+ @UserHandleAware(requiresPermissionIfNotCaller = INTERACT_ACROSS_USERS)
public void setSupervisionEnabledForUser(@UserIdInt int userId, boolean enabled) {
if (mService != null) {
try {
diff --git a/core/java/android/app/supervision/flags.aconfig b/core/java/android/app/supervision/flags.aconfig
index 232883cbfe00..94de03877fd7 100644
--- a/core/java/android/app/supervision/flags.aconfig
+++ b/core/java/android/app/supervision/flags.aconfig
@@ -64,3 +64,11 @@ flag {
description: "Flag that enables the Supervision pin recovery screen with Supervision settings entry point"
bug: "390500290"
}
+
+flag {
+ name: "supervision_manager_apis"
+ is_exported: true
+ namespace: "supervision"
+ description: "Flag that enables system APIs in Supervision Manager"
+ bug: "382034839"
+}
diff --git a/core/java/android/companion/virtual/flags/flags.aconfig b/core/java/android/companion/virtual/flags/flags.aconfig
index fcdb02ab5da2..ba1473cf5ed7 100644
--- a/core/java/android/companion/virtual/flags/flags.aconfig
+++ b/core/java/android/companion/virtual/flags/flags.aconfig
@@ -120,6 +120,16 @@ flag {
}
flag {
+ name: "correct_virtual_display_power_state"
+ namespace: "virtual_devices"
+ description: "Fix the virtual display power state"
+ bug: "371125136"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+flag {
name: "vdm_settings"
namespace: "virtual_devices"
description: "Show virtual devices in Settings"
diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java
index 3391e79b2ae4..55d78f9b8c48 100644
--- a/core/java/android/content/Context.java
+++ b/core/java/android/content/Context.java
@@ -17,8 +17,8 @@
package android.content;
import static android.app.appfunctions.flags.Flags.FLAG_ENABLE_APP_FUNCTION_MANAGER;
-import static android.content.flags.Flags.FLAG_ENABLE_BIND_PACKAGE_ISOLATED_PROCESS;
import static android.app.ondeviceintelligence.flags.Flags.FLAG_ENABLE_ON_DEVICE_INTELLIGENCE_MODULE;
+import static android.content.flags.Flags.FLAG_ENABLE_BIND_PACKAGE_ISOLATED_PROCESS;
import static android.security.Flags.FLAG_SECURE_LOCKDOWN;
import android.annotation.AttrRes;
@@ -6858,6 +6858,8 @@ public abstract class Context {
* @see android.app.supervision.SupervisionManager
* @hide
*/
+ @SystemApi
+ @FlaggedApi(android.app.supervision.flags.Flags.FLAG_SUPERVISION_MANAGER_APIS)
public static final String SUPERVISION_SERVICE = "supervision";
/**
diff --git a/core/java/android/content/pm/multiuser.aconfig b/core/java/android/content/pm/multiuser.aconfig
index e6082d0df1f8..5c904c15e706 100644
--- a/core/java/android/content/pm/multiuser.aconfig
+++ b/core/java/android/content/pm/multiuser.aconfig
@@ -622,3 +622,10 @@ flag {
description: "Add API to logout user"
bug: "350045389"
}
+
+flag {
+ name: "enable_moving_content_into_private_space"
+ namespace: "profile_experiences"
+ description: "Enable moving content into the Private Space"
+ bug: "360066001"
+}
diff --git a/core/java/android/content/res/ApkAssets.java b/core/java/android/content/res/ApkAssets.java
index 075457885586..f538e9ffffdd 100644
--- a/core/java/android/content/res/ApkAssets.java
+++ b/core/java/android/content/res/ApkAssets.java
@@ -25,6 +25,7 @@ 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;
@@ -50,6 +51,7 @@ 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.
@@ -134,6 +136,17 @@ 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.
*
@@ -304,7 +317,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);
+ this(format, flags, assets, path);
Objects.requireNonNull(path, "path");
mNativePtr = nativeLoad(format, path, flags, assets);
mStringBlock = new StringBlock(nativeGetStringBlock(mNativePtr), true /*useSparse*/);
@@ -313,7 +326,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);
+ this(format, flags, assets, friendlyName);
Objects.requireNonNull(fd, "fd");
Objects.requireNonNull(friendlyName, "friendlyName");
mNativePtr = nativeLoadFd(format, fd, friendlyName, flags, assets);
@@ -323,7 +336,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);
+ this(format, flags, assets, friendlyName);
Objects.requireNonNull(fd, "fd");
Objects.requireNonNull(friendlyName, "friendlyName");
mNativePtr = nativeLoadFdOffsets(format, fd, friendlyName, offset, length, flags, assets);
@@ -331,16 +344,17 @@ public final class ApkAssets {
}
private ApkAssets(@PropertyFlags int flags, @Nullable AssetsProvider assets) {
- this(FORMAT_APK, flags, assets);
+ this(FORMAT_APK, flags, assets, "empty");
mNativePtr = nativeLoadEmpty(flags, assets);
mStringBlock = null;
}
private ApkAssets(@FormatType int format, @PropertyFlags int flags,
- @Nullable AssetsProvider assets) {
+ @Nullable AssetsProvider assets, @NonNull String name) {
mFlags = flags;
mAssets = assets;
mIsOverlay = format == FORMAT_IDMAP;
+ if (DEBUG) mName = name;
}
@UnsupportedAppUsage
@@ -421,13 +435,41 @@ 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) {
- return nativeIsUpToDate(mNativePtr);
+ 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));
+ }
}
+ mPreviousUpToDateResult = res;
+ return res != UPTODATE_FALSE;
}
public boolean isSystem() {
@@ -487,7 +529,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 boolean nativeIsUpToDate(long ptr);
+ @CriticalNative private static native int 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 d51f64ce8106..2d1bf4d9d296 100644
--- a/core/java/android/content/res/ResourceTimer.java
+++ b/core/java/android/content/res/ResourceTimer.java
@@ -17,13 +17,10 @@
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;
@@ -33,6 +30,7 @@ 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;
@@ -277,38 +275,40 @@ public final class ResourceTimer {
* Update the metrics information and dump it.
* @hide
*/
- 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)) {
+ 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) {
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/content/res/XmlBlock.java b/core/java/android/content/res/XmlBlock.java
index 2ead17588be4..36fa05905814 100644
--- a/core/java/android/content/res/XmlBlock.java
+++ b/core/java/android/content/res/XmlBlock.java
@@ -346,7 +346,7 @@ public final class XmlBlock implements AutoCloseable {
if (ev == ERROR_BAD_DOCUMENT) {
throw new XmlPullParserException("Corrupt XML binary file");
}
- if (Flags.layoutReadwriteFlags() && ev == START_TAG) {
+ if (useLayoutReadwrite() && ev == START_TAG) {
AconfigFlags flags = ParsingPackageUtils.getAconfigFlags();
if (flags.skipCurrentElement(/* pkg= */ null, this)) {
int depth = 1;
@@ -388,6 +388,18 @@ public final class XmlBlock implements AutoCloseable {
}
return ev;
}
+
+ // Until ravenwood supports AconfigFlags, we just don't do layoutReadwriteFlags().
+ @android.ravenwood.annotation.RavenwoodReplace(
+ bug = 396458006, blockedBy = AconfigFlags.class)
+ private static boolean useLayoutReadwrite() {
+ return Flags.layoutReadwriteFlags();
+ }
+
+ private static boolean useLayoutReadwrite$ravenwood() {
+ return false;
+ }
+
public void require(int type, String namespace, String name) throws XmlPullParserException,IOException {
if (type != getEventType()
|| (namespace != null && !namespace.equals( getNamespace () ) )
diff --git a/core/java/android/hardware/display/ColorDisplayManager.java b/core/java/android/hardware/display/ColorDisplayManager.java
index 0d9db1fa3c91..7debab946bc0 100644
--- a/core/java/android/hardware/display/ColorDisplayManager.java
+++ b/core/java/android/hardware/display/ColorDisplayManager.java
@@ -17,7 +17,6 @@
package android.hardware.display;
import android.Manifest;
-import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.IntRange;
import android.annotation.NonNull;
@@ -401,7 +400,6 @@ public final class ColorDisplayManager {
* @hide
*/
@TestApi
- @FlaggedApi(android.app.Flags.FLAG_MODES_API)
@RequiresPermission(Manifest.permission.CONTROL_DISPLAY_COLOR_TRANSFORMS)
public boolean isSaturationActivated() {
return mManager.isSaturationActivated();
diff --git a/core/java/android/hardware/display/DisplayManager.java b/core/java/android/hardware/display/DisplayManager.java
index d8919160320a..7850e377ec4d 100644
--- a/core/java/android/hardware/display/DisplayManager.java
+++ b/core/java/android/hardware/display/DisplayManager.java
@@ -612,6 +612,7 @@ public final class DisplayManager {
PRIVATE_EVENT_TYPE_DISPLAY_BRIGHTNESS,
PRIVATE_EVENT_TYPE_HDR_SDR_RATIO_CHANGED,
PRIVATE_EVENT_TYPE_DISPLAY_CONNECTION_CHANGED,
+ PRIVATE_EVENT_TYPE_DISPLAY_COMMITTED_STATE_CHANGED
})
@Retention(RetentionPolicy.SOURCE)
public @interface PrivateEventType {}
@@ -677,7 +678,7 @@ public final class DisplayManager {
* through the {@link DisplayListener#onDisplayChanged} callback method. New brightness
* values can be retrieved via {@link android.view.Display#getBrightnessInfo()}.
*
- * @see #registerDisplayListener(DisplayListener, Handler, long)
+ * @see #registerDisplayListener(DisplayListener, Handler, long, long)
*
* @hide
*/
@@ -690,7 +691,7 @@ public final class DisplayManager {
*
* Requires that {@link Display#isHdrSdrRatioAvailable()} is true.
*
- * @see #registerDisplayListener(DisplayListener, Handler, long)
+ * @see #registerDisplayListener(DisplayListener, Handler, long, long)
*
* @hide
*/
@@ -699,11 +700,19 @@ public final class DisplayManager {
/**
* Event type to register for a display's connection changed.
*
- * @see #registerDisplayListener(DisplayListener, Handler, long)
+ * @see #registerDisplayListener(DisplayListener, Handler, long, long)
* @hide
*/
public static final long PRIVATE_EVENT_TYPE_DISPLAY_CONNECTION_CHANGED = 1L << 2;
+ /**
+ * Event type to register for a display's committed state changes.
+ *
+ * @see #registerDisplayListener(DisplayListener, Handler, long, long)
+ * @hide
+ */
+ public static final long PRIVATE_EVENT_TYPE_DISPLAY_COMMITTED_STATE_CHANGED = 1L << 3;
+
/** @hide */
public DisplayManager(Context context) {
diff --git a/core/java/android/hardware/display/DisplayManagerGlobal.java b/core/java/android/hardware/display/DisplayManagerGlobal.java
index 339dbf2c2029..a7d610e54e2c 100644
--- a/core/java/android/hardware/display/DisplayManagerGlobal.java
+++ b/core/java/android/hardware/display/DisplayManagerGlobal.java
@@ -113,7 +113,8 @@ public final class DisplayManagerGlobal {
EVENT_DISPLAY_CONNECTED,
EVENT_DISPLAY_DISCONNECTED,
EVENT_DISPLAY_REFRESH_RATE_CHANGED,
- EVENT_DISPLAY_STATE_CHANGED
+ EVENT_DISPLAY_STATE_CHANGED,
+ EVENT_DISPLAY_COMMITTED_STATE_CHANGED
})
@Retention(RetentionPolicy.SOURCE)
public @interface DisplayEvent {}
@@ -128,6 +129,8 @@ public final class DisplayManagerGlobal {
public static final int EVENT_DISPLAY_DISCONNECTED = 7;
public static final int EVENT_DISPLAY_REFRESH_RATE_CHANGED = 8;
public static final int EVENT_DISPLAY_STATE_CHANGED = 9;
+ public static final int EVENT_DISPLAY_COMMITTED_STATE_CHANGED = 10;
+
@LongDef(prefix = {"INTERNAL_EVENT_FLAG_"}, flag = true, value = {
INTERNAL_EVENT_FLAG_DISPLAY_ADDED,
@@ -139,6 +142,7 @@ public final class DisplayManagerGlobal {
INTERNAL_EVENT_FLAG_DISPLAY_REFRESH_RATE,
INTERNAL_EVENT_FLAG_DISPLAY_STATE,
INTERNAL_EVENT_FLAG_TOPOLOGY_UPDATED,
+ INTERNAL_EVENT_FLAG_DISPLAY_COMMITTED_STATE_CHANGED
})
@Retention(RetentionPolicy.SOURCE)
public @interface InternalEventFlag {}
@@ -152,6 +156,8 @@ public final class DisplayManagerGlobal {
public static final long INTERNAL_EVENT_FLAG_DISPLAY_REFRESH_RATE = 1L << 6;
public static final long INTERNAL_EVENT_FLAG_DISPLAY_STATE = 1L << 7;
public static final long INTERNAL_EVENT_FLAG_TOPOLOGY_UPDATED = 1L << 8;
+ public static final long INTERNAL_EVENT_FLAG_DISPLAY_COMMITTED_STATE_CHANGED = 1L << 9;
+
@UnsupportedAppUsage
private static DisplayManagerGlobal sInstance;
@@ -1550,6 +1556,12 @@ public final class DisplayManagerGlobal {
mListener.onDisplayChanged(displayId);
}
break;
+ case EVENT_DISPLAY_COMMITTED_STATE_CHANGED:
+ if ((mInternalEventFlagsMask
+ & INTERNAL_EVENT_FLAG_DISPLAY_COMMITTED_STATE_CHANGED) != 0) {
+ mListener.onDisplayChanged(displayId);
+ }
+ break;
}
if (DEBUG) {
Trace.endSection();
@@ -1710,6 +1722,8 @@ public final class DisplayManagerGlobal {
return "EVENT_DISPLAY_REFRESH_RATE_CHANGED";
case EVENT_DISPLAY_STATE_CHANGED:
return "EVENT_DISPLAY_STATE_CHANGED";
+ case EVENT_DISPLAY_COMMITTED_STATE_CHANGED:
+ return "EVENT_DISPLAY_COMMITTED_STATE_CHANGED";
}
return "UNKNOWN";
}
@@ -1756,6 +1770,13 @@ public final class DisplayManagerGlobal {
& DisplayManager.PRIVATE_EVENT_TYPE_DISPLAY_CONNECTION_CHANGED) != 0) {
baseEventMask |= INTERNAL_EVENT_FLAG_DISPLAY_CONNECTION_CHANGED;
}
+
+ if (Flags.committedStateSeparateEvent()) {
+ if ((privateEventFlags
+ & DisplayManager.PRIVATE_EVENT_TYPE_DISPLAY_COMMITTED_STATE_CHANGED) != 0) {
+ baseEventMask |= INTERNAL_EVENT_FLAG_DISPLAY_COMMITTED_STATE_CHANGED;
+ }
+ }
return baseEventMask;
}
diff --git a/core/java/android/hardware/fingerprint/FingerprintSensorConfigurations.java b/core/java/android/hardware/fingerprint/FingerprintSensorConfigurations.java
index 48c5887d80d0..586830c8d189 100644
--- a/core/java/android/hardware/fingerprint/FingerprintSensorConfigurations.java
+++ b/core/java/android/hardware/fingerprint/FingerprintSensorConfigurations.java
@@ -224,6 +224,10 @@ public class FingerprintSensorConfigurations implements Parcelable {
} catch (RemoteException e) {
Log.d(TAG, "Unable to get sensor properties!");
}
+
+ if (props == null) {
+ props = new SensorProps[]{};
+ }
return props;
}
}
diff --git a/core/java/android/hardware/input/InputSettings.java b/core/java/android/hardware/input/InputSettings.java
index 3d4b8854b01f..7c82abe083c2 100644
--- a/core/java/android/hardware/input/InputSettings.java
+++ b/core/java/android/hardware/input/InputSettings.java
@@ -386,7 +386,7 @@ public class InputSettings {
*/
public static boolean isTouchpadAccelerationEnabled(@NonNull Context context) {
if (!isPointerAccelerationFeatureFlagEnabled()) {
- return false;
+ return true;
}
return Settings.System.getIntForUser(context.getContentResolver(),
@@ -833,7 +833,7 @@ public class InputSettings {
*/
public static boolean isMousePointerAccelerationEnabled(@NonNull Context context) {
if (!isPointerAccelerationFeatureFlagEnabled()) {
- return false;
+ return true;
}
return Settings.System.getIntForUser(context.getContentResolver(),
diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java
index 08365c935626..33bf4a29ecc6 100644
--- a/core/java/android/os/UserManager.java
+++ b/core/java/android/os/UserManager.java
@@ -2776,7 +2776,7 @@ public class UserManager {
}
/**
- * Returns whether logging out is currently allowed for the context user.
+ * Returns whether logging out is currently allowed for the specified user.
*
* <p>Logging out is not allowed in the following cases:
* <ol>
@@ -2794,11 +2794,10 @@ public class UserManager {
* {@link #LOGOUTABILITY_STATUS_CANNOT_SWITCH}.
* @hide
*/
- @UserHandleAware
@RequiresPermission(Manifest.permission.MANAGE_USERS)
- public @UserLogoutability int getUserLogoutability() {
+ public @UserLogoutability int getUserLogoutability(@UserIdInt int userId) {
try {
- return mService.getUserLogoutability(mUserId);
+ return mService.getUserLogoutability(userId);
} catch (RemoteException re) {
throw re.rethrowFromSystemServer();
}
diff --git a/core/java/android/os/vibrator/VibratorFrequencyProfile.java b/core/java/android/os/vibrator/VibratorFrequencyProfile.java
index 2b5f9bf2a22e..a8ed81846663 100644
--- a/core/java/android/os/vibrator/VibratorFrequencyProfile.java
+++ b/core/java/android/os/vibrator/VibratorFrequencyProfile.java
@@ -51,8 +51,7 @@ public final class VibratorFrequencyProfile {
Preconditions.checkArgument(!frequencyProfile.isEmpty(),
"Frequency profile must not be empty");
mFrequencyProfile = frequencyProfile;
- mFrequenciesOutputAcceleration = generateFrequencyToAccelerationMap(
- frequencyProfile.getFrequenciesHz(), frequencyProfile.getOutputAccelerationsGs());
+ mFrequenciesOutputAcceleration = generateFrequencyToAccelerationMap(mFrequencyProfile);
}
/**
@@ -133,18 +132,21 @@ public final class VibratorFrequencyProfile {
}
private static SparseArray<Float> generateFrequencyToAccelerationMap(
- float[] frequencies, float[] accelerations) {
- SparseArray<Float> sparseArray = new SparseArray<>(frequencies.length);
-
+ VibratorInfo.FrequencyProfile frequencyProfile) {
+ float[] frequencies = frequencyProfile.getFrequenciesHz();
+ SparseArray<Float> frequencyToAcceleration = new SparseArray<>(frequencies.length);
+ int lastFrequency = -1;
for (int i = 0; i < frequencies.length; i++) {
int frequency = (int) frequencies[i];
- float acceleration = accelerations[i];
-
- sparseArray.put(frequency,
- Math.min(acceleration, sparseArray.get(frequency, Float.MAX_VALUE)));
+ if (frequency == lastFrequency) {
+ continue; // Skip duplicate frequencies
+ }
+ float acceleration = frequencyProfile.getOutputAccelerationGs(frequency);
+ frequencyToAcceleration.put(frequency, acceleration);
+ lastFrequency = frequency;
}
- return sparseArray;
+ return frequencyToAcceleration;
}
}
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 670709846d4c..1a9b42e46a1c 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -2119,7 +2119,6 @@ public final class Settings {
* <p>
* Output: Nothing.
*/
- @FlaggedApi(android.app.Flags.FLAG_MODES_API)
@SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
public static final String ACTION_AUTOMATIC_ZEN_RULE_SETTINGS
= "android.settings.AUTOMATIC_ZEN_RULE_SETTINGS";
@@ -2129,7 +2128,6 @@ public final class Settings {
* <p>
* This must be passed as an extra field to the {@link #ACTION_AUTOMATIC_ZEN_RULE_SETTINGS}.
*/
- @FlaggedApi(android.app.Flags.FLAG_MODES_API)
public static final String EXTRA_AUTOMATIC_ZEN_RULE_ID
= "android.provider.extra.AUTOMATIC_ZEN_RULE_ID";
diff --git a/core/java/android/security/FileIntegrityManager.java b/core/java/android/security/FileIntegrityManager.java
index 9e02ecd19aee..903f8170104e 100644
--- a/core/java/android/security/FileIntegrityManager.java
+++ b/core/java/android/security/FileIntegrityManager.java
@@ -65,13 +65,7 @@ public final class FileIntegrityManager {
* other fs-verity APIs.
*/
public boolean isApkVeritySupported() {
- try {
- // Go through the service just to avoid exposing the vendor controlled system property
- // to all apps.
- return mService.isApkVeritySupported();
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- }
+ return VerityUtils.isFsVeritySupported();
}
/**
diff --git a/core/java/android/security/IFileIntegrityService.aidl b/core/java/android/security/IFileIntegrityService.aidl
index c6def239d59a..5a1a6a0ea6d9 100644
--- a/core/java/android/security/IFileIntegrityService.aidl
+++ b/core/java/android/security/IFileIntegrityService.aidl
@@ -24,8 +24,6 @@ import android.os.IInstalld;
* @hide
*/
interface IFileIntegrityService {
- boolean isApkVeritySupported();
-
IInstalld.IFsveritySetupAuthToken createAuthToken(in ParcelFileDescriptor authFd);
@EnforcePermission("SETUP_FSVERITY")
diff --git a/core/java/android/security/intrusiondetection/IntrusionDetectionEvent.java b/core/java/android/security/intrusiondetection/IntrusionDetectionEvent.java
index 76ee4480c222..f5f334891d2d 100644
--- a/core/java/android/security/intrusiondetection/IntrusionDetectionEvent.java
+++ b/core/java/android/security/intrusiondetection/IntrusionDetectionEvent.java
@@ -19,10 +19,10 @@ package android.security.intrusiondetection;
import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.NonNull;
+import android.annotation.SystemApi;
import android.app.admin.ConnectEvent;
import android.app.admin.DnsEvent;
import android.app.admin.SecurityLog.SecurityEvent;
-import android.annotation.SystemApi;
import android.os.Parcel;
import android.os.Parcelable;
import android.security.Flags;
@@ -223,13 +223,13 @@ public final class IntrusionDetectionEvent implements Parcelable {
out.writeInt(mType);
switch (mType) {
case SECURITY_EVENT:
- out.writeParcelable(mSecurityEvent, flags);
+ mSecurityEvent.writeToParcel(out, flags);
break;
case NETWORK_EVENT_DNS:
- out.writeParcelable(mNetworkEventDns, flags);
+ mNetworkEventDns.writeToParcel(out, flags);
break;
case NETWORK_EVENT_CONNECT:
- out.writeParcelable(mNetworkEventConnect, flags);
+ mNetworkEventConnect.writeToParcel(out, flags);
break;
default:
throw new IllegalArgumentException("Invalid event type: " + mType);
diff --git a/core/java/android/service/notification/Condition.java b/core/java/android/service/notification/Condition.java
index 6e771f8f0ffe..c375cfb900ac 100644
--- a/core/java/android/service/notification/Condition.java
+++ b/core/java/android/service/notification/Condition.java
@@ -18,11 +18,9 @@ package android.service.notification;
import static com.android.internal.util.Preconditions.checkArgument;
-import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
-import android.app.Flags;
import android.content.Context;
import android.net.Uri;
import android.os.Parcel;
@@ -105,20 +103,15 @@ public final class Condition implements Parcelable {
public @interface Source {}
/** The state is changing due to an unknown reason. */
- @FlaggedApi(Flags.FLAG_MODES_API)
public static final int SOURCE_UNKNOWN = 0;
/** The state is changing due to an explicit user action. */
- @FlaggedApi(Flags.FLAG_MODES_API)
public static final int SOURCE_USER_ACTION = 1;
/** The state is changing due to an automatic schedule (alarm, set time, etc). */
- @FlaggedApi(Flags.FLAG_MODES_API)
public static final int SOURCE_SCHEDULE = 2;
/** The state is changing due to a change in context (such as detected driving or sleeping). */
- @FlaggedApi(Flags.FLAG_MODES_API)
public static final int SOURCE_CONTEXT = 3;
/** The source of, or reason for, the state change represented by this Condition. **/
- @FlaggedApi(Flags.FLAG_MODES_API)
public final @Source int source; // default = SOURCE_UNKNOWN
/**
@@ -145,7 +138,6 @@ public final class Condition implements Parcelable {
* @param state whether the mode should be activated or deactivated
* @param source the source of, or reason for, the state change represented by this Condition
*/
- @FlaggedApi(Flags.FLAG_MODES_API)
public Condition(@Nullable Uri id, @Nullable String summary, @State int state,
@Source int source) {
this(id, summary, "", "", -1, state, source, FLAG_RELEVANT_ALWAYS);
@@ -168,7 +160,6 @@ public final class Condition implements Parcelable {
* @param source the source of, or reason for, the state change represented by this Condition
* @param flags flags on this condition
*/
- @FlaggedApi(Flags.FLAG_MODES_API)
public Condition(@Nullable Uri id, @Nullable String summary, @Nullable String line1,
@Nullable String line2, int icon, @State int state, @Source int source,
int flags) {
@@ -195,15 +186,13 @@ public final class Condition implements Parcelable {
source.readString(),
source.readInt(),
source.readInt(),
- Flags.modesApi() ? source.readInt() : SOURCE_UNKNOWN,
+ source.readInt(),
source.readInt());
}
/** @hide */
public void validate() {
- if (Flags.modesApi()) {
- checkValidSource(source);
- }
+ checkValidSource(source);
}
private static boolean isValidState(int state) {
@@ -211,11 +200,9 @@ public final class Condition implements Parcelable {
}
private static int checkValidSource(@Source int source) {
- if (Flags.modesApi()) {
- checkArgument(source >= SOURCE_UNKNOWN && source <= SOURCE_CONTEXT,
- "Condition source must be one of SOURCE_UNKNOWN, SOURCE_USER_ACTION, "
- + "SOURCE_SCHEDULE, or SOURCE_CONTEXT");
- }
+ checkArgument(source >= SOURCE_UNKNOWN && source <= SOURCE_CONTEXT,
+ "Condition source must be one of SOURCE_UNKNOWN, SOURCE_USER_ACTION, "
+ + "SOURCE_SCHEDULE, or SOURCE_CONTEXT");
return source;
}
@@ -227,25 +214,21 @@ public final class Condition implements Parcelable {
dest.writeString(line2);
dest.writeInt(icon);
dest.writeInt(state);
- if (Flags.modesApi()) {
- dest.writeInt(this.source);
- }
+ dest.writeInt(this.source);
dest.writeInt(this.flags);
}
@Override
public String toString() {
- StringBuilder sb = new StringBuilder(Condition.class.getSimpleName()).append('[')
+ return new StringBuilder(Condition.class.getSimpleName()).append('[')
.append("state=").append(stateToString(state))
.append(",id=").append(id)
.append(",summary=").append(summary)
.append(",line1=").append(line1)
.append(",line2=").append(line2)
- .append(",icon=").append(icon);
- if (Flags.modesApi()) {
- sb.append(",source=").append(sourceToString(source));
- }
- return sb.append(",flags=").append(flags)
+ .append(",icon=").append(icon)
+ .append(",source=").append(sourceToString(source))
+ .append(",flags=").append(flags)
.append(']').toString();
}
@@ -279,7 +262,6 @@ public final class Condition implements Parcelable {
* Provides a human-readable string version of the Source enum.
* @hide
*/
- @FlaggedApi(Flags.FLAG_MODES_API)
public static @NonNull String sourceToString(@Source int source) {
if (source == SOURCE_UNKNOWN) return "SOURCE_UNKNOWN";
if (source == SOURCE_USER_ACTION) return "SOURCE_USER_ACTION";
@@ -301,25 +283,19 @@ public final class Condition implements Parcelable {
if (!(o instanceof Condition)) return false;
if (o == this) return true;
final Condition other = (Condition) o;
- boolean finalEquals = Objects.equals(other.id, id)
+ return Objects.equals(other.id, id)
&& Objects.equals(other.summary, summary)
&& Objects.equals(other.line1, line1)
&& Objects.equals(other.line2, line2)
&& other.icon == icon
&& other.state == state
- && other.flags == flags;
- if (Flags.modesApi()) {
- return finalEquals && other.source == source;
- }
- return finalEquals;
+ && other.flags == flags
+ && other.source == source;
}
@Override
public int hashCode() {
- if (Flags.modesApi()) {
- return Objects.hash(id, summary, line1, line2, icon, state, source, flags);
- }
- return Objects.hash(id, summary, line1, line2, icon, state, flags);
+ return Objects.hash(id, summary, line1, line2, icon, state, source, flags);
}
@Override
diff --git a/core/java/android/service/notification/SystemZenRules.java b/core/java/android/service/notification/SystemZenRules.java
index f11ce1621f93..fbee06e113fc 100644
--- a/core/java/android/service/notification/SystemZenRules.java
+++ b/core/java/android/service/notification/SystemZenRules.java
@@ -16,7 +16,6 @@
package android.service.notification;
-import android.annotation.FlaggedApi;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.StringRes;
@@ -47,7 +46,6 @@ public final class SystemZenRules {
public static final String PACKAGE_ANDROID = "android";
/** Updates existing system-owned rules to use the new Modes fields (type, etc). */
- @FlaggedApi(Flags.FLAG_MODES_API)
public static void maybeUpgradeRules(Context context, ZenModeConfig config) {
for (ZenRule rule : config.automaticRules.values()) {
if (isSystemOwnedRule(rule)) {
@@ -69,7 +67,6 @@ public final class SystemZenRules {
return PACKAGE_ANDROID.equals(rule.pkg);
}
- @FlaggedApi(Flags.FLAG_MODES_API)
private static void upgradeSystemProviderRule(Context context, ZenRule rule) {
ScheduleInfo scheduleInfo = ZenModeConfig.tryParseScheduleConditionId(rule.conditionId);
if (scheduleInfo != null) {
diff --git a/core/java/android/service/notification/ZenAdapters.java b/core/java/android/service/notification/ZenAdapters.java
index a122b7155b18..4f53bfa841ef 100644
--- a/core/java/android/service/notification/ZenAdapters.java
+++ b/core/java/android/service/notification/ZenAdapters.java
@@ -17,7 +17,6 @@
package android.service.notification;
import android.annotation.NonNull;
-import android.app.Flags;
import android.app.NotificationManager.Policy;
/**
@@ -50,7 +49,8 @@ public class ZenAdapters {
: ZenPolicy.PEOPLE_TYPE_NONE)
.allowReminders(policy.allowReminders())
.allowRepeatCallers(policy.allowRepeatCallers())
- .allowSystem(policy.allowSystem());
+ .allowSystem(policy.allowSystem())
+ .allowPriorityChannels(policy.allowPriorityChannels());
if (policy.suppressedVisualEffects != Policy.SUPPRESSED_EFFECTS_UNSET) {
zenPolicyBuilder.showBadges(policy.showBadges())
@@ -62,10 +62,6 @@ public class ZenAdapters {
.showStatusBarIcons(policy.showStatusBarIcons());
}
- if (Flags.modesApi()) {
- zenPolicyBuilder.allowPriorityChannels(policy.allowPriorityChannels());
- }
-
return zenPolicyBuilder.build();
}
diff --git a/core/java/android/service/notification/ZenDeviceEffects.java b/core/java/android/service/notification/ZenDeviceEffects.java
index 06bd2555c2f8..d88fb3e35b1e 100644
--- a/core/java/android/service/notification/ZenDeviceEffects.java
+++ b/core/java/android/service/notification/ZenDeviceEffects.java
@@ -16,12 +16,10 @@
package android.service.notification;
-import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.TestApi;
-import android.app.Flags;
import android.os.Parcel;
import android.os.Parcelable;
@@ -37,7 +35,6 @@ import java.util.Set;
* Represents the set of device effects (affecting display and device behavior in general) that
* are applied whenever an {@link android.app.AutomaticZenRule} is active.
*/
-@FlaggedApi(Flags.FLAG_MODES_API)
public final class ZenDeviceEffects implements Parcelable {
/**
@@ -157,7 +154,6 @@ public final class ZenDeviceEffects implements Parcelable {
}
/** @hide */
- @FlaggedApi(Flags.FLAG_MODES_API)
public void validate() {
int extraEffectsLength = 0;
for (String extraEffect : mExtraEffects) {
@@ -435,7 +431,6 @@ public final class ZenDeviceEffects implements Parcelable {
}
/** Builder class for {@link ZenDeviceEffects} objects. */
- @FlaggedApi(Flags.FLAG_MODES_API)
public static final class Builder {
private boolean mGrayscale;
diff --git a/core/java/android/service/notification/ZenModeConfig.java b/core/java/android/service/notification/ZenModeConfig.java
index 4f459aa9131a..6f94c1b2d274 100644
--- a/core/java/android/service/notification/ZenModeConfig.java
+++ b/core/java/android/service/notification/ZenModeConfig.java
@@ -228,7 +228,7 @@ public class ZenModeConfig implements Parcelable {
private static final boolean DEFAULT_ALLOW_CONV = true;
private static final int DEFAULT_ALLOW_CONV_FROM = ZenPolicy.CONVERSATION_SENDERS_IMPORTANT;
private static final boolean DEFAULT_ALLOW_PRIORITY_CHANNELS = true;
- private static final boolean DEFAULT_CHANNELS_BYPASSING_DND = false;
+ private static final boolean DEFAULT_HAS_PRIORITY_CHANNELS = false;
// Default setting here is 010011101 = 157
private static final int DEFAULT_SUPPRESSED_VISUAL_EFFECTS =
SUPPRESSED_EFFECT_SCREEN_OFF | SUPPRESSED_EFFECT_FULL_SCREEN_INTENT
@@ -242,9 +242,6 @@ public class ZenModeConfig implements Parcelable {
public static final int XML_VERSION_MODES_API = 11;
public static final int XML_VERSION_MODES_UI = 12;
- // TODO: b/310620812, b/344831624 - Update XML_VERSION and update default_zen_config.xml
- // accordingly when modes_api / modes_ui are inlined.
- private static final int XML_VERSION_PRE_MODES = 10;
public static final String ZEN_TAG = "zen";
private static final String ZEN_ATT_VERSION = "version";
private static final String ZEN_ATT_USER = "user";
@@ -269,7 +266,7 @@ public class ZenModeConfig implements Parcelable {
private static final String DISALLOW_TAG = "disallow";
private static final String DISALLOW_ATT_VISUAL_EFFECTS = "visualEffects";
private static final String STATE_TAG = "state";
- private static final String STATE_ATT_CHANNELS_BYPASSING_DND = "areChannelsBypassingDnd";
+ private static final String STATE_HAS_PRIORITY_CHANNELS = "areChannelsBypassingDnd";
// zen policy visual effects attributes
private static final String SHOW_ATT_FULL_SCREEN_INTENT = "showFullScreenIntent";
@@ -303,7 +300,6 @@ public class ZenModeConfig implements Parcelable {
private static final String RULE_ATT_CONDITION_ID = "conditionId";
private static final String RULE_ATT_CREATION_TIME = "creationTime";
private static final String RULE_ATT_ENABLER = "enabler";
- private static final String RULE_ATT_MODIFIED = "modified";
private static final String RULE_ATT_ALLOW_MANUAL = "userInvokable";
private static final String RULE_ATT_TYPE = "type";
private static final String RULE_ATT_USER_MODIFIED_FIELDS = "userModifiedFields";
@@ -313,6 +309,7 @@ public class ZenModeConfig implements Parcelable {
private static final String RULE_ATT_DISABLED_ORIGIN = "disabledOrigin";
private static final String RULE_ATT_LEGACY_SUPPRESSED_EFFECTS = "legacySuppressedEffects";
private static final String RULE_ATT_CONDITION_OVERRIDE = "conditionOverride";
+ private static final String RULE_ATT_LAST_ACTIVATION = "lastActivation";
private static final String DEVICE_EFFECT_DISPLAY_GRAYSCALE = "zdeDisplayGrayscale";
private static final String DEVICE_EFFECT_SUPPRESS_AMBIENT_DISPLAY =
@@ -348,11 +345,11 @@ public class ZenModeConfig implements Parcelable {
public int allowConversationsFrom = DEFAULT_ALLOW_CONV_FROM;
public int user = UserHandle.USER_SYSTEM;
public int suppressedVisualEffects = DEFAULT_SUPPRESSED_VISUAL_EFFECTS;
- // Note that when the modes_api flag is true, the areChannelsBypassingDnd boolean only tracks
- // whether the current user has any priority channels. These channels may bypass DND when
- // allowPriorityChannels is true.
- // TODO: b/310620812 - Rename to be more accurate when modes_api flag is inlined.
- public boolean areChannelsBypassingDnd = DEFAULT_CHANNELS_BYPASSING_DND;
+ /**
+ * Whether the current user has any priority channels. These channels may bypass DND when
+ * {@link #allowPriorityChannels} is true.
+ */
+ public boolean hasPriorityChannels = DEFAULT_HAS_PRIORITY_CHANNELS;
public boolean allowPriorityChannels = DEFAULT_ALLOW_PRIORITY_CHANNELS;
public int version;
@@ -384,22 +381,18 @@ public class ZenModeConfig implements Parcelable {
user = source.readInt();
manualRule = source.readParcelable(null, ZenRule.class);
readRulesFromParcel(automaticRules, source);
- if (Flags.modesApi()) {
- readRulesFromParcel(deletedRules, source);
- }
+ readRulesFromParcel(deletedRules, source);
if (!Flags.modesUi()) {
allowAlarms = source.readInt() == 1;
allowMedia = source.readInt() == 1;
allowSystem = source.readInt() == 1;
suppressedVisualEffects = source.readInt();
}
- areChannelsBypassingDnd = source.readInt() == 1;
+ hasPriorityChannels = source.readInt() == 1;
if (!Flags.modesUi()) {
allowConversations = source.readBoolean();
allowConversationsFrom = source.readInt();
- if (Flags.modesApi()) {
- allowPriorityChannels = source.readBoolean();
- }
+ allowPriorityChannels = source.readBoolean();
}
}
@@ -493,22 +486,18 @@ public class ZenModeConfig implements Parcelable {
dest.writeInt(user);
dest.writeParcelable(manualRule, 0);
writeRulesToParcel(automaticRules, dest);
- if (Flags.modesApi()) {
- writeRulesToParcel(deletedRules, dest);
- }
+ writeRulesToParcel(deletedRules, dest);
if (!Flags.modesUi()) {
dest.writeInt(allowAlarms ? 1 : 0);
dest.writeInt(allowMedia ? 1 : 0);
dest.writeInt(allowSystem ? 1 : 0);
dest.writeInt(suppressedVisualEffects);
}
- dest.writeInt(areChannelsBypassingDnd ? 1 : 0);
+ dest.writeInt(hasPriorityChannels ? 1 : 0);
if (!Flags.modesUi()) {
dest.writeBoolean(allowConversations);
dest.writeInt(allowConversationsFrom);
- if (Flags.modesApi()) {
- dest.writeBoolean(allowPriorityChannels);
- }
+ dest.writeBoolean(allowPriorityChannels);
}
}
@@ -549,17 +538,13 @@ public class ZenModeConfig implements Parcelable {
(allowConversationsFrom))
.append("\nsuppressedVisualEffects=").append(suppressedVisualEffects);
}
- if (Flags.modesApi()) {
- sb.append("\nhasPriorityChannels=").append(areChannelsBypassingDnd);
- sb.append(",allowPriorityChannels=").append(allowPriorityChannels);
- } else {
- sb.append("\nareChannelsBypassingDnd=").append(areChannelsBypassingDnd);
- }
+
+ sb.append("\nhasPriorityChannels=").append(hasPriorityChannels);
+ sb.append(",allowPriorityChannels=").append(allowPriorityChannels);
sb.append(",\nautomaticRules=").append(rulesToString(automaticRules));
sb.append(",\nmanualRule=").append(manualRule);
- if (Flags.modesApi()) {
- sb.append(",\ndeletedRules=").append(rulesToString(deletedRules));
- }
+ sb.append(",\ndeletedRules=").append(rulesToString(deletedRules));
+
return sb.append(']').toString();
}
@@ -854,7 +839,7 @@ public class ZenModeConfig implements Parcelable {
final ZenModeConfig other = (ZenModeConfig) o;
// The policy fields that live on config are compared directly because the fields will
// contain data until MODES_UI is rolled out/cleaned up.
- boolean eq = other.allowAlarms == allowAlarms
+ return other.allowAlarms == allowAlarms
&& other.allowMedia == allowMedia
&& other.allowSystem == allowSystem
&& other.allowCalls == allowCalls
@@ -868,35 +853,23 @@ public class ZenModeConfig implements Parcelable {
&& Objects.equals(other.automaticRules, automaticRules)
&& Objects.equals(other.manualRule, manualRule)
&& other.suppressedVisualEffects == suppressedVisualEffects
- && other.areChannelsBypassingDnd == areChannelsBypassingDnd
+ && other.hasPriorityChannels == hasPriorityChannels
&& other.allowConversations == allowConversations
- && other.allowConversationsFrom == allowConversationsFrom;
- if (Flags.modesApi()) {
- return eq
- && Objects.equals(other.deletedRules, deletedRules)
- && other.allowPriorityChannels == allowPriorityChannels;
- }
- return eq;
+ && other.allowConversationsFrom == allowConversationsFrom
+ && Objects.equals(other.deletedRules, deletedRules)
+ && other.allowPriorityChannels == allowPriorityChannels;
}
@Override
public int hashCode() {
// The policy fields that live on config are compared directly because the fields will
// contain data until MODES_UI is rolled out/cleaned up.
- if (Flags.modesApi()) {
- return Objects.hash(allowAlarms, allowMedia, allowSystem, allowCalls,
- allowRepeatCallers, allowMessages,
- allowCallsFrom, allowMessagesFrom, allowReminders, allowEvents,
- user, automaticRules, manualRule,
- suppressedVisualEffects, areChannelsBypassingDnd, allowConversations,
- allowConversationsFrom, allowPriorityChannels);
- }
return Objects.hash(allowAlarms, allowMedia, allowSystem, allowCalls,
allowRepeatCallers, allowMessages,
allowCallsFrom, allowMessagesFrom, allowReminders, allowEvents,
user, automaticRules, manualRule,
- suppressedVisualEffects, areChannelsBypassingDnd, allowConversations,
- allowConversationsFrom);
+ suppressedVisualEffects, hasPriorityChannels, allowConversations,
+ allowConversationsFrom, allowPriorityChannels);
}
private static String toDayList(int[] days) {
@@ -952,10 +925,8 @@ public class ZenModeConfig implements Parcelable {
public static int getCurrentXmlVersion() {
if (Flags.modesUi()) {
return XML_VERSION_MODES_UI;
- } else if (Flags.modesApi()) {
- return XML_VERSION_MODES_API;
} else {
- return XML_VERSION_PRE_MODES;
+ return XML_VERSION_MODES_API;
}
}
@@ -1006,10 +977,8 @@ public class ZenModeConfig implements Parcelable {
rt.allowMedia = safeBoolean(parser, ALLOW_ATT_MEDIA,
DEFAULT_ALLOW_MEDIA);
rt.allowSystem = safeBoolean(parser, ALLOW_ATT_SYSTEM, DEFAULT_ALLOW_SYSTEM);
- if (Flags.modesApi()) {
- rt.allowPriorityChannels = safeBoolean(parser, ALLOW_ATT_CHANNELS,
- DEFAULT_ALLOW_PRIORITY_CHANNELS);
- }
+ rt.allowPriorityChannels = safeBoolean(parser, ALLOW_ATT_CHANNELS,
+ DEFAULT_ALLOW_PRIORITY_CHANNELS);
// migrate old suppressed visual effects fields, if they still exist in the xml
Boolean allowWhenScreenOff = unsafeBoolean(parser, ALLOW_ATT_SCREEN_OFF);
@@ -1054,13 +1023,12 @@ public class ZenModeConfig implements Parcelable {
} else {
readRuleCount++;
}
- } else if (AUTOMATIC_TAG.equals(tag)
- || (Flags.modesApi() && AUTOMATIC_DELETED_TAG.equals(tag))) {
+ } else if (AUTOMATIC_TAG.equals(tag) || AUTOMATIC_DELETED_TAG.equals(tag)) {
final String id = parser.getAttributeValue(null, RULE_ATT_ID);
if (id != null) {
final ZenRule automaticRule = readRuleXml(parser);
automaticRule.id = id;
- if (Flags.modesApi() && AUTOMATIC_DELETED_TAG.equals(tag)) {
+ if (AUTOMATIC_DELETED_TAG.equals(tag)) {
String deletedRuleKey = deletedRuleKey(automaticRule);
if (deletedRuleKey != null) {
rt.deletedRules.put(deletedRuleKey, automaticRule);
@@ -1071,8 +1039,8 @@ public class ZenModeConfig implements Parcelable {
}
}
} else if (STATE_TAG.equals(tag)) {
- rt.areChannelsBypassingDnd = safeBoolean(parser,
- STATE_ATT_CHANNELS_BYPASSING_DND, DEFAULT_CHANNELS_BYPASSING_DND);
+ rt.hasPriorityChannels = safeBoolean(parser,
+ STATE_HAS_PRIORITY_CHANNELS, DEFAULT_HAS_PRIORITY_CHANNELS);
}
}
if (type == XmlPullParser.END_TAG && ZEN_TAG.equals(tag)) {
@@ -1149,9 +1117,7 @@ public class ZenModeConfig implements Parcelable {
out.attributeBoolean(null, ALLOW_ATT_SYSTEM, allowSystem);
out.attributeBoolean(null, ALLOW_ATT_CONV, allowConversations);
out.attributeInt(null, ALLOW_ATT_CONV_FROM, allowConversationsFrom);
- if (Flags.modesApi()) {
- out.attributeBoolean(null, ALLOW_ATT_CHANNELS, allowPriorityChannels);
- }
+ out.attributeBoolean(null, ALLOW_ATT_CHANNELS, allowPriorityChannels);
out.endTag(null, ALLOW_TAG);
out.startTag(null, DISALLOW_TAG);
@@ -1174,7 +1140,7 @@ public class ZenModeConfig implements Parcelable {
out.endTag(null, AUTOMATIC_TAG);
writtenRuleCount++;
}
- if (Flags.modesApi() && !forBackup) {
+ if (!forBackup) {
for (int i = 0; i < deletedRules.size(); i++) {
final ZenRule deletedRule = deletedRules.valueAt(i);
out.startTag(null, AUTOMATIC_DELETED_TAG);
@@ -1185,7 +1151,7 @@ public class ZenModeConfig implements Parcelable {
}
out.startTag(null, STATE_TAG);
- out.attributeBoolean(null, STATE_ATT_CHANNELS_BYPASSING_DND, areChannelsBypassingDnd);
+ out.attributeBoolean(null, STATE_HAS_PRIORITY_CHANNELS, hasPriorityChannels);
out.endTag(null, STATE_TAG);
out.endTag(null, ZEN_TAG);
@@ -1212,39 +1178,29 @@ public class ZenModeConfig implements Parcelable {
rt.creationTime = safeLong(parser, RULE_ATT_CREATION_TIME, 0);
rt.enabler = parser.getAttributeValue(null, RULE_ATT_ENABLER);
rt.condition = readConditionXml(parser);
-
- if (!Flags.modesApi() && rt.zenMode != ZEN_MODE_IMPORTANT_INTERRUPTIONS
- && Condition.isValidId(rt.conditionId, SYSTEM_AUTHORITY)) {
- // all default rules and user created rules updated to zenMode important interruptions
- Slog.i(TAG, "Updating zenMode of automatic rule " + rt.name);
- rt.zenMode = ZEN_MODE_IMPORTANT_INTERRUPTIONS;
- }
- rt.modified = safeBoolean(parser, RULE_ATT_MODIFIED, false);
rt.zenPolicy = readZenPolicyXml(parser);
- if (Flags.modesApi()) {
- rt.zenDeviceEffects = readZenDeviceEffectsXml(parser);
- rt.allowManualInvocation = safeBoolean(parser, RULE_ATT_ALLOW_MANUAL, false);
- rt.iconResName = parser.getAttributeValue(null, RULE_ATT_ICON);
- rt.triggerDescription = parser.getAttributeValue(null, RULE_ATT_TRIGGER_DESC);
- rt.type = safeInt(parser, RULE_ATT_TYPE, AutomaticZenRule.TYPE_UNKNOWN);
- rt.userModifiedFields = safeInt(parser, RULE_ATT_USER_MODIFIED_FIELDS, 0);
- rt.zenPolicyUserModifiedFields = safeInt(parser, POLICY_USER_MODIFIED_FIELDS, 0);
- rt.zenDeviceEffectsUserModifiedFields = safeInt(parser,
- DEVICE_EFFECT_USER_MODIFIED_FIELDS, 0);
- Long deletionInstant = tryParseLong(
- parser.getAttributeValue(null, RULE_ATT_DELETION_INSTANT), null);
- if (deletionInstant != null) {
- rt.deletionInstant = Instant.ofEpochMilli(deletionInstant);
- }
- if (Flags.modesUi()) {
- rt.disabledOrigin = safeInt(parser, RULE_ATT_DISABLED_ORIGIN,
- ORIGIN_UNKNOWN);
- rt.legacySuppressedEffects = safeInt(parser,
- RULE_ATT_LEGACY_SUPPRESSED_EFFECTS, 0);
- rt.conditionOverride = safeInt(parser, RULE_ATT_CONDITION_OVERRIDE,
- ZenRule.OVERRIDE_NONE);
+ rt.zenDeviceEffects = readZenDeviceEffectsXml(parser);
+ rt.allowManualInvocation = safeBoolean(parser, RULE_ATT_ALLOW_MANUAL, false);
+ rt.iconResName = parser.getAttributeValue(null, RULE_ATT_ICON);
+ rt.triggerDescription = parser.getAttributeValue(null, RULE_ATT_TRIGGER_DESC);
+ rt.type = safeInt(parser, RULE_ATT_TYPE, AutomaticZenRule.TYPE_UNKNOWN);
+ rt.userModifiedFields = safeInt(parser, RULE_ATT_USER_MODIFIED_FIELDS, 0);
+ rt.zenPolicyUserModifiedFields = safeInt(parser, POLICY_USER_MODIFIED_FIELDS, 0);
+ rt.zenDeviceEffectsUserModifiedFields = safeInt(parser,
+ DEVICE_EFFECT_USER_MODIFIED_FIELDS, 0);
+ rt.deletionInstant = safeInstant(parser, RULE_ATT_DELETION_INSTANT, null);
+ if (Flags.modesUi()) {
+ rt.disabledOrigin = safeInt(parser, RULE_ATT_DISABLED_ORIGIN,
+ ORIGIN_UNKNOWN);
+ rt.legacySuppressedEffects = safeInt(parser,
+ RULE_ATT_LEGACY_SUPPRESSED_EFFECTS, 0);
+ rt.conditionOverride = safeInt(parser, RULE_ATT_CONDITION_OVERRIDE,
+ ZenRule.OVERRIDE_NONE);
+ if (Flags.modesCleanupImplicit()) {
+ rt.lastActivation = safeInstant(parser, RULE_ATT_LAST_ACTIVATION, null);
}
}
+
return rt;
}
@@ -1278,38 +1234,42 @@ public class ZenModeConfig implements Parcelable {
if (rule.zenPolicy != null) {
writeZenPolicyXml(rule.zenPolicy, out);
}
- if (Flags.modesApi() && rule.zenDeviceEffects != null) {
+ if (rule.zenDeviceEffects != null) {
writeZenDeviceEffectsXml(rule.zenDeviceEffects, out);
}
- out.attributeBoolean(null, RULE_ATT_MODIFIED, rule.modified);
- if (Flags.modesApi()) {
- out.attributeBoolean(null, RULE_ATT_ALLOW_MANUAL, rule.allowManualInvocation);
- if (rule.iconResName != null) {
- out.attribute(null, RULE_ATT_ICON, rule.iconResName);
- }
- if (rule.triggerDescription != null) {
- out.attribute(null, RULE_ATT_TRIGGER_DESC, rule.triggerDescription);
- }
- out.attributeInt(null, RULE_ATT_TYPE, rule.type);
- out.attributeInt(null, RULE_ATT_USER_MODIFIED_FIELDS, rule.userModifiedFields);
- out.attributeInt(null, POLICY_USER_MODIFIED_FIELDS, rule.zenPolicyUserModifiedFields);
- out.attributeInt(null, DEVICE_EFFECT_USER_MODIFIED_FIELDS,
- rule.zenDeviceEffectsUserModifiedFields);
- if (rule.deletionInstant != null) {
- out.attributeLong(null, RULE_ATT_DELETION_INSTANT,
- rule.deletionInstant.toEpochMilli());
+ out.attributeBoolean(null, RULE_ATT_ALLOW_MANUAL, rule.allowManualInvocation);
+ if (rule.iconResName != null) {
+ out.attribute(null, RULE_ATT_ICON, rule.iconResName);
+ }
+ if (rule.triggerDescription != null) {
+ out.attribute(null, RULE_ATT_TRIGGER_DESC, rule.triggerDescription);
+ }
+ out.attributeInt(null, RULE_ATT_TYPE, rule.type);
+ out.attributeInt(null, RULE_ATT_USER_MODIFIED_FIELDS, rule.userModifiedFields);
+ out.attributeInt(null, POLICY_USER_MODIFIED_FIELDS, rule.zenPolicyUserModifiedFields);
+ out.attributeInt(null, DEVICE_EFFECT_USER_MODIFIED_FIELDS,
+ rule.zenDeviceEffectsUserModifiedFields);
+ writeXmlAttributeInstant(out, RULE_ATT_DELETION_INSTANT, rule.deletionInstant);
+ if (Flags.modesUi()) {
+ out.attributeInt(null, RULE_ATT_DISABLED_ORIGIN, rule.disabledOrigin);
+ out.attributeInt(null, RULE_ATT_LEGACY_SUPPRESSED_EFFECTS,
+ rule.legacySuppressedEffects);
+ if (rule.conditionOverride == ZenRule.OVERRIDE_ACTIVATE && !forBackup) {
+ out.attributeInt(null, RULE_ATT_CONDITION_OVERRIDE, rule.conditionOverride);
}
- if (Flags.modesUi()) {
- out.attributeInt(null, RULE_ATT_DISABLED_ORIGIN, rule.disabledOrigin);
- out.attributeInt(null, RULE_ATT_LEGACY_SUPPRESSED_EFFECTS,
- rule.legacySuppressedEffects);
- if (rule.conditionOverride == ZenRule.OVERRIDE_ACTIVATE && !forBackup) {
- out.attributeInt(null, RULE_ATT_CONDITION_OVERRIDE, rule.conditionOverride);
- }
+ if (Flags.modesCleanupImplicit()) {
+ writeXmlAttributeInstant(out, RULE_ATT_LAST_ACTIVATION, rule.lastActivation);
}
}
}
+ private static void writeXmlAttributeInstant(TypedXmlSerializer out, String att,
+ @Nullable Instant instant) throws IOException {
+ if (instant != null) {
+ out.attributeLong(null, att, instant.toEpochMilli());
+ }
+ }
+
public static Condition readConditionXml(TypedXmlPullParser parser) {
final Uri id = safeUri(parser, CONDITION_ATT_ID);
if (id == null) return null;
@@ -1320,12 +1280,8 @@ public class ZenModeConfig implements Parcelable {
final int state = safeInt(parser, CONDITION_ATT_STATE, -1);
final int flags = safeInt(parser, CONDITION_ATT_FLAGS, -1);
try {
- if (Flags.modesApi()) {
- final int source = safeInt(parser, CONDITION_ATT_SOURCE, Condition.SOURCE_UNKNOWN);
- return new Condition(id, summary, line1, line2, icon, state, source, flags);
- } else {
- return new Condition(id, summary, line1, line2, icon, state, flags);
- }
+ final int source = safeInt(parser, CONDITION_ATT_SOURCE, Condition.SOURCE_UNKNOWN);
+ return new Condition(id, summary, line1, line2, icon, state, source, flags);
} catch (IllegalArgumentException e) {
Slog.w(TAG, "Unable to read condition xml", e);
return null;
@@ -1339,9 +1295,7 @@ public class ZenModeConfig implements Parcelable {
out.attribute(null, CONDITION_ATT_LINE2, c.line2);
out.attributeInt(null, CONDITION_ATT_ICON, c.icon);
out.attributeInt(null, CONDITION_ATT_STATE, c.state);
- if (Flags.modesApi()) {
- out.attributeInt(null, CONDITION_ATT_SOURCE, c.source);
- }
+ out.attributeInt(null, CONDITION_ATT_SOURCE, c.source);
out.attributeInt(null, CONDITION_ATT_FLAGS, c.flags);
}
@@ -1363,12 +1317,11 @@ public class ZenModeConfig implements Parcelable {
final int system = safeInt(parser, ALLOW_ATT_SYSTEM, ZenPolicy.STATE_UNSET);
final int events = safeInt(parser, ALLOW_ATT_EVENTS, ZenPolicy.STATE_UNSET);
final int reminders = safeInt(parser, ALLOW_ATT_REMINDERS, ZenPolicy.STATE_UNSET);
- if (Flags.modesApi()) {
- final int channels = safeInt(parser, ALLOW_ATT_CHANNELS, ZenPolicy.STATE_UNSET);
- if (channels != ZenPolicy.STATE_UNSET) {
- builder.allowPriorityChannels(channels == STATE_ALLOW);
- policySet = true;
- }
+ final int channels = safeInt(parser, ALLOW_ATT_CHANNELS, ZenPolicy.STATE_UNSET);
+
+ if (channels != ZenPolicy.STATE_UNSET) {
+ builder.allowPriorityChannels(channels == STATE_ALLOW);
+ policySet = true;
}
if (calls != ZenPolicy.PEOPLE_TYPE_UNSET) {
@@ -1478,10 +1431,7 @@ public class ZenModeConfig implements Parcelable {
writeZenPolicyState(SHOW_ATT_AMBIENT, policy.getVisualEffectAmbient(), out);
writeZenPolicyState(SHOW_ATT_NOTIFICATION_LIST, policy.getVisualEffectNotificationList(),
out);
-
- if (Flags.modesApi()) {
- writeZenPolicyState(ALLOW_ATT_CHANNELS, policy.getPriorityChannelsAllowed(), out);
- }
+ writeZenPolicyState(ALLOW_ATT_CHANNELS, policy.getPriorityChannelsAllowed(), out);
}
private static void writeZenPolicyState(String attr, int val, TypedXmlSerializer out)
@@ -1495,7 +1445,7 @@ public class ZenModeConfig implements Parcelable {
if (val != ZenPolicy.CONVERSATION_SENDERS_UNSET) {
out.attributeInt(null, attr, val);
}
- } else if (Flags.modesApi() && Objects.equals(attr, ALLOW_ATT_CHANNELS)) {
+ } else if (Objects.equals(attr, ALLOW_ATT_CHANNELS)) {
if (val != ZenPolicy.STATE_UNSET) {
out.attributeInt(null, attr, val);
}
@@ -1506,7 +1456,6 @@ public class ZenModeConfig implements Parcelable {
}
}
- @FlaggedApi(Flags.FLAG_MODES_API)
@Nullable
private static ZenDeviceEffects readZenDeviceEffectsXml(TypedXmlPullParser parser) {
ZenDeviceEffects deviceEffects =
@@ -1539,7 +1488,6 @@ public class ZenModeConfig implements Parcelable {
return deviceEffects.hasEffects() ? deviceEffects : null;
}
- @FlaggedApi(Flags.FLAG_MODES_API)
private static void writeZenDeviceEffectsXml(ZenDeviceEffects deviceEffects,
TypedXmlSerializer out) throws IOException {
writeBooleanIfTrue(out, DEVICE_EFFECT_DISPLAY_GRAYSCALE,
@@ -1659,6 +1607,19 @@ public class ZenModeConfig implements Parcelable {
return values;
}
+ @Nullable
+ private static Instant safeInstant(TypedXmlPullParser parser, String att,
+ @Nullable Instant defValue) {
+ final String strValue = parser.getAttributeValue(null, att);
+ if (!TextUtils.isEmpty(strValue)) {
+ Long longValue = tryParseLong(strValue, null);
+ if (longValue != null) {
+ return Instant.ofEpochMilli(longValue);
+ }
+ }
+ return defValue;
+ }
+
@Override
public int describeContents() {
return 0;
@@ -1732,9 +1693,7 @@ public class ZenModeConfig implements Parcelable {
(suppressedVisualEffects & Policy.SUPPRESSED_EFFECT_NOTIFICATION_LIST) == 0);
}
- if (Flags.modesApi()) {
- builder.allowPriorityChannels(allowPriorityChannels);
- }
+ builder.allowPriorityChannels(allowPriorityChannels);
return builder.build();
}
@@ -1860,12 +1819,9 @@ public class ZenModeConfig implements Parcelable {
suppressedVisualEffects |= Policy.SUPPRESSED_EFFECT_NOTIFICATION_LIST;
}
- int state = defaultPolicy.state;
- if (Flags.modesApi()) {
- state = Policy.policyState(defaultPolicy.hasPriorityChannels(),
- ZenPolicy.stateToBoolean(zenPolicy.getPriorityChannelsAllowed(),
- DEFAULT_ALLOW_PRIORITY_CHANNELS));
- }
+ int state = Policy.policyState(defaultPolicy.hasPriorityChannels(),
+ ZenPolicy.stateToBoolean(zenPolicy.getPriorityChannelsAllowed(),
+ DEFAULT_ALLOW_PRIORITY_CHANNELS));
return new NotificationManager.Policy(priorityCategories, callSenders,
messageSenders, suppressedVisualEffects, state, conversationSenders);
@@ -1930,7 +1886,7 @@ public class ZenModeConfig implements Parcelable {
priorityMessageSenders = peopleTypeToPrioritySenders(
manualRule.zenPolicy.getPriorityMessageSenders(), DEFAULT_SOURCE);
- state = Policy.policyState(areChannelsBypassingDnd,
+ state = Policy.policyState(hasPriorityChannels,
manualRule.zenPolicy.getPriorityChannelsAllowed() != STATE_DISALLOW);
boolean suppressFullScreenIntent = !manualRule.zenPolicy.isVisualEffectAllowed(
@@ -2030,10 +1986,7 @@ public class ZenModeConfig implements Parcelable {
priorityConversationSenders = zenPolicyConversationSendersToNotificationPolicy(
getAllowConversationsFrom(), priorityConversationSenders);
- state = areChannelsBypassingDnd ? Policy.STATE_CHANNELS_BYPASSING_DND : 0;
- if (Flags.modesApi()) {
- state = Policy.policyState(areChannelsBypassingDnd, allowPriorityChannels);
- }
+ state = Policy.policyState(hasPriorityChannels, allowPriorityChannels);
suppressedVisualEffects = getSuppressedVisualEffects();
}
@@ -2114,13 +2067,11 @@ public class ZenModeConfig implements Parcelable {
policy.priorityConversationSenders,
allowConversationsFrom);
if (policy.state != Policy.STATE_UNSET) {
- if (Flags.modesApi()) {
- setAllowPriorityChannels(policy.allowPriorityChannels());
- }
+ setAllowPriorityChannels(policy.allowPriorityChannels());
}
}
if (policy.state != Policy.STATE_UNSET) {
- areChannelsBypassingDnd = (policy.state & Policy.STATE_CHANNELS_BYPASSING_DND) != 0;
+ hasPriorityChannels = (policy.state & Policy.STATE_HAS_PRIORITY_CHANNELS) != 0;
}
}
@@ -2618,8 +2569,9 @@ public class ZenModeConfig implements Parcelable {
@UnsupportedAppUsage
public boolean enabled;
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ // TODO: b/368247671 - Obsolete with MODES_UI; delete when the flag is inlined
@Deprecated
- public boolean snoozing; // user manually disabled this instance. Obsolete with MODES_UI
+ public boolean snoozing; // user manually disabled this instance.
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
public String name; // required for automatic
@UnsupportedAppUsage
@@ -2635,9 +2587,7 @@ public class ZenModeConfig implements Parcelable {
// package name, only used for manual rules when they have turned DND on.
public String enabler;
public ZenPolicy zenPolicy;
- @FlaggedApi(Flags.FLAG_MODES_API)
@Nullable public ZenDeviceEffects zenDeviceEffects;
- public boolean modified; // rule has been modified from initial creation
public String pkg;
@AutomaticZenRule.Type
public int type = AutomaticZenRule.TYPE_UNKNOWN;
@@ -2668,6 +2618,18 @@ public class ZenModeConfig implements Parcelable {
@ConditionOverride
int conditionOverride = OVERRIDE_NONE;
+ /**
+ * Last time at which the rule was activated (for any reason, including overrides).
+ * If {@code null}, the rule has never been activated since its creation.
+ *
+ * <p>Note that this was previously untracked, so it will also be {@code null} for rules
+ * created before we started tracking and never activated since -- make sure to account for
+ * it, for example by falling back to {@link #creationTime} in logic involving this field.
+ */
+ @Nullable
+ @FlaggedApi(Flags.FLAG_MODES_CLEANUP_IMPLICIT)
+ public Instant lastActivation;
+
public ZenRule() { }
public ZenRule(Parcel source) {
@@ -2689,46 +2651,45 @@ public class ZenModeConfig implements Parcelable {
enabler = source.readString();
}
zenPolicy = source.readParcelable(null, android.service.notification.ZenPolicy.class);
- if (Flags.modesApi()) {
- zenDeviceEffects = source.readParcelable(null, ZenDeviceEffects.class);
- }
- modified = source.readInt() == 1;
+ zenDeviceEffects = source.readParcelable(null, ZenDeviceEffects.class);
pkg = source.readString();
- if (Flags.modesApi()) {
- allowManualInvocation = source.readBoolean();
- iconResName = source.readString();
- triggerDescription = source.readString();
- type = source.readInt();
- userModifiedFields = source.readInt();
- zenPolicyUserModifiedFields = source.readInt();
- zenDeviceEffectsUserModifiedFields = source.readInt();
- if (source.readInt() == 1) {
- deletionInstant = Instant.ofEpochMilli(source.readLong());
- }
- if (Flags.modesUi()) {
- disabledOrigin = source.readInt();
- legacySuppressedEffects = source.readInt();
- conditionOverride = source.readInt();
+ allowManualInvocation = source.readBoolean();
+ iconResName = source.readString();
+ triggerDescription = source.readString();
+ type = source.readInt();
+ userModifiedFields = source.readInt();
+ zenPolicyUserModifiedFields = source.readInt();
+ zenDeviceEffectsUserModifiedFields = source.readInt();
+ if (source.readInt() == 1) {
+ deletionInstant = Instant.ofEpochMilli(source.readLong());
+ }
+ if (Flags.modesUi()) {
+ disabledOrigin = source.readInt();
+ legacySuppressedEffects = source.readInt();
+ conditionOverride = source.readInt();
+ if (Flags.modesCleanupImplicit()) {
+ if (source.readInt() == 1) {
+ lastActivation = Instant.ofEpochMilli(source.readLong());
+ }
}
}
}
/**
- * Whether this ZenRule can be updated by an app. In general, rules that have been
- * customized by the user cannot be further updated by an app, with some exceptions:
+ * Whether this ZenRule has been customized by the user in any way.
+
+ * <p>In general, rules that have been customized by the user cannot be further updated by
+ * an app, with some exceptions:
* <ul>
* <li>Non user-configurable fields, like type, icon, configurationActivity, etc.
* <li>Name, if the name was not specifically modified by the user (to support language
* switches).
* </ul>
*/
- @FlaggedApi(Flags.FLAG_MODES_API)
- public boolean canBeUpdatedByApp() {
- // The rule is considered updateable if its bitmask has no user modifications, and
- // the bitmasks of the policy and device effects have no modification.
- return userModifiedFields == 0
- && zenPolicyUserModifiedFields == 0
- && zenDeviceEffectsUserModifiedFields == 0;
+ public boolean isUserModified() {
+ return userModifiedFields != 0
+ || zenPolicyUserModifiedFields != 0
+ || zenDeviceEffectsUserModifiedFields != 0;
}
@Override
@@ -2765,29 +2726,32 @@ public class ZenModeConfig implements Parcelable {
dest.writeInt(0);
}
dest.writeParcelable(zenPolicy, 0);
- if (Flags.modesApi()) {
- dest.writeParcelable(zenDeviceEffects, 0);
- }
- dest.writeInt(modified ? 1 : 0);
+ dest.writeParcelable(zenDeviceEffects, 0);
dest.writeString(pkg);
- if (Flags.modesApi()) {
- dest.writeBoolean(allowManualInvocation);
- dest.writeString(iconResName);
- dest.writeString(triggerDescription);
- dest.writeInt(type);
- dest.writeInt(userModifiedFields);
- dest.writeInt(zenPolicyUserModifiedFields);
- dest.writeInt(zenDeviceEffectsUserModifiedFields);
- if (deletionInstant != null) {
- dest.writeInt(1);
- dest.writeLong(deletionInstant.toEpochMilli());
- } else {
- dest.writeInt(0);
- }
- if (Flags.modesUi()) {
- dest.writeInt(disabledOrigin);
- dest.writeInt(legacySuppressedEffects);
- dest.writeInt(conditionOverride);
+ dest.writeBoolean(allowManualInvocation);
+ dest.writeString(iconResName);
+ dest.writeString(triggerDescription);
+ dest.writeInt(type);
+ dest.writeInt(userModifiedFields);
+ dest.writeInt(zenPolicyUserModifiedFields);
+ dest.writeInt(zenDeviceEffectsUserModifiedFields);
+ if (deletionInstant != null) {
+ dest.writeInt(1);
+ dest.writeLong(deletionInstant.toEpochMilli());
+ } else {
+ dest.writeInt(0);
+ }
+ if (Flags.modesUi()) {
+ dest.writeInt(disabledOrigin);
+ dest.writeInt(legacySuppressedEffects);
+ dest.writeInt(conditionOverride);
+ if (Flags.modesCleanupImplicit()) {
+ if (lastActivation != null) {
+ dest.writeInt(1);
+ dest.writeLong(lastActivation.toEpochMilli());
+ } else {
+ dest.writeInt(0);
+ }
}
}
}
@@ -2816,34 +2780,33 @@ public class ZenModeConfig implements Parcelable {
.append(",creationTime=").append(creationTime)
.append(",enabler=").append(enabler)
.append(",zenPolicy=").append(zenPolicy)
- .append(",modified=").append(modified)
- .append(",condition=").append(condition);
-
- if (Flags.modesApi()) {
- sb.append(",deviceEffects=").append(zenDeviceEffects)
- .append(",allowManualInvocation=").append(allowManualInvocation)
- .append(",iconResName=").append(iconResName)
- .append(",triggerDescription=").append(triggerDescription)
- .append(",type=").append(type);
- if (userModifiedFields != 0) {
- sb.append(",userModifiedFields=")
- .append(AutomaticZenRule.fieldsToString(userModifiedFields));
- }
- if (zenPolicyUserModifiedFields != 0) {
- sb.append(",zenPolicyUserModifiedFields=")
- .append(ZenPolicy.fieldsToString(zenPolicyUserModifiedFields));
- }
- if (zenDeviceEffectsUserModifiedFields != 0) {
- sb.append(",zenDeviceEffectsUserModifiedFields=")
- .append(ZenDeviceEffects.fieldsToString(
- zenDeviceEffectsUserModifiedFields));
- }
- if (deletionInstant != null) {
- sb.append(",deletionInstant=").append(deletionInstant);
- }
- if (Flags.modesUi()) {
- sb.append(",disabledOrigin=").append(disabledOrigin);
- sb.append(",legacySuppressedEffects=").append(legacySuppressedEffects);
+ .append(",condition=").append(condition)
+ .append(",deviceEffects=").append(zenDeviceEffects)
+ .append(",allowManualInvocation=").append(allowManualInvocation)
+ .append(",iconResName=").append(iconResName)
+ .append(",triggerDescription=").append(triggerDescription)
+ .append(",type=").append(type);
+ if (userModifiedFields != 0) {
+ sb.append(",userModifiedFields=")
+ .append(AutomaticZenRule.fieldsToString(userModifiedFields));
+ }
+ if (zenPolicyUserModifiedFields != 0) {
+ sb.append(",zenPolicyUserModifiedFields=")
+ .append(ZenPolicy.fieldsToString(zenPolicyUserModifiedFields));
+ }
+ if (zenDeviceEffectsUserModifiedFields != 0) {
+ sb.append(",zenDeviceEffectsUserModifiedFields=")
+ .append(ZenDeviceEffects.fieldsToString(
+ zenDeviceEffectsUserModifiedFields));
+ }
+ if (deletionInstant != null) {
+ sb.append(",deletionInstant=").append(deletionInstant);
+ }
+ if (Flags.modesUi()) {
+ sb.append(",disabledOrigin=").append(disabledOrigin);
+ sb.append(",legacySuppressedEffects=").append(legacySuppressedEffects);
+ if (Flags.modesCleanupImplicit()) {
+ sb.append(",lastActivation=").append(lastActivation);
}
}
@@ -2869,7 +2832,7 @@ public class ZenModeConfig implements Parcelable {
proto.write(ZenRuleProto.CREATION_TIME_MS, creationTime);
proto.write(ZenRuleProto.ENABLED, enabled);
proto.write(ZenRuleProto.ENABLER, enabler);
- if (Flags.modesApi() && Flags.modesUi()) {
+ if (Flags.modesUi()) {
proto.write(ZenRuleProto.IS_SNOOZING, conditionOverride == OVERRIDE_DEACTIVATE);
} else {
proto.write(ZenRuleProto.IS_SNOOZING, snoozing);
@@ -2887,7 +2850,6 @@ public class ZenModeConfig implements Parcelable {
if (zenPolicy != null) {
zenPolicy.dumpDebug(proto, ZenRuleProto.ZEN_POLICY);
}
- proto.write(ZenRuleProto.MODIFIED, modified);
proto.end(token);
}
@@ -2908,26 +2870,25 @@ public class ZenModeConfig implements Parcelable {
&& Objects.equals(other.enabler, enabler)
&& Objects.equals(other.zenPolicy, zenPolicy)
&& Objects.equals(other.pkg, pkg)
- && other.modified == modified;
+ && Objects.equals(other.zenDeviceEffects, zenDeviceEffects)
+ && other.allowManualInvocation == allowManualInvocation
+ && Objects.equals(other.iconResName, iconResName)
+ && Objects.equals(other.triggerDescription, triggerDescription)
+ && other.type == type
+ && other.userModifiedFields == userModifiedFields
+ && other.zenPolicyUserModifiedFields == zenPolicyUserModifiedFields
+ && other.zenDeviceEffectsUserModifiedFields
+ == zenDeviceEffectsUserModifiedFields
+ && Objects.equals(other.deletionInstant, deletionInstant);
- if (Flags.modesApi()) {
+ if (Flags.modesUi()) {
finalEquals = finalEquals
- && Objects.equals(other.zenDeviceEffects, zenDeviceEffects)
- && other.allowManualInvocation == allowManualInvocation
- && Objects.equals(other.iconResName, iconResName)
- && Objects.equals(other.triggerDescription, triggerDescription)
- && other.type == type
- && other.userModifiedFields == userModifiedFields
- && other.zenPolicyUserModifiedFields == zenPolicyUserModifiedFields
- && other.zenDeviceEffectsUserModifiedFields
- == zenDeviceEffectsUserModifiedFields
- && Objects.equals(other.deletionInstant, deletionInstant);
-
- if (Flags.modesUi()) {
+ && other.disabledOrigin == disabledOrigin
+ && other.legacySuppressedEffects == legacySuppressedEffects
+ && other.conditionOverride == conditionOverride;
+ if (Flags.modesCleanupImplicit()) {
finalEquals = finalEquals
- && other.disabledOrigin == disabledOrigin
- && other.legacySuppressedEffects == legacySuppressedEffects
- && other.conditionOverride == conditionOverride;
+ && Objects.equals(other.lastActivation, lastActivation);
}
}
@@ -2936,26 +2897,32 @@ public class ZenModeConfig implements Parcelable {
@Override
public int hashCode() {
- if (Flags.modesApi()) {
- if (Flags.modesUi()) {
+ if (Flags.modesUi()) {
+ if (Flags.modesCleanupImplicit()) {
return Objects.hash(enabled, snoozing, name, zenMode, conditionId, condition,
component, configurationActivity, pkg, id, enabler, zenPolicy,
- zenDeviceEffects, modified, allowManualInvocation, iconResName,
+ zenDeviceEffects, allowManualInvocation, iconResName,
triggerDescription, type, userModifiedFields,
zenPolicyUserModifiedFields, zenDeviceEffectsUserModifiedFields,
deletionInstant, disabledOrigin, legacySuppressedEffects,
- conditionOverride);
+ conditionOverride, lastActivation);
} else {
return Objects.hash(enabled, snoozing, name, zenMode, conditionId, condition,
component, configurationActivity, pkg, id, enabler, zenPolicy,
- zenDeviceEffects, modified, allowManualInvocation, iconResName,
+ zenDeviceEffects, allowManualInvocation, iconResName,
triggerDescription, type, userModifiedFields,
zenPolicyUserModifiedFields, zenDeviceEffectsUserModifiedFields,
- deletionInstant);
+ deletionInstant, disabledOrigin, legacySuppressedEffects,
+ conditionOverride);
}
+ } else {
+ return Objects.hash(enabled, snoozing, name, zenMode, conditionId, condition,
+ component, configurationActivity, pkg, id, enabler, zenPolicy,
+ zenDeviceEffects, allowManualInvocation, iconResName,
+ triggerDescription, type, userModifiedFields,
+ zenPolicyUserModifiedFields, zenDeviceEffectsUserModifiedFields,
+ deletionInstant);
}
- return Objects.hash(enabled, snoozing, name, zenMode, conditionId, condition,
- component, configurationActivity, pkg, id, enabler, zenPolicy, modified);
}
/** Returns a deep copy of the {@link ZenRule}. */
@@ -2971,7 +2938,7 @@ public class ZenModeConfig implements Parcelable {
}
public boolean isActive() {
- if (Flags.modesApi() && Flags.modesUi()) {
+ if (Flags.modesUi()) {
if (!enabled || getPkg() == null) {
return false;
} else if (conditionOverride == OVERRIDE_ACTIVATE) {
@@ -2989,7 +2956,7 @@ public class ZenModeConfig implements Parcelable {
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
@ConditionOverride
public int getConditionOverride() {
- if (Flags.modesApi() && Flags.modesUi()) {
+ if (Flags.modesUi()) {
return conditionOverride;
} else {
return snoozing ? OVERRIDE_DEACTIVATE : OVERRIDE_NONE;
@@ -2997,7 +2964,7 @@ public class ZenModeConfig implements Parcelable {
}
public void setConditionOverride(@ConditionOverride int value) {
- if (Flags.modesApi() && Flags.modesUi()) {
+ if (Flags.modesUi()) {
conditionOverride = value;
} else {
if (value == OVERRIDE_ACTIVATE) {
@@ -3026,7 +2993,7 @@ public class ZenModeConfig implements Parcelable {
* manual deactivation (which used to be called "snoozing").
*/
public void reconsiderConditionOverride() {
- if (Flags.modesApi() && Flags.modesUi()) {
+ if (Flags.modesUi()) {
if (conditionOverride == OVERRIDE_ACTIVATE && isTrueOrUnknown()) {
resetConditionOverride();
} else if (conditionOverride == OVERRIDE_DEACTIVATE && !isTrueOrUnknown()) {
@@ -3085,11 +3052,8 @@ public class ZenModeConfig implements Parcelable {
& NotificationManager.Policy.PRIORITY_CATEGORY_REPEAT_CALLERS) != 0;
boolean allowConversations = (policy.priorityConversationSenders
& Policy.PRIORITY_CATEGORY_CONVERSATIONS) != 0;
- boolean areChannelsBypassingDnd = (policy.state & Policy.STATE_CHANNELS_BYPASSING_DND) != 0;
- if (Flags.modesApi()) {
- areChannelsBypassingDnd = policy.hasPriorityChannels()
- && policy.allowPriorityChannels();
- }
+ boolean areChannelsBypassingDnd =
+ policy.hasPriorityChannels() && policy.allowPriorityChannels();
boolean allowSystem = (policy.priorityCategories & Policy.PRIORITY_CATEGORY_SYSTEM) != 0;
return !allowReminders && !allowCalls && !allowMessages && !allowEvents
&& !allowRepeatCallers && !areChannelsBypassingDnd && !allowSystem
@@ -3129,15 +3093,12 @@ public class ZenModeConfig implements Parcelable {
&& !policy.isCategoryAllowed(PRIORITY_CATEGORY_EVENTS, false)
&& !policy.isCategoryAllowed(PRIORITY_CATEGORY_REPEAT_CALLERS, false)
&& !policy.isCategoryAllowed(PRIORITY_CATEGORY_SYSTEM, false)
- && !(config.areChannelsBypassingDnd && policy.getPriorityChannelsAllowed()
+ && !(config.hasPriorityChannels && policy.getPriorityChannelsAllowed()
== STATE_ALLOW);
} else {
- boolean areChannelsBypassingDnd = config.areChannelsBypassingDnd;
- if (Flags.modesApi()) {
- areChannelsBypassingDnd = config.areChannelsBypassingDnd
- && config.isAllowPriorityChannels();
- }
+ boolean areChannelsBypassingDnd = config.hasPriorityChannels
+ && config.isAllowPriorityChannels();
return !config.isAllowReminders() && !config.isAllowCalls() && !config.isAllowMessages()
&& !config.isAllowEvents() && !config.isAllowRepeatCallers()
&& !areChannelsBypassingDnd && !config.isAllowSystem();
diff --git a/core/java/android/service/notification/ZenModeDiff.java b/core/java/android/service/notification/ZenModeDiff.java
index 31acd248dcc0..c159e4016095 100644
--- a/core/java/android/service/notification/ZenModeDiff.java
+++ b/core/java/android/service/notification/ZenModeDiff.java
@@ -16,7 +16,6 @@
package android.service.notification;
-import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.Nullable;
import android.app.Flags;
@@ -249,7 +248,7 @@ public class ZenModeDiff {
public static final String FIELD_ALLOW_MESSAGES_FROM = "allowMessagesFrom";
public static final String FIELD_ALLOW_CONVERSATIONS_FROM = "allowConversationsFrom";
public static final String FIELD_SUPPRESSED_VISUAL_EFFECTS = "suppressedVisualEffects";
- public static final String FIELD_ARE_CHANNELS_BYPASSING_DND = "areChannelsBypassingDnd";
+ public static final String FIELD_HAS_PRIORITY_CHANNELS = "hasPriorityChannels";
public static final String FIELD_ALLOW_PRIORITY_CHANNELS = "allowPriorityChannels";
private static final Set<String> PEOPLE_TYPE_FIELDS =
Set.of(FIELD_ALLOW_CALLS_FROM, FIELD_ALLOW_MESSAGES_FROM);
@@ -323,15 +322,13 @@ public class ZenModeDiff {
addField(FIELD_SUPPRESSED_VISUAL_EFFECTS,
new FieldDiff<>(from.suppressedVisualEffects, to.suppressedVisualEffects));
}
- if (from.areChannelsBypassingDnd != to.areChannelsBypassingDnd) {
- addField(FIELD_ARE_CHANNELS_BYPASSING_DND,
- new FieldDiff<>(from.areChannelsBypassingDnd, to.areChannelsBypassingDnd));
+ if (from.hasPriorityChannels != to.hasPriorityChannels) {
+ addField(FIELD_HAS_PRIORITY_CHANNELS,
+ new FieldDiff<>(from.hasPriorityChannels, to.hasPriorityChannels));
}
- if (Flags.modesApi()) {
- if (from.allowPriorityChannels != to.allowPriorityChannels) {
- addField(FIELD_ALLOW_PRIORITY_CHANNELS,
- new FieldDiff<>(from.allowPriorityChannels, to.allowPriorityChannels));
- }
+ if (from.allowPriorityChannels != to.allowPriorityChannels) {
+ addField(FIELD_ALLOW_PRIORITY_CHANNELS,
+ new FieldDiff<>(from.allowPriorityChannels, to.allowPriorityChannels));
}
// Compare automatic and manual rules
@@ -491,7 +488,6 @@ public class ZenModeDiff {
public static final String FIELD_ENABLER = "enabler";
public static final String FIELD_ZEN_POLICY = "zenPolicy";
public static final String FIELD_ZEN_DEVICE_EFFECTS = "zenDeviceEffects";
- public static final String FIELD_MODIFIED = "modified";
public static final String FIELD_PKG = "pkg";
public static final String FIELD_ALLOW_MANUAL = "allowManualInvocation";
public static final String FIELD_ICON_RES = "iconResName";
@@ -532,7 +528,7 @@ public class ZenModeDiff {
if (from.enabled != to.enabled) {
addField(FIELD_ENABLED, new FieldDiff<>(from.enabled, to.enabled));
}
- if (Flags.modesApi() && Flags.modesUi()) {
+ if (Flags.modesUi()) {
if (from.conditionOverride != to.conditionOverride) {
addField(FIELD_CONDITION_OVERRIDE,
new FieldDiff<>(from.conditionOverride, to.conditionOverride));
@@ -572,51 +568,40 @@ public class ZenModeDiff {
if (!Objects.equals(from.enabler, to.enabler)) {
addField(FIELD_ENABLER, new FieldDiff<>(from.enabler, to.enabler));
}
- if (android.app.Flags.modesApi()) {
- PolicyDiff policyDiff = new PolicyDiff(from.zenPolicy, to.zenPolicy);
- if (policyDiff.hasDiff()) {
- addField(FIELD_ZEN_POLICY, new FieldDiff<>(from.zenPolicy, to.zenPolicy,
- policyDiff));
- }
- } else {
- if (!Objects.equals(from.zenPolicy, to.zenPolicy)) {
- addField(FIELD_ZEN_POLICY, new FieldDiff<>(from.zenPolicy, to.zenPolicy));
- }
- }
- if (from.modified != to.modified) {
- addField(FIELD_MODIFIED, new FieldDiff<>(from.modified, to.modified));
+ PolicyDiff policyDiff = new PolicyDiff(from.zenPolicy, to.zenPolicy);
+ if (policyDiff.hasDiff()) {
+ addField(FIELD_ZEN_POLICY, new FieldDiff<>(from.zenPolicy, to.zenPolicy,
+ policyDiff));
}
if (!Objects.equals(from.pkg, to.pkg)) {
addField(FIELD_PKG, new FieldDiff<>(from.pkg, to.pkg));
}
- if (android.app.Flags.modesApi()) {
- DeviceEffectsDiff deviceEffectsDiff = new DeviceEffectsDiff(from.zenDeviceEffects,
- to.zenDeviceEffects);
- if (deviceEffectsDiff.hasDiff()) {
- addField(FIELD_ZEN_DEVICE_EFFECTS,
- new FieldDiff<>(from.zenDeviceEffects, to.zenDeviceEffects,
- deviceEffectsDiff));
- }
- if (!Objects.equals(from.triggerDescription, to.triggerDescription)) {
- addField(FIELD_TRIGGER_DESCRIPTION,
- new FieldDiff<>(from.triggerDescription, to.triggerDescription));
- }
- if (from.type != to.type) {
- addField(FIELD_TYPE, new FieldDiff<>(from.type, to.type));
- }
- if (from.allowManualInvocation != to.allowManualInvocation) {
- addField(FIELD_ALLOW_MANUAL,
- new FieldDiff<>(from.allowManualInvocation, to.allowManualInvocation));
- }
- if (!Objects.equals(from.iconResName, to.iconResName)) {
- addField(FIELD_ICON_RES, new FieldDiff<>(from.iconResName, to.iconResName));
- }
- if (android.app.Flags.modesUi()) {
- if (from.legacySuppressedEffects != to.legacySuppressedEffects) {
- addField(FIELD_LEGACY_SUPPRESSED_EFFECTS,
- new FieldDiff<>(from.legacySuppressedEffects,
- to.legacySuppressedEffects));
- }
+ DeviceEffectsDiff deviceEffectsDiff = new DeviceEffectsDiff(from.zenDeviceEffects,
+ to.zenDeviceEffects);
+ if (deviceEffectsDiff.hasDiff()) {
+ addField(FIELD_ZEN_DEVICE_EFFECTS,
+ new FieldDiff<>(from.zenDeviceEffects, to.zenDeviceEffects,
+ deviceEffectsDiff));
+ }
+ if (!Objects.equals(from.triggerDescription, to.triggerDescription)) {
+ addField(FIELD_TRIGGER_DESCRIPTION,
+ new FieldDiff<>(from.triggerDescription, to.triggerDescription));
+ }
+ if (from.type != to.type) {
+ addField(FIELD_TYPE, new FieldDiff<>(from.type, to.type));
+ }
+ if (from.allowManualInvocation != to.allowManualInvocation) {
+ addField(FIELD_ALLOW_MANUAL,
+ new FieldDiff<>(from.allowManualInvocation, to.allowManualInvocation));
+ }
+ if (!Objects.equals(from.iconResName, to.iconResName)) {
+ addField(FIELD_ICON_RES, new FieldDiff<>(from.iconResName, to.iconResName));
+ }
+ if (android.app.Flags.modesUi()) {
+ if (from.legacySuppressedEffects != to.legacySuppressedEffects) {
+ addField(FIELD_LEGACY_SUPPRESSED_EFFECTS,
+ new FieldDiff<>(from.legacySuppressedEffects,
+ to.legacySuppressedEffects));
}
}
}
@@ -702,7 +687,6 @@ public class ZenModeDiff {
* Diff class representing a change between two
* {@link android.service.notification.ZenDeviceEffects}.
*/
- @FlaggedApi(Flags.FLAG_MODES_API)
public static class DeviceEffectsDiff extends BaseDiff {
public static final String FIELD_GRAYSCALE = "mGrayscale";
public static final String FIELD_SUPPRESS_AMBIENT_DISPLAY = "mSuppressAmbientDisplay";
@@ -843,7 +827,6 @@ public class ZenModeDiff {
/**
* Diff class representing a change between two {@link android.service.notification.ZenPolicy}.
*/
- @FlaggedApi(Flags.FLAG_MODES_API)
public static class PolicyDiff extends BaseDiff {
public static final String FIELD_PRIORITY_CATEGORY_REMINDERS =
"mPriorityCategories_Reminders";
diff --git a/core/java/android/service/notification/ZenPolicy.java b/core/java/android/service/notification/ZenPolicy.java
index 4cff67e24a0f..6b98c4144f91 100644
--- a/core/java/android/service/notification/ZenPolicy.java
+++ b/core/java/android/service/notification/ZenPolicy.java
@@ -16,13 +16,11 @@
package android.service.notification;
-import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SuppressLint;
import android.annotation.TestApi;
-import android.app.Flags;
import android.app.Notification;
import android.app.NotificationChannel;
import android.os.Parcel;
@@ -78,91 +76,74 @@ public final class ZenPolicy implements Parcelable {
* the same time.
* @hide
*/
- @FlaggedApi(Flags.FLAG_MODES_API)
public static final int FIELD_MESSAGES = 1 << 0;
/**
* Covers modifications to CALL_SENDERS and PRIORITY_CATEGORY_CALLS, which are set at
* the same time.
* @hide
*/
- @FlaggedApi(Flags.FLAG_MODES_API)
public static final int FIELD_CALLS = 1 << 1;
/**
* Covers modifications to CONVERSATION_SENDERS and PRIORITY_CATEGORY_CONVERSATIONS, which are
* set at the same time.
* @hide
*/
- @FlaggedApi(Flags.FLAG_MODES_API)
public static final int FIELD_CONVERSATIONS = 1 << 2;
/**
* @hide
*/
- @FlaggedApi(Flags.FLAG_MODES_API)
public static final int FIELD_ALLOW_CHANNELS = 1 << 3;
/**
* @hide
*/
- @FlaggedApi(Flags.FLAG_MODES_API)
public static final int FIELD_PRIORITY_CATEGORY_REMINDERS = 1 << 4;
/**
* @hide
*/
- @FlaggedApi(Flags.FLAG_MODES_API)
public static final int FIELD_PRIORITY_CATEGORY_EVENTS = 1 << 5;
/**
* @hide
*/
- @FlaggedApi(Flags.FLAG_MODES_API)
public static final int FIELD_PRIORITY_CATEGORY_REPEAT_CALLERS = 1 << 6;
/**
* @hide
*/
- @FlaggedApi(Flags.FLAG_MODES_API)
public static final int FIELD_PRIORITY_CATEGORY_ALARMS = 1 << 7;
/**
* @hide
*/
- @FlaggedApi(Flags.FLAG_MODES_API)
public static final int FIELD_PRIORITY_CATEGORY_MEDIA = 1 << 8;
/**
* @hide
*/
- @FlaggedApi(Flags.FLAG_MODES_API)
public static final int FIELD_PRIORITY_CATEGORY_SYSTEM = 1 << 9;
/**
* @hide
*/
- @FlaggedApi(Flags.FLAG_MODES_API)
public static final int FIELD_VISUAL_EFFECT_FULL_SCREEN_INTENT = 1 << 10;
/**
* @hide
*/
- @FlaggedApi(Flags.FLAG_MODES_API)
public static final int FIELD_VISUAL_EFFECT_LIGHTS = 1 << 11;
/**
* @hide
*/
- @FlaggedApi(Flags.FLAG_MODES_API)
public static final int FIELD_VISUAL_EFFECT_PEEK = 1 << 12;
/**
* @hide
*/
- @FlaggedApi(Flags.FLAG_MODES_API)
public static final int FIELD_VISUAL_EFFECT_STATUS_BAR = 1 << 13;
/**
* @hide
*/
- @FlaggedApi(Flags.FLAG_MODES_API)
public static final int FIELD_VISUAL_EFFECT_BADGE = 1 << 14;
/**
* @hide
*/
- @FlaggedApi(Flags.FLAG_MODES_API)
public static final int FIELD_VISUAL_EFFECT_AMBIENT = 1 << 15;
/**
* @hide
*/
- @FlaggedApi(Flags.FLAG_MODES_API)
public static final int FIELD_VISUAL_EFFECT_NOTIFICATION_LIST = 1 << 16;
private List<Integer> mPriorityCategories;
@@ -170,7 +151,6 @@ public final class ZenPolicy implements Parcelable {
private @PeopleType int mPriorityMessages = PEOPLE_TYPE_UNSET;
private @PeopleType int mPriorityCalls = PEOPLE_TYPE_UNSET;
private @ConversationSenders int mConversationSenders = CONVERSATION_SENDERS_UNSET;
- @FlaggedApi(Flags.FLAG_MODES_API)
private @ChannelType int mAllowChannels = CHANNEL_POLICY_UNSET;
/** @hide */
@@ -358,7 +338,6 @@ public final class ZenPolicy implements Parcelable {
*
* @hide
*/
- @FlaggedApi(Flags.FLAG_MODES_API)
public static final int CHANNEL_POLICY_UNSET = 0;
/**
@@ -367,7 +346,6 @@ public final class ZenPolicy implements Parcelable {
*
* @hide
*/
- @FlaggedApi(Flags.FLAG_MODES_API)
public static final int CHANNEL_POLICY_PRIORITY = 1;
/**
@@ -376,7 +354,6 @@ public final class ZenPolicy implements Parcelable {
*
* @hide
*/
- @FlaggedApi(Flags.FLAG_MODES_API)
public static final int CHANNEL_POLICY_NONE = 2;
/** @hide */
@@ -386,7 +363,6 @@ public final class ZenPolicy implements Parcelable {
}
/** @hide */
- @FlaggedApi(Flags.FLAG_MODES_API)
public ZenPolicy(List<Integer> priorityCategories, List<Integer> visualEffects,
@PeopleType int priorityMessages, @PeopleType int priorityCalls,
@ConversationSenders int conversationSenders, @ChannelType int allowChannels) {
@@ -409,7 +385,6 @@ public final class ZenPolicy implements Parcelable {
*
* @hide
*/
- @FlaggedApi(Flags.FLAG_MODES_API)
public static ZenPolicy getBasePolicyInterruptionFilterAlarms() {
return new ZenPolicy.Builder()
.disallowAllSounds()
@@ -430,7 +405,6 @@ public final class ZenPolicy implements Parcelable {
*
* @hide
*/
- @FlaggedApi(Flags.FLAG_MODES_API)
public static ZenPolicy getBasePolicyInterruptionFilterNone() {
return new ZenPolicy.Builder()
.disallowAllSounds()
@@ -628,7 +602,6 @@ public final class ZenPolicy implements Parcelable {
* channels may bypass; if {@link #STATE_DISALLOW}, then even notifications from channels
* with {@link NotificationChannel#canBypassDnd()} will be intercepted.
*/
- @FlaggedApi(Flags.FLAG_MODES_API)
public @State int getPriorityChannelsAllowed() {
switch (mAllowChannels) {
case CHANNEL_POLICY_PRIORITY:
@@ -695,14 +668,10 @@ public final class ZenPolicy implements Parcelable {
* Builds the current ZenPolicy.
*/
public @NonNull ZenPolicy build() {
- if (Flags.modesApi()) {
- return new ZenPolicy(new ArrayList<>(mZenPolicy.mPriorityCategories),
- new ArrayList<>(mZenPolicy.mVisualEffects),
- mZenPolicy.mPriorityMessages, mZenPolicy.mPriorityCalls,
- mZenPolicy.mConversationSenders, mZenPolicy.mAllowChannels);
- } else {
- return mZenPolicy.copy();
- }
+ return new ZenPolicy(new ArrayList<>(mZenPolicy.mPriorityCategories),
+ new ArrayList<>(mZenPolicy.mVisualEffects),
+ mZenPolicy.mPriorityMessages, mZenPolicy.mPriorityCalls,
+ mZenPolicy.mConversationSenders, mZenPolicy.mAllowChannels);
}
/**
@@ -1054,7 +1023,6 @@ public final class ZenPolicy implements Parcelable {
* Set whether priority channels are permitted to break through DND.
*/
@SuppressLint("BuilderSetStyle")
- @FlaggedApi(Flags.FLAG_MODES_API)
public @NonNull Builder allowPriorityChannels(boolean allow) {
mZenPolicy.mAllowChannels = allow ? CHANNEL_POLICY_PRIORITY : CHANNEL_POLICY_NONE;
return this;
@@ -1079,38 +1047,21 @@ public final class ZenPolicy implements Parcelable {
dest.writeInt(mPriorityMessages);
dest.writeInt(mPriorityCalls);
dest.writeInt(mConversationSenders);
- if (Flags.modesApi()) {
- dest.writeInt(mAllowChannels);
- }
+ dest.writeInt(mAllowChannels);
}
public static final @NonNull Creator<ZenPolicy> CREATOR =
new Creator<ZenPolicy>() {
@Override
public ZenPolicy createFromParcel(Parcel source) {
- ZenPolicy policy;
- if (Flags.modesApi()) {
- policy = new ZenPolicy(
- trimList(source.readArrayList(Integer.class.getClassLoader(),
- Integer.class), NUM_PRIORITY_CATEGORIES),
- trimList(source.readArrayList(Integer.class.getClassLoader(),
- Integer.class), NUM_VISUAL_EFFECTS),
- source.readInt(), source.readInt(), source.readInt(),
- source.readInt()
+ return new ZenPolicy(
+ trimList(source.readArrayList(Integer.class.getClassLoader(),
+ Integer.class), NUM_PRIORITY_CATEGORIES),
+ trimList(source.readArrayList(Integer.class.getClassLoader(),
+ Integer.class), NUM_VISUAL_EFFECTS),
+ source.readInt(), source.readInt(), source.readInt(),
+ source.readInt()
);
- } else {
- policy = new ZenPolicy();
- policy.mPriorityCategories =
- trimList(source.readArrayList(Integer.class.getClassLoader(),
- Integer.class), NUM_PRIORITY_CATEGORIES);
- policy.mVisualEffects =
- trimList(source.readArrayList(Integer.class.getClassLoader(),
- Integer.class), NUM_VISUAL_EFFECTS);
- policy.mPriorityMessages = source.readInt();
- policy.mPriorityCalls = source.readInt();
- policy.mConversationSenders = source.readInt();
- }
- return policy;
}
@Override
@@ -1121,18 +1072,16 @@ public final class ZenPolicy implements Parcelable {
@Override
public String toString() {
- StringBuilder sb = new StringBuilder(ZenPolicy.class.getSimpleName())
+ return new StringBuilder(ZenPolicy.class.getSimpleName())
.append('{')
.append("priorityCategories=[").append(priorityCategoriesToString())
.append("], visualEffects=[").append(visualEffectsToString())
.append("], priorityCallsSenders=").append(peopleTypeToString(mPriorityCalls))
.append(", priorityMessagesSenders=").append(peopleTypeToString(mPriorityMessages))
.append(", priorityConversationSenders=").append(
- conversationTypeToString(mConversationSenders));
- if (Flags.modesApi()) {
- sb.append(", allowChannels=").append(channelTypeToString(mAllowChannels));
- }
- return sb.append('}').toString();
+ conversationTypeToString(mConversationSenders))
+ .append(", allowChannels=").append(channelTypeToString(mAllowChannels))
+ .append('}').toString();
}
/** @hide */
@@ -1325,7 +1274,6 @@ public final class ZenPolicy implements Parcelable {
/**
* @hide
*/
- @FlaggedApi(Flags.FLAG_MODES_API)
public static String channelTypeToString(@ChannelType int channelType) {
switch (channelType) {
case CHANNEL_POLICY_UNSET:
@@ -1344,25 +1292,18 @@ public final class ZenPolicy implements Parcelable {
if (o == this) return true;
final ZenPolicy other = (ZenPolicy) o;
- boolean eq = Objects.equals(other.mPriorityCategories, mPriorityCategories)
+ return Objects.equals(other.mPriorityCategories, mPriorityCategories)
&& Objects.equals(other.mVisualEffects, mVisualEffects)
&& other.mPriorityCalls == mPriorityCalls
&& other.mPriorityMessages == mPriorityMessages
- && other.mConversationSenders == mConversationSenders;
- if (Flags.modesApi()) {
- return eq && other.mAllowChannels == mAllowChannels;
- }
- return eq;
+ && other.mConversationSenders == mConversationSenders
+ && other.mAllowChannels == mAllowChannels;
}
@Override
public int hashCode() {
- if (Flags.modesApi()) {
- return Objects.hash(mPriorityCategories, mVisualEffects, mPriorityCalls,
- mPriorityMessages, mConversationSenders, mAllowChannels);
- }
- return Objects.hash(mPriorityCategories, mVisualEffects, mPriorityCalls, mPriorityMessages,
- mConversationSenders);
+ return Objects.hash(mPriorityCategories, mVisualEffects, mPriorityCalls,
+ mPriorityMessages, mConversationSenders, mAllowChannels);
}
private @State int getZenPolicyPriorityCategoryState(@PriorityCategory int
@@ -1480,13 +1421,10 @@ public final class ZenPolicy implements Parcelable {
}
}
- // apply allowed channels
- if (Flags.modesApi()) {
- // if no channels are allowed, can't newly allow them
- if (mAllowChannels != CHANNEL_POLICY_NONE
- && policyToApply.mAllowChannels != CHANNEL_POLICY_UNSET) {
- mAllowChannels = policyToApply.mAllowChannels;
- }
+ // apply allowed channels -> if no channels are allowed, can't newly allow them
+ if (mAllowChannels != CHANNEL_POLICY_NONE
+ && policyToApply.mAllowChannels != CHANNEL_POLICY_UNSET) {
+ mAllowChannels = policyToApply.mAllowChannels;
}
}
@@ -1499,7 +1437,6 @@ public final class ZenPolicy implements Parcelable {
* @hide
*/
@TestApi
- @FlaggedApi(Flags.FLAG_MODES_API)
public @NonNull ZenPolicy overwrittenWith(@Nullable ZenPolicy newPolicy) {
ZenPolicy result = this.copy();
@@ -1596,10 +1533,7 @@ public final class ZenPolicy implements Parcelable {
proto.write(DNDPolicyProto.ALLOW_CALLS_FROM, getPriorityCallSenders());
proto.write(DNDPolicyProto.ALLOW_MESSAGES_FROM, getPriorityMessageSenders());
proto.write(DNDPolicyProto.ALLOW_CONVERSATIONS_FROM, getPriorityConversationSenders());
-
- if (Flags.modesApi()) {
- proto.write(DNDPolicyProto.ALLOW_CHANNELS, getPriorityChannelsAllowed());
- }
+ proto.write(DNDPolicyProto.ALLOW_CHANNELS, getPriorityChannelsAllowed());
proto.flush();
return bytes.toByteArray();
diff --git a/core/java/android/service/wallpaper/WallpaperService.java b/core/java/android/service/wallpaper/WallpaperService.java
index 464756842caf..41a64e22e058 100644
--- a/core/java/android/service/wallpaper/WallpaperService.java
+++ b/core/java/android/service/wallpaper/WallpaperService.java
@@ -1595,8 +1595,17 @@ public abstract class WallpaperService extends Service {
mWindow.setSession(mSession);
mLayout.packageName = getPackageName();
- mIWallpaperEngine.mDisplayManager.registerDisplayListener(mDisplayListener,
- mCaller.getHandler());
+ if (com.android.server.display.feature.flags.Flags
+ .displayListenerPerformanceImprovements()
+ && com.android.server.display.feature.flags.Flags
+ .committedStateSeparateEvent()) {
+ mIWallpaperEngine.mDisplayManager.registerDisplayListener(mDisplayListener,
+ mCaller.getHandler(), DisplayManager.EVENT_TYPE_DISPLAY_CHANGED,
+ DisplayManager.PRIVATE_EVENT_TYPE_DISPLAY_COMMITTED_STATE_CHANGED);
+ } else {
+ mIWallpaperEngine.mDisplayManager.registerDisplayListener(mDisplayListener,
+ mCaller.getHandler());
+ }
mDisplay = mIWallpaperEngine.mDisplay;
// Use window context of TYPE_WALLPAPER so client can access UI resources correctly.
mDisplayContext = createDisplayContext(mDisplay)
diff --git a/core/java/android/view/Display.java b/core/java/android/view/Display.java
index ca0959af3ff8..231aa6816908 100644
--- a/core/java/android/view/Display.java
+++ b/core/java/android/view/Display.java
@@ -1599,7 +1599,6 @@ public final class Display {
mGlobal.registerDisplayListener(toRegister, executor,
DisplayManagerGlobal
.INTERNAL_EVENT_FLAG_DISPLAY_BASIC_CHANGED
- | DisplayManagerGlobal.INTERNAL_EVENT_FLAG_DISPLAY_REFRESH_RATE
| DisplayManagerGlobal
.INTERNAL_EVENT_FLAG_DISPLAY_HDR_SDR_RATIO_CHANGED,
ActivityThread.currentPackageName());
diff --git a/core/java/android/view/DisplayInfo.java b/core/java/android/view/DisplayInfo.java
index ecdbaa3cd2f4..d880072aa404 100644
--- a/core/java/android/view/DisplayInfo.java
+++ b/core/java/android/view/DisplayInfo.java
@@ -44,6 +44,7 @@ import android.util.SparseArray;
import android.util.proto.ProtoOutputStream;
import com.android.internal.display.BrightnessSynchronizer;
+import com.android.server.display.feature.flags.Flags;
import java.util.Arrays;
import java.util.Objects;
@@ -447,18 +448,20 @@ public final class DisplayInfo implements Parcelable {
}
public boolean equals(DisplayInfo other) {
- return equals(other, /* compareRefreshRate */ true);
+ return equals(other, /* compareOnlyBasicChanges */ false);
}
/**
* Compares if the two DisplayInfo objects are equal or not
* @param other The other DisplayInfo against which the comparison is to be done
- * @param compareRefreshRate Indicates if the refresh rate is also to be considered in
- * comparison
+ * @param compareOnlyBasicChanges Indicates if the changes to be compared are the ones which
+ * could lead to an emission of
+ * {@link android.hardware.display.DisplayManager.EVENT_TYPE_DISPLAY_CHANGED}
+ * event
* @return
*/
- public boolean equals(DisplayInfo other, boolean compareRefreshRate) {
- boolean isEqualWithoutRefreshRate = other != null
+ public boolean equals(DisplayInfo other, boolean compareOnlyBasicChanges) {
+ boolean isEqualWithOnlyBasicChanges = other != null
&& layerStack == other.layerStack
&& flags == other.flags
&& type == other.type
@@ -494,7 +497,6 @@ public final class DisplayInfo implements Parcelable {
&& physicalXDpi == other.physicalXDpi
&& physicalYDpi == other.physicalYDpi
&& state == other.state
- && committedState == other.committedState
&& ownerUid == other.ownerUid
&& Objects.equals(ownerPackageName, other.ownerPackageName)
&& removeMode == other.removeMode
@@ -512,14 +514,19 @@ public final class DisplayInfo implements Parcelable {
thermalBrightnessThrottlingDataId, other.thermalBrightnessThrottlingDataId)
&& canHostTasks == other.canHostTasks;
- if (compareRefreshRate) {
- return isEqualWithoutRefreshRate
+ if (!Flags.committedStateSeparateEvent()) {
+ isEqualWithOnlyBasicChanges = isEqualWithOnlyBasicChanges
+ && (committedState == other.committedState);
+ }
+ if (!compareOnlyBasicChanges) {
+ return isEqualWithOnlyBasicChanges
&& (getRefreshRate() == other.getRefreshRate())
&& appVsyncOffsetNanos == other.appVsyncOffsetNanos
&& presentationDeadlineNanos == other.presentationDeadlineNanos
- && (modeId == other.modeId);
+ && (modeId == other.modeId)
+ && (committedState == other.committedState);
}
- return isEqualWithoutRefreshRate;
+ return isEqualWithOnlyBasicChanges;
}
@Override
diff --git a/core/java/android/view/NotificationHeaderView.java b/core/java/android/view/NotificationHeaderView.java
index 73cd5ecd39ef..df680c054f56 100644
--- a/core/java/android/view/NotificationHeaderView.java
+++ b/core/java/android/view/NotificationHeaderView.java
@@ -32,7 +32,6 @@ import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.util.AttributeSet;
-import android.util.TypedValue;
import android.widget.FrameLayout;
import android.widget.RelativeLayout;
import android.widget.RemoteViews;
@@ -266,20 +265,14 @@ public class NotificationHeaderView extends RelativeLayout {
? R.style.TextAppearance_DeviceDefault_Notification_Title
: R.style.TextAppearance_DeviceDefault_Notification_Info;
// Most of the time, we're showing text in the minimized state
- if (findViewById(R.id.header_text) instanceof TextView headerText) {
- headerText.setTextAppearance(styleResId);
- if (notificationsRedesignTemplates()) {
- // TODO: b/378660052 - When inlining the redesign flag, this should be updated
- // directly in TextAppearance_DeviceDefault_Notification_Title so we won't need to
- // override it here.
- float textSize = getContext().getResources().getDimension(
- R.dimen.notification_2025_title_text_size);
- headerText.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize);
- }
+ View headerText = findViewById(R.id.header_text);
+ if (headerText instanceof TextView) {
+ ((TextView) headerText).setTextAppearance(styleResId);
}
// If there's no summary or text, we show the app name instead of nothing
- if (findViewById(R.id.app_name_text) instanceof TextView appNameText) {
- appNameText.setTextAppearance(styleResId);
+ View appNameText = findViewById(R.id.app_name_text);
+ if (appNameText instanceof TextView) {
+ ((TextView) appNameText).setTextAppearance(styleResId);
}
}
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index 80b4f2caabbb..6b6147a3749d 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -118,6 +118,7 @@ import static android.view.flags.Flags.addSchandleToVriSurface;
import static android.view.flags.Flags.disableDrawWakeLock;
import static android.view.flags.Flags.sensitiveContentAppProtection;
import static android.view.flags.Flags.sensitiveContentPrematureProtectionRemovedFix;
+import static android.view.flags.Flags.toolkitFrameRateDebug;
import static android.view.flags.Flags.toolkitFrameRateFunctionEnablingReadOnly;
import static android.view.flags.Flags.toolkitFrameRateTouchBoost25q1;
import static android.view.flags.Flags.toolkitFrameRateTypingReadOnly;
@@ -538,6 +539,11 @@ public final class ViewRootImpl implements ViewParent,
private static boolean sAlwaysAssignFocus;
/**
+ * whether we pre-initialized the Buffer Allocator
+ */
+ private static boolean sPreInitializedBufferAllocator = false;
+
+ /**
* This list must only be modified by the main thread.
*/
final ArrayList<WindowCallbacks> mWindowCallbacks = new ArrayList<>();
@@ -1222,6 +1228,7 @@ public final class ViewRootImpl implements ViewParent,
com.android.graphics.surfaceflinger.flags.Flags.vrrBugfix24q4();
private static final boolean sEnableVrr = ViewProperties.vrr_enabled().orElse(true);
private static final boolean sToolkitInitialTouchBoostFlagValue = toolkitInitialTouchBoost();
+ private static boolean sToolkitFrameRateDebugFlagValue = toolkitFrameRateDebug();
static {
sToolkitSetFrameRateReadOnlyFlagValue = toolkitSetFrameRateReadOnly();
@@ -1342,6 +1349,11 @@ public final class ViewRootImpl implements ViewParent,
com.android.server.display.feature.flags.Flags.subscribeGranularDisplayEvents();
mSendPerfHintOnTouch = adpfViewrootimplActionDownBoost();
+
+ if (!sPreInitializedBufferAllocator) {
+ preInitBufferAllocator();
+ sPreInitializedBufferAllocator = true;
+ }
}
public static void addFirstDrawHandler(Runnable callback) {
@@ -13129,6 +13141,11 @@ public final class ViewRootImpl implements ViewParent,
if (sToolkitFrameRateFunctionEnablingReadOnlyFlagValue) {
mFrameRateTransaction.setFrameRateCategory(mSurfaceControl,
frameRateCategory, false).applyAsyncUnsafe();
+
+ if (sToolkitFrameRateDebugFlagValue) {
+ Log.v(mTag, "### ViewRootImpl setFrameRateCategory '"
+ + categoryToString(frameRateCategory) + "'");
+ }
}
mLastPreferredFrameRateCategory = frameRateCategory;
}
@@ -13191,8 +13208,15 @@ public final class ViewRootImpl implements ViewParent,
if (preferredFrameRate > 0) {
mFrameRateTransaction.setFrameRate(mSurfaceControl, preferredFrameRate,
mFrameRateCompatibility);
+ if (sToolkitFrameRateDebugFlagValue) {
+ Log.v(mTag, "### ViewRootImpl setFrameRate '"
+ + preferredFrameRate + "'");
+ }
} else {
mFrameRateTransaction.clearFrameRate(mSurfaceControl);
+ if (sToolkitFrameRateDebugFlagValue) {
+ Log.v(mTag, "### ViewRootImpl setFrameRate 0 Hz");
+ }
}
mFrameRateTransaction.applyAsyncUnsafe();
}
@@ -13246,6 +13270,12 @@ public final class ViewRootImpl implements ViewParent,
// mFrameRateCategoryView = view == null ? "-" : view.getClass().getSimpleName();
}
mDrawnThisFrame = true;
+
+ if (sToolkitFrameRateDebugFlagValue) {
+ String viewName = view == null ? "-" : view.getClass().getSimpleName();
+ Log.v(mTag, "### View: " + viewName + " votes '"
+ + categoryToString(frameRateCategory) + "'");
+ }
}
/**
@@ -13562,4 +13592,10 @@ public final class ViewRootImpl implements ViewParent,
sProtoLogInitialized = true;
}
}
+
+ private void preInitBufferAllocator() {
+ if (com.android.graphics.hwui.flags.Flags.earlyPreinitBufferAllocator()) {
+ ThreadedRenderer.preInitBufferAllocator();
+ }
+ }
}
diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java
index db699d7bfb06..93eed370004b 100644
--- a/core/java/android/view/WindowManager.java
+++ b/core/java/android/view/WindowManager.java
@@ -625,12 +625,6 @@ public interface WindowManager extends ViewManager {
int TRANSIT_FLAG_PHYSICAL_DISPLAY_SWITCH = (1 << 14); // 0x4000
/**
- * Transition flag: Indicates that aod is showing hidden by entering doze
- * @hide
- */
- int TRANSIT_FLAG_AOD_APPEARING = (1 << 15); // 0x8000
-
- /**
* @hide
*/
@IntDef(flag = true, prefix = { "TRANSIT_FLAG_" }, value = {
@@ -649,7 +643,6 @@ public interface WindowManager extends ViewManager {
TRANSIT_FLAG_KEYGUARD_OCCLUDING,
TRANSIT_FLAG_KEYGUARD_UNOCCLUDING,
TRANSIT_FLAG_PHYSICAL_DISPLAY_SWITCH,
- TRANSIT_FLAG_AOD_APPEARING,
})
@Retention(RetentionPolicy.SOURCE)
@interface TransitionFlags {}
@@ -666,8 +659,7 @@ public interface WindowManager extends ViewManager {
(TRANSIT_FLAG_KEYGUARD_GOING_AWAY
| TRANSIT_FLAG_KEYGUARD_APPEARING
| TRANSIT_FLAG_KEYGUARD_OCCLUDING
- | TRANSIT_FLAG_KEYGUARD_UNOCCLUDING
- | TRANSIT_FLAG_AOD_APPEARING);
+ | TRANSIT_FLAG_KEYGUARD_UNOCCLUDING);
/**
* Remove content mode: Indicates remove content mode is currently not defined.
diff --git a/core/java/android/view/accessibility/AccessibilityNodeInfo.java b/core/java/android/view/accessibility/AccessibilityNodeInfo.java
index 578b7b6a63fa..ede0b3cf8cce 100644
--- a/core/java/android/view/accessibility/AccessibilityNodeInfo.java
+++ b/core/java/android/view/accessibility/AccessibilityNodeInfo.java
@@ -2684,9 +2684,10 @@ public class AccessibilityNodeInfo implements Parcelable {
* <p><b>Note:</b> The start and end {@link SelectionPosition} of the provided {@link Selection}
* should be constructed with {@code this} node or a descendant of it.
*
- * <p><b>Note:</b> {@link AccessibilityNodeInfo#setFocusable} and {@link
- * AccessibilityNodeInfo#setFocused} should both be called with {@code true} before setting the
- * selection in order to make {@code this} node a candidate to contain a selection.
+ * <p><b>Note:</b> {@link AccessibilityNodeInfo#setFocusable} and
+ * {@link AccessibilityNodeInfo#setFocused} should both be called with {@code true}
+ * before setting the selection in order to make {@code this} node a candidate to
+ * contain a selection.
*
* <p><b>Note:</b> Cannot be called from an AccessibilityService. This class is made immutable
* before being delivered to an AccessibilityService.
@@ -2706,12 +2707,10 @@ public class AccessibilityNodeInfo implements Parcelable {
* Gets the extended selection, which is a representation of selection that spans multiple nodes
* that exist within the subtree of the node defining selection.
*
- * <p><b>Note:</b> The start and end {@link SelectionPosition} of the provided {@link Selection}
- * should be constructed with {@code this} node or a descendant of it.
- *
- * <p><b>Note:</b> In order for a node to be a candidate to contain a selection, {@link
- * AccessibilityNodeInfo#isFocusable()} ()} and {@link AccessibilityNodeInfo#isFocused()} should
- * both be return with {@code true}.
+ * <p><b>Note:</b> Nodes that are candidates to contain a selection should return
+ * {@code true} from {@link #isFocusable()} and {@link #isFocused()}.
+ * The start and end {@link SelectionPosition}s of this {@link Selection}
+ * should exist within {@code this} node or its descendants.
*
* @return The extended selection within the node's subtree, or {@code null} if no selection
* exists.
@@ -5840,8 +5839,8 @@ public class AccessibilityNodeInfo implements Parcelable {
/**
* Instantiates a new SelectionPosition.
*
- * @param view The {@link View} containing the virtual descendant associated with the
- * selection position.
+ * @param view The {@link View} containing the text associated with this selection
+ * position.
* @param offset The offset for a selection position within {@code view}'s text content,
* which should be a value between 0 and the length of {@code view}'s text.
*/
diff --git a/core/java/android/view/flags/refresh_rate_flags.aconfig b/core/java/android/view/flags/refresh_rate_flags.aconfig
index 3bc2205f8e1c..18fa0f353f36 100644
--- a/core/java/android/view/flags/refresh_rate_flags.aconfig
+++ b/core/java/android/view/flags/refresh_rate_flags.aconfig
@@ -143,4 +143,11 @@ flag {
namespace: "toolkit"
description: "Feature flag to update initial touch boost logic"
bug: "393004744"
+}
+
+flag {
+ name: "toolkit_frame_rate_debug"
+ namespace: "toolkit"
+ description: "Feature flag to ennable ARR debug message"
+ bug: "394614443"
} \ No newline at end of file
diff --git a/core/java/android/widget/RemoteViews.java b/core/java/android/widget/RemoteViews.java
index 0f5476f58f74..0a5c14e3a08b 100644
--- a/core/java/android/widget/RemoteViews.java
+++ b/core/java/android/widget/RemoteViews.java
@@ -8565,12 +8565,16 @@ public class RemoteViews implements Parcelable, Filter {
return context;
}
try {
- // Use PackageManager as the source of truth for application information, rather
- // than the parceled ApplicationInfo provided by the app.
- ApplicationInfo sanitizedApplication =
- context.getPackageManager().getApplicationInfoAsUser(
- mApplication.packageName, 0,
- UserHandle.getUserId(mApplication.uid));
+ ApplicationInfo sanitizedApplication = mApplication;
+ try {
+ // Use PackageManager as the source of truth for application information, rather
+ // than the parceled ApplicationInfo provided by the app.
+ sanitizedApplication = context.getPackageManager().getApplicationInfoAsUser(
+ mApplication.packageName, 0, UserHandle.getUserId(mApplication.uid));
+ } catch(SecurityException se) {
+ Log.d(LOG_TAG, "Unable to fetch appInfo for " + mApplication.packageName);
+ }
+
Context applicationContext = context.createApplicationContext(
sanitizedApplication,
Context.CONTEXT_RESTRICTED);
diff --git a/core/java/android/window/DesktopModeFlags.java b/core/java/android/window/DesktopModeFlags.java
index d43469fa76ca..ca1017b72854 100644
--- a/core/java/android/window/DesktopModeFlags.java
+++ b/core/java/android/window/DesktopModeFlags.java
@@ -96,6 +96,7 @@ public enum DesktopModeFlags {
ENABLE_DESKTOP_WINDOWING_TASK_LIMIT(Flags::enableDesktopWindowingTaskLimit, true),
ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY(Flags::enableDesktopWindowingWallpaperActivity,
true),
+ ENABLE_DRAG_RESIZE_SET_UP_IN_BG_THREAD(Flags::enableDragResizeSetUpInBgThread, false),
ENABLE_FULLY_IMMERSIVE_IN_DESKTOP(Flags::enableFullyImmersiveInDesktop, true),
ENABLE_HANDLE_INPUT_FIX(Flags::enableHandleInputFix, true),
ENABLE_HOLD_TO_DRAG_APP_HANDLE(Flags::enableHoldToDragAppHandle, true),
diff --git a/core/java/android/window/TransitionInfo.java b/core/java/android/window/TransitionInfo.java
index cf21e50e0a19..4f34aa36a204 100644
--- a/core/java/android/window/TransitionInfo.java
+++ b/core/java/android/window/TransitionInfo.java
@@ -29,7 +29,6 @@ import static android.view.Display.INVALID_DISPLAY;
import static android.view.WindowManager.LayoutParams.ROTATION_ANIMATION_UNSPECIFIED;
import static android.view.WindowManager.TRANSIT_CHANGE;
import static android.view.WindowManager.TRANSIT_CLOSE;
-import static android.view.WindowManager.TRANSIT_FLAG_AOD_APPEARING;
import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_APPEARING;
import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY;
import static android.view.WindowManager.TRANSIT_NONE;
@@ -406,8 +405,7 @@ public final class TransitionInfo implements Parcelable {
*/
public boolean hasChangesOrSideEffects() {
return !mChanges.isEmpty() || isKeyguardGoingAway()
- || (mFlags & TRANSIT_FLAG_KEYGUARD_APPEARING) != 0
- || (mFlags & TRANSIT_FLAG_AOD_APPEARING) != 0;
+ || (mFlags & TRANSIT_FLAG_KEYGUARD_APPEARING) != 0;
}
/**
diff --git a/core/java/android/window/flags/lse_desktop_experience.aconfig b/core/java/android/window/flags/lse_desktop_experience.aconfig
index 3266ad4d93ae..79120b22c205 100644
--- a/core/java/android/window/flags/lse_desktop_experience.aconfig
+++ b/core/java/android/window/flags/lse_desktop_experience.aconfig
@@ -79,6 +79,16 @@ flag {
}
flag {
+ name: "enable_drag_resize_set_up_in_bg_thread"
+ namespace: "lse_desktop_experience"
+ description: "Enables setting up the drag-resize input listener in a bg thread"
+ bug: "396445663"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+flag {
name: "enable_desktop_windowing_wallpaper_activity"
namespace: "lse_desktop_experience"
description: "Enables desktop wallpaper activity to show wallpaper in the desktop mode"
@@ -165,6 +175,16 @@ flag {
}
flag {
+ name: "enable_camera_compat_track_task_and_app_bugfix"
+ namespace: "lse_desktop_experience"
+ description: "Whether to use taskId and app process to track camera apps, and notify the policies only on first camera open and final close"
+ bug: "380840084"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+flag {
name: "enable_task_stack_observer_in_shell"
namespace: "lse_desktop_experience"
description: "Introduces a new observer in shell to track the task stack."
diff --git a/core/java/com/android/internal/pm/pkg/component/AconfigFlags.java b/core/java/com/android/internal/pm/pkg/component/AconfigFlags.java
index 21d000dc5224..b57acf3d97fd 100644
--- a/core/java/com/android/internal/pm/pkg/component/AconfigFlags.java
+++ b/core/java/com/android/internal/pm/pkg/component/AconfigFlags.java
@@ -226,6 +226,21 @@ public class AconfigFlags {
}
private Boolean getFlagValueFromNewStorage(String flagPackageAndName) {
+ // We still need to check mFlagValues in case addFlagValuesForTesting() was called for
+ // testing purposes.
+ if (!mFlagValues.isEmpty() && mFlagValues.containsKey(flagPackageAndName)) {
+ Boolean value = mFlagValues.get(flagPackageAndName);
+ if (DEBUG) {
+ Slog.v(
+ LOG_TAG,
+ "Aconfig flag value (FOR TESTING) for "
+ + flagPackageAndName
+ + " = "
+ + value);
+ }
+ return value;
+ }
+
int index = flagPackageAndName.lastIndexOf('.');
if (index < 0) {
Slog.e(LOG_TAG, "Unable to parse package name from " + flagPackageAndName);
diff --git a/core/java/com/android/internal/security/VerityUtils.java b/core/java/com/android/internal/security/VerityUtils.java
index 37500766a4ac..ac186d0a26b5 100644
--- a/core/java/com/android/internal/security/VerityUtils.java
+++ b/core/java/com/android/internal/security/VerityUtils.java
@@ -56,8 +56,7 @@ public abstract class VerityUtils {
private static final int HASH_SIZE_BYTES = 32;
public static boolean isFsVeritySupported() {
- return Build.VERSION.DEVICE_INITIAL_SDK_INT >= Build.VERSION_CODES.R
- || SystemProperties.getInt("ro.apk_verity.mode", 0) == 2;
+ return Build.VERSION.DEVICE_INITIAL_SDK_INT >= Build.VERSION_CODES.R;
}
/** Enables fs-verity for the file without signature. */
diff --git a/core/java/com/android/internal/widget/LockPatternView.java b/core/java/com/android/internal/widget/LockPatternView.java
index 0ec55f958f38..1f907602cb9b 100644
--- a/core/java/com/android/internal/widget/LockPatternView.java
+++ b/core/java/com/android/internal/widget/LockPatternView.java
@@ -87,10 +87,10 @@ public class LockPatternView extends View {
private static final int CELL_ACTIVATE = 0;
private static final int CELL_DEACTIVATE = 1;
- private final int mDotSize;
- private final int mDotSizeActivated;
+ private int mDotSize;
+ private int mDotSizeActivated;
private final float mDotHitFactor;
- private final int mPathWidth;
+ private int mPathWidth;
private final int mLineFadeOutAnimationDurationMs;
private final int mLineFadeOutAnimationDelayMs;
private final int mFadePatternAnimationDurationMs;
@@ -1341,6 +1341,38 @@ public class LockPatternView extends View {
invalidate();
}
+ /**
+ * Change dot colors
+ */
+ public void setDotColors(int dotColor, int dotActivatedColor) {
+ mDotColor = dotColor;
+ mDotActivatedColor = dotActivatedColor;
+ invalidate();
+ }
+
+ /**
+ * Keeps dot activated until the next dot gets activated.
+ */
+ public void setKeepDotActivated(boolean keepDotActivated) {
+ mKeepDotActivated = keepDotActivated;
+ }
+
+ /**
+ * Set dot sizes in dp
+ */
+ public void setDotSizes(int dotSizeDp, int dotSizeActivatedDp) {
+ mDotSize = dotSizeDp;
+ mDotSizeActivated = dotSizeActivatedDp;
+ }
+
+ /**
+ * Set the stroke width of the pattern line.
+ */
+ public void setPathWidth(int pathWidthDp) {
+ mPathWidth = pathWidthDp;
+ mPathPaint.setStrokeWidth(mPathWidth);
+ }
+
private float getCenterXForColumn(int column) {
return mPaddingLeft + column * mSquareWidth + mSquareWidth / 2f;
}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/CoreDocument.java b/core/java/com/android/internal/widget/remotecompose/core/CoreDocument.java
index ca355c41f7a9..b8503da2c09b 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/CoreDocument.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/CoreDocument.java
@@ -65,7 +65,7 @@ public class CoreDocument implements Serializable {
// We also keep a more fine-grained BUILD number, exposed as
// ID_API_LEVEL = DOCUMENT_API_LEVEL + BUILD
- static final float BUILD = 0.1f;
+ static final float BUILD = 0.2f;
@NonNull ArrayList<Operation> mOperations = new ArrayList<>();
@@ -742,6 +742,7 @@ public class CoreDocument implements Serializable {
if (op instanceof Component) {
mComponentMap.put(((Component) op).getComponentId(), (Component) op);
registerVariables(context, ((Component) op).getList());
+ ((Component) op).registerVariables(context);
}
if (op instanceof ComponentValue) {
ComponentValue v = (ComponentValue) op;
diff --git a/core/java/com/android/internal/widget/remotecompose/core/Operations.java b/core/java/com/android/internal/widget/remotecompose/core/Operations.java
index 9bb8d9f39975..09ec40271f4d 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/Operations.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/Operations.java
@@ -35,6 +35,7 @@ import com.android.internal.widget.remotecompose.core.operations.DrawBitmapFontT
import com.android.internal.widget.remotecompose.core.operations.DrawBitmapInt;
import com.android.internal.widget.remotecompose.core.operations.DrawBitmapScaled;
import com.android.internal.widget.remotecompose.core.operations.DrawCircle;
+import com.android.internal.widget.remotecompose.core.operations.DrawContent;
import com.android.internal.widget.remotecompose.core.operations.DrawLine;
import com.android.internal.widget.remotecompose.core.operations.DrawOval;
import com.android.internal.widget.remotecompose.core.operations.DrawPath;
@@ -81,6 +82,7 @@ import com.android.internal.widget.remotecompose.core.operations.Theme;
import com.android.internal.widget.remotecompose.core.operations.TimeAttribute;
import com.android.internal.widget.remotecompose.core.operations.TouchExpression;
import com.android.internal.widget.remotecompose.core.operations.layout.CanvasContent;
+import com.android.internal.widget.remotecompose.core.operations.layout.CanvasOperations;
import com.android.internal.widget.remotecompose.core.operations.layout.ClickModifierOperation;
import com.android.internal.widget.remotecompose.core.operations.layout.ComponentStart;
import com.android.internal.widget.remotecompose.core.operations.layout.ContainerEnd;
@@ -105,6 +107,7 @@ import com.android.internal.widget.remotecompose.core.operations.layout.modifier
import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.BorderModifierOperation;
import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.ClipRectModifierOperation;
import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.ComponentVisibilityOperation;
+import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.DrawContentOperation;
import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.GraphicsLayerModifierOperation;
import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.HeightInModifierOperation;
import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.HeightModifierOperation;
@@ -172,6 +175,7 @@ public class Operations {
public static final int DATA_PATH = 123;
public static final int DRAW_PATH = 124;
public static final int DRAW_TWEEN_PATH = 125;
+ public static final int DRAW_CONTENT = 139;
public static final int MATRIX_SCALE = 126;
public static final int MATRIX_TRANSLATE = 127;
public static final int MATRIX_SKEW = 128;
@@ -215,6 +219,8 @@ public class Operations {
public static final int ATTRIBUTE_TEXT = 170;
public static final int ATTRIBUTE_IMAGE = 171;
public static final int ATTRIBUTE_TIME = 172;
+ public static final int CANVAS_OPERATIONS = 173;
+ public static final int MODIFIER_DRAW_CONTENT = 174;
///////////////////////////////////////// ======================
@@ -366,6 +372,7 @@ public class Operations {
map.put(MODIFIER_SCROLL, ScrollModifierOperation::read);
map.put(MODIFIER_MARQUEE, MarqueeModifierOperation::read);
map.put(MODIFIER_RIPPLE, RippleModifierOperation::read);
+ map.put(MODIFIER_DRAW_CONTENT, DrawContentOperation::read);
map.put(CONTAINER_END, ContainerEnd::read);
@@ -393,6 +400,7 @@ public class Operations {
map.put(LAYOUT_TEXT, TextLayout::read);
map.put(LAYOUT_STATE, StateLayout::read);
+ map.put(DRAW_CONTENT, DrawContent::read);
map.put(COMPONENT_VALUE, ComponentValue::read);
map.put(DRAW_ARC, DrawArc::read);
@@ -409,6 +417,7 @@ public class Operations {
map.put(PARTICLE_LOOP, ParticlesLoop::read);
map.put(FUNCTION_CALL, FloatFunctionCall::read);
map.put(FUNCTION_DEFINE, FloatFunctionDefine::read);
+ map.put(CANVAS_OPERATIONS, CanvasOperations::read);
map.put(ACCESSIBILITY_SEMANTICS, CoreSemantics::read);
map.put(ATTRIBUTE_IMAGE, ImageAttribute::read);
diff --git a/core/java/com/android/internal/widget/remotecompose/core/RemoteComposeBuffer.java b/core/java/com/android/internal/widget/remotecompose/core/RemoteComposeBuffer.java
index 39f85f600310..e75bd30b381d 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/RemoteComposeBuffer.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/RemoteComposeBuffer.java
@@ -38,6 +38,7 @@ import com.android.internal.widget.remotecompose.core.operations.DrawBitmapFontT
import com.android.internal.widget.remotecompose.core.operations.DrawBitmapInt;
import com.android.internal.widget.remotecompose.core.operations.DrawBitmapScaled;
import com.android.internal.widget.remotecompose.core.operations.DrawCircle;
+import com.android.internal.widget.remotecompose.core.operations.DrawContent;
import com.android.internal.widget.remotecompose.core.operations.DrawLine;
import com.android.internal.widget.remotecompose.core.operations.DrawOval;
import com.android.internal.widget.remotecompose.core.operations.DrawPath;
@@ -84,6 +85,7 @@ import com.android.internal.widget.remotecompose.core.operations.TimeAttribute;
import com.android.internal.widget.remotecompose.core.operations.TouchExpression;
import com.android.internal.widget.remotecompose.core.operations.Utils;
import com.android.internal.widget.remotecompose.core.operations.layout.CanvasContent;
+import com.android.internal.widget.remotecompose.core.operations.layout.CanvasOperations;
import com.android.internal.widget.remotecompose.core.operations.layout.ComponentStart;
import com.android.internal.widget.remotecompose.core.operations.layout.ContainerEnd;
import com.android.internal.widget.remotecompose.core.operations.layout.ImpulseOperation;
@@ -1887,7 +1889,7 @@ public class RemoteComposeBuffer {
}
/** Add a component end tag */
- public void addComponentEnd() {
+ public void addContainerEnd() {
ContainerEnd.apply(mBuffer);
}
@@ -2231,6 +2233,11 @@ public class RemoteComposeBuffer {
LayoutComponentContent.apply(mBuffer, mLastComponentId);
}
+ /** Add a canvas operations start tag */
+ public void addCanvasOperationsStart() {
+ CanvasOperations.apply(mBuffer);
+ }
+
/**
* Add a component width value
*
@@ -2427,4 +2434,9 @@ public class RemoteComposeBuffer {
TimeAttribute.apply(mBuffer, id, timeId, attribute, args);
return Utils.asNan(id);
}
+
+ /** In the context of a component draw modifier, draw the content of the component */
+ public void drawComponentContent() {
+ DrawContent.apply(mBuffer);
+ }
}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/DrawContent.java b/core/java/com/android/internal/widget/remotecompose/core/operations/DrawContent.java
new file mode 100644
index 000000000000..e2e22acbeb8f
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/DrawContent.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.internal.widget.remotecompose.core.operations;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+
+import com.android.internal.widget.remotecompose.core.Operation;
+import com.android.internal.widget.remotecompose.core.Operations;
+import com.android.internal.widget.remotecompose.core.PaintContext;
+import com.android.internal.widget.remotecompose.core.PaintOperation;
+import com.android.internal.widget.remotecompose.core.WireBuffer;
+import com.android.internal.widget.remotecompose.core.documentation.DocumentationBuilder;
+import com.android.internal.widget.remotecompose.core.operations.layout.LayoutComponent;
+import com.android.internal.widget.remotecompose.core.serialize.MapSerializer;
+import com.android.internal.widget.remotecompose.core.serialize.Serializable;
+
+import java.util.List;
+
+/** The DrawContent command */
+public class DrawContent extends PaintOperation implements Serializable {
+ private static final int OP_CODE = Operations.DRAW_CONTENT;
+ private static final String CLASS_NAME = "DrawContent";
+ private @Nullable LayoutComponent mComponent;
+
+ @Override
+ public void write(@NonNull WireBuffer buffer) {
+ apply(buffer);
+ }
+
+ /**
+ * Set the component to be painted
+ *
+ * @param component
+ */
+ public void setComponent(LayoutComponent component) {
+ mComponent = component;
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ return "DrawContent;";
+ }
+
+ /**
+ * Read this operation and add it to the list of operations
+ *
+ * @param buffer the buffer to read
+ * @param operations the list of operations that will be added to
+ */
+ public static void read(@NonNull WireBuffer buffer, @NonNull List<Operation> operations) {
+ DrawContent op = new DrawContent();
+ operations.add(op);
+ }
+
+ /**
+ * The name of the class
+ *
+ * @return the name
+ */
+ @NonNull
+ public static String name() {
+ return CLASS_NAME;
+ }
+
+ /**
+ * The OP_CODE for this command
+ *
+ * @return the opcode
+ */
+ public static int id() {
+ return OP_CODE;
+ }
+
+ /**
+ * add a draw content operation to the buffer
+ *
+ * @param buffer the buffer to add to
+ */
+ public static void apply(@NonNull WireBuffer buffer) {
+ buffer.start(Operations.DRAW_CONTENT);
+ }
+
+ /**
+ * Populate the documentation with a description of this operation
+ *
+ * @param doc to append the description to.
+ */
+ public static void documentation(@NonNull DocumentationBuilder doc) {
+ doc.operation("Layout Operations", OP_CODE, CLASS_NAME)
+ .description("Draw the component content");
+ }
+
+ @Override
+ public void paint(@NonNull PaintContext context) {
+ if (mComponent != null) {
+ mComponent.drawContent(context);
+ }
+ }
+
+ @Override
+ public void serialize(MapSerializer serializer) {
+ serializer.add("type", CLASS_NAME);
+ }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/CanvasOperations.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/CanvasOperations.java
new file mode 100644
index 000000000000..3e7f1d304315
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/CanvasOperations.java
@@ -0,0 +1,175 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.internal.widget.remotecompose.core.operations.layout;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+
+import com.android.internal.widget.remotecompose.core.Operation;
+import com.android.internal.widget.remotecompose.core.Operations;
+import com.android.internal.widget.remotecompose.core.PaintContext;
+import com.android.internal.widget.remotecompose.core.PaintOperation;
+import com.android.internal.widget.remotecompose.core.RemoteContext;
+import com.android.internal.widget.remotecompose.core.VariableSupport;
+import com.android.internal.widget.remotecompose.core.WireBuffer;
+import com.android.internal.widget.remotecompose.core.documentation.DocumentationBuilder;
+import com.android.internal.widget.remotecompose.core.operations.ComponentValue;
+import com.android.internal.widget.remotecompose.core.operations.DrawContent;
+import com.android.internal.widget.remotecompose.core.serialize.MapSerializer;
+import com.android.internal.widget.remotecompose.core.serialize.Serializable;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/** Represents a list of canvas operations. */
+public class CanvasOperations extends PaintOperation
+ implements VariableSupport, Container, Serializable {
+ private static final int OP_CODE = Operations.CANVAS_OPERATIONS;
+ private static final String CLASS_NAME = "CanvasOperations";
+
+ @NonNull public ArrayList<Operation> mList = new ArrayList<>();
+ @Nullable LayoutComponent mComponent;
+
+ /** The constructor */
+ public CanvasOperations() {}
+
+ @Override
+ public void registerListening(RemoteContext context) {
+ for (Operation operation : mList) {
+ if (operation instanceof VariableSupport) {
+ VariableSupport variableSupport = (VariableSupport) operation;
+ variableSupport.registerListening(context);
+ }
+ if (operation instanceof ComponentValue) {
+ ComponentValue v = (ComponentValue) operation;
+ mComponent.addComponentValue(v);
+ }
+ }
+ }
+
+ @Override
+ public void updateVariables(RemoteContext context) {
+ for (Operation operation : mList) {
+ if (operation instanceof VariableSupport) {
+ VariableSupport variableSupport = (VariableSupport) operation;
+ variableSupport.updateVariables(context);
+ }
+ }
+ }
+
+ /**
+ * The returns a list to be filled
+ *
+ * @return list to be filled
+ */
+ @NonNull
+ public ArrayList<Operation> getList() {
+ return mList;
+ }
+
+ @Override
+ public void write(@NonNull WireBuffer buffer) {
+ apply(buffer);
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ StringBuilder builder = new StringBuilder(CLASS_NAME + "\n");
+ for (Operation operation : mList) {
+ builder.append(" ");
+ builder.append(operation);
+ builder.append("\n");
+ }
+ return builder.toString();
+ }
+
+ @NonNull
+ @Override
+ public String deepToString(@NonNull String indent) {
+ return (indent != null ? indent : "") + toString();
+ }
+
+ @Override
+ public void paint(@NonNull PaintContext context) {
+ RemoteContext remoteContext = context.getContext();
+ for (Operation op : mList) {
+ if (op instanceof VariableSupport && op.isDirty()) {
+ ((VariableSupport) op).updateVariables(context.getContext());
+ }
+ remoteContext.incrementOpCount();
+ op.apply(context.getContext());
+ }
+ }
+
+ /**
+ * The name of the class
+ *
+ * @return the name
+ */
+ @NonNull
+ public static String name() {
+ return "Loop";
+ }
+
+ /**
+ * Apply this operation to the buffer
+ *
+ * @param buffer
+ */
+ public static void apply(@NonNull WireBuffer buffer) {
+ buffer.start(OP_CODE);
+ }
+
+ /**
+ * Read this operation and add it to the list of operations
+ *
+ * @param buffer the buffer to read
+ * @param operations the list of operations that will be added to
+ */
+ public static void read(@NonNull WireBuffer buffer, @NonNull List<Operation> operations) {
+ operations.add(new CanvasOperations());
+ }
+
+ /**
+ * Populate the documentation with a description of this operation
+ *
+ * @param doc to append the description to.
+ */
+ public static void documentation(@NonNull DocumentationBuilder doc) {
+ doc.operation("Operations", OP_CODE, name())
+ .description("Impulse Process that runs a list of operations");
+ }
+
+ @Override
+ public void serialize(MapSerializer serializer) {
+ serializer.add("type", CLASS_NAME).add("list", mList);
+ }
+
+ /**
+ * Set layout component
+ *
+ * @param layoutComponent
+ */
+ public void setComponent(LayoutComponent layoutComponent) {
+ mComponent = layoutComponent;
+ for (Operation op : mList) {
+ if (op instanceof DrawContent) {
+ ((DrawContent) op).setComponent(layoutComponent);
+ }
+ }
+ }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/Component.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/Component.java
index e332e4be4c8d..c73643682b55 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/Component.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/Component.java
@@ -329,6 +329,15 @@ public class Component extends PaintOperation
mAnimationSpec = animationSpec;
}
+ /**
+ * If the component contains variables beside mList, make sure to register them here
+ *
+ * @param context
+ */
+ public void registerVariables(RemoteContext context) {
+ // Nothing here
+ }
+
public enum Visibility {
GONE,
VISIBLE,
@@ -976,6 +985,17 @@ public class Component extends PaintOperation
}
}
+ /** Extract CanvasOperations if present */
+ public @Nullable CanvasOperations getCanvasOperations(LayoutComponent layoutComponent) {
+ for (Operation op : mList) {
+ if (op instanceof CanvasOperations) {
+ ((CanvasOperations) op).setComponent(layoutComponent);
+ return (CanvasOperations) op;
+ }
+ }
+ return null;
+ }
+
/**
* Extract child TextData elements
*
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/LayoutComponent.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/LayoutComponent.java
index 10cbd4ca2a50..7e2a4ccec222 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/LayoutComponent.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/LayoutComponent.java
@@ -75,6 +75,7 @@ public class LayoutComponent extends Component {
protected ArrayList<Component> mChildrenComponents = new ArrayList<>(); // members are not null
protected boolean mChildrenHaveZIndex = false;
+ private CanvasOperations mDrawContentOperations;
public LayoutComponent(
@Nullable Component parent,
@@ -138,6 +139,7 @@ public class LayoutComponent extends Component {
mChildrenComponents.clear();
LayoutComponentContent content = (LayoutComponentContent) op;
content.getComponents(mChildrenComponents);
+ mDrawContentOperations = content.getCanvasOperations(this);
if (USE_IMAGE_TEMP_FIX) {
if (mChildrenComponents.isEmpty() && !mContent.mList.isEmpty()) {
CanvasContent canvasContent =
@@ -315,6 +317,31 @@ public class LayoutComponent extends Component {
}
@Override
+ public void paint(@NonNull PaintContext context) {
+ if (mDrawContentOperations != null) {
+ context.save();
+ context.translate(mX, mY);
+ mDrawContentOperations.paint(context);
+ context.restore();
+ return;
+ }
+ super.paint(context);
+ }
+
+ /**
+ * Paint the component content. Used by the DrawContent operation. (back out mX/mY -- TODO:
+ * refactor paintingComponent instead, to not include mX/mY etc.)
+ *
+ * @param context painting context
+ */
+ public void drawContent(@NonNull PaintContext context) {
+ context.save();
+ context.translate(-mX, -mY);
+ paintingComponent(context);
+ context.restore();
+ }
+
+ @Override
public void paintingComponent(@NonNull PaintContext context) {
Component prev = context.getContext().mLastComponent;
RemoteContext remoteContext = context.getContext();
@@ -514,4 +541,11 @@ public class LayoutComponent extends Component {
return null;
}
+
+ @Override
+ public void registerVariables(RemoteContext context) {
+ if (mDrawContentOperations != null) {
+ mDrawContentOperations.registerListening(context);
+ }
+ }
}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/DrawContentOperation.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/DrawContentOperation.java
new file mode 100644
index 000000000000..d7abdbae4962
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/DrawContentOperation.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.internal.widget.remotecompose.core.operations.layout.modifiers;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+
+import com.android.internal.widget.remotecompose.core.Operation;
+import com.android.internal.widget.remotecompose.core.Operations;
+import com.android.internal.widget.remotecompose.core.RemoteContext;
+import com.android.internal.widget.remotecompose.core.VariableSupport;
+import com.android.internal.widget.remotecompose.core.WireBuffer;
+import com.android.internal.widget.remotecompose.core.documentation.DocumentationBuilder;
+import com.android.internal.widget.remotecompose.core.operations.layout.Component;
+import com.android.internal.widget.remotecompose.core.operations.layout.DecoratorComponent;
+import com.android.internal.widget.remotecompose.core.operations.layout.LayoutComponent;
+import com.android.internal.widget.remotecompose.core.operations.utilities.StringSerializer;
+import com.android.internal.widget.remotecompose.core.serialize.MapSerializer;
+import com.android.internal.widget.remotecompose.core.serialize.SerializeTags;
+
+import java.util.List;
+
+/** Represent a drawing of a component */
+public class DrawContentOperation extends Operation
+ implements ModifierOperation, VariableSupport, DecoratorComponent {
+ private static final int OP_CODE = Operations.MODIFIER_DRAW_CONTENT;
+
+ private LayoutComponent mParent;
+
+ public DrawContentOperation() {}
+
+ @NonNull
+ @Override
+ public String toString() {
+ return "DrawContentOperation()";
+ }
+
+ /**
+ * Returns the serialized name for this operation
+ *
+ * @return the serialized name
+ */
+ @NonNull
+ public String serializedName() {
+ return "DRAW_CONTENT";
+ }
+
+ @Override
+ public void serializeToString(int indent, @NonNull StringSerializer serializer) {
+ serializer.append(indent, serializedName());
+ }
+
+ @Override
+ public void apply(@NonNull RemoteContext context) {}
+
+ @NonNull
+ @Override
+ public String deepToString(@NonNull String indent) {
+ return (indent != null ? indent : "") + toString();
+ }
+
+ @Override
+ public void write(@NonNull WireBuffer buffer) {}
+
+ /**
+ * Write the operation to the buffer
+ *
+ * @param buffer a WireBuffer
+ */
+ public static void apply(@NonNull WireBuffer buffer) {
+ buffer.start(OP_CODE);
+ }
+
+ /**
+ * Read this operation and add it to the list of operations
+ *
+ * @param buffer the buffer to read
+ * @param operations the list of operations that will be added to
+ */
+ public static void read(@NonNull WireBuffer buffer, @NonNull List<Operation> operations) {
+ operations.add(new DrawContentOperation());
+ }
+
+ /**
+ * Populate the documentation with a description of this operation
+ *
+ * @param doc to append the description to.
+ */
+ public static void documentation(@NonNull DocumentationBuilder doc) {
+ doc.operation("Layout Operations", OP_CODE, "ComponentVisibility")
+ .description("This operation represents a draw of a component");
+ }
+
+ @Override
+ public void registerListening(@NonNull RemoteContext context) {}
+
+ @Override
+ public void updateVariables(@NonNull RemoteContext context) {}
+
+ public void setParent(@Nullable LayoutComponent parent) {
+ mParent = parent;
+ }
+
+ @Override
+ public void layout(
+ @NonNull RemoteContext context, Component component, float width, float height) {}
+
+ @Override
+ public void serialize(MapSerializer serializer) {
+ serializer.addTags(SerializeTags.MODIFIER).add("type", "DrawContentOperation");
+ }
+}
diff --git a/core/jni/android_content_res_ApkAssets.cpp b/core/jni/android_content_res_ApkAssets.cpp
index 1e7bfe32ba79..66c65d0ac1aa 100644
--- a/core/jni/android_content_res_ApkAssets.cpp
+++ b/core/jni/android_content_res_ApkAssets.cpp
@@ -111,9 +111,8 @@ 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 (!assets_provider) ? EmptyAssetsProvider::Create()
- : std::unique_ptr<AssetsProvider>(new LoaderAssetsProvider(
- env, assets_provider));
+ return std::unique_ptr<AssetsProvider>{
+ assets_provider ? new LoaderAssetsProvider(env, assets_provider) : nullptr};
}
bool ForEachFile(const std::string& /* root_path */,
@@ -129,8 +128,8 @@ class LoaderAssetsProvider : public AssetsProvider {
return debug_name_;
}
- bool IsUpToDate() const override {
- return true;
+ UpToDate IsUpToDate() const override {
+ return UpToDate::Always;
}
~LoaderAssetsProvider() override {
@@ -212,7 +211,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(), str.size());
+ debug_name_ = std::string(str.c_str());
}
// The global reference to the AssetsProvider
@@ -233,9 +232,9 @@ static jlong NativeLoad(JNIEnv* env, jclass /*clazz*/, const format_type_t forma
AssetManager2::ApkAssetsPtr apk_assets;
switch (format) {
case FORMAT_APK: {
- auto assets = MultiAssetsProvider::Create(std::move(loader_assets),
- ZipAssetsProvider::Create(path.c_str(),
- property_flags));
+ auto assets = AssetsProvider::CreateWithOverride(ZipAssetsProvider::Create(path.c_str(),
+ property_flags),
+ std::move(loader_assets));
apk_assets = ApkAssets::Load(std::move(assets), property_flags);
break;
}
@@ -243,15 +242,17 @@ 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()),
- std::move(loader_assets),
- property_flags);
- break;
+ apk_assets =
+ ApkAssets::LoadTable(AssetsProvider::CreateAssetFromFile(path.c_str()),
+ AssetsProvider::CreateFromNullable(std::move(loader_assets)),
+ property_flags);
+ break;
case FORMAT_DIRECTORY: {
- auto assets = MultiAssetsProvider::Create(std::move(loader_assets),
- DirectoryAssetsProvider::Create(path.c_str()));
- apk_assets = ApkAssets::Load(std::move(assets), property_flags);
- break;
+ auto assets =
+ AssetsProvider::CreateWithOverride(DirectoryAssetsProvider::Create(path.c_str()),
+ std::move(loader_assets));
+ apk_assets = ApkAssets::Load(std::move(assets), property_flags);
+ break;
}
default:
const std::string error_msg = base::StringPrintf("Unsupported format type %d", format);
@@ -308,18 +309,21 @@ static jlong NativeLoadFromFd(JNIEnv* env, jclass /*clazz*/, const format_type_t
switch (format) {
case FORMAT_APK: {
auto assets =
- MultiAssetsProvider::Create(std::move(loader_assets),
- ZipAssetsProvider::Create(std::move(dup_fd),
- friendly_name_utf8.c_str(),
- property_flags));
+ AssetsProvider::CreateWithOverride(ZipAssetsProvider::Create(std::move(dup_fd),
+ friendly_name_utf8
+ .c_str(),
+ property_flags),
+ std::move(loader_assets));
apk_assets = ApkAssets::Load(std::move(assets), property_flags);
break;
}
case FORMAT_ARSC:
- apk_assets = ApkAssets::LoadTable(
- AssetsProvider::CreateAssetFromFd(std::move(dup_fd), nullptr /* path */),
- std::move(loader_assets), property_flags);
- break;
+ apk_assets =
+ ApkAssets::LoadTable(AssetsProvider::CreateAssetFromFd(std::move(dup_fd),
+ nullptr /* path */),
+ AssetsProvider::CreateFromNullable(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());
@@ -375,23 +379,28 @@ static jlong NativeLoadFromFdOffset(JNIEnv* env, jclass /*clazz*/, const format_
switch (format) {
case FORMAT_APK: {
auto assets =
- MultiAssetsProvider::Create(std::move(loader_assets),
- ZipAssetsProvider::Create(std::move(dup_fd),
- friendly_name_utf8.c_str(),
- property_flags,
- static_cast<off64_t>(offset),
- static_cast<off64_t>(
- length)));
+ AssetsProvider::CreateWithOverride(ZipAssetsProvider::Create(std::move(dup_fd),
+ friendly_name_utf8
+ .c_str(),
+ property_flags,
+ static_cast<off64_t>(
+ offset),
+ static_cast<off64_t>(
+ length)),
+ std::move(loader_assets));
apk_assets = ApkAssets::Load(std::move(assets), property_flags);
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)),
- 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)),
+ AssetsProvider::CreateFromNullable(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());
@@ -408,13 +417,16 @@ 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(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(AssetsProvider::CreateFromNullable(
+ 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));
}
@@ -443,10 +455,10 @@ static jlong NativeGetStringBlock(JNIEnv* /*env*/, jclass /*clazz*/, jlong ptr)
return reinterpret_cast<jlong>(apk_assets->GetLoadedArsc()->GetStringPool());
}
-static jboolean NativeIsUpToDate(CRITICAL_JNI_PARAMS_COMMA jlong ptr) {
+static jint NativeIsUpToDate(CRITICAL_JNI_PARAMS_COMMA jlong ptr) {
auto scoped_apk_assets = ScopedLock(ApkAssetsFromLong(ptr));
auto apk_assets = scoped_apk_assets->get();
- return apk_assets->IsUpToDate() ? JNI_TRUE : JNI_FALSE;
+ return (jint)apk_assets->IsUpToDate();
}
static jlong NativeOpenXml(JNIEnv* env, jclass /*clazz*/, jlong ptr, jstring file_name) {
@@ -558,7 +570,7 @@ static const JNINativeMethod gApkAssetsMethods[] = {
{"nativeGetDebugName", "(J)Ljava/lang/String;", (void*)NativeGetDebugName},
{"nativeGetStringBlock", "(J)J", (void*)NativeGetStringBlock},
// @CriticalNative
- {"nativeIsUpToDate", "(J)Z", (void*)NativeIsUpToDate},
+ {"nativeIsUpToDate", "(J)I", (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_media_AudioSystem.cpp b/core/jni/android_media_AudioSystem.cpp
index 2ba6bc4912c3..b679688959b1 100644
--- a/core/jni/android_media_AudioSystem.cpp
+++ b/core/jni/android_media_AudioSystem.cpp
@@ -664,14 +664,16 @@ static void android_media_AudioSystem_vol_range_init_req_callback()
static jint android_media_AudioSystem_setDeviceConnectionState(JNIEnv *env, jobject thiz,
jint state, jobject jParcel,
- jint codec) {
+ jint codec, jboolean deviceSwitch) {
int status;
if (Parcel *parcel = parcelForJavaObject(env, jParcel); parcel != nullptr) {
android::media::audio::common::AudioPort port{};
if (status_t statusOfParcel = port.readFromParcel(parcel); statusOfParcel == OK) {
- status = check_AudioSystem_Command(
- AudioSystem::setDeviceConnectionState(static_cast<audio_policy_dev_state_t>(state),
- port, static_cast<audio_format_t>(codec)));
+ status = check_AudioSystem_Command(
+ AudioSystem::setDeviceConnectionState(static_cast<audio_policy_dev_state_t>(
+ state),
+ port, static_cast<audio_format_t>(codec),
+ deviceSwitch));
} else {
ALOGE("Failed to read from parcel: %s", statusToString(statusOfParcel).c_str());
status = kAudioStatusError;
@@ -3457,7 +3459,7 @@ static const JNINativeMethod gMethods[] = {
MAKE_AUDIO_SYSTEM_METHOD(newAudioSessionId),
MAKE_AUDIO_SYSTEM_METHOD(newAudioPlayerId),
MAKE_AUDIO_SYSTEM_METHOD(newAudioRecorderId),
- MAKE_JNI_NATIVE_METHOD("setDeviceConnectionState", "(ILandroid/os/Parcel;I)I",
+ MAKE_JNI_NATIVE_METHOD("setDeviceConnectionState", "(ILandroid/os/Parcel;IZ)I",
android_media_AudioSystem_setDeviceConnectionState),
MAKE_AUDIO_SYSTEM_METHOD(getDeviceConnectionState),
MAKE_AUDIO_SYSTEM_METHOD(handleDeviceConfigChange),
diff --git a/core/jni/com_android_internal_os_ZygoteCommandBuffer.cpp b/core/jni/com_android_internal_os_ZygoteCommandBuffer.cpp
index e0cc055a62a6..c4259f41e380 100644
--- a/core/jni/com_android_internal_os_ZygoteCommandBuffer.cpp
+++ b/core/jni/com_android_internal_os_ZygoteCommandBuffer.cpp
@@ -266,16 +266,24 @@ class NativeCommandBuffer {
}
// Picky version of atoi(). No sign or unexpected characters allowed. Return -1 on failure.
static int digitsVal(char* start, char* end) {
+ constexpr int vmax = std::numeric_limits<int>::max();
int result = 0;
- if (end - start > 6) {
- return -1;
- }
for (char* dp = start; dp < end; ++dp) {
if (*dp < '0' || *dp > '9') {
- ALOGW("Argument failed integer format check");
+ ALOGW("Argument contains non-integer characters");
+ return -1;
+ }
+ int digit = *dp - '0';
+ if (result > vmax / 10) {
+ ALOGW("Argument exceeds int limit");
+ return -1;
+ }
+ result *= 10;
+ if (result > vmax - digit) {
+ ALOGW("Argument exceeds int limit");
return -1;
}
- result = 10 * result + (*dp - '0');
+ result += digit;
}
return result;
}
diff --git a/core/res/Android.bp b/core/res/Android.bp
index be4fb8bdecfb..1199d77d04c6 100644
--- a/core/res/Android.bp
+++ b/core/res/Android.bp
@@ -174,6 +174,7 @@ android_app {
"android.media.tv.flags-aconfig",
"android.security.flags-aconfig",
"device_policy_aconfig_flags",
+ "android.xr.flags-aconfig",
"com.android.hardware.input.input-aconfig",
"aconfig_trade_in_mode_flags",
"art-aconfig-flags",
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 51049889ecd6..78526ad4a06b 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -5230,6 +5230,182 @@
android:protectionLevel="signature|privileged" />
<!-- ==================================== -->
+ <!-- Permissions for XR perception data -->
+ <!-- ==================================== -->
+ <eat-comment />
+
+ <!-- Used for permissions that are associated with accessing XR
+ tracked information about the person using the device and the
+ environment around them.
+
+ @FlaggedApi(android.xr.Flags.FLAG_XR_MANIFEST_ENTRIES) -->
+ <permission-group android:name="android.permission-group.XR_TRACKING"
+ android:label="@string/permgrouplab_xr_tracking"
+ android:description="@string/permgroupdesc_xr_tracking"
+ android:priority="100"
+ android:featureFlag="android.xr.xr_manifest_entries" />
+
+ <!-- Allows an application to get approximate eye gaze.
+
+ <p>Protection level: dangerous
+
+ @FlaggedApi(android.xr.Flags.FLAG_XR_MANIFEST_ENTRIES) -->
+ <permission android:name="android.permission.EYE_TRACKING_COARSE"
+ android:protectionLevel="dangerous"
+ android:permissionGroup="android.permission-group.UNDEFINED"
+ android:label="@string/permlab_eye_tracking_coarse"
+ android:description="@string/permdesc_eye_tracking_coarse"
+ android:featureFlag="android.xr.xr_manifest_entries" />
+
+ <!-- Allows an application to get face tracking data.
+
+ <p>Protection level: dangerous
+
+ @FlaggedApi(android.xr.Flags.FLAG_XR_MANIFEST_ENTRIES) -->
+ <permission android:name="android.permission.FACE_TRACKING"
+ android:protectionLevel="dangerous"
+ android:permissionGroup="android.permission-group.UNDEFINED"
+ android:label="@string/permlab_face_tracking"
+ android:description="@string/permdesc_face_tracking"
+ android:featureFlag="android.xr.xr_manifest_entries" />
+
+ <!-- Allows an application to get hand tracking data.
+
+ <p>Protection level: dangerous
+
+ @FlaggedApi(android.xr.Flags.FLAG_XR_MANIFEST_ENTRIES) -->
+ <permission android:name="android.permission.HAND_TRACKING"
+ android:protectionLevel="dangerous"
+ android:permissionGroup="android.permission-group.UNDEFINED"
+ android:label="@string/permlab_hand_tracking"
+ android:description="@string/permdesc_hand_tracking"
+ android:featureFlag="android.xr.xr_manifest_entries" />
+
+ <!-- Allows an application to get data derived by sensing the
+ user's environment.
+
+ <p>Protection level: dangerous
+
+ @FlaggedApi(android.xr.Flags.FLAG_XR_MANIFEST_ENTRIES) -->
+ <permission android:name="android.permission.SCENE_UNDERSTANDING_COARSE"
+ android:protectionLevel="dangerous"
+ android:permissionGroup="android.permission-group.UNDEFINED"
+ android:description="@string/permdesc_scene_understanding_coarse"
+ android:label="@string/permlab_scene_understanding_coarse"
+ android:featureFlag="android.xr.xr_manifest_entries" />
+
+ <!-- Used for permissions that are associated with accessing
+ particularly sensitive XR tracking data.
+
+ @FlaggedApi(android.xr.Flags.FLAG_XR_MANIFEST_ENTRIES) -->
+ <permission-group android:name="android.permission-group.XR_TRACKING_SENSITIVE"
+ android:label="@string/permgrouplab_xr_tracking_sensitive"
+ android:description="@string/permgroupdesc_xr_tracking_sensitive"
+ android:priority="100"
+ android:featureFlag="android.xr.xr_manifest_entries" />
+
+ <!-- Allows an application to get precise eye gaze data.
+
+ <p>Protection level: dangerous
+
+ @FlaggedApi(android.xr.Flags.FLAG_XR_MANIFEST_ENTRIES) -->
+ <permission android:name="android.permission.EYE_TRACKING_FINE"
+ android:protectionLevel="dangerous"
+ android:permissionGroup="android.permission-group.UNDEFINED"
+ android:label="@string/permlab_eye_tracking_fine"
+ android:description="@string/permdesc_eye_tracking_fine"
+ android:featureFlag="android.xr.xr_manifest_entries" />
+
+ <!-- Allows an application to get head tracking data. Unmanaged
+ activities (OpenXR activities with the manifest property
+ "android.window.PROPERTY_XR_ACTIVITY_START_MODE" set to
+ "XR_ACTIVITY_START_MODE_FULL_SPACE_UNMANAGED") do not require
+ this permission to get head tracking data.
+
+ {@see https://developer.android.com/develop/xr/get-started#property_activity_xr_start_mode_property}
+
+ <p>Protection level: dangerous
+
+ @FlaggedApi(android.xr.Flags.FLAG_XR_MANIFEST_ENTRIES) -->
+ <permission android:name="android.permission.HEAD_TRACKING"
+ android:protectionLevel="dangerous"
+ android:permissionGroup="android.permission-group.UNDEFINED"
+ android:label="@string/permlab_head_tracking"
+ android:description="@string/permdesc_head_tracking"
+ android:featureFlag="android.xr.xr_manifest_entries" />
+
+ <!-- Allows an application to get highly precise data derived by sensing the
+ user's environment, such as a depth map.
+
+ <p>Protection level: dangerous
+
+ @FlaggedApi(android.xr.Flags.FLAG_XR_MANIFEST_ENTRIES) -->
+ <permission android:name="android.permission.SCENE_UNDERSTANDING_FINE"
+ android:protectionLevel="dangerous"
+ android:permissionGroup="android.permission-group.UNDEFINED"
+ android:description="@string/permdesc_scene_understanding_fine"
+ android:label="@string/permlab_scene_understanding_fine"
+ android:featureFlag="android.xr.xr_manifest_entries" />
+
+ <!-- Allows an application to trigger Eye Calibration, which
+ calibrates for IPD (inter-pupillary distance) adjustment and
+ eye tracking.
+
+ <p>Protection level: signature|privileged
+
+ @SystemApi
+ @FlaggedApi(android.xr.Flags.FLAG_XR_MANIFEST_ENTRIES)
+ @hide -->
+ <permission android:name="android.permission.EYE_CALIBRATION"
+ android:protectionLevel="signature|privileged"
+ android:featureFlag="android.xr.xr_manifest_entries" />
+
+ <!-- Allows an application to trigger Face Tracking Calibration.
+
+ <p>Protection level: signature|privileged
+
+ @SystemApi
+ @FlaggedApi(android.xr.Flags.FLAG_XR_MANIFEST_ENTRIES)
+ @hide -->
+ <permission android:name="android.permission.FACE_TRACKING_CALIBRATION"
+ android:protectionLevel="signature|privileged"
+ android:featureFlag="android.xr.xr_manifest_entries" />
+
+ <!-- Allows an application to import an anchor created and
+ exported by another application.
+
+ <p>Protection level: signature|privileged
+
+ @SystemApi
+ @FlaggedApi(android.xr.Flags.FLAG_XR_MANIFEST_ENTRIES)
+ @hide -->
+ <permission android:name="android.permission.IMPORT_XR_ANCHOR"
+ android:protectionLevel="signature|privileged"
+ android:featureFlag="android.xr.xr_manifest_entries" />
+
+ <!-- Allows an application to access XR tracking data while in the
+ background. Without this permission, XR tracking data such as
+ head tracking, hand tracking, eye tracking, or face tracking
+ is only available to an activity it is in the
+ foreground. With this permission, such data is also available
+ to services and to activities that are in the background.
+
+ <p>This permission must be granted in addition to the
+ corresponding permission such as {@link #HEAD_TRACKING} or
+ {@link #FACE_TRACKING} for the data being accessed.
+
+ <p>Protection level: normal|appop
+
+ @SystemApi
+ @FlaggedApi(android.xr.Flags.FLAG_XR_MANIFEST_ENTRIES)
+ @hide -->
+ <permission android:name="android.permission.XR_TRACKING_IN_BACKGROUND"
+ android:protectionLevel="normal|appop"
+ android:description="@string/permdesc_xr_tracking_in_background"
+ android:label="@string/permlab_xr_tracking_in_background"
+ android:featureFlag="android.xr.xr_manifest_entries" />
+
+ <!-- ==================================== -->
<!-- Private permissions -->
<!-- ==================================== -->
<eat-comment />
@@ -7688,12 +7864,12 @@
<permission android:name="android.permission.ACCESS_SMARTSPACE"
android:protectionLevel="signature|privileged|development" />
- <!-- @SystemApi Allows an application to start a contextual search.
- @FlaggedApi("android.app.contextualsearch.flags.enable_service")
- @hide <p>Not for use by third-party applications.</p> -->
+ <!-- @SystemApi Allows a system application to start a contextual search.
+ Other applications can start a contextual search only if they have a
+ foreground activity.
+ @hide <p>Not for use by third-party applications.</p> -->
<permission android:name="android.permission.ACCESS_CONTEXTUAL_SEARCH"
- android:protectionLevel="signature|privileged"
- android:featureFlag="android.app.contextualsearch.flags.enable_service"/>
+ android:protectionLevel="signature|privileged" />
<!-- @SystemApi Allows an application to manage the wallpaper effects
generation service.
diff --git a/core/res/res/drawable-w192dp/loader_horizontal_watch.xml b/core/res/res/drawable-w192dp/loader_horizontal_watch.xml
new file mode 100644
index 000000000000..18cea6e0d87d
--- /dev/null
+++ b/core/res/res/drawable-w192dp/loader_horizontal_watch.xml
@@ -0,0 +1,97 @@
+<!--
+ ~ 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="15dp" android:width="67dp" android:viewportHeight="15" android:viewportWidth="67">
+ <group android:name="_R_G">
+ <group android:name="_R_G_L_1_G" android:translateX="33.5" android:translateY="7.5">
+ <path android:name="_R_G_L_1_G_D_0_P_0" android:fillColor="#000000" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M33.5 -7.5 C33.5,-7.5 33.5,7.5 33.5,7.5 C33.5,7.5 -33.5,7.5 -33.5,7.5 C-33.5,7.5 -33.5,-7.5 -33.5,-7.5 C-33.5,-7.5 33.5,-7.5 33.5,-7.5c "/>
+ </group>
+ <group android:name="_R_G_L_0_G" android:translateX="-296.5" android:translateY="-62.5" android:pivotX="330" android:pivotY="70" android:scaleX="0.1" android:scaleY="0.1">
+ <group android:name="_R_G_L_0_G_L_6_G" android:translateX="-224.84700000000004" android:translateY="-321.948" android:pivotX="555.09" android:pivotY="-329" android:rotation="28.9" android:scaleY="0">
+ <path android:name="_R_G_L_0_G_L_6_G_D_0_P_0" android:fillColor="#303030" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M194.88 359 C190,359 185.05,357.81 180.48,355.3 C59.86,289.14 -41.55,191.9 -112.79,74.11 C-186.14,-47.16 -224.91,-186.55 -224.91,-329 C-224.91,-345.57 -211.48,-359 -194.91,-359 C-178.34,-359 -164.91,-345.57 -164.91,-329 C-164.91,-197.5 -129.13,-68.84 -61.45,43.06 C4.33,151.82 97.97,241.6 209.33,302.69 C223.86,310.66 229.18,328.9 221.21,343.42 C215.75,353.37 205.48,359 194.88,359c "/>
+ </group>
+ <group android:name="_R_G_L_0_G_L_5_G" android:translateX="744.323" android:translateY="-277.96299999999997" android:pivotX="-414.08" android:pivotY="-372.985" android:rotation="28.9">
+ <path android:name="_R_G_L_0_G_L_5_G_D_0_P_0" android:fillColor="#303030" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M-335.95 402.99 C-351.13,402.99 -364.16,391.5 -365.76,376.07 C-367.46,359.59 -355.49,344.85 -339.01,343.14 C-162.93,324.91 -0.15,242.33 119.34,110.62 C239.66,-22.01 305.92,-193.76 305.92,-372.98 C305.92,-389.55 319.35,-402.98 335.92,-402.98 C352.49,-402.98 365.92,-389.55 365.92,-372.98 C365.92,-178.82 294.13,7.24 163.78,150.93 C34.34,293.61 -142.03,383.07 -332.83,402.82 C-333.88,402.93 -334.92,402.99 -335.95,402.99c "/>
+ </group>
+ <group android:name="_R_G_L_0_G_L_2_G" android:translateX="185.385" android:translateY="70.09100000000001" android:pivotX="144.858" android:pivotY="-721.039" android:rotation="28.9" android:scaleY="0">
+ <path android:name="_R_G_L_0_G_L_2_G_D_0_P_0" android:fillColor="#ffffff" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M144.62 58.96 C144.61,58.96 144.61,58.96 144.6,58.96 C40.39,58.93 -60.82,38.66 -156.19,-1.28 C-171.48,-7.68 -178.68,-25.26 -172.28,-40.54 C-165.88,-55.82 -148.3,-63.02 -133.02,-56.62 C-45.02,-19.77 48.4,-1.07 144.63,-1.04 C161.19,-1.03 174.62,12.4 174.62,28.97 C174.61,45.53 161.18,58.96 144.62,58.96c "/>
+ </group>
+ <group android:name="_R_G_L_0_G_L_0_G" android:translateX="330" android:translateY="70">
+ <path android:name="_R_G_L_0_G_L_0_G_D_0_P_0" android:fillColor="#000000" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M-660 -313 C-660,-313 -660,313 -660,313 C-660,313 660,313 660,313 C660,313 660,-313 660,-313 C660,-313 -660,-313 -660,-313c M300.74 -1.16 C205.46,38.62 103.22,59.09 -0.03,59.05 C-103.28,59.01 -205.51,38.48 -300.76,-1.37 C-316.05,-7.76 -323.26,-25.34 -316.86,-40.62 C-310.47,-55.91 -292.9,-63.12 -277.61,-56.72 C-189.68,-19.94 -95.32,-0.98 -0.01,-0.95 C95.3,-0.92 189.67,-19.81 277.63,-56.53 C292.92,-62.91 310.49,-55.69 316.87,-40.4 C323.25,-25.11 316.03,-7.54 300.74,-1.16c "/>
+ </group>
+ </group>
+ </group>
+ <group android:name="time_group"/>
+ </vector>
+ </aapt:attr>
+ <target android:name="_R_G_L_0_G_L_6_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator android:repeatCount="infinite" android:propertyName="rotation" android:duration="983" android:startOffset="0" android:valueFrom="28.9" android:valueTo="-51.4" android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.402,0.135 0.202,0.848 1.0,1.0"/>
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_0_G_L_5_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator android:repeatCount="infinite" android:propertyName="rotation" android:duration="983" android:startOffset="0" android:valueFrom="28.9" android:valueTo="-51.4" android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.402,0.135 0.202,0.848 1.0,1.0"/>
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_0_G_L_5_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator android:repeatCount="infinite" android:propertyName="scaleY" android:duration="0" android:startOffset="0" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_0_G_L_2_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator android:repeatCount="infinite" android:propertyName="rotation" android:duration="983" android:startOffset="0" android:valueFrom="28.9" android:valueTo="-51.4" android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.402,0.135 0.202,0.848 1.0,1.0"/>
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_0_G_L_2_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator android:repeatCount="infinite" android:propertyName="scaleY" android:duration="0" android:startOffset="133" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="time_group">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator android:repeatCount="infinite" android:propertyName="translateX" android:duration="1000" android:startOffset="0" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/>
+ </set>
+ </aapt:attr>
+ </target>
+</animated-vector>
+
diff --git a/core/res/res/drawable-w204dp/loader_horizontal_watch.xml b/core/res/res/drawable-w204dp/loader_horizontal_watch.xml
new file mode 100644
index 000000000000..fbc6eab320eb
--- /dev/null
+++ b/core/res/res/drawable-w204dp/loader_horizontal_watch.xml
@@ -0,0 +1,104 @@
+<!--
+ ~ 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="15dp" android:width="70dp" android:viewportHeight="15" android:viewportWidth="70">
+ <group android:name="_R_G">
+ <group android:name="_R_G_L_1_G" android:translateX="35" android:translateY="7.5">
+ <path android:name="_R_G_L_1_G_D_0_P_0" android:fillColor="#000000" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M35 -7.5 C35,-7.5 35,7.5 35,7.5 C35,7.5 -35,7.5 -35,7.5 C-35,7.5 -35,-7.5 -35,-7.5 C-35,-7.5 35,-7.5 35,-7.5c "/>
+ </group>
+ <group android:name="_R_G_L_0_G" android:translateX="-310" android:translateY="-64" android:pivotX="345" android:pivotY="71.5" android:scaleX="0.1" android:scaleY="0.1">
+ <group android:name="_R_G_L_0_G_L_6_G" android:translateX="-239.44799999999998" android:translateY="-341.45" android:pivotX="584.448" android:pivotY="-346.55" android:rotation="28.8" android:scaleY="0">
+ <path android:name="_R_G_L_0_G_L_6_G_D_0_P_0" android:fillColor="#303030" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M205.28 376.55 C200.4,376.55 195.46,375.36 190.88,372.85 C64.08,303.29 -42.54,201.07 -117.44,77.24 C-194.55,-50.25 -235.31,-196.79 -235.31,-346.55 C-235.31,-363.12 -221.88,-376.55 -205.31,-376.55 C-188.74,-376.55 -175.31,-363.12 -175.31,-346.55 C-175.31,-207.74 -137.54,-71.93 -66.1,46.19 C3.34,160.99 102.18,255.76 219.73,320.24 C234.26,328.21 239.58,346.45 231.61,360.97 C226.15,370.92 215.88,376.55 205.28,376.55c "/>
+ </group>
+ <group android:name="_R_G_L_0_G_L_5_G" android:translateX="781.413" android:translateY="-295.124" android:pivotX="-436.413" android:pivotY="-392.876" android:rotation="28.8">
+ <path android:name="_R_G_L_0_G_L_5_G_D_0_P_0" android:fillColor="#303030" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M-353.86 422.88 C-369.04,422.88 -382.07,411.4 -383.67,395.97 C-385.37,379.49 -373.4,364.74 -356.92,363.03 C-171.06,343.79 0.76,256.62 126.89,117.59 C253.89,-22.41 323.83,-203.7 323.83,-392.88 C323.83,-409.44 337.26,-422.88 353.83,-422.88 C370.4,-422.88 383.83,-409.44 383.83,-392.88 C383.83,-188.76 308.36,6.84 171.32,157.9 C35.25,307.89 -150.15,401.94 -350.74,422.72 C-351.79,422.82 -352.83,422.88 -353.86,422.88c "/>
+ </group>
+ <group android:name="_R_G_L_0_G_L_2_G" android:translateX="192.671" android:translateY="71.49599999999998" android:pivotX="152.329" android:pivotY="-759.496" android:rotation="28.8" android:scaleY="0">
+ <path android:name="_R_G_L_0_G_L_2_G_D_0_P_0" android:fillColor="#ffffff" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M152.33 60.5 C152.33,60.5 152.32,60.5 152.32,60.5 C42.76,60.47 -63.64,39.16 -163.91,-2.82 C-179.19,-9.22 -186.39,-26.8 -179.99,-42.08 C-173.59,-57.36 -156.02,-64.57 -140.73,-58.16 C-47.84,-19.27 50.77,0.47 152.34,0.5 C168.91,0.51 182.33,13.94 182.33,30.51 C182.32,47.08 168.89,60.5 152.33,60.5c "/>
+ </group>
+ <group android:name="_R_G_L_0_G_L_0_G" android:translateX="345" android:translateY="71.5">
+ <path android:name="_R_G_L_0_G_L_0_G_D_0_P_0" android:fillColor="#000000" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M-579 -259.5 C-579,-259.5 -579,259.5 -579,259.5 C-579,259.5 579,259.5 579,259.5 C579,259.5 579,-259.5 579,-259.5 C579,-259.5 -579,-259.5 -579,-259.5c M316.17 -2.8 C216,39.02 108.52,60.54 -0.03,60.5 C-108.58,60.46 -216.04,38.87 -316.18,-3.02 C-331.47,-9.41 -338.68,-26.99 -332.28,-42.27 C-325.89,-57.56 -308.32,-64.76 -293.03,-58.37 C-200.22,-19.55 -100.61,0.46 -0.01,0.5 C100.6,0.54 200.22,-19.41 293.06,-58.17 C308.35,-64.55 325.92,-57.33 332.3,-42.04 C338.68,-26.75 331.46,-9.18 316.17,-2.8c "/>
+ </group>
+ </group>
+ </group>
+ <group android:name="time_group"/>
+ </vector>
+ </aapt:attr>
+ <target android:name="_R_G_L_0_G_L_6_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator android:repeatCount="infinite" android:propertyName="rotation" android:duration="983" android:startOffset="0" android:valueFrom="28.8" android:valueTo="-51.4" android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.402,0.136 0.202,0.847 1.0,1.0"/>
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_0_G_L_6_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator android:repeatCount="infinite" android:propertyName="scaleY" android:duration="0" android:startOffset="333" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_0_G_L_5_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator android:repeatCount="infinite" android:propertyName="rotation" android:duration="983" android:startOffset="0" android:valueFrom="28.8" android:valueTo="-51.4" android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.402,0.136 0.202,0.847 1.0,1.0"/>
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_0_G_L_5_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator android:repeatCount="infinite" android:propertyName="scaleY" android:duration="0" android:startOffset="0" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_0_G_L_2_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator android:repeatCount="infinite" android:propertyName="rotation" android:duration="983" android:startOffset="0" android:valueFrom="28.8" android:valueTo="-51.4" android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.402,0.136 0.202,0.847 1.0,1.0"/>
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_0_G_L_2_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator android:repeatCount="infinite" android:propertyName="scaleY" android:duration="0" android:startOffset="133" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="time_group">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator android:repeatCount="infinite" android:propertyName="translateX" android:duration="1000" android:startOffset="0" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/>
+ </set>
+ </aapt:attr>
+ </target>
+</animated-vector>
+
diff --git a/core/res/res/drawable-w216dp/loader_horizontal_watch.xml b/core/res/res/drawable-w216dp/loader_horizontal_watch.xml
new file mode 100644
index 000000000000..ed4b7ea0ff02
--- /dev/null
+++ b/core/res/res/drawable-w216dp/loader_horizontal_watch.xml
@@ -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.
+ -->
+
+<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="16dp" android:width="74dp" android:viewportHeight="16" android:viewportWidth="74">
+ <group android:name="_R_G">
+ <group android:name="_R_G_L_1_G" android:translateX="37" android:translateY="8">
+ <path android:name="_R_G_L_1_G_D_0_P_0" android:fillColor="#000000" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M37 -8 C37,-8 37,8 37,8 C37,8 -37,8 -37,8 C-37,8 -37,-8 -37,-8 C-37,-8 37,-8 37,-8c "/>
+ </group>
+ <group android:name="_R_G_L_0_G" android:translateX="-328" android:translateY="-65.5" android:pivotX="365" android:pivotY="73.5" android:scaleX="0.1" android:scaleY="0.1">
+ <group android:name="_R_G_L_0_G_L_6_G" android:translateX="-256.447" android:translateY="-365.014" android:pivotX="621.447" android:pivotY="-368.486" android:rotation="28.8" android:scaleY="0">
+ <path android:name="_R_G_L_0_G_L_6_G_D_0_P_0" android:fillColor="#303030" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M218.28 398.49 C213.4,398.49 208.46,397.3 203.88,394.78 C69.34,320.99 -43.78,212.53 -123.25,81.15 C-205.06,-54.11 -248.31,-209.59 -248.31,-368.49 C-248.31,-385.05 -234.88,-398.49 -218.31,-398.49 C-201.74,-398.49 -188.31,-385.05 -188.31,-368.49 C-188.31,-220.54 -148.06,-75.8 -71.91,50.09 C2.1,172.45 107.45,273.45 232.73,342.18 C247.26,350.15 252.58,368.38 244.61,382.91 C239.15,392.86 228.88,398.49 218.28,398.49c "/>
+ </group>
+ <group android:name="_R_G_L_0_G_L_5_G" android:translateX="829.0260000000001" android:translateY="-315.759" android:pivotX="-464.026" android:pivotY="-417.741" android:rotation="28.8">
+ <path android:name="_R_G_L_0_G_L_5_G_D_0_P_0" android:fillColor="#303030" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M-376.25 447.74 C-391.43,447.74 -404.46,436.26 -406.05,420.83 C-407.76,404.35 -395.78,389.61 -379.3,387.9 C-181.22,367.38 1.9,274.48 136.32,126.3 C271.67,-22.9 346.22,-216.12 346.22,-417.74 C346.22,-434.31 359.65,-447.74 376.22,-447.74 C392.79,-447.74 406.22,-434.31 406.22,-417.74 C406.22,-201.18 326.15,6.35 180.76,166.61 C36.39,325.75 -160.31,425.54 -373.12,447.58 C-374.17,447.69 -375.22,447.74 -376.25,447.74c "/>
+ </group>
+ <group android:name="_R_G_L_0_G_L_2_G" android:translateX="203.029" android:translateY="74.06899999999996" android:pivotX="161.971" android:pivotY="-807.569" android:rotation="28.8" android:scaleY="0">
+ <path android:name="_R_G_L_0_G_L_2_G_D_0_P_0" android:fillColor="#ffffff" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M161.97 62.43 C161.97,62.43 161.96,62.43 161.96,62.43 C45.71,62.4 -67.17,39.79 -173.55,-4.75 C-188.83,-11.15 -196.03,-28.72 -189.63,-44.01 C-183.24,-59.29 -165.66,-66.49 -150.38,-60.09 C-51.37,-18.64 53.72,2.4 161.98,2.43 C178.55,2.44 191.98,15.87 191.97,32.44 C191.97,49 178.54,62.43 161.97,62.43c "/>
+ </group>
+ <group android:name="_R_G_L_0_G_L_0_G" android:translateX="365" android:translateY="73.5">
+ <path android:name="_R_G_L_0_G_L_0_G_D_0_P_0" android:fillColor="#000000" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M-609 -244.5 C-609,-244.5 -609,244.5 -609,244.5 C-609,244.5 609,244.5 609,244.5 C609,244.5 609,-244.5 609,-244.5 C609,-244.5 -609,-244.5 -609,-244.5c M335.44 -4.16 C229.17,40.21 115.13,63.04 -0.04,63 C-115.21,62.96 -229.22,40.05 -335.47,-4.39 C-350.76,-10.79 -357.95,-28.36 -351.56,-43.65 C-345.17,-58.93 -327.59,-66.14 -312.31,-59.74 C-213.39,-18.36 -107.24,2.96 -0.02,3 C107.21,3.04 213.38,-18.22 312.33,-59.53 C327.62,-65.91 345.19,-58.69 351.57,-43.4 C357.95,-28.11 350.73,-10.54 335.44,-4.16c "/>
+ </group>
+ </group>
+ </group>
+ <group android:name="time_group"/>
+ </vector>
+ </aapt:attr>
+ <target android:name="_R_G_L_0_G_L_6_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator android:repeatCount="infinite" android:propertyName="rotation" android:duration="983" android:startOffset="0" android:valueFrom="28.8" android:valueTo="-51.3" android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.402,0.136 0.202,0.847 1.0,1.0"/>
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_0_G_L_6_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator android:repeatCount="infinite" android:propertyName="scaleY" android:duration="0" android:startOffset="333" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_0_G_L_5_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator android:repeatCount="infinite" android:propertyName="rotation" android:duration="983" android:startOffset="0" android:valueFrom="28.8" android:valueTo="-51.3" android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.402,0.136 0.202,0.847 1.0,1.0"/>
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_0_G_L_5_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator android:repeatCount="infinite" android:propertyName="scaleY" android:duration="0" android:startOffset="0" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_0_G_L_2_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator android:repeatCount="infinite" android:propertyName="rotation" android:duration="983" android:startOffset="0" android:valueFrom="28.8" android:valueTo="-51.3" android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.402,0.136 0.202,0.847 1.0,1.0"/>
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_0_G_L_2_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator android:repeatCount="infinite" android:propertyName="scaleY" android:duration="0" android:startOffset="133" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="time_group">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator android:repeatCount="infinite" android:propertyName="translateX" android:duration="1000" android:startOffset="0" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/>
+ </set>
+ </aapt:attr>
+ </target>
+</animated-vector>
+
+
diff --git a/core/res/res/drawable-w228dp/loader_horizontal_watch.xml b/core/res/res/drawable-w228dp/loader_horizontal_watch.xml
new file mode 100644
index 000000000000..6b86c634d554
--- /dev/null
+++ b/core/res/res/drawable-w228dp/loader_horizontal_watch.xml
@@ -0,0 +1,103 @@
+<!--
+ ~ 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="14dp" android:width="76dp" android:viewportHeight="14" android:viewportWidth="76">
+ <group android:name="_R_G">
+ <group android:name="_R_G_L_1_G" android:translateX="39" android:translateY="8">
+ <path android:name="_R_G_L_1_G_D_0_P_0" android:fillColor="#000000" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M39 -8 C39,-8 39,8 39,8 C39,8 -39,8 -39,8 C-39,8 -39,-8 -39,-8 C-39,-8 39,-8 39,-8c "/>
+ </group>
+ <group android:name="_R_G_L_0_G" android:translateX="-345" android:translateY="-67" android:pivotX="384" android:pivotY="75" android:scaleX="0.1" android:scaleY="0.1">
+ <group android:name="_R_G_L_0_G_L_6_G" android:translateX="-274.19" android:translateY="-390.077" android:pivotX="658.448" android:pivotY="-390.423" android:rotation="28.7" android:scaleY="0">
+ <path android:name="_R_G_L_0_G_L_6_G_D_0_P_0" android:fillColor="#303030" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M231.28 420.42 C226.4,420.42 221.45,419.23 216.88,416.72 C74.61,338.68 -45.02,224 -129.06,85.06 C-171.54,14.82 -204.38,-60.73 -226.66,-139.5 C-249.65,-220.76 -261.31,-305.18 -261.31,-390.42 C-261.31,-406.99 -247.88,-420.42 -231.31,-420.42 C-214.74,-420.42 -201.31,-406.99 -201.31,-390.42 C-201.31,-310.71 -190.42,-231.78 -168.93,-155.83 C-148.11,-82.23 -117.42,-11.63 -77.72,54 C0.86,183.92 112.71,291.15 245.73,364.12 C260.26,372.08 265.58,390.32 257.61,404.85 C252.15,414.79 241.88,420.42 231.28,420.42c "/>
+ </group>
+ <group android:name="_R_G_L_0_G_L_5_G" android:translateX="875.8979999999999" android:translateY="-337.894" android:pivotX="-491.64" android:pivotY="-442.606" android:rotation="28.7">
+ <path android:name="_R_G_L_0_G_L_5_G_D_0_P_0" android:fillColor="#303030" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M-398.64 472.61 C-413.82,472.61 -426.84,461.13 -428.44,445.7 C-430.15,429.22 -418.17,414.47 -401.69,412.77 C-191.38,390.98 3.04,292.33 145.75,135.01 C289.46,-23.4 368.6,-228.54 368.6,-442.61 C368.6,-459.17 382.04,-472.61 398.6,-472.61 C415.17,-472.61 428.6,-459.17 428.6,-442.61 C428.6,-213.6 343.93,5.85 190.19,175.33 C37.53,343.61 -170.48,449.13 -395.51,472.44 C-396.56,472.55 -397.6,472.61 -398.64,472.61c "/>
+ </group>
+ <group android:name="_R_G_L_0_G_L_2_G" android:translateX="212.64499999999998" android:translateY="75.14200000000005" android:pivotX="171.613" android:pivotY="-855.642" android:rotation="28.7" android:scaleY="0">
+ <path android:name="_R_G_L_0_G_L_2_G_D_0_P_0" android:fillColor="#ffffff" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M171.61 64.36 C171.61,64.36 171.61,64.36 171.61,64.36 C48.68,64.32 -70.7,40.42 -183.19,-6.68 C-198.47,-13.07 -205.68,-30.65 -199.28,-45.93 C-192.88,-61.22 -175.3,-68.42 -160.02,-62.02 C-54.9,-18.01 56.68,4.33 171.62,4.36 C188.19,4.36 201.62,17.8 201.61,34.36 C201.61,50.93 188.18,64.36 171.61,64.36c "/>
+ </group>
+ <group android:name="_R_G_L_0_G_L_0_G" android:translateX="384" android:translateY="75">
+ <path android:name="_R_G_L_0_G_L_0_G_D_0_P_0" android:fillColor="#000000" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M-611 -259 C-611,-259 -611,259 -611,259 C-611,259 611,259 611,259 C611,259 611,-259 611,-259 C611,-259 -611,-259 -611,-259c M354.66 -6.52 C242.36,40.4 121.76,64.54 -0.04,64.5 C-121.84,64.46 -242.44,40.23 -354.74,-6.76 C-370.04,-13.16 -377.24,-30.73 -370.84,-46.02 C-364.44,-61.3 -346.94,-68.51 -331.64,-62.12 C-226.54,-18.18 -113.84,4.46 -0.04,4.5 C113.76,4.54 226.56,-18.02 331.56,-61.89 C346.86,-68.27 364.46,-61.05 370.86,-45.76 C377.26,-30.47 369.96,-12.9 354.66,-6.52c "/>
+ </group>
+ </group>
+ </group>
+ <group android:name="time_group"/>
+ </vector>
+ </aapt:attr>
+ <target android:name="_R_G_L_0_G_L_6_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator android:repeatCount="infinite" android:propertyName="rotation" android:duration="983" android:startOffset="0" android:valueFrom="28.7" android:valueTo="-51.4" android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.402,0.136 0.202,0.847 1.0,1.0"/>
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_0_G_L_6_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator android:repeatCount="infinite" android:propertyName="scaleY" android:duration="0" android:startOffset="333" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_0_G_L_5_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator android:repeatCount="infinite" android:propertyName="rotation" android:duration="983" android:startOffset="0" android:valueFrom="28.7" android:valueTo="-51.4" android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.402,0.136 0.202,0.847 1.0,1.0"/>
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_0_G_L_5_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator android:repeatCount="infinite" android:propertyName="scaleY" android:duration="0" android:startOffset="0" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_0_G_L_2_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator android:repeatCount="infinite" android:propertyName="rotation" android:duration="983" android:startOffset="0" android:valueFrom="28.7" android:valueTo="-51.4" android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.402,0.136 0.202,0.847 1.0,1.0"/>
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_0_G_L_2_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator android:repeatCount="infinite" android:propertyName="scaleY" android:duration="0" android:startOffset="133" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="time_group">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator android:repeatCount="infinite" android:propertyName="translateX" android:duration="1000" android:startOffset="0" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/>
+ </set>
+ </aapt:attr>
+ </target>
+</animated-vector>
diff --git a/core/res/res/drawable-w240dp/loader_horizontal_watch.xml b/core/res/res/drawable-w240dp/loader_horizontal_watch.xml
new file mode 100644
index 000000000000..ad60bbdc420c
--- /dev/null
+++ b/core/res/res/drawable-w240dp/loader_horizontal_watch.xml
@@ -0,0 +1,104 @@
+<!--
+ ~ 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="17dp" android:width="82dp" android:viewportHeight="17" android:viewportWidth="82">
+ <group android:name="_R_G">
+ <group android:name="_R_G_L_1_G" android:translateX="41" android:translateY="8.5">
+ <path android:name="_R_G_L_1_G_D_0_P_0" android:fillColor="#000000" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M41 -8.5 C41,-8.5 41,8.5 41,8.5 C41,8.5 -41,8.5 -41,8.5 C-41,8.5 -41,-8.5 -41,-8.5 C-41,-8.5 41,-8.5 41,-8.5c "/>
+ </group>
+ <group android:name="_R_G_L_0_G" android:translateX="-362.5" android:translateY="-69" android:pivotX="403.5" android:pivotY="77.5" android:scaleX="0.1" android:scaleY="0.1">
+ <group android:name="_R_G_L_0_G_L_6_G" android:translateX="-291.64799999999997" android:translateY="-414.141" android:pivotX="695.448" android:pivotY="-412.359" android:rotation="28.7" android:scaleY="0">
+ <path android:name="_R_G_L_0_G_L_6_G_D_0_P_0" android:fillColor="#303030" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M244.28 442.36 C239.4,442.36 234.45,441.17 229.88,438.66 C79.87,356.38 -46.26,235.46 -134.87,88.96 C-179.66,14.91 -214.28,-64.74 -237.78,-147.79 C-262.02,-233.47 -274.31,-322.49 -274.31,-412.36 C-274.31,-428.93 -260.88,-442.36 -244.31,-442.36 C-227.74,-442.36 -214.31,-428.93 -214.31,-412.36 C-214.31,-328.01 -202.78,-244.49 -180.05,-164.12 C-158.01,-86.24 -125.54,-11.54 -83.53,57.91 C-0.38,195.38 117.97,308.85 258.73,386.05 C273.26,394.02 278.58,412.26 270.61,426.78 C265.15,436.73 254.88,442.36 244.28,442.36c "/>
+ </group>
+ <group android:name="_R_G_L_0_G_L_5_G" android:translateX="923.0530000000001" android:translateY="-359.029" android:pivotX="-519.253" android:pivotY="-467.471" android:rotation="28.7">
+ <path android:name="_R_G_L_0_G_L_5_G_D_0_P_0" android:fillColor="#303030" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M-421.02 497.47 C-436.2,497.47 -449.23,485.99 -450.83,470.56 C-452.53,454.08 -440.56,439.34 -424.08,437.63 C-201.54,414.57 4.18,310.19 155.19,143.73 C229.54,61.77 287.7,-31.75 328.04,-134.22 C369.81,-240.3 390.99,-352.42 390.99,-467.47 C390.99,-484.04 404.42,-497.47 420.99,-497.47 C437.56,-497.47 450.99,-484.04 450.99,-467.47 C450.99,-226.02 361.72,5.35 199.63,184.04 C38.67,361.47 -180.63,472.73 -417.89,497.31 C-418.94,497.42 -419.99,497.47 -421.02,497.47c "/>
+ </group>
+ <group android:name="_R_G_L_0_G_L_2_G" android:translateX="222.54600000000002" android:translateY="77.21400000000006" android:pivotX="181.254" android:pivotY="-903.714" android:rotation="28.7" android:scaleY="0">
+ <path android:name="_R_G_L_0_G_L_2_G_D_0_P_0" android:fillColor="#ffffff" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M181.26 66.28 C181.25,66.28 181.25,66.28 181.25,66.28 C51.64,66.25 -74.22,41.06 -192.83,-8.6 C-208.12,-15 -215.32,-32.58 -208.92,-47.86 C-202.52,-63.15 -184.94,-70.35 -169.66,-63.95 C-58.42,-17.38 59.64,6.25 181.26,6.28 C197.83,6.29 211.26,19.72 211.26,36.29 C211.25,52.86 197.82,66.28 181.26,66.28c "/>
+ </group>
+ <group android:name="_R_G_L_0_G_L_0_G" android:translateX="403.5" android:translateY="77.5">
+ <path android:name="_R_G_L_0_G_L_0_G_D_0_P_0" android:fillColor="#000000" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M-630.5 -255.5 C-630.5,-255.5 -630.5,255.5 -630.5,255.5 C-630.5,255.5 630.5,255.5 630.5,255.5 C630.5,255.5 630.5,-255.5 630.5,-255.5 C630.5,-255.5 -630.5,-255.5 -630.5,-255.5c M374 -8.88 C255.5,40.59 128.4,66.04 0,66 C-128.4,65.95 -255.6,40.42 -374,-9.14 C-389.3,-15.53 -396.5,-33.11 -390.1,-48.39 C-383.7,-63.68 -366.2,-70.88 -350.9,-64.49 C-239.7,-18 -120.5,5.96 0,6 C120.4,6.04 239.7,-17.84 350.9,-64.25 C366.2,-70.63 383.7,-63.41 390.1,-48.12 C396.5,-32.83 389.3,-15.26 374,-8.88c "/>
+ </group>
+ </group>
+ </group>
+ <group android:name="time_group"/>
+ </vector>
+ </aapt:attr>
+ <target android:name="_R_G_L_0_G_L_6_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator android:repeatCount="infinite" android:propertyName="rotation" android:duration="983" android:startOffset="0" android:valueFrom="28.7" android:valueTo="-51.4" android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.402,0.136 0.202,0.847 1.0,1.0"/>
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_0_G_L_6_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator android:repeatCount="infinite" android:propertyName="scaleY" android:duration="0" android:startOffset="333" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_0_G_L_5_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator android:repeatCount="infinite" android:propertyName="rotation" android:duration="983" android:startOffset="0" android:valueFrom="28.7" android:valueTo="-51.4" android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.402,0.136 0.202,0.847 1.0,1.0"/>
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_0_G_L_5_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator android:repeatCount="infinite" android:propertyName="scaleY" android:duration="0" android:startOffset="0" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_0_G_L_2_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator android:repeatCount="infinite" android:propertyName="rotation" android:duration="983" android:startOffset="0" android:valueFrom="28.7" android:valueTo="-51.4" android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.402,0.136 0.202,0.847 1.0,1.0"/>
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_0_G_L_2_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator android:repeatCount="infinite" android:propertyName="scaleY" android:duration="0" android:startOffset="133" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="time_group">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator android:repeatCount="infinite" android:propertyName="translateX" android:duration="1000" android:startOffset="0" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/>
+ </set>
+ </aapt:attr>
+ </target>
+</animated-vector>
+
diff --git a/core/res/res/drawable/ic_notification_summarization.xml b/core/res/res/drawable/ic_notification_summarization.xml
index de905fa10728..d476872a0e20 100644
--- a/core/res/res/drawable/ic_notification_summarization.xml
+++ b/core/res/res/drawable/ic_notification_summarization.xml
@@ -19,5 +19,6 @@ Copyright (C) 2025 The Android Open Source Project
android:tint="?android:attr/colorControlNormal"
android:viewportHeight="960"
android:viewportWidth="960">
- <path android:fillColor="#ffffff" android:pathData="M354,673L480,597L606,674L573,530L684,434L538,421L480,285L422,420L276,433L387,530L354,673ZM233,840L298,559L80,370L368,345L480,80L592,345L880,370L662,559L727,840L480,691L233,840ZM480,490L480,490L480,490L480,490L480,490L480,490L480,490L480,490L480,490L480,490Z"/>
+ <path android:fillColor="#ffffff"
+ android:pathData="M120,840L120,760L600,760L600,840L120,840ZM120,640L120,560L840,560L840,640L120,640ZM120,440L120,360L560,360L560,440L120,440ZM700,480Q700,388 636,324Q572,260 480,260Q572,260 636,196Q700,132 700,40Q700,132 764,196Q828,260 920,260Q828,260 764,324Q700,388 700,480Z"/>
</vector> \ No newline at end of file
diff --git a/core/res/res/drawable/loader_horizontal_watch.xml b/core/res/res/drawable/loader_horizontal_watch.xml
new file mode 100644
index 000000000000..6b86c634d554
--- /dev/null
+++ b/core/res/res/drawable/loader_horizontal_watch.xml
@@ -0,0 +1,103 @@
+<!--
+ ~ 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="14dp" android:width="76dp" android:viewportHeight="14" android:viewportWidth="76">
+ <group android:name="_R_G">
+ <group android:name="_R_G_L_1_G" android:translateX="39" android:translateY="8">
+ <path android:name="_R_G_L_1_G_D_0_P_0" android:fillColor="#000000" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M39 -8 C39,-8 39,8 39,8 C39,8 -39,8 -39,8 C-39,8 -39,-8 -39,-8 C-39,-8 39,-8 39,-8c "/>
+ </group>
+ <group android:name="_R_G_L_0_G" android:translateX="-345" android:translateY="-67" android:pivotX="384" android:pivotY="75" android:scaleX="0.1" android:scaleY="0.1">
+ <group android:name="_R_G_L_0_G_L_6_G" android:translateX="-274.19" android:translateY="-390.077" android:pivotX="658.448" android:pivotY="-390.423" android:rotation="28.7" android:scaleY="0">
+ <path android:name="_R_G_L_0_G_L_6_G_D_0_P_0" android:fillColor="#303030" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M231.28 420.42 C226.4,420.42 221.45,419.23 216.88,416.72 C74.61,338.68 -45.02,224 -129.06,85.06 C-171.54,14.82 -204.38,-60.73 -226.66,-139.5 C-249.65,-220.76 -261.31,-305.18 -261.31,-390.42 C-261.31,-406.99 -247.88,-420.42 -231.31,-420.42 C-214.74,-420.42 -201.31,-406.99 -201.31,-390.42 C-201.31,-310.71 -190.42,-231.78 -168.93,-155.83 C-148.11,-82.23 -117.42,-11.63 -77.72,54 C0.86,183.92 112.71,291.15 245.73,364.12 C260.26,372.08 265.58,390.32 257.61,404.85 C252.15,414.79 241.88,420.42 231.28,420.42c "/>
+ </group>
+ <group android:name="_R_G_L_0_G_L_5_G" android:translateX="875.8979999999999" android:translateY="-337.894" android:pivotX="-491.64" android:pivotY="-442.606" android:rotation="28.7">
+ <path android:name="_R_G_L_0_G_L_5_G_D_0_P_0" android:fillColor="#303030" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M-398.64 472.61 C-413.82,472.61 -426.84,461.13 -428.44,445.7 C-430.15,429.22 -418.17,414.47 -401.69,412.77 C-191.38,390.98 3.04,292.33 145.75,135.01 C289.46,-23.4 368.6,-228.54 368.6,-442.61 C368.6,-459.17 382.04,-472.61 398.6,-472.61 C415.17,-472.61 428.6,-459.17 428.6,-442.61 C428.6,-213.6 343.93,5.85 190.19,175.33 C37.53,343.61 -170.48,449.13 -395.51,472.44 C-396.56,472.55 -397.6,472.61 -398.64,472.61c "/>
+ </group>
+ <group android:name="_R_G_L_0_G_L_2_G" android:translateX="212.64499999999998" android:translateY="75.14200000000005" android:pivotX="171.613" android:pivotY="-855.642" android:rotation="28.7" android:scaleY="0">
+ <path android:name="_R_G_L_0_G_L_2_G_D_0_P_0" android:fillColor="#ffffff" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M171.61 64.36 C171.61,64.36 171.61,64.36 171.61,64.36 C48.68,64.32 -70.7,40.42 -183.19,-6.68 C-198.47,-13.07 -205.68,-30.65 -199.28,-45.93 C-192.88,-61.22 -175.3,-68.42 -160.02,-62.02 C-54.9,-18.01 56.68,4.33 171.62,4.36 C188.19,4.36 201.62,17.8 201.61,34.36 C201.61,50.93 188.18,64.36 171.61,64.36c "/>
+ </group>
+ <group android:name="_R_G_L_0_G_L_0_G" android:translateX="384" android:translateY="75">
+ <path android:name="_R_G_L_0_G_L_0_G_D_0_P_0" android:fillColor="#000000" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M-611 -259 C-611,-259 -611,259 -611,259 C-611,259 611,259 611,259 C611,259 611,-259 611,-259 C611,-259 -611,-259 -611,-259c M354.66 -6.52 C242.36,40.4 121.76,64.54 -0.04,64.5 C-121.84,64.46 -242.44,40.23 -354.74,-6.76 C-370.04,-13.16 -377.24,-30.73 -370.84,-46.02 C-364.44,-61.3 -346.94,-68.51 -331.64,-62.12 C-226.54,-18.18 -113.84,4.46 -0.04,4.5 C113.76,4.54 226.56,-18.02 331.56,-61.89 C346.86,-68.27 364.46,-61.05 370.86,-45.76 C377.26,-30.47 369.96,-12.9 354.66,-6.52c "/>
+ </group>
+ </group>
+ </group>
+ <group android:name="time_group"/>
+ </vector>
+ </aapt:attr>
+ <target android:name="_R_G_L_0_G_L_6_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator android:repeatCount="infinite" android:propertyName="rotation" android:duration="983" android:startOffset="0" android:valueFrom="28.7" android:valueTo="-51.4" android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.402,0.136 0.202,0.847 1.0,1.0"/>
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_0_G_L_6_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator android:repeatCount="infinite" android:propertyName="scaleY" android:duration="0" android:startOffset="333" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_0_G_L_5_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator android:repeatCount="infinite" android:propertyName="rotation" android:duration="983" android:startOffset="0" android:valueFrom="28.7" android:valueTo="-51.4" android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.402,0.136 0.202,0.847 1.0,1.0"/>
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_0_G_L_5_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator android:repeatCount="infinite" android:propertyName="scaleY" android:duration="0" android:startOffset="0" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_0_G_L_2_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator android:repeatCount="infinite" android:propertyName="rotation" android:duration="983" android:startOffset="0" android:valueFrom="28.7" android:valueTo="-51.4" android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.402,0.136 0.202,0.847 1.0,1.0"/>
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_0_G_L_2_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator android:repeatCount="infinite" android:propertyName="scaleY" android:duration="0" android:startOffset="133" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="time_group">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator android:repeatCount="infinite" android:propertyName="translateX" android:duration="1000" android:startOffset="0" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/>
+ </set>
+ </aapt:attr>
+ </target>
+</animated-vector>
diff --git a/core/res/res/layout/notification_2025_conversation_header.xml b/core/res/res/layout/notification_2025_conversation_header.xml
index 75bd244cbbf4..1bde17358825 100644
--- a/core/res/res/layout/notification_2025_conversation_header.xml
+++ b/core/res/res/layout/notification_2025_conversation_header.xml
@@ -29,7 +29,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="@style/TextAppearance.DeviceDefault.Notification.Title"
- android:textSize="@dimen/notification_2025_title_text_size"
+ android:textSize="16sp"
android:singleLine="true"
android:layout_weight="1"
/>
diff --git a/core/res/res/layout/notification_2025_template_collapsed_base.xml b/core/res/res/layout/notification_2025_template_collapsed_base.xml
index 054583297d37..d29b7af9e24e 100644
--- a/core/res/res/layout/notification_2025_template_collapsed_base.xml
+++ b/core/res/res/layout/notification_2025_template_collapsed_base.xml
@@ -102,7 +102,6 @@
android:singleLine="true"
android:textAlignment="viewStart"
android:textAppearance="@style/TextAppearance.DeviceDefault.Notification.Title"
- android:textSize="@dimen/notification_2025_title_text_size"
/>
<include layout="@layout/notification_2025_top_line_views" />
diff --git a/core/res/res/layout/notification_2025_template_collapsed_media.xml b/core/res/res/layout/notification_2025_template_collapsed_media.xml
index 9959b666b3bf..5beab508aecf 100644
--- a/core/res/res/layout/notification_2025_template_collapsed_media.xml
+++ b/core/res/res/layout/notification_2025_template_collapsed_media.xml
@@ -104,7 +104,6 @@
android:singleLine="true"
android:textAlignment="viewStart"
android:textAppearance="@style/TextAppearance.DeviceDefault.Notification.Title"
- android:textSize="@dimen/notification_2025_title_text_size"
/>
<include layout="@layout/notification_2025_top_line_views" />
diff --git a/core/res/res/layout/notification_2025_template_collapsed_messaging.xml b/core/res/res/layout/notification_2025_template_collapsed_messaging.xml
index 85ca124de8ff..d7c3263904d4 100644
--- a/core/res/res/layout/notification_2025_template_collapsed_messaging.xml
+++ b/core/res/res/layout/notification_2025_template_collapsed_messaging.xml
@@ -130,7 +130,6 @@
android:singleLine="true"
android:textAlignment="viewStart"
android:textAppearance="@style/TextAppearance.DeviceDefault.Notification.Title"
- android:textSize="@dimen/notification_2025_title_text_size"
/>
<include layout="@layout/notification_2025_top_line_views" />
diff --git a/core/res/res/layout/notification_2025_template_compact_heads_up_base.xml b/core/res/res/layout/notification_2025_template_compact_heads_up_base.xml
index 11fc48668ad7..52bc7b8ea3bb 100644
--- a/core/res/res/layout/notification_2025_template_compact_heads_up_base.xml
+++ b/core/res/res/layout/notification_2025_template_compact_heads_up_base.xml
@@ -69,7 +69,6 @@
android:singleLine="true"
android:textAlignment="viewStart"
android:textAppearance="@style/TextAppearance.DeviceDefault.Notification.Title"
- android:textSize="@dimen/notification_2025_title_text_size"
/>
<include layout="@layout/notification_2025_top_line_views" />
</NotificationTopLineView>
diff --git a/core/res/res/layout/notification_2025_template_compact_heads_up_messaging.xml b/core/res/res/layout/notification_2025_template_compact_heads_up_messaging.xml
index bf70a5eff47e..cf9ff6bef6f8 100644
--- a/core/res/res/layout/notification_2025_template_compact_heads_up_messaging.xml
+++ b/core/res/res/layout/notification_2025_template_compact_heads_up_messaging.xml
@@ -90,7 +90,6 @@
android:singleLine="true"
android:textAlignment="viewStart"
android:textAppearance="@style/TextAppearance.DeviceDefault.Notification.Title"
- android:textSize="@dimen/notification_2025_title_text_size"
/>
<include layout="@layout/notification_2025_top_line_views" />
</NotificationTopLineView>
diff --git a/core/res/res/values-w192dp/dimens_watch.xml b/core/res/res/values-w192dp/dimens_watch.xml
new file mode 100644
index 000000000000..c6bf767ab6b8
--- /dev/null
+++ b/core/res/res/values-w192dp/dimens_watch.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2025 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<resources>
+ <!-- 16.7% of display size -->
+ <dimen name="base_error_dialog_top_padding">32dp</dimen>
+ <!-- 5.2% of display size -->
+ <dimen name="base_error_dialog_padding">10dp</dimen>
+ <!-- 20.83% of display size -->
+ <dimen name="base_error_dialog_bottom_padding">40dp</dimen>
+
+ <!-- watch's indeterminate progress bar dimens based on the current screen size -->
+ <dimen name="loader_horizontal_min_width_watch">67dp</dimen>
+ <dimen name="loader_horizontal_min_height_watch">15dp</dimen>
+</resources>
diff --git a/core/res/res/values-w204dp-round-watch/dimens_watch.xml b/core/res/res/values-w204dp-round-watch/dimens_watch.xml
new file mode 100644
index 000000000000..3509474c5c2e
--- /dev/null
+++ b/core/res/res/values-w204dp-round-watch/dimens_watch.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2025 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<resources>
+ <!-- watch's indeterminate progress bar dimens based on the current screen size -->
+ <dimen name="loader_horizontal_min_width_watch">70dp</dimen>
+ <dimen name="loader_horizontal_min_height_watch">15dp</dimen>
+</resources>
diff --git a/core/res/res/values-w216dp/dimens_watch.xml b/core/res/res/values-w216dp/dimens_watch.xml
new file mode 100644
index 000000000000..e14ce5e98f55
--- /dev/null
+++ b/core/res/res/values-w216dp/dimens_watch.xml
@@ -0,0 +1,21 @@
+<!--
+ ~ Copyright (C) 2025 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<resources>
+ <!-- watch's indeterminate progress bar dimens based on the current screen size -->
+ <dimen name="loader_horizontal_min_width_watch">72dp</dimen>
+ <dimen name="loader_horizontal_min_height_watch">16dp</dimen>
+</resources> \ No newline at end of file
diff --git a/core/res/res/values-w228dp/dimens_watch.xml b/core/res/res/values-w228dp/dimens_watch.xml
new file mode 100644
index 000000000000..3c6265690c3c
--- /dev/null
+++ b/core/res/res/values-w228dp/dimens_watch.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2025 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<resources>
+ <!-- watch's indeterminate progress bar dimens based on the current screen size -->
+ <dimen name="loader_horizontal_min_width">76dp</dimen>
+ <dimen name="loader_horizontal_min_height">14dp</dimen>
+</resources>
diff --git a/core/res/res/values-w240dp/dimens_material.xml b/core/res/res/values-w240dp/dimens_material.xml
index bd26c8bf84df..e30aea46aec7 100644
--- a/core/res/res/values-w240dp/dimens_material.xml
+++ b/core/res/res/values-w240dp/dimens_material.xml
@@ -21,4 +21,8 @@
<dimen name="screen_percentage_12">28.8dp</dimen>
<dimen name="screen_percentage_15">36dp</dimen>
<dimen name="screen_percentage_3646">87.5dp</dimen>
+
+ <!-- watch's indeterminate progress bar dimens based on the current screen size -->
+ <dimen name="progress_indeterminate_horizontal_min_width_watch">80dp</dimen>
+ <dimen name="progress_indeterminate_horizontal_min_height_watch">17dp</dimen>
</resources>
diff --git a/core/res/res/values-watch/styles_device_defaults.xml b/core/res/res/values-watch/styles_device_defaults.xml
index fb7dbb0660c5..eeb66e7cf6a8 100644
--- a/core/res/res/values-watch/styles_device_defaults.xml
+++ b/core/res/res/values-watch/styles_device_defaults.xml
@@ -42,5 +42,8 @@
<item name="indeterminateOnly">false</item>
<!-- Use Wear Material3 ring shape as default determinate drawable -->
<item name="progressDrawable">@drawable/progress_ring_watch</item>
+ <item name="indeterminateDrawable">@drawable/loader_horizontal_watch</item>
+ <item name="android:minWidth">@dimen/loader_horizontal_min_width_watch</item>
+ <item name="android:minHeight">@dimen/loader_horizontal_min_height_watch</item>
</style>
</resources>
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 1a311d572e0b..2188469bdf03 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -2643,6 +2643,15 @@
<!-- MMS user agent prolfile url -->
<string name="config_mms_user_agent_profile_url" translatable="false"></string>
+ <!-- The default list of possible CMF Names|Style|ColorSource. This array can be
+ overridden device-specific resources. A wildcard (fallback) must be supplied.
+ Name - Read from `ro.boot.hardware.color` sysprop. Fallback (*) required.
+ Styles - frameworks/libs/systemui/monet/src/com/android/systemui/monet/Style.java
+ Color - `home_wallpaper` (for color extraction) or a hexadecimal int (#FFcc99) -->
+ <string-array name="theming_defaults">
+ <item>*|TONAL_SPOT|home_wallpaper</item>
+ </string-array>
+
<!-- National Language Identifier codes for the following two config items.
(from 3GPP TS 23.038 V9.1.1 Table 6.2.1.2.4.1):
0 - reserved
diff --git a/core/res/res/values/dimens.xml b/core/res/res/values/dimens.xml
index e9d87e4b5f5b..9acb2427aaab 100644
--- a/core/res/res/values/dimens.xml
+++ b/core/res/res/values/dimens.xml
@@ -580,9 +580,6 @@
<dimen name="notification_text_size">14sp</dimen>
<!-- Size of notification text titles (see TextAppearance.StatusBar.EventContent.Title) -->
<dimen name="notification_title_text_size">14sp</dimen>
- <!-- Size of notification text titles, 2025 redesign version (see TextAppearance.StatusBar.EventContent.Title) -->
- <!-- TODO: b/378660052 - When inlining the redesign flag, this should be updated directly in TextAppearance.DeviceDefault.Notification.Title -->
- <dimen name="notification_2025_title_text_size">16sp</dimen>
<!-- Size of big notification text titles (see TextAppearance.StatusBar.EventContent.BigTitle) -->
<dimen name="notification_big_title_text_size">16sp</dimen>
<!-- Size of smaller notification text (see TextAppearance.StatusBar.EventContent.Line2, Info, Time) -->
diff --git a/core/res/res/values/dimens_watch.xml b/core/res/res/values/dimens_watch.xml
index 2aae98715973..19845916984b 100644
--- a/core/res/res/values/dimens_watch.xml
+++ b/core/res/res/values/dimens_watch.xml
@@ -61,4 +61,8 @@
<dimen name="disabled_alpha_wear_material3">0.12</dimen>
<!-- Alpha transparency applied to elements which are considered primary (e.g. primary text) -->
<dimen name="primary_content_alpha_wear_material3">0.38</dimen>
+
+ <!-- watch's indeterminate progress bar dimens -->
+ <dimen name="loader_horizontal_min_width">68dp</dimen>
+ <dimen name="loader_horizontal_min_height">13dp</dimen>
</resources>
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index abbba9d1bffa..7a93ca1e9ac6 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -1011,6 +1011,16 @@
<!-- Description of a category of application permissions, listed so the user can choose whether they want to allow the application to do this. [CHAR LIMIT=NONE]-->
<string name="permgroupdesc_notifications">show notifications</string>
+ <!-- Title of a category of application permissions, listed so the user can choose whether they want to allow the application to do this. [CHAR LIMIT=40]-->
+ <string name="permgrouplab_xr_tracking">XR tracking data</string>
+ <!-- Description of a category of application permissions, listed so the user can choose whether they want to allow the application to do this. [CHAR LIMIT=NONE]-->
+ <string name="permgroupdesc_xr_tracking">access XR data about you and the environment around you</string>
+
+ <!-- Title of a category of application permissions, listed so the user can choose whether they want to allow the application to do this. [CHAR LIMIT=40]-->
+ <string name="permgrouplab_xr_tracking_sensitive">sensitive XR tracking data</string>
+ <!-- Description of a category of application permissions, listed so the user can choose whether they want to allow the application to do this. [CHAR LIMIT=NONE]-->
+ <string name="permgroupdesc_xr_tracking_sensitive">access sensitive tracking data, such as eye gaze</string>
+
<!-- Title for the capability of an accessibility service to retrieve window content. -->
<string name="capability_title_canRetrieveWindowContent">Retrieve window content</string>
<!-- Description for the capability of an accessibility service to retrieve window content. -->
@@ -1875,6 +1885,45 @@
<!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
<string name="permdesc_mediaLocation">Allows the app to read locations from your media collection.</string>
+ <string name="permlab_eye_tracking_coarse">track your approximate eye gaze</string>
+ <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
+ <string name="permdesc_eye_tracking_coarse">Allows the app to track your approximate eye gaze.</string>
+
+ <!-- Title of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
+ <string name="permlab_eye_tracking_fine">track where you are looking</string>
+ <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
+ <string name="permdesc_eye_tracking_fine">Allows the app to access precise eye gaze data.</string>
+
+ <!-- Title of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
+ <string name="permlab_face_tracking">track your face</string>
+ <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
+ <string name="permdesc_face_tracking">Allows the app to access face tracking data.</string>
+
+ <!-- Title of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
+ <string name="permlab_hand_tracking">track your hands</string>
+ <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
+ <string name="permdesc_hand_tracking">Allows the app to access hand tracking data.</string>
+
+ <!-- Title of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
+ <string name="permlab_head_tracking">track your head</string>
+ <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
+ <string name="permdesc_head_tracking">Allows the app to access head tracking data.</string>
+
+ <!-- Title of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
+ <string name="permlab_scene_understanding_coarse">understand your immediate environment</string>
+ <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
+ <string name="permdesc_scene_understanding_coarse">Allows the app to access tracking data about the environment directly around you.</string>
+
+ <!-- Title of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
+ <string name="permlab_scene_understanding_fine">understand your immediate environment at high detail</string>
+ <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
+ <string name="permdesc_scene_understanding_fine">Allows the app to access tracking data about the environment directly around you with very high detail.</string>
+
+ <!-- Title of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
+ <string name="permlab_xr_tracking_in_background">access XR data while not in the foreground</string>
+ <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
+ <string name="permdesc_xr_tracking_in_background">Allows the app to access XR data while not in the foreground.</string>
+
<!-- Name for an app setting that lets the user authenticate for that app using biometrics (e.g. fingerprint or face). [CHAR LIMIT=30] -->
<string name="biometric_app_setting_name">Use biometrics</string>
<!-- Name for an app setting that lets the user authenticate for that app using biometrics (e.g. fingerprint or face) or their screen lock credential (i.e. PIN, pattern, or password). [CHAR LIMIT=70] -->
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index c62732d36038..ffcfce9c420e 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -575,7 +575,6 @@
<java-symbol type="dimen" name="notification_text_size" />
<java-symbol type="dimen" name="notification_title_text_size" />
<java-symbol type="dimen" name="notification_subtext_size" />
- <java-symbol type="dimen" name="notification_2025_title_text_size" />
<java-symbol type="dimen" name="notification_top_pad" />
<java-symbol type="dimen" name="notification_top_pad_narrow" />
<java-symbol type="dimen" name="notification_top_pad_large_text" />
@@ -5904,6 +5903,9 @@
<java-symbol type="drawable" name="ic_notification_summarization" />
<java-symbol type="dimen" name="notification_collapsed_height_with_summarization" />
+ <!-- Device CMF Theming Settings -->
+ <java-symbol type="array" name="theming_defaults" />
+
<!-- Advanced Protection Service USB feature -->
<java-symbol type="string" name="usb_apm_usb_plugged_in_when_locked_notification_title" />
<java-symbol type="string" name="usb_apm_usb_plugged_in_when_locked_notification_text" />
diff --git a/core/tests/coretests/src/android/app/AutomaticZenRuleTest.java b/core/tests/coretests/src/android/app/AutomaticZenRuleTest.java
index 5765562e2383..6fe3b6ca0c6c 100644
--- a/core/tests/coretests/src/android/app/AutomaticZenRuleTest.java
+++ b/core/tests/coretests/src/android/app/AutomaticZenRuleTest.java
@@ -26,7 +26,6 @@ import static org.junit.Assert.assertThrows;
import android.content.ComponentName;
import android.net.Uri;
import android.os.Parcel;
-import android.platform.test.annotations.EnableFlags;
import android.platform.test.annotations.Presubmit;
import android.platform.test.flag.junit.SetFlagsRule;
@@ -112,7 +111,6 @@ public class AutomaticZenRuleTest {
}
@Test
- @EnableFlags(Flags.FLAG_MODES_API)
public void testLongInputsFromParcel() {
// Create a rule with long fields, set directly via reflection so that we can confirm that
// a rule with too-long fields that comes in via a parcel has its fields truncated directly.
@@ -169,7 +167,6 @@ public class AutomaticZenRuleTest {
}
@Test
- @EnableFlags(Flags.FLAG_MODES_API)
public void builderConstructor_nullInputs_throws() {
assertThrows(NullPointerException.class,
() -> new AutomaticZenRule.Builder(null, Uri.parse("condition")));
@@ -178,7 +175,6 @@ public class AutomaticZenRuleTest {
}
@Test
- @EnableFlags(Flags.FLAG_MODES_API)
public void constructor_defaultTypeUnknown() {
AutomaticZenRule rule = new AutomaticZenRule("name", new ComponentName("pkg", "cps"), null,
Uri.parse("conditionId"), null, NotificationManager.INTERRUPTION_FILTER_PRIORITY,
@@ -188,7 +184,6 @@ public class AutomaticZenRuleTest {
}
@Test
- @EnableFlags(Flags.FLAG_MODES_API)
public void builder_defaultsAreSensible() {
AutomaticZenRule rule = new AutomaticZenRule.Builder("name",
Uri.parse("conditionId")).build();
@@ -200,7 +195,6 @@ public class AutomaticZenRuleTest {
}
@Test
- @EnableFlags(Flags.FLAG_MODES_API)
public void validate_builderWithValidType_succeeds() throws Exception {
AutomaticZenRule rule = new AutomaticZenRule.Builder("rule", Uri.parse("uri"))
.setType(AutomaticZenRule.TYPE_BEDTIME)
@@ -209,14 +203,12 @@ public class AutomaticZenRuleTest {
}
@Test
- @EnableFlags(Flags.FLAG_MODES_API)
public void validate_builderWithoutType_succeeds() throws Exception {
AutomaticZenRule rule = new AutomaticZenRule.Builder("rule", Uri.parse("uri")).build();
rule.validate(); // No exception.
}
@Test
- @EnableFlags(Flags.FLAG_MODES_API)
public void validate_constructorWithoutType_succeeds() throws Exception {
AutomaticZenRule rule = new AutomaticZenRule("rule", new ComponentName("pkg", "cps"),
new ComponentName("pkg", "activity"), Uri.parse("condition"), null,
@@ -225,7 +217,6 @@ public class AutomaticZenRuleTest {
}
@Test
- @EnableFlags(Flags.FLAG_MODES_API)
public void validate_invalidType_throws() throws Exception {
AutomaticZenRule rule = new AutomaticZenRule.Builder("rule", Uri.parse("uri")).build();
@@ -238,7 +229,6 @@ public class AutomaticZenRuleTest {
}
@Test
- @EnableFlags(Flags.FLAG_MODES_API)
public void setType_invalidType_throws() {
AutomaticZenRule rule = new AutomaticZenRule.Builder("rule", Uri.parse("uri")).build();
@@ -246,7 +236,6 @@ public class AutomaticZenRuleTest {
}
@Test
- @EnableFlags(Flags.FLAG_MODES_API)
public void setTypeBuilder_invalidType_throws() {
AutomaticZenRule.Builder builder = new AutomaticZenRule.Builder("rule", Uri.parse("uri"));
diff --git a/core/tests/coretests/src/android/app/NotificationManagerTest.java b/core/tests/coretests/src/android/app/NotificationManagerTest.java
index d816039d0d3c..250b9ce8d89d 100644
--- a/core/tests/coretests/src/android/app/NotificationManagerTest.java
+++ b/core/tests/coretests/src/android/app/NotificationManagerTest.java
@@ -521,7 +521,7 @@ public class NotificationManagerTest {
}
@Test
- @EnableFlags({Flags.FLAG_MODES_API, Flags.FLAG_MODES_UI})
+ @EnableFlags(Flags.FLAG_MODES_UI)
public void areAutomaticZenRulesUserManaged_handheld_isTrue() {
PackageManager pm = mock(PackageManager.class);
when(pm.hasSystemFeature(any())).thenReturn(false);
@@ -531,7 +531,7 @@ public class NotificationManagerTest {
}
@Test
- @EnableFlags({Flags.FLAG_MODES_API, Flags.FLAG_MODES_UI})
+ @EnableFlags(Flags.FLAG_MODES_UI)
public void areAutomaticZenRulesUserManaged_auto_isFalse() {
PackageManager pm = mock(PackageManager.class);
when(pm.hasSystemFeature(eq(PackageManager.FEATURE_AUTOMOTIVE))).thenReturn(true);
@@ -541,7 +541,7 @@ public class NotificationManagerTest {
}
@Test
- @EnableFlags({Flags.FLAG_MODES_API, Flags.FLAG_MODES_UI})
+ @EnableFlags(Flags.FLAG_MODES_UI)
public void areAutomaticZenRulesUserManaged_tv_isFalse() {
PackageManager pm = mock(PackageManager.class);
when(pm.hasSystemFeature(eq(PackageManager.FEATURE_LEANBACK))).thenReturn(true);
@@ -551,7 +551,7 @@ public class NotificationManagerTest {
}
@Test
- @EnableFlags({Flags.FLAG_MODES_API, Flags.FLAG_MODES_UI})
+ @EnableFlags(Flags.FLAG_MODES_UI)
public void areAutomaticZenRulesUserManaged_watch_isFalse() {
PackageManager pm = mock(PackageManager.class);
when(pm.hasSystemFeature(eq(PackageManager.FEATURE_WATCH))).thenReturn(true);
diff --git a/core/tests/coretests/src/android/hardware/display/DisplayManagerGlobalTest.java b/core/tests/coretests/src/android/hardware/display/DisplayManagerGlobalTest.java
index dc2f0a69375d..9383807ec761 100644
--- a/core/tests/coretests/src/android/hardware/display/DisplayManagerGlobalTest.java
+++ b/core/tests/coretests/src/android/hardware/display/DisplayManagerGlobalTest.java
@@ -33,6 +33,7 @@ import android.content.Context;
import android.os.Handler;
import android.os.RemoteException;
import android.platform.test.annotations.Presubmit;
+import android.platform.test.annotations.RequiresFlagsDisabled;
import android.platform.test.annotations.RequiresFlagsEnabled;
import android.platform.test.flag.junit.CheckFlagsRule;
import android.platform.test.flag.junit.DeviceFlagsValueProvider;
@@ -348,6 +349,26 @@ public class DisplayManagerGlobalTest {
DisplayManager.PRIVATE_EVENT_TYPE_DISPLAY_BRIGHTNESS));
}
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_COMMITTED_STATE_SEPARATE_EVENT)
+ public void test_mapPrivateEventCommittedStateChanged_flagEnabled() {
+ // Test public flags mapping
+ assertEquals(DisplayManagerGlobal.INTERNAL_EVENT_FLAG_DISPLAY_COMMITTED_STATE_CHANGED,
+ mDisplayManagerGlobal
+ .mapFiltersToInternalEventFlag(0,
+ DisplayManager.PRIVATE_EVENT_TYPE_DISPLAY_COMMITTED_STATE_CHANGED));
+ }
+
+ @Test
+ @RequiresFlagsDisabled(Flags.FLAG_COMMITTED_STATE_SEPARATE_EVENT)
+ public void test_mapPrivateEventCommittedStateChanged_flagDisabled() {
+ // Test public flags mapping
+ assertEquals(0,
+ mDisplayManagerGlobal
+ .mapFiltersToInternalEventFlag(0,
+ DisplayManager.PRIVATE_EVENT_TYPE_DISPLAY_COMMITTED_STATE_CHANGED));
+ }
+
private void waitForHandler() {
mHandler.runWithScissors(() -> {
}, 0);
diff --git a/core/tests/coretests/src/android/service/notification/ConditionTest.java b/core/tests/coretests/src/android/service/notification/ConditionTest.java
index e94273e1ada7..65c108a827ef 100644
--- a/core/tests/coretests/src/android/service/notification/ConditionTest.java
+++ b/core/tests/coretests/src/android/service/notification/ConditionTest.java
@@ -23,10 +23,8 @@ import static junit.framework.Assert.fail;
import static org.junit.Assert.assertThrows;
-import android.app.Flags;
import android.net.Uri;
import android.os.Parcel;
-import android.platform.test.annotations.EnableFlags;
import android.platform.test.flag.junit.SetFlagsRule;
import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -113,7 +111,6 @@ public class ConditionTest {
@Test
public void testLongFields_inConstructors() {
- mSetFlagsRule.enableFlags(Flags.FLAG_MODES_API);
String longString = Strings.repeat("A", 65536);
Uri longUri = Uri.parse("uri://" + Strings.repeat("A", 65530));
@@ -136,7 +133,6 @@ public class ConditionTest {
@Test
public void testLongFields_viaParcel() throws Exception {
- mSetFlagsRule.enableFlags(Flags.FLAG_MODES_API);
// Set fields via reflection to force them to be long, then parcel and unparcel to make sure
// it gets truncated upon unparcelling.
Condition cond = new Condition(Uri.parse("uri://placeholder"), "placeholder",
@@ -170,8 +166,6 @@ public class ConditionTest {
@Test
public void testEquals() {
- mSetFlagsRule.enableFlags(Flags.FLAG_MODES_API);
-
Condition cond1 = new Condition(Uri.parse("uri://placeholder"), "placeholder",
Condition.STATE_TRUE, Condition.SOURCE_USER_ACTION);
Condition cond2 = new Condition(Uri.parse("uri://placeholder"), "placeholder",
@@ -186,8 +180,6 @@ public class ConditionTest {
@Test
public void testParcelConstructor() {
- mSetFlagsRule.enableFlags(Flags.FLAG_MODES_API);
-
Condition cond = new Condition(Uri.parse("uri://placeholder"), "placeholder",
Condition.STATE_TRUE, Condition.SOURCE_USER_ACTION);
@@ -200,28 +192,24 @@ public class ConditionTest {
}
@Test
- @EnableFlags(Flags.FLAG_MODES_API)
public void constructor_unspecifiedSource_succeeds() {
new Condition(Uri.parse("id"), "Summary", Condition.STATE_TRUE);
// No exception.
}
@Test
- @EnableFlags(Flags.FLAG_MODES_API)
public void constructor_validSource_succeeds() {
new Condition(Uri.parse("id"), "Summary", Condition.STATE_TRUE, Condition.SOURCE_CONTEXT);
// No exception.
}
@Test
- @EnableFlags(Flags.FLAG_MODES_API)
public void constructor_invalidSource_throws() {
assertThrows(IllegalArgumentException.class,
() -> new Condition(Uri.parse("uri"), "Summary", Condition.STATE_TRUE, 1000));
}
@Test
- @EnableFlags(Flags.FLAG_MODES_API)
public void constructor_parcelWithInvalidSource_throws() {
Condition original = new Condition(Uri.parse("condition"), "Summary", Condition.STATE_TRUE,
Condition.SOURCE_SCHEDULE);
@@ -237,7 +225,6 @@ public class ConditionTest {
}
@Test
- @EnableFlags(Flags.FLAG_MODES_API)
public void validate_invalidSource_throws() throws Exception {
Condition condition = new Condition(Uri.parse("condition"), "Summary", Condition.STATE_TRUE,
Condition.SOURCE_SCHEDULE);
diff --git a/core/tests/coretests/src/com/android/internal/app/ChooserActivityTest.java b/core/tests/coretests/src/com/android/internal/app/ChooserActivityTest.java
index 9f398ecf5492..b7d6ab56d6b3 100644
--- a/core/tests/coretests/src/com/android/internal/app/ChooserActivityTest.java
+++ b/core/tests/coretests/src/com/android/internal/app/ChooserActivityTest.java
@@ -79,6 +79,7 @@ import android.graphics.drawable.Icon;
import android.metrics.LogMaker;
import android.net.Uri;
import android.os.UserHandle;
+import android.os.UserManager;
import android.provider.DeviceConfig;
import android.service.chooser.ChooserTarget;
import android.util.Pair;
@@ -3178,7 +3179,11 @@ public class ChooserActivityTest {
}
private void markWorkProfileUserAvailable() {
- ChooserActivityOverrideData.getInstance().workProfileUserHandle = UserHandle.of(10);
+ if (UserManager.isHeadlessSystemUserMode()) {
+ ChooserActivityOverrideData.getInstance().workProfileUserHandle = UserHandle.of(11);
+ } else {
+ ChooserActivityOverrideData.getInstance().workProfileUserHandle = UserHandle.of(10);
+ }
}
private void markCloneProfileUserAvailable() {
diff --git a/core/tests/coretests/src/com/android/internal/app/ChooserWrapperActivity.java b/core/tests/coretests/src/com/android/internal/app/ChooserWrapperActivity.java
index dcea9120e2a5..be7f84e3ea8f 100644
--- a/core/tests/coretests/src/com/android/internal/app/ChooserWrapperActivity.java
+++ b/core/tests/coretests/src/com/android/internal/app/ChooserWrapperActivity.java
@@ -155,12 +155,12 @@ public class ChooserWrapperActivity extends ChooserActivity implements IChooserW
@Override
protected ResolverListController createListController(UserHandle userHandle) {
- if (userHandle == UserHandle.SYSTEM) {
- when(sOverrides.resolverListController.getUserHandle()).thenReturn(UserHandle.SYSTEM);
- return sOverrides.resolverListController;
+ if (userHandle.equals(sOverrides.workProfileUserHandle)) {
+ when(sOverrides.workResolverListController.getUserHandle()).thenReturn(userHandle);
+ return sOverrides.workResolverListController;
}
- when(sOverrides.workResolverListController.getUserHandle()).thenReturn(userHandle);
- return sOverrides.workResolverListController;
+ when(sOverrides.resolverListController.getUserHandle()).thenReturn(userHandle);
+ return sOverrides.resolverListController;
}
@Override
diff --git a/graphics/java/android/graphics/FrameInfo.java b/graphics/java/android/graphics/FrameInfo.java
index 7d236d203201..3b8f46630344 100644
--- a/graphics/java/android/graphics/FrameInfo.java
+++ b/graphics/java/android/graphics/FrameInfo.java
@@ -93,10 +93,12 @@ public final class FrameInfo {
// Interval between two consecutive frames
public static final int FRAME_INTERVAL = 11;
+ // Workload target deadline for a frame
+ public static final int WORKLOAD_TARGET = 12;
+
// Must be the last one
// This value must be in sync with `UI_THREAD_FRAME_INFO_SIZE` in FrameInfo.h
- // In calculating size, + 1 for Flags, and + 1 for WorkloadTarget from FrameInfo.h
- private static final int FRAME_INFO_SIZE = FRAME_INTERVAL + 2;
+ private static final int FRAME_INFO_SIZE = WORKLOAD_TARGET + 1;
/** checkstyle */
public void setVsync(long intendedVsync, long usedVsync, long frameTimelineVsyncId,
@@ -108,6 +110,7 @@ public final class FrameInfo {
frameInfo[FRAME_DEADLINE] = frameDeadline;
frameInfo[FRAME_START_TIME] = frameStartTime;
frameInfo[FRAME_INTERVAL] = frameInterval;
+ frameInfo[WORKLOAD_TARGET] = frameDeadline - intendedVsync;
}
/** checkstyle */
diff --git a/graphics/java/android/graphics/HardwareRenderer.java b/graphics/java/android/graphics/HardwareRenderer.java
index 940cd93c53f2..65854dd51a91 100644
--- a/graphics/java/android/graphics/HardwareRenderer.java
+++ b/graphics/java/android/graphics/HardwareRenderer.java
@@ -1467,6 +1467,18 @@ public class HardwareRenderer {
public static native void preload();
/**
+ * Initialize the Buffer Allocator singleton
+ *
+ * This takes 10-20ms on low-resourced devices, so doing it on-demand when an app
+ * tries to render its first frame causes drawFrames to be blocked for buffer
+ * allocation due to just initializing the allocator.
+ *
+ * Should only be called when a buffer is expected to be used.
+ * @hide
+ */
+ public static native void preInitBufferAllocator();
+
+ /**
* @hide
*/
protected static native boolean isWebViewOverlaysEnabled();
diff --git a/graphics/java/android/graphics/RuntimeShader.java b/graphics/java/android/graphics/RuntimeShader.java
index 3543e991924e..db2376e008f5 100644
--- a/graphics/java/android/graphics/RuntimeShader.java
+++ b/graphics/java/android/graphics/RuntimeShader.java
@@ -20,6 +20,7 @@ import android.annotation.ColorInt;
import android.annotation.ColorLong;
import android.annotation.FlaggedApi;
import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.util.ArrayMap;
import android.view.Window;
@@ -76,6 +77,7 @@ import libcore.util.NativeAllocationRegistry;
* Additionally, if the shader is invoked by another using {@link #setInputShader(String, Shader)},
* then that parent shader may modify the input coordinates arbitrarily.</p>
*
+ * <a id="agsl-and-color-spaces"/>
* <h3>AGSL and Color Spaces</h3>
* <p>Android Graphics and by extension {@link RuntimeShader} are color managed. The working
* {@link ColorSpace} for an AGSL shader is defined to be the color space of the destination, which
@@ -267,6 +269,8 @@ public class RuntimeShader extends Shader {
private ArrayMap<String, ColorFilter> mColorFilterUniforms = new ArrayMap<>();
private ArrayMap<String, RuntimeXfermode> mXfermodeUniforms = new ArrayMap<>();
+ private ColorSpace mWorkingColorSpace = null;
+
/**
* Creates a new RuntimeShader.
@@ -286,6 +290,35 @@ public class RuntimeShader extends Shader {
}
/**
+ * Sets the working color space for this shader. That is, the shader will be evaluated
+ * in the given colorspace before being converted to the output destination's colorspace.
+ *
+ * <p>By default the RuntimeShader is evaluated in the context of the
+ * <a href="#agsl-and-color-spaces">destination colorspace</a>. By calling this method
+ * that can be overridden to force the shader to be evaluated in the given colorspace first
+ * before then being color converted to the destination colorspace.</p>
+ *
+ * @param colorSpace The ColorSpace to evaluate in. Must be an {@link ColorSpace#getModel() RGB}
+ * ColorSpace. Passing null restores default behavior of working in the
+ * destination colorspace.
+ * @throws IllegalArgumentException If the colorspace is not RGB
+ */
+ @FlaggedApi(Flags.FLAG_SHADER_COLOR_SPACE)
+ public void setWorkingColorSpace(@Nullable ColorSpace colorSpace) {
+ if (colorSpace != null && colorSpace.getModel() != ColorSpace.Model.RGB) {
+ throw new IllegalArgumentException("ColorSpace must be RGB, given " + colorSpace);
+ }
+ if (mWorkingColorSpace != colorSpace) {
+ mWorkingColorSpace = colorSpace;
+ if (mWorkingColorSpace != null) {
+ // Just to enforce this can be resolved instead of erroring out later
+ mWorkingColorSpace.getNativeInstance();
+ }
+ discardNativeInstance();
+ }
+ }
+
+ /**
* Sets the uniform color value corresponding to this shader. If the shader does not have a
* uniform with that name or if the uniform is declared with a type other than vec3 or vec4 and
* corresponding layout(color) annotation then an IllegalArgumentException is thrown.
@@ -578,7 +611,8 @@ public class RuntimeShader extends Shader {
/** @hide */
@Override
protected long createNativeInstance(long nativeMatrix, boolean filterFromPaint) {
- return nativeCreateShader(mNativeInstanceRuntimeShaderBuilder, nativeMatrix);
+ return nativeCreateShader(mNativeInstanceRuntimeShaderBuilder, nativeMatrix,
+ mWorkingColorSpace != null ? mWorkingColorSpace.getNativeInstance() : 0);
}
/** @hide */
@@ -589,6 +623,8 @@ public class RuntimeShader extends Shader {
private static native long nativeGetFinalizer();
private static native long nativeCreateBuilder(String agsl);
private static native long nativeCreateShader(long shaderBuilder, long matrix);
+ private static native long nativeCreateShader(long shaderBuilder, long matrix,
+ long colorSpacePtr);
private static native void nativeUpdateUniforms(
long shaderBuilder, String uniformName, float[] uniforms, boolean isColor);
private static native void nativeUpdateUniforms(
diff --git a/keystore/java/android/security/GateKeeper.java b/keystore/java/android/security/GateKeeper.java
index 464714fe2895..c2792e1f2394 100644
--- a/keystore/java/android/security/GateKeeper.java
+++ b/keystore/java/android/security/GateKeeper.java
@@ -28,7 +28,7 @@ import android.service.gatekeeper.IGateKeeperService;
*
* @hide
*/
-public abstract class GateKeeper {
+public final class GateKeeper {
public static final long INVALID_SECURE_USER_ID = 0;
diff --git a/keystore/java/android/security/keystore/ArrayUtils.java b/keystore/java/android/security/keystore/ArrayUtils.java
index f22b6041800f..6472ca9957d0 100644
--- a/keystore/java/android/security/keystore/ArrayUtils.java
+++ b/keystore/java/android/security/keystore/ArrayUtils.java
@@ -23,7 +23,7 @@ import java.util.function.Consumer;
/**
* @hide
*/
-public abstract class ArrayUtils {
+public final class ArrayUtils {
private ArrayUtils() {}
public static String[] nullToEmpty(String[] array) {
diff --git a/keystore/java/android/security/keystore/Utils.java b/keystore/java/android/security/keystore/Utils.java
index e58b1ccb5370..c38ce8e86a15 100644
--- a/keystore/java/android/security/keystore/Utils.java
+++ b/keystore/java/android/security/keystore/Utils.java
@@ -23,7 +23,7 @@ import java.util.Date;
*
* @hide
*/
-abstract class Utils {
+public final class Utils {
private Utils() {}
static Date cloneIfNotNull(Date value) {
diff --git a/keystore/java/android/security/keystore2/KeyStore2ParameterUtils.java b/keystore/java/android/security/keystore2/KeyStore2ParameterUtils.java
index 1394bd443f03..9d306ce1ed38 100644
--- a/keystore/java/android/security/keystore2/KeyStore2ParameterUtils.java
+++ b/keystore/java/android/security/keystore2/KeyStore2ParameterUtils.java
@@ -38,7 +38,9 @@ import java.util.function.Consumer;
/**
* @hide
*/
-public abstract class KeyStore2ParameterUtils {
+public final class KeyStore2ParameterUtils {
+
+ private KeyStore2ParameterUtils() {}
/**
* This function constructs a {@link KeyParameter} expressing a boolean value.
diff --git a/keystore/java/android/security/keystore2/KeymasterUtils.java b/keystore/java/android/security/keystore2/KeymasterUtils.java
index 614e3684c417..02f3f578d03e 100644
--- a/keystore/java/android/security/keystore2/KeymasterUtils.java
+++ b/keystore/java/android/security/keystore2/KeymasterUtils.java
@@ -16,13 +16,10 @@
package android.security.keystore2;
-import android.security.keymaster.KeymasterArguments;
import android.security.keymaster.KeymasterDefs;
-import android.security.keystore.KeyProperties;
import java.security.AlgorithmParameters;
import java.security.NoSuchAlgorithmException;
-import java.security.ProviderException;
import java.security.spec.ECGenParameterSpec;
import java.security.spec.ECParameterSpec;
import java.security.spec.InvalidParameterSpecException;
@@ -30,7 +27,7 @@ import java.security.spec.InvalidParameterSpecException;
/**
* @hide
*/
-public abstract class KeymasterUtils {
+public final class KeymasterUtils {
private KeymasterUtils() {}
@@ -86,47 +83,6 @@ public abstract class KeymasterUtils {
}
}
- /**
- * Adds {@code KM_TAG_MIN_MAC_LENGTH} tag, if necessary, to the keymaster arguments for
- * generating or importing a key. This tag may only be needed for symmetric keys (e.g., HMAC,
- * AES-GCM).
- */
- public static void addMinMacLengthAuthorizationIfNecessary(KeymasterArguments args,
- int keymasterAlgorithm,
- int[] keymasterBlockModes,
- int[] keymasterDigests) {
- switch (keymasterAlgorithm) {
- case KeymasterDefs.KM_ALGORITHM_AES:
- if (com.android.internal.util.ArrayUtils.contains(
- keymasterBlockModes, KeymasterDefs.KM_MODE_GCM)) {
- // AES GCM key needs the minimum length of AEAD tag specified.
- args.addUnsignedInt(KeymasterDefs.KM_TAG_MIN_MAC_LENGTH,
- AndroidKeyStoreAuthenticatedAESCipherSpi.GCM
- .MIN_SUPPORTED_TAG_LENGTH_BITS);
- }
- break;
- case KeymasterDefs.KM_ALGORITHM_HMAC:
- // HMAC key needs the minimum length of MAC set to the output size of the associated
- // digest. This is because we do not offer a way to generate shorter MACs and
- // don't offer a way to verify MACs (other than by generating them).
- if (keymasterDigests.length != 1) {
- throw new ProviderException(
- "Unsupported number of authorized digests for HMAC key: "
- + keymasterDigests.length
- + ". Exactly one digest must be authorized");
- }
- int keymasterDigest = keymasterDigests[0];
- int digestOutputSizeBits = getDigestOutputSizeBits(keymasterDigest);
- if (digestOutputSizeBits == -1) {
- throw new ProviderException(
- "HMAC key authorized for unsupported digest: "
- + KeyProperties.Digest.fromKeymaster(keymasterDigest));
- }
- args.addUnsignedInt(KeymasterDefs.KM_TAG_MIN_MAC_LENGTH, digestOutputSizeBits);
- break;
- }
- }
-
static String getEcCurveFromKeymaster(int ecCurve) {
switch (ecCurve) {
case android.hardware.security.keymint.EcCurve.P_224:
diff --git a/libs/WindowManager/Shell/OWNERS b/libs/WindowManager/Shell/OWNERS
index ab2f3ef94eb6..68970e68de07 100644
--- a/libs/WindowManager/Shell/OWNERS
+++ b/libs/WindowManager/Shell/OWNERS
@@ -3,5 +3,5 @@ pbdr@google.com
pragyabajoria@google.com
# Give submodule owners in shell resource approval
-per-file res*/*/*.xml = atsjenk@google.com, hwwang@google.com, lbill@google.com, madym@google.com, vaniadesmonda@google.com, pbdr@google.com, mpodolian@google.com, liranb@google.com, pragyabajoria@google.com, uysalorhan@google.com, gsennton@google.com, mattsziklay@google.com, mdehaini@google.com
+per-file res*/*/*.xml = atsjenk@google.com, hwwang@google.com, lbill@google.com, madym@google.com, vaniadesmonda@google.com, pbdr@google.com, mpodolian@google.com, liranb@google.com, pragyabajoria@google.com, uysalorhan@google.com, gsennton@google.com, mattsziklay@google.com, mdehaini@google.com, peanutbutter@google.com, jeremysim@google.com
per-file res*/*/tv_*.xml = bronger@google.com
diff --git a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml
index 8d7e5fd95957..d50a14cf5dae 100644
--- a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml
+++ b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml
@@ -18,23 +18,28 @@
xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
android:id="@+id/maximize_menu"
android:layout_width="wrap_content"
- android:layout_height="@dimen/desktop_mode_maximize_menu_height"
+ android:layout_height="wrap_content"
android:background="@drawable/desktop_mode_maximize_menu_background"
android:elevation="1dp">
<LinearLayout
android:id="@+id/container"
android:layout_width="wrap_content"
- android:layout_height="@dimen/desktop_mode_maximize_menu_height"
+ android:layout_height="wrap_content"
android:orientation="horizontal"
- android:padding="16dp"
+ android:paddingHorizontal="12dp"
+ android:paddingVertical="16dp"
+ android:measureWithLargestChild="true"
android:gravity="center">
<LinearLayout
android:id="@+id/maximize_menu_immersive_toggle_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:orientation="vertical">
+ android:layout_weight="1"
+ android:orientation="vertical"
+ android:layout_marginStart="4dp"
+ android:layout_marginEnd="4dp">
<Button
android:layout_width="94dp"
@@ -44,21 +49,22 @@
android:stateListAnimator="@null"
android:importantForAccessibility="yes"
android:contentDescription="@string/desktop_mode_maximize_menu_immersive_button_text"
- android:layout_marginEnd="8dp"
android:layout_marginBottom="4dp"
android:alpha="0"/>
<TextView
android:id="@+id/maximize_menu_immersive_toggle_button_text"
- android:layout_width="94dp"
- android:layout_height="18dp"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
android:textSize="11sp"
- android:layout_marginBottom="76dp"
+ android:lineHeight="16sp"
android:gravity="center"
android:fontFamily="google-sans-text"
+ android:textFontWeight="500"
android:importantForAccessibility="no"
android:text="@string/desktop_mode_maximize_menu_immersive_button_text"
android:textColor="@androidprv:color/materialColorOnSurface"
+ android:singleLine="true"
android:alpha="0"/>
</LinearLayout>
@@ -66,7 +72,11 @@
android:id="@+id/maximize_menu_size_toggle_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:orientation="vertical">
+ android:layout_weight="1"
+ android:orientation="vertical"
+ android:gravity="center_horizontal"
+ android:layout_marginStart="4dp"
+ android:layout_marginEnd="4dp">
<Button
android:layout_width="94dp"
@@ -81,15 +91,17 @@
<TextView
android:id="@+id/maximize_menu_size_toggle_button_text"
- android:layout_width="94dp"
- android:layout_height="18dp"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
android:textSize="11sp"
- android:layout_marginBottom="76dp"
+ android:lineHeight="16sp"
android:gravity="center"
android:fontFamily="google-sans-text"
+ android:textFontWeight="500"
android:importantForAccessibility="no"
android:text="@string/desktop_mode_maximize_menu_maximize_text"
android:textColor="@androidprv:color/materialColorOnSurface"
+ android:singleLine="true"
android:alpha="0"/>
</LinearLayout>
@@ -97,7 +109,11 @@
android:id="@+id/maximize_menu_snap_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:orientation="vertical">
+ android:layout_weight="1"
+ android:orientation="vertical"
+ android:gravity="center_horizontal"
+ android:layout_marginStart="4dp"
+ android:layout_marginEnd="4dp">
<LinearLayout
android:id="@+id/maximize_menu_snap_menu_layout"
android:layout_width="wrap_content"
@@ -106,7 +122,6 @@
android:padding="4dp"
android:background="@drawable/desktop_mode_maximize_menu_layout_background"
android:layout_marginBottom="4dp"
- android:layout_marginStart="8dp"
android:alpha="0">
<Button
android:id="@+id/maximize_menu_snap_left_button"
@@ -131,16 +146,17 @@
</LinearLayout>
<TextView
android:id="@+id/maximize_menu_snap_window_text"
- android:layout_width="94dp"
- android:layout_height="18dp"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
android:textSize="11sp"
- android:layout_marginBottom="76dp"
- android:layout_gravity="center"
+ android:lineHeight="16sp"
android:gravity="center"
android:importantForAccessibility="no"
android:fontFamily="google-sans-text"
+ android:textFontWeight="500"
android:text="@string/desktop_mode_maximize_menu_snap_text"
android:textColor="@androidprv:color/materialColorOnSurface"
+ android:singleLine="true"
android:alpha="0"/>
</LinearLayout>
</LinearLayout>
@@ -150,6 +166,6 @@
<View
android:id="@+id/maximize_menu_overlay"
android:layout_width="match_parent"
- android:layout_height="@dimen/desktop_mode_maximize_menu_height"/>
+ android:layout_height="match_parent"/>
</FrameLayout>
diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml
index e395341a5792..f5f3f0fe52eb 100644
--- a/libs/WindowManager/Shell/res/values/dimen.xml
+++ b/libs/WindowManager/Shell/res/values/dimen.xml
@@ -498,14 +498,6 @@
<!-- The default minimum allowed window height when resizing a window in desktop mode. -->
<dimen name="desktop_mode_minimum_window_height">352dp</dimen>
- <!-- The width of the maximize menu in desktop mode, depending on the number of options -->
- <dimen name="desktop_mode_maximize_menu_width_one_options">126dp</dimen>
- <dimen name="desktop_mode_maximize_menu_width_two_options">228dp</dimen>
- <dimen name="desktop_mode_maximize_menu_width_three_options">330dp</dimen>
-
- <!-- The height of the maximize menu in desktop mode. -->
- <dimen name="desktop_mode_maximize_menu_height">114dp</dimen>
-
<!-- The padding of the maximize menu in desktop mode. -->
<dimen name="desktop_mode_menu_padding">16dp</dimen>
diff --git a/libs/WindowManager/Shell/shared/res/color/bubble_drop_target_background_color.xml b/libs/WindowManager/Shell/shared/res/color/bubble_drop_target_background_color.xml
new file mode 100644
index 000000000000..975d25b25953
--- /dev/null
+++ b/libs/WindowManager/Shell/shared/res/color/bubble_drop_target_background_color.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ Copyright (C) 2025 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android">
+ <item android:alpha="0.35" android:color="@androidprv:color/materialColorPrimaryContainer" />
+</selector>
diff --git a/libs/WindowManager/Shell/shared/res/drawable/bubble_drop_target_background.xml b/libs/WindowManager/Shell/shared/res/drawable/bubble_drop_target_background.xml
new file mode 100644
index 000000000000..89546f9b0807
--- /dev/null
+++ b/libs/WindowManager/Shell/shared/res/drawable/bubble_drop_target_background.xml
@@ -0,0 +1,25 @@
+<?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.
+ -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+ android:shape="rectangle">
+ <corners android:radius="28dp" />
+ <solid android:color="@color/bubble_drop_target_background_color" />
+ <stroke
+ android:width="1dp"
+ android:color="@androidprv:color/materialColorPrimaryContainer" />
+</shape>
diff --git a/libs/WindowManager/Shell/shared/res/values/dimen.xml b/libs/WindowManager/Shell/shared/res/values/dimen.xml
index d280083ae7f5..5f013c52d70d 100644
--- a/libs/WindowManager/Shell/shared/res/values/dimen.xml
+++ b/libs/WindowManager/Shell/shared/res/values/dimen.xml
@@ -36,4 +36,13 @@
<dimen name="drag_zone_v_split_from_expanded_view_height_tablet">285dp</dimen>
<dimen name="drag_zone_v_split_from_expanded_view_height_fold_tall">150dp</dimen>
<dimen name="drag_zone_v_split_from_expanded_view_height_fold_short">100dp</dimen>
+
+ <!-- Bubble drop target dimensions -->
+ <dimen name="drop_target_full_screen_padding">20dp</dimen>
+ <dimen name="drop_target_desktop_window_padding_small">100dp</dimen>
+ <dimen name="drop_target_desktop_window_padding_large">130dp</dimen>
+ <dimen name="drop_target_expanded_view_width">364</dimen>
+ <dimen name="drop_target_expanded_view_height">578</dimen>
+ <dimen name="drop_target_expanded_view_padding_bottom">108</dimen>
+ <dimen name="drop_target_expanded_view_padding_horizontal">24</dimen>
</resources> \ No newline at end of file
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/TransitionUtil.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/TransitionUtil.java
index 4d00c74155a8..851987269c10 100644
--- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/TransitionUtil.java
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/TransitionUtil.java
@@ -21,6 +21,7 @@ import static android.view.RemoteAnimationTarget.MODE_CHANGING;
import static android.view.RemoteAnimationTarget.MODE_CLOSING;
import static android.view.RemoteAnimationTarget.MODE_OPENING;
import static android.view.WindowManager.LayoutParams.INVALID_WINDOW_TYPE;
+import static android.view.WindowManager.LayoutParams.LAST_SYSTEM_WINDOW;
import static android.view.WindowManager.LayoutParams.TYPE_DOCK_DIVIDER;
import static android.view.WindowManager.TRANSIT_CHANGE;
import static android.view.WindowManager.TRANSIT_CLOSE;
@@ -55,9 +56,15 @@ import java.util.function.Predicate;
public class TransitionUtil {
/** Flag applied to a transition change to identify it as a divider bar for animation. */
public static final int FLAG_IS_DIVIDER_BAR = FLAG_FIRST_CUSTOM;
+ public static final int FLAG_IS_DIM_LAYER = FLAG_FIRST_CUSTOM << 1;
/** Flag applied to a transition change to identify it as a desktop wallpaper activity. */
- public static final int FLAG_IS_DESKTOP_WALLPAPER_ACTIVITY = FLAG_FIRST_CUSTOM << 1;
+ public static final int FLAG_IS_DESKTOP_WALLPAPER_ACTIVITY = FLAG_FIRST_CUSTOM << 2;
+
+ /**
+ * Applied to a {@link RemoteAnimationTarget} to identify dim layers for animation in Launcher.
+ */
+ public static final int TYPE_SPLIT_SCREEN_DIM_LAYER = LAST_SYSTEM_WINDOW + 1;
/** @return true if the transition was triggered by opening something vs closing something */
public static boolean isOpeningType(@WindowManager.TransitionType int type) {
@@ -117,6 +124,11 @@ public class TransitionUtil {
return isNonApp(change) && change.hasFlags(FLAG_IS_DIVIDER_BAR);
}
+ /** Returns `true` if `change` is an app's dim layer. */
+ public static boolean isDimLayer(TransitionInfo.Change change) {
+ return isNonApp(change) && change.hasFlags(FLAG_IS_DIM_LAYER);
+ }
+
/** Returns `true` if `change` is only re-ordering. */
public static boolean isOrderOnly(TransitionInfo.Change change) {
return change.getMode() == TRANSIT_CHANGE
@@ -231,6 +243,14 @@ public class TransitionUtil {
t.setLayer(leash, Integer.MAX_VALUE);
return;
}
+ if (isDimLayer(change)) {
+ // When a dim layer gets reparented onto the transition root, we need to zero out its
+ // position so that it's in line with everything else on the transition root. Also,
+ // we need to set a crop because we don't want it applying MATCH_PARENT on the whole
+ // root surface.
+ t.setPosition(leash, 0, 0);
+ t.setCrop(leash, change.getEndAbsBounds());
+ }
// Put all the OPEN/SHOW on top
if ((change.getFlags() & FLAG_IS_WALLPAPER) != 0) {
@@ -284,14 +304,19 @@ public class TransitionUtil {
// Copied Transitions setup code (which expects bottom-to-top order, so we swap here)
setupLeash(leashSurface, change, info.getChanges().size() - order, info, t);
t.reparent(change.getLeash(), leashSurface);
- t.setAlpha(change.getLeash(), 1.0f);
- t.show(change.getLeash());
+ if (!isDimLayer(change)) {
+ // Most leashes going onto the transition root should have their alpha set here to make
+ // them visible. But dim layers should be left untouched (their alpha value is their
+ // actual dim value).
+ t.setAlpha(change.getLeash(), 1.0f);
+ }
if (!isDividerBar(change)) {
// For divider, don't modify its inner leash position when creating the outer leash
// for the transition. In case the position being wrong after the transition finished.
t.setPosition(change.getLeash(), 0, 0);
}
t.setLayer(change.getLeash(), 0);
+ t.show(change.getLeash());
return leashSurface;
}
@@ -333,6 +358,9 @@ public class TransitionUtil {
if (isDividerBar(change)) {
return getDividerTarget(change, leash);
}
+ if (isDimLayer(change)) {
+ return getDimLayerTarget(change, leash);
+ }
int taskId;
boolean isNotInRecents;
@@ -439,6 +467,17 @@ public class TransitionUtil {
TYPE_DOCK_DIVIDER);
}
+ private static RemoteAnimationTarget getDimLayerTarget(TransitionInfo.Change change,
+ SurfaceControl leash) {
+ return new RemoteAnimationTarget(-1 /* taskId */, newModeToLegacyMode(change.getMode()),
+ leash, false /* isTranslucent */, null /* clipRect */,
+ null /* contentInsets */, Integer.MAX_VALUE /* prefixOrderIndex */,
+ new android.graphics.Point(0, 0) /* position */, change.getStartAbsBounds(),
+ change.getStartAbsBounds(), new WindowConfiguration(), true, null /* startLeash */,
+ null /* startBounds */, null /* taskInfo */, false /* allowEnterPip */,
+ TYPE_SPLIT_SCREEN_DIM_LAYER);
+ }
+
/**
* Finds the "correct" root idx for a change. The change's end display is prioritized, then
* the start display. If there is no display, it will fallback on the 0th root in the
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/animation/Interpolators.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/animation/Interpolators.java
index f45dc3a1e892..e92c1eb81e89 100644
--- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/animation/Interpolators.java
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/animation/Interpolators.java
@@ -93,10 +93,21 @@ public class Interpolators {
public static final PathInterpolator SLOWDOWN_INTERPOLATOR =
new PathInterpolator(0.5f, 1f, 0.5f, 1f);
+ /**
+ * An interpolator used for dimming a task as it travels offscreen, or towards a distant dismiss
+ * point. A sharp rise, followed by a steady middle, and ending with another sharp rise.
+ */
public static final PathInterpolator DIM_INTERPOLATOR =
new PathInterpolator(.23f, .87f, .52f, -0.11f);
/**
+ * An interpolator used for dimming a task very quickly. Roughly approximates one of the "sharp
+ * rises" of {@link #DIM_INTERPOLATOR}.
+ */
+ public static final PathInterpolator FAST_DIM_INTERPOLATOR =
+ new PathInterpolator(0.23f, 0.87f, 0.83f, 0.83f);
+
+ /**
* Use this interpolator for animating progress values coming from the back callback to get
* the predictive-back-typical decelerate motion.
*
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/BubbleBarLocation.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/BubbleBarLocation.kt
index 481fc7fcb869..6acd9dbe8b91 100644
--- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/BubbleBarLocation.kt
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/BubbleBarLocation.kt
@@ -70,7 +70,8 @@ enum class BubbleBarLocation : Parcelable {
UpdateSource.A11Y_ACTION_BAR,
UpdateSource.A11Y_ACTION_BUBBLE,
UpdateSource.A11Y_ACTION_EXP_VIEW,
- UpdateSource.APP_ICON_DRAG
+ UpdateSource.APP_ICON_DRAG,
+ UpdateSource.DRAG_TASK,
)
@Retention(AnnotationRetention.SOURCE)
annotation class UpdateSource {
@@ -95,6 +96,9 @@ enum class BubbleBarLocation : Parcelable {
/** Location changed from dragging the application icon to the bubble bar */
const val APP_ICON_DRAG = 7
+
+ /** Location changed from dragging a running task to the bubble bar */
+ const val DRAG_TASK = 8
}
}
}
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DragZoneFactory.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DragZoneFactory.kt
index 909e9d2c4428..1a80b0f29aa9 100644
--- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DragZoneFactory.kt
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DragZoneFactory.kt
@@ -18,6 +18,7 @@ package com.android.wm.shell.shared.bubbles
import android.content.Context
import android.graphics.Rect
+import android.util.TypedValue
import androidx.annotation.DimenRes
import com.android.wm.shell.shared.R
import com.android.wm.shell.shared.bubbles.DragZoneFactory.SplitScreenModeChecker.SplitScreenMode
@@ -50,6 +51,60 @@ class DragZoneFactory(
private var vSplitFromExpandedViewDragZoneHeightFoldTall = 0
private var vSplitFromExpandedViewDragZoneHeightFoldShort = 0
+ private var fullScreenDropTargetPadding = 0
+ private var desktopWindowDropTargetPaddingSmall = 0
+ private var desktopWindowDropTargetPaddingLarge = 0
+ private var expandedViewDropTargetWidth = 0
+ private var expandedViewDropTargetHeight = 0
+ private var expandedViewDropTargetPaddingBottom = 0
+ private var expandedViewDropTargetPaddingHorizontal = 0
+
+ private val fullScreenDropTarget: Rect
+ get() =
+ Rect(windowBounds).apply {
+ inset(fullScreenDropTargetPadding, fullScreenDropTargetPadding)
+ }
+
+ private val desktopWindowDropTarget: Rect
+ get() =
+ Rect(windowBounds).apply {
+ if (deviceConfig.isLandscape) {
+ inset(
+ /* dx= */ desktopWindowDropTargetPaddingLarge,
+ /* dy= */ desktopWindowDropTargetPaddingSmall
+ )
+ } else {
+ inset(
+ /* dx= */ desktopWindowDropTargetPaddingSmall,
+ /* dy= */ desktopWindowDropTargetPaddingLarge
+ )
+ }
+ }
+
+ private val expandedViewDropTargetLeft: Rect
+ get() =
+ Rect(
+ expandedViewDropTargetPaddingHorizontal,
+ windowBounds.bottom -
+ expandedViewDropTargetPaddingBottom -
+ expandedViewDropTargetHeight,
+ expandedViewDropTargetWidth + expandedViewDropTargetPaddingHorizontal,
+ windowBounds.bottom - expandedViewDropTargetPaddingBottom
+ )
+
+ private val expandedViewDropTargetRight: Rect
+ get() =
+ Rect(
+ windowBounds.right -
+ expandedViewDropTargetPaddingHorizontal -
+ expandedViewDropTargetWidth,
+ windowBounds.bottom -
+ expandedViewDropTargetPaddingBottom -
+ expandedViewDropTargetHeight,
+ windowBounds.right - expandedViewDropTargetPaddingHorizontal,
+ windowBounds.bottom - expandedViewDropTargetPaddingBottom
+ )
+
init {
onConfigurationUpdated()
}
@@ -88,11 +143,32 @@ class DragZoneFactory(
context.resolveDimension(R.dimen.drag_zone_v_split_from_expanded_view_height_fold_tall)
vSplitFromExpandedViewDragZoneHeightFoldShort =
context.resolveDimension(R.dimen.drag_zone_v_split_from_expanded_view_height_fold_short)
+ fullScreenDropTargetPadding =
+ context.resolveDimension(R.dimen.drop_target_full_screen_padding)
+ desktopWindowDropTargetPaddingSmall =
+ context.resolveDimension(R.dimen.drop_target_desktop_window_padding_small)
+ desktopWindowDropTargetPaddingLarge =
+ context.resolveDimension(R.dimen.drop_target_desktop_window_padding_large)
+
+ // TODO b/393172431: Use the shared xml resources once we can easily access them from
+ // launcher
+ expandedViewDropTargetWidth = 364.dpToPx()
+ expandedViewDropTargetHeight = 578.dpToPx()
+ expandedViewDropTargetPaddingBottom = 108.dpToPx()
+ expandedViewDropTargetPaddingHorizontal = 24.dpToPx()
}
private fun Context.resolveDimension(@DimenRes dimension: Int) =
resources.getDimensionPixelSize(dimension)
+ private fun Int.dpToPx() =
+ TypedValue.applyDimension(
+ TypedValue.COMPLEX_UNIT_DIP,
+ this.toFloat(),
+ context.resources.displayMetrics
+ )
+ .toInt()
+
/**
* Creates the list of drag zones for the dragged object.
*
@@ -155,7 +231,7 @@ class DragZoneFactory(
DragZone.Bubble.Left(
bounds =
Rect(0, windowBounds.bottom - dragZoneSize, dragZoneSize, windowBounds.bottom),
- dropTarget = Rect(0, 0, 0, 0),
+ dropTarget = expandedViewDropTargetLeft,
),
DragZone.Bubble.Right(
bounds =
@@ -165,7 +241,7 @@ class DragZoneFactory(
windowBounds.right,
windowBounds.bottom,
),
- dropTarget = Rect(0, 0, 0, 0),
+ dropTarget = expandedViewDropTargetRight,
)
)
}
@@ -174,7 +250,7 @@ class DragZoneFactory(
return listOf(
DragZone.Bubble.Left(
bounds = Rect(0, 0, windowBounds.right / 2, windowBounds.bottom),
- dropTarget = Rect(0, 0, 0, 0),
+ dropTarget = expandedViewDropTargetLeft,
),
DragZone.Bubble.Right(
bounds =
@@ -184,7 +260,7 @@ class DragZoneFactory(
windowBounds.right,
windowBounds.bottom,
),
- dropTarget = Rect(0, 0, 0, 0),
+ dropTarget = expandedViewDropTargetRight,
)
)
}
@@ -198,7 +274,7 @@ class DragZoneFactory(
windowBounds.right / 2 + fullScreenDragZoneWidth / 2,
fullScreenDragZoneHeight
),
- dropTarget = Rect(0, 0, 0, 0)
+ dropTarget = fullScreenDropTarget
)
}
@@ -223,7 +299,7 @@ class DragZoneFactory(
windowBounds.bottom / 2 + desktopWindowDragZoneHeight / 2
)
},
- dropTarget = Rect(0, 0, 0, 0)
+ dropTarget = desktopWindowDropTarget
)
}
@@ -236,7 +312,7 @@ class DragZoneFactory(
windowBounds.right / 2 + desktopWindowFromExpandedViewDragZoneWidth / 2,
windowBounds.bottom / 2 + desktopWindowFromExpandedViewDragZoneHeight / 2
),
- dropTarget = Rect(0, 0, 0, 0)
+ dropTarget = desktopWindowDropTarget
)
}
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 643c1506e4c2..00c446c3da60 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
@@ -27,6 +27,7 @@ import android.hardware.display.DisplayManager;
import android.os.SystemProperties;
import android.view.Display;
import android.view.WindowManager;
+import android.window.DesktopExperienceFlags;
import android.window.DesktopModeFlags;
import com.android.internal.R;
@@ -226,6 +227,7 @@ public class DesktopModeStatus {
return context.getResources().getBoolean(R.bool.config_canInternalDisplayHostDesktops);
}
+
/**
* Return {@code true} if desktop mode dev option should be shown on current device
*/
@@ -239,23 +241,22 @@ public class DesktopModeStatus {
*/
public static boolean canShowDesktopExperienceDevOption(@NonNull Context context) {
return Flags.showDesktopExperienceDevOption()
- && isInternalDisplayEligibleToHostDesktops(context);
+ && isDeviceEligibleForDesktopMode(context);
}
/** Returns if desktop mode dev option should be enabled if there is no user override. */
public static boolean shouldDevOptionBeEnabledByDefault(Context context) {
- return isInternalDisplayEligibleToHostDesktops(context)
- && Flags.enableDesktopWindowingMode();
+ return isDeviceEligibleForDesktopMode(context)
+ && Flags.enableDesktopWindowingMode();
}
/**
* Return {@code true} if desktop mode is enabled and can be entered on the current device.
*/
public static boolean canEnterDesktopMode(@NonNull Context context) {
- return (isInternalDisplayEligibleToHostDesktops(context)
- && DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_MODE.isTrue()
- && (isDesktopModeSupported(context) || !enforceDeviceRestrictions())
- || isDesktopModeEnabledByDevOption(context));
+ return (isDeviceEligibleForDesktopMode(context)
+ && DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_MODE.isTrue())
+ || isDesktopModeEnabledByDevOption(context);
}
/**
@@ -271,7 +272,7 @@ public class DesktopModeStatus {
* frontend implementations).
*/
public static boolean enableMultipleDesktops(@NonNull Context context) {
- return Flags.enableMultipleDesktopsBackend()
+ return DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue()
&& Flags.enableMultipleDesktopsFrontend()
&& canEnterDesktopMode(context);
}
@@ -323,25 +324,34 @@ public class DesktopModeStatus {
}
/**
- * Return {@code true} if desktop sessions is unrestricted and can be host for the device's
- * internal display.
+ * Return {@code true} if desktop mode is unrestricted and is supported on the device.
*/
- public static boolean isInternalDisplayEligibleToHostDesktops(@NonNull Context context) {
- return !enforceDeviceRestrictions() || canInternalDisplayHostDesktops(context) || (
- Flags.enableDesktopModeThroughDevOption() && isDesktopModeDevOptionSupported(
- context));
+ public static boolean isDeviceEligibleForDesktopMode(@NonNull Context context) {
+ if (!enforceDeviceRestrictions()) {
+ return true;
+ }
+ final boolean desktopModeSupported = isDesktopModeSupported(context)
+ && canInternalDisplayHostDesktops(context);
+ final boolean desktopModeSupportedByDevOptions =
+ Flags.enableDesktopModeThroughDevOption()
+ && isDesktopModeDevOptionSupported(context);
+ return desktopModeSupported || desktopModeSupportedByDevOptions;
}
/**
* Return {@code true} if the developer option for desktop mode is unrestricted and is supported
* in the device.
*
- * Note that, if {@link #isInternalDisplayEligibleToHostDesktops(Context)} is true, then
+ * Note that, if {@link #isDeviceEligibleForDesktopMode(Context)} is true, then
* {@link #isDeviceEligibleForDesktopModeDevOption(Context)} is also true.
*/
private static boolean isDeviceEligibleForDesktopModeDevOption(@NonNull Context context) {
- return !enforceDeviceRestrictions() || isDesktopModeSupported(context)
- || isDesktopModeDevOptionSupported(context);
+ if (!enforceDeviceRestrictions()) {
+ return true;
+ }
+ final boolean desktopModeSupported = isDesktopModeSupported(context)
+ && canInternalDisplayHostDesktops(context);
+ return desktopModeSupported || isDesktopModeDevOptionSupported(context);
}
/**
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/split/SplitScreenConstants.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/split/SplitScreenConstants.java
index b48296f5f76a..759e711100c3 100644
--- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/split/SplitScreenConstants.java
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/split/SplitScreenConstants.java
@@ -262,6 +262,7 @@ public class SplitScreenConstants {
/** Flag applied to a transition change to identify it as a divider bar for animation. */
public static final int FLAG_IS_DIVIDER_BAR = TransitionUtil.FLAG_IS_DIVIDER_BAR;
+ public static final int FLAG_IS_DIM_LAYER = TransitionUtil.FLAG_IS_DIM_LAYER;
public static final String splitPositionToString(@SplitPosition int pos) {
switch (pos) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
index f51023fcaaf5..58b46d202599 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
@@ -845,6 +845,10 @@ public class BubbleController implements ConfigurationChangeListener,
mLogger.log(onLeft ? BubbleLogger.Event.BUBBLE_BAR_MOVED_LEFT_APP_ICON_DROP
: BubbleLogger.Event.BUBBLE_BAR_MOVED_RIGHT_APP_ICON_DROP);
break;
+ case BubbleBarLocation.UpdateSource.DRAG_TASK:
+ mLogger.log(onLeft ? BubbleLogger.Event.BUBBLE_BAR_MOVED_LEFT_DRAG_TASK
+ : BubbleLogger.Event.BUBBLE_BAR_MOVED_RIGHT_DRAG_TASK);
+ break;
}
}
@@ -1291,6 +1295,11 @@ public class BubbleController implements ConfigurationChangeListener,
mContext.getResources().getDimensionPixelSize(
com.android.internal.R.dimen.importance_ring_stroke_width));
mStackView.onDisplaySizeChanged();
+ // TODO b/392893178: Merge the unfold and the task view transition so that we don't
+ // have to post a delayed runnable to the looper to update the bounds
+ if (mStackView.isExpanded()) {
+ mStackView.postDelayed(() -> mStackView.updateExpandedView(), 500);
+ }
}
if (newConfig.fontScale != mFontScale) {
mFontScale = newConfig.fontScale;
@@ -1598,13 +1607,21 @@ public class BubbleController implements ConfigurationChangeListener,
if (!BubbleAnythingFlagHelper.enableBubbleToFullscreen()) return;
Bubble b = mBubbleData.getOrCreateBubble(taskInfo); // Removes from overflow
ProtoLog.v(WM_SHELL_BUBBLES, "expandStackAndSelectBubble - intent=%s", taskInfo.taskId);
+ BubbleBarLocation location = null;
+ if (dragData != null) {
+ location =
+ dragData.isReleasedOnLeft() ? BubbleBarLocation.LEFT : BubbleBarLocation.RIGHT;
+ }
if (b.isInflated()) {
- mBubbleData.setSelectedBubbleAndExpandStack(b);
+ mBubbleData.setSelectedBubbleAndExpandStack(b, location);
if (dragData != null && dragData.getPendingWct() != null) {
mTransitions.startTransition(TRANSIT_CHANGE,
dragData.getPendingWct(), /* handler= */ null);
}
} else {
+ if (location != null) {
+ setBubbleBarLocation(location, BubbleBarLocation.UpdateSource.DRAG_TASK);
+ }
b.enable(Notification.BubbleMetadata.FLAG_AUTO_EXPAND_BUBBLE);
// Lazy init stack view when a bubble is created
ensureBubbleViewsAndWindowCreated();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleLogger.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleLogger.java
index 831f2271d500..a0c473173bf1 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleLogger.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleLogger.java
@@ -156,6 +156,12 @@ public class BubbleLogger {
@UiEvent(doc = "while bubble bar is expanded, switch to another/existing bubble")
BUBBLE_BAR_BUBBLE_SWITCHED(1977),
+ @UiEvent(doc = "bubble bar moved to the left edge of the screen by dragging a task")
+ BUBBLE_BAR_MOVED_LEFT_DRAG_TASK(2146),
+
+ @UiEvent(doc = "bubble bar moved to the right edge of the screen by dragging a task")
+ BUBBLE_BAR_MOVED_RIGHT_DRAG_TASK(2147),
+
// endregion
;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
index dad627f85d95..92724178cf84 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
@@ -3548,7 +3548,7 @@ public class BubbleStackView extends FrameLayout
}
}
- private void updateExpandedView() {
+ void updateExpandedView() {
boolean isOverflowExpanded = mExpandedBubble != null
&& BubbleOverflow.KEY.equals(mExpandedBubble.getKey());
int[] paddings = mPositioner.getExpandedViewContainerPadding(
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTransitions.java
index 6be3c1f18b39..a676f41baafe 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTransitions.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTransitions.java
@@ -156,14 +156,18 @@ public class BubbleTransitions {
public static class DragData {
private final Rect mBounds;
private final WindowContainerTransaction mPendingWct;
+ private final boolean mReleasedOnLeft;
/**
* @param bounds bounds of the dragged task when the drag was released
* @param wct pending operations to be applied when finishing the drag
+ * @param releasedOnLeft true if the bubble was released in the left drop target
*/
- public DragData(@Nullable Rect bounds, @Nullable WindowContainerTransaction wct) {
+ public DragData(@Nullable Rect bounds, @Nullable WindowContainerTransaction wct,
+ boolean releasedOnLeft) {
mBounds = bounds;
mPendingWct = wct;
+ mReleasedOnLeft = releasedOnLeft;
}
/**
@@ -181,6 +185,13 @@ public class BubbleTransitions {
public WindowContainerTransaction getPendingWct() {
return mPendingWct;
}
+
+ /**
+ * @return true if the bubble was released in the left drop target
+ */
+ public boolean isReleasedOnLeft() {
+ return mReleasedOnLeft;
+ }
}
/**
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 4c3bde9b2b3a..97184c859d4d 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
@@ -67,6 +67,7 @@ public class DisplayController {
private final SparseArray<DisplayRecord> mDisplays = new SparseArray<>();
private final ArrayList<OnDisplaysChangedListener> mDisplayChangedListeners = new ArrayList<>();
private final Map<Integer, RectF> mUnpopulatedDisplayBounds = new HashMap<>();
+ private DisplayTopology mDisplayTopology;
public DisplayController(Context context, IWindowManager wmService, ShellInit shellInit,
ShellExecutor mainExecutor, DisplayManager displayManager) {
@@ -157,6 +158,7 @@ public class DisplayController {
for (int i = 0; i < mDisplays.size(); ++i) {
listener.onDisplayAdded(mDisplays.keyAt(i));
}
+ listener.onTopologyChanged(mDisplayTopology);
}
}
@@ -245,6 +247,7 @@ public class DisplayController {
if (topology == null) {
return;
}
+ mDisplayTopology = topology;
SparseArray<RectF> absoluteBounds = topology.getAbsoluteBounds();
mUnpopulatedDisplayBounds.clear();
for (int i = 0; i < absoluteBounds.size(); ++i) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/WindowContainerTransactionSupplier.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/common/WindowContainerTransactionSupplier.kt
new file mode 100644
index 000000000000..a1d700af5569
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/WindowContainerTransactionSupplier.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.common
+
+import android.window.WindowContainerTransaction
+import com.android.wm.shell.dagger.WMSingleton
+import java.util.function.Supplier
+import javax.inject.Inject
+
+/**
+ * An Injectable [Supplier<WindowContainerTransaction>]. This can be used in place of kotlin default
+ * parameters values [builder = ::WindowContainerTransaction] which requires the
+ * [@JvmOverloads] annotation to make this available in Java.
+ * This can be used every time a component needs the dependency to the default [Supplier] for
+ * [WindowContainerTransaction]s.
+ */
+@WMSingleton
+class WindowContainerTransactionSupplier @Inject constructor(
+) : Supplier<WindowContainerTransaction> {
+ override fun get(): WindowContainerTransaction = WindowContainerTransaction()
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/CenterParallaxSpec.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/CenterParallaxSpec.java
new file mode 100644
index 000000000000..fb2a324375b6
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/CenterParallaxSpec.java
@@ -0,0 +1,39 @@
+/*
+ * 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.common.split;
+
+import android.graphics.Point;
+import android.graphics.Rect;
+
+/**
+ * Calculation class, used when
+ * {@link com.android.wm.shell.common.split.SplitLayout#PARALLAX_ALIGN_CENTER} is the desired
+ * parallax effect.
+ */
+public class CenterParallaxSpec implements ParallaxSpec {
+ @Override
+ public void getParallax(Point retreatingOut, Point advancingOut, int position,
+ DividerSnapAlgorithm snapAlgorithm, boolean isLeftRightSplit, Rect displayBounds,
+ Rect retreatingSurface, Rect retreatingContent, Rect advancingSurface,
+ Rect advancingContent, int dimmingSide, boolean topLeftShrink) {
+ if (isLeftRightSplit) {
+ retreatingOut.x = (retreatingSurface.width() - retreatingContent.width()) / 2;
+ } else {
+ retreatingOut.y = (retreatingSurface.height() - retreatingContent.height()) / 2;
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DismissingParallaxSpec.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DismissingParallaxSpec.java
new file mode 100644
index 000000000000..39ecbb379d7d
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DismissingParallaxSpec.java
@@ -0,0 +1,75 @@
+/*
+ * 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.common.split;
+
+import static android.view.WindowManager.DOCKED_INVALID;
+
+import static com.android.wm.shell.shared.animation.Interpolators.SLOWDOWN_INTERPOLATOR;
+
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.view.WindowManager;
+
+/**
+ * Calculation class, used when
+ * {@link com.android.wm.shell.common.split.SplitLayout#PARALLAX_DISMISSING} is the desired parallax
+ * effect.
+ */
+public class DismissingParallaxSpec implements ParallaxSpec {
+ @Override
+ public void getParallax(Point retreatingOut, Point advancingOut, int position,
+ DividerSnapAlgorithm snapAlgorithm, boolean isLeftRightSplit, Rect displayBounds,
+ Rect retreatingSurface, Rect retreatingContent, Rect advancingSurface,
+ Rect advancingContent, int dimmingSide, boolean topLeftShrink) {
+ if (dimmingSide == DOCKED_INVALID) {
+ return;
+ }
+
+ float progressTowardScreenEdge =
+ Math.max(0, Math.min(snapAlgorithm.calculateDismissingFraction(position), 1f));
+ int totalDismissingDistance = 0;
+ if (position < snapAlgorithm.getFirstSplitTarget().getPosition()) {
+ totalDismissingDistance = snapAlgorithm.getDismissStartTarget().getPosition()
+ - snapAlgorithm.getFirstSplitTarget().getPosition();
+ } else if (position > snapAlgorithm.getLastSplitTarget().getPosition()) {
+ totalDismissingDistance = snapAlgorithm.getLastSplitTarget().getPosition()
+ - snapAlgorithm.getDismissEndTarget().getPosition();
+ }
+
+ float parallaxFraction =
+ calculateParallaxDismissingFraction(progressTowardScreenEdge, dimmingSide);
+ if (isLeftRightSplit) {
+ retreatingOut.x = (int) (parallaxFraction * totalDismissingDistance);
+ } else {
+ retreatingOut.y = (int) (parallaxFraction * totalDismissingDistance);
+ }
+ }
+
+ /**
+ * @return for a specified {@code fraction}, this returns an adjusted value that simulates a
+ * slowing down parallax effect
+ */
+ private float calculateParallaxDismissingFraction(float fraction, int dockSide) {
+ float result = SLOWDOWN_INTERPOLATOR.getInterpolation(fraction) / 3.5f;
+
+ // Less parallax at the top, just because.
+ if (dockSide == WindowManager.DOCKED_TOP) {
+ result /= 2f;
+ }
+ return result;
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerSnapAlgorithm.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerSnapAlgorithm.java
index 2f5afcaa907b..5b2dd97a338f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerSnapAlgorithm.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerSnapAlgorithm.java
@@ -465,5 +465,9 @@ public class DividerSnapAlgorithm {
this.snapPosition = snapPosition;
this.distanceMultiplier = distanceMultiplier;
}
+
+ public int getPosition() {
+ return position;
+ }
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/FlexParallaxSpec.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/FlexParallaxSpec.java
new file mode 100644
index 000000000000..9fa162164e0e
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/FlexParallaxSpec.java
@@ -0,0 +1,172 @@
+/*
+ * 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.common.split;
+
+import static android.view.WindowManager.DOCKED_BOTTOM;
+import static android.view.WindowManager.DOCKED_INVALID;
+import static android.view.WindowManager.DOCKED_LEFT;
+import static android.view.WindowManager.DOCKED_RIGHT;
+import static android.view.WindowManager.DOCKED_TOP;
+
+import static com.android.wm.shell.common.split.ResizingEffectPolicy.DEFAULT_OFFSCREEN_DIM;
+import static com.android.wm.shell.shared.animation.Interpolators.DIM_INTERPOLATOR;
+import static com.android.wm.shell.shared.animation.Interpolators.FAST_DIM_INTERPOLATOR;
+
+import android.graphics.Point;
+import android.graphics.Rect;
+
+/**
+ * Calculation class, used when {@link com.android.wm.shell.common.split.SplitLayout#PARALLAX_FLEX}
+ * is the desired parallax effect.
+ */
+public class FlexParallaxSpec implements ParallaxSpec {
+ final Rect mTempRect = new Rect();
+
+ @Override
+ public int getDimmingSide(int position, DividerSnapAlgorithm snapAlgorithm,
+ boolean isLeftRightSplit) {
+ if (position < snapAlgorithm.getMiddleTarget().getPosition()) {
+ return isLeftRightSplit ? DOCKED_LEFT : DOCKED_TOP;
+ } else if (position > snapAlgorithm.getMiddleTarget().getPosition()) {
+ return isLeftRightSplit ? DOCKED_RIGHT : DOCKED_BOTTOM;
+ }
+ return DOCKED_INVALID;
+ }
+
+ /**
+ * Calculates the amount of dim to apply to a task surface moving offscreen in flexible split.
+ * In flexible split, there are two dimming "behaviors".
+ * 1) "slow dim": when moving the divider from the middle of the screen to a target at 10% or
+ * 90%, we dim the app slightly as it moves partially offscreen.
+ * 2) "fast dim": when moving the divider from a side snap target further toward the screen
+ * edge, we dim the app rapidly as it approaches the dismiss point.
+ * @return 0f = no dim applied. 1f = full black.
+ */
+ public float getDimValue(int position, DividerSnapAlgorithm snapAlgorithm) {
+ int startDismissPos = snapAlgorithm.getDismissStartTarget().getPosition();
+ int firstTargetPos = snapAlgorithm.getFirstSplitTarget().getPosition();
+ int middleTargetPos = snapAlgorithm.getMiddleTarget().getPosition();
+ int lastTargetPos = snapAlgorithm.getLastSplitTarget().getPosition();
+ int endDismissPos = snapAlgorithm.getDismissEndTarget().getPosition();
+ float progress;
+
+ if (startDismissPos <= position && position < firstTargetPos) {
+ // Divider is on the left/top (between 0% and 10% of screen), "fast dim" as it moves
+ // toward the screen edge
+ progress = (float) (firstTargetPos - position) / (firstTargetPos - startDismissPos);
+ return fastDim(progress);
+ } else if (firstTargetPos <= position && position < middleTargetPos) {
+ // Divider is between 10% and 50%, "slow dim" as it moves toward the left/top target
+ progress = (float) (middleTargetPos - position) / (middleTargetPos - firstTargetPos);
+ return slowDim(progress);
+ } else if (middleTargetPos <= position && position < lastTargetPos) {
+ // Divider is between 50% and 90%, "slow dim" as it moves toward the right/bottom target
+ progress = (float) (position - middleTargetPos) / (lastTargetPos - middleTargetPos);
+ return slowDim(progress);
+ } else if (lastTargetPos <= position && position <= endDismissPos) {
+ // Divider is on the right/bottom (between 90% and 100% of screen), "fast dim" as it
+ // moves toward screen edge
+ progress = (float) (position - lastTargetPos) / (endDismissPos - lastTargetPos);
+ return fastDim(progress);
+ }
+ return 0f;
+ }
+
+ /**
+ * Used by {@link #getDimValue} to determine the amount to dim an app. Starts at zero and ramps
+ * up to the default amount of dimming for an offscreen app,
+ * {@link ResizingEffectPolicy#DEFAULT_OFFSCREEN_DIM}.
+ */
+ private float slowDim(float progress) {
+ return DIM_INTERPOLATOR.getInterpolation(progress) * DEFAULT_OFFSCREEN_DIM;
+ }
+
+ /**
+ * Used by {@link #getDimValue} to determine the amount to dim an app. Starts at
+ * {@link ResizingEffectPolicy#DEFAULT_OFFSCREEN_DIM} and ramps up to 100% dim (full black).
+ */
+ private float fastDim(float progress) {
+ return DEFAULT_OFFSCREEN_DIM + (FAST_DIM_INTERPOLATOR.getInterpolation(progress)
+ * (1 - DEFAULT_OFFSCREEN_DIM));
+ }
+
+ @Override
+ public void getParallax(Point retreatingOut, Point advancingOut, int position,
+ DividerSnapAlgorithm snapAlgorithm, boolean isLeftRightSplit, Rect displayBounds,
+ Rect retreatingSurface, Rect retreatingContent, Rect advancingSurface,
+ Rect advancingContent, int dimmingSide, boolean topLeftShrink) {
+ // Whether an app is getting pushed offscreen by the divider.
+ boolean isRetreatingOffscreen = !displayBounds.contains(retreatingSurface);
+ // Whether an app was getting pulled onscreen at the beginning of the drag.
+ boolean advancingSideStartedOffscreen = !displayBounds.contains(advancingContent);
+
+ // The simpler case when an app gets pushed offscreen (e.g. 50:50 -> 90:10)
+ if (isRetreatingOffscreen && !advancingSideStartedOffscreen) {
+ // On the left side, we use parallax to simulate the contents sticking to the
+ // divider. This is because surfaces naturally expand to the bottom and right,
+ // so when a surface's area expands, the contents stick to the left. This is
+ // correct behavior on the right-side surface, but not the left.
+ if (topLeftShrink) {
+ if (isLeftRightSplit) {
+ retreatingOut.x = retreatingSurface.width() - retreatingContent.width();
+ } else {
+ retreatingOut.y = retreatingSurface.height() - retreatingContent.height();
+ }
+ }
+ // All other cases (e.g. 10:90 -> 50:50, 10:90 -> 90:10, 10:90 -> dismiss)
+ } else {
+ mTempRect.set(retreatingSurface);
+ Point rootOffset = new Point();
+ // 10:90 -> 50:50, 10:90, or dismiss right
+ if (advancingSideStartedOffscreen) {
+ // We have to handle a complicated case here to keep the parallax smooth.
+ // When the divider crosses the 50% mark, the retreating-side app surface
+ // will start expanding offscreen. This is expected and unavoidable, but
+ // makes the parallax look disjointed. In order to preserve the illusion,
+ // we add another offset (rootOffset) to simulate the surface staying
+ // onscreen.
+ if (mTempRect.intersect(displayBounds)) {
+ if (retreatingSurface.left < displayBounds.left) {
+ rootOffset.x = displayBounds.left - retreatingSurface.left;
+ }
+ if (retreatingSurface.top < displayBounds.top) {
+ rootOffset.y = displayBounds.top - retreatingSurface.top;
+ }
+ }
+
+ // On the left side, we again have to simulate the contents sticking to the
+ // divider.
+ if (!topLeftShrink) {
+ if (isLeftRightSplit) {
+ advancingOut.x = advancingSurface.width() - advancingContent.width();
+ } else {
+ advancingOut.y = advancingSurface.height() - advancingContent.height();
+ }
+ }
+ }
+
+ // In all these cases, the shrinking app also receives a center parallax.
+ if (isLeftRightSplit) {
+ retreatingOut.x = rootOffset.x
+ + ((mTempRect.width() - retreatingContent.width()) / 2);
+ } else {
+ retreatingOut.y = rootOffset.y
+ + ((mTempRect.height() - retreatingContent.height()) / 2);
+ }
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/NoParallaxSpec.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/NoParallaxSpec.java
new file mode 100644
index 000000000000..043b2880f28b
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/NoParallaxSpec.java
@@ -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.wm.shell.common.split;
+
+import android.graphics.Point;
+import android.graphics.Rect;
+
+/**
+ * Calculation class, used when {@link com.android.wm.shell.common.split.SplitLayout#PARALLAX_NONE}
+ * is the desired parallax effect.
+ */
+public class NoParallaxSpec implements ParallaxSpec {
+ @Override
+ public void getParallax(Point retreatingOut, Point advancingOut, int position,
+ DividerSnapAlgorithm snapAlgorithm, boolean isLeftRightSplit, Rect displayBounds,
+ Rect retreatingSurface, Rect retreatingContent, Rect advancingSurface,
+ Rect advancingContent, int dimmingSide, boolean topLeftShrink) {
+ // no-op
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/ParallaxSpec.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/ParallaxSpec.java
new file mode 100644
index 000000000000..84d849b3c1f9
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/ParallaxSpec.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.common.split;
+
+import static android.view.WindowManager.DOCKED_BOTTOM;
+import static android.view.WindowManager.DOCKED_INVALID;
+import static android.view.WindowManager.DOCKED_LEFT;
+import static android.view.WindowManager.DOCKED_RIGHT;
+import static android.view.WindowManager.DOCKED_TOP;
+
+import static com.android.wm.shell.shared.animation.Interpolators.DIM_INTERPOLATOR;
+
+import android.graphics.Point;
+import android.graphics.Rect;
+
+/**
+ * Default interface for a set of calculation classes, used for calculating various parallax and
+ * dimming effects in split screen.
+ */
+public interface ParallaxSpec {
+ /** Returns an int indicating which side of the screen is being dimmed (if any). */
+ default int getDimmingSide(int position, DividerSnapAlgorithm snapAlgorithm,
+ boolean isLeftRightSplit) {
+ if (position < snapAlgorithm.getFirstSplitTarget().getPosition()) {
+ return isLeftRightSplit ? DOCKED_LEFT : DOCKED_TOP;
+ } else if (position > snapAlgorithm.getLastSplitTarget().getPosition()) {
+ return isLeftRightSplit ? DOCKED_RIGHT : DOCKED_BOTTOM;
+ }
+ return DOCKED_INVALID;
+ }
+
+ /** Returns the dim amount that we'll apply to the app surface. 0f = no dim, 1f = full black */
+ default float getDimValue(int position, DividerSnapAlgorithm snapAlgorithm) {
+ float progressTowardScreenEdge =
+ Math.max(0, Math.min(snapAlgorithm.calculateDismissingFraction(position), 1f));
+ return DIM_INTERPOLATOR.getInterpolation(progressTowardScreenEdge);
+ }
+
+ /**
+ * Calculates the amount to offset app surfaces to create nice parallax effects. Writes to
+ * {@link ResizingEffectPolicy#mRetreatingSideParallax} and
+ * {@link ResizingEffectPolicy#mAdvancingSideParallax}.
+ */
+ void getParallax(Point retreatingOut, Point advancingOut, int position,
+ DividerSnapAlgorithm snapAlgorithm, boolean isLeftRightSplit, Rect displayBounds,
+ Rect retreatingSurface, Rect retreatingContent, Rect advancingSurface,
+ Rect advancingContent, int dimmingSide, boolean topLeftShrink);
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/ResizingEffectPolicy.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/ResizingEffectPolicy.java
index 3f76fd0220ff..e2e1f9698a90 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/ResizingEffectPolicy.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/ResizingEffectPolicy.java
@@ -26,27 +26,32 @@ import static com.android.wm.shell.common.split.SplitLayout.PARALLAX_ALIGN_CENTE
import static com.android.wm.shell.common.split.SplitLayout.PARALLAX_DISMISSING;
import static com.android.wm.shell.common.split.SplitLayout.PARALLAX_FLEX;
import static com.android.wm.shell.common.split.SplitLayout.PARALLAX_NONE;
-import static com.android.wm.shell.shared.animation.Interpolators.DIM_INTERPOLATOR;
-import static com.android.wm.shell.shared.animation.Interpolators.SLOWDOWN_INTERPOLATOR;
import android.graphics.Point;
import android.graphics.Rect;
import android.view.SurfaceControl;
-import android.view.WindowManager;
/**
* This class governs how and when parallax and dimming effects are applied to task surfaces,
* usually when the divider is being moved around by the user (or during an animation).
*/
class ResizingEffectPolicy {
+ /** The default amount to dim an app that is partially offscreen. */
+ public static float DEFAULT_OFFSCREEN_DIM = 0.32f;
+
private final SplitLayout mSplitLayout;
/** The parallax algorithm we are currently using. */
private final int mParallaxType;
+ /**
+ * A convenience class, corresponding to {@link #mParallaxType}, that performs all the
+ * calculations for parallax and dimming values.
+ */
+ private final ParallaxSpec mParallaxSpec;
int mShrinkSide = DOCKED_INVALID;
// The current dismissing side.
- int mDismissingSide = DOCKED_INVALID;
+ int mDimmingSide = DOCKED_INVALID;
/**
* A {@link Point} that stores a single x and y value, representing the parallax translation
@@ -62,7 +67,7 @@ class ResizingEffectPolicy {
final Point mAdvancingSideParallax = new Point();
// The dimming value to hint the dismissing side and progress.
- float mDismissingDimValue = 0.0f;
+ float mDimValue = 0.0f;
/**
* Content bounds for the app that the divider is moving toward. This is the content that is
@@ -95,35 +100,38 @@ class ResizingEffectPolicy {
ResizingEffectPolicy(int parallaxType, SplitLayout splitLayout) {
mParallaxType = parallaxType;
mSplitLayout = splitLayout;
+ switch (mParallaxType) {
+ case PARALLAX_DISMISSING:
+ mParallaxSpec = new DismissingParallaxSpec();
+ break;
+ case PARALLAX_ALIGN_CENTER:
+ mParallaxSpec = new CenterParallaxSpec();
+ break;
+ case PARALLAX_FLEX:
+ mParallaxSpec = new FlexParallaxSpec();
+ break;
+ case PARALLAX_NONE:
+ default:
+ mParallaxSpec = new NoParallaxSpec();
+ break;
+ }
}
/**
- * Calculates the desired parallax values and stores them in {@link #mRetreatingSideParallax}
- * and {@link #mAdvancingSideParallax}. These values will be then be applied in
- * {@link #adjustRootSurface}.
- *
- * @param position The divider's position on the screen (x-coordinate in left-right split,
- * y-coordinate in top-bottom split).
+ * Calculates the desired parallax and dimming values for a task surface and stores them in
+ * {@link #mRetreatingSideParallax}, {@link #mAdvancingSideParallax}, and
+ * {@link #mDimValue} These values will be then be applied in
+ * {@link #adjustRootSurface} and {@link #adjustDimSurface} respectively.
*/
void applyDividerPosition(
int position, boolean isLeftRightSplit, DividerSnapAlgorithm snapAlgorithm) {
- mDismissingSide = DOCKED_INVALID;
+ mDimmingSide = DOCKED_INVALID;
mRetreatingSideParallax.set(0, 0);
mAdvancingSideParallax.set(0, 0);
- mDismissingDimValue = 0;
+ mDimValue = 0;
Rect displayBounds = mSplitLayout.getRootBounds();
- int totalDismissingDistance = 0;
- if (position < snapAlgorithm.getFirstSplitTarget().position) {
- mDismissingSide = isLeftRightSplit ? DOCKED_LEFT : DOCKED_TOP;
- totalDismissingDistance = snapAlgorithm.getDismissStartTarget().position
- - snapAlgorithm.getFirstSplitTarget().position;
- } else if (position > snapAlgorithm.getLastSplitTarget().position) {
- mDismissingSide = isLeftRightSplit ? DOCKED_RIGHT : DOCKED_BOTTOM;
- totalDismissingDistance = snapAlgorithm.getLastSplitTarget().position
- - snapAlgorithm.getDismissEndTarget().position;
- }
-
+ // Figure out which side is shrinking, and assign retreating/advancing bounds
final boolean topLeftShrink = isLeftRightSplit
? position < mSplitLayout.getTopLeftContentBounds().right
: position < mSplitLayout.getTopLeftContentBounds().bottom;
@@ -141,106 +149,20 @@ class ResizingEffectPolicy {
mAdvancingSurface.set(mSplitLayout.getTopLeftBounds());
}
- if (mDismissingSide != DOCKED_INVALID) {
- float fraction =
- Math.max(0, Math.min(snapAlgorithm.calculateDismissingFraction(position), 1f));
- mDismissingDimValue = DIM_INTERPOLATOR.getInterpolation(fraction);
- if (mParallaxType == PARALLAX_DISMISSING) {
- fraction = calculateParallaxDismissingFraction(fraction, mDismissingSide);
- if (isLeftRightSplit) {
- mRetreatingSideParallax.x = (int) (fraction * totalDismissingDistance);
- } else {
- mRetreatingSideParallax.y = (int) (fraction * totalDismissingDistance);
- }
- }
- }
-
- if (mParallaxType == PARALLAX_ALIGN_CENTER) {
- if (isLeftRightSplit) {
- mRetreatingSideParallax.x =
- (mRetreatingSurface.width() - mRetreatingContent.width()) / 2;
- } else {
- mRetreatingSideParallax.y =
- (mRetreatingSurface.height() - mRetreatingContent.height()) / 2;
- }
- } else if (mParallaxType == PARALLAX_FLEX) {
- // Whether an app is getting pushed offscreen by the divider.
- boolean isRetreatingOffscreen = !displayBounds.contains(mRetreatingSurface);
- // Whether an app was getting pulled onscreen at the beginning of the drag.
- boolean advancingSideStartedOffscreen = !displayBounds.contains(mAdvancingContent);
+ // Figure out if we should be dimming one side
+ mDimmingSide = mParallaxSpec.getDimmingSide(position, snapAlgorithm, isLeftRightSplit);
- // The simpler case when an app gets pushed offscreen (e.g. 50:50 -> 90:10)
- if (isRetreatingOffscreen && !advancingSideStartedOffscreen) {
- // On the left side, we use parallax to simulate the contents sticking to the
- // divider. This is because surfaces naturally expand to the bottom and right,
- // so when a surface's area expands, the contents stick to the left. This is
- // correct behavior on the right-side surface, but not the left.
- if (topLeftShrink) {
- if (isLeftRightSplit) {
- mRetreatingSideParallax.x =
- mRetreatingSurface.width() - mRetreatingContent.width();
- } else {
- mRetreatingSideParallax.y =
- mRetreatingSurface.height() - mRetreatingContent.height();
- }
- }
- // All other cases (e.g. 10:90 -> 50:50, 10:90 -> 90:10, 10:90 -> dismiss)
- } else {
- mTempRect.set(mRetreatingSurface);
- Point rootOffset = new Point();
- // 10:90 -> 50:50, 10:90, or dismiss right
- if (advancingSideStartedOffscreen) {
- // We have to handle a complicated case here to keep the parallax smooth.
- // When the divider crosses the 50% mark, the retreating-side app surface
- // will start expanding offscreen. This is expected and unavoidable, but
- // makes the parallax look disjointed. In order to preserve the illusion,
- // we add another offset (rootOffset) to simulate the surface staying
- // onscreen.
- mTempRect.intersect(displayBounds);
- if (mRetreatingSurface.left < displayBounds.left) {
- rootOffset.x = displayBounds.left - mRetreatingSurface.left;
- }
- if (mRetreatingSurface.top < displayBounds.top) {
- rootOffset.y = displayBounds.top - mRetreatingSurface.top;
- }
-
- // On the left side, we again have to simulate the contents sticking to the
- // divider.
- if (!topLeftShrink) {
- if (isLeftRightSplit) {
- mAdvancingSideParallax.x =
- mAdvancingSurface.width() - mAdvancingContent.width();
- } else {
- mAdvancingSideParallax.y =
- mAdvancingSurface.height() - mAdvancingContent.height();
- }
- }
- }
-
- // In all these cases, the shrinking app also receives a center parallax.
- if (isLeftRightSplit) {
- mRetreatingSideParallax.x = rootOffset.x
- + ((mTempRect.width() - mRetreatingContent.width()) / 2);
- } else {
- mRetreatingSideParallax.y = rootOffset.y
- + ((mTempRect.height() - mRetreatingContent.height()) / 2);
- }
- }
+ // If so, calculate dimming
+ if (mDimmingSide != DOCKED_INVALID) {
+ mDimValue = mParallaxSpec.getDimValue(position, snapAlgorithm);
}
- }
- /**
- * @return for a specified {@code fraction}, this returns an adjusted value that simulates a
- * slowing down parallax effect
- */
- private float calculateParallaxDismissingFraction(float fraction, int dockSide) {
- float result = SLOWDOWN_INTERPOLATOR.getInterpolation(fraction) / 3.5f;
-
- // Less parallax at the top, just because.
- if (dockSide == WindowManager.DOCKED_TOP) {
- result /= 2f;
- }
- return result;
+ // Calculate parallax and modify mRetreatingSideParallax and mAdvancingSideParallax, for use
+ // in adjustRootSurface().
+ mParallaxSpec.getParallax(mRetreatingSideParallax, mAdvancingSideParallax, position,
+ snapAlgorithm, isLeftRightSplit, displayBounds, mRetreatingSurface,
+ mRetreatingContent, mAdvancingSurface, mAdvancingContent, mDimmingSide,
+ topLeftShrink);
}
/** Applies the calculated parallax and dimming values to task surfaces. */
@@ -250,7 +172,7 @@ class ResizingEffectPolicy {
SurfaceControl advancingLeash = null;
if (mParallaxType == PARALLAX_DISMISSING) {
- switch (mDismissingSide) {
+ switch (mDimmingSide) {
case DOCKED_TOP:
case DOCKED_LEFT:
retreatingLeash = leash1;
@@ -303,14 +225,17 @@ class ResizingEffectPolicy {
void adjustDimSurface(SurfaceControl.Transaction t,
SurfaceControl dimLayer1, SurfaceControl dimLayer2) {
SurfaceControl targetDimLayer;
- switch (mDismissingSide) {
+ SurfaceControl oppositeDimLayer;
+ switch (mDimmingSide) {
case DOCKED_TOP:
case DOCKED_LEFT:
targetDimLayer = dimLayer1;
+ oppositeDimLayer = dimLayer2;
break;
case DOCKED_BOTTOM:
case DOCKED_RIGHT:
targetDimLayer = dimLayer2;
+ oppositeDimLayer = dimLayer1;
break;
case DOCKED_INVALID:
default:
@@ -318,7 +243,9 @@ class ResizingEffectPolicy {
t.setAlpha(dimLayer2, 0).hide(dimLayer2);
return;
}
- t.setAlpha(targetDimLayer, mDismissingDimValue)
- .setVisibility(targetDimLayer, mDismissingDimValue > 0.001f);
+ t.setAlpha(targetDimLayer, mDimValue)
+ .setVisibility(targetDimLayer, mDimValue > 0.001f);
+ t.setAlpha(oppositeDimLayer, 0f)
+ .setVisibility(oppositeDimLayer, false);
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java
index bd89f5cf45f6..708e26cc5546 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java
@@ -128,6 +128,8 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange
// The touch layer is on a stage root, and is sibling with things like the app activity itself
// and the app veil. We want it to be above all those.
public static final int RESTING_TOUCH_LAYER = Integer.MAX_VALUE;
+ // The dim layer is also on the stage root, and stays under the touch layer.
+ public static final int RESTING_DIM_LAYER = RESTING_TOUCH_LAYER - 1;
// Animation specs for the swap animation
private static final int SWAP_ANIMATION_TOTAL_DURATION = 500;
@@ -1201,6 +1203,12 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange
// Resets layer of divider bar to make sure it is always on top.
t.setLayer(dividerLeash, RESTING_DIVIDER_LAYER);
}
+ if (dimLayer1 != null) {
+ t.setLayer(dimLayer1, RESTING_DIM_LAYER);
+ }
+ if (dimLayer2 != null) {
+ t.setLayer(dimLayer2, RESTING_DIM_LAYER);
+ }
copyTopLeftRefBounds(mTempRect);
t.setPosition(leash1, mTempRect.left, mTempRect.top)
.setWindowCrop(leash1, mTempRect.width(), mTempRect.height());
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitState.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitState.java
index d1d133d16ae4..ad0e7fc187e9 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitState.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitState.java
@@ -57,4 +57,9 @@ public class SplitState {
public List<RectF> getLayout(@SplitScreenState int state) {
return mSplitSpec.getSpec(state);
}
+
+ /** Returns the layout associated with the current split state. */
+ public List<RectF> getCurrentLayout() {
+ return getLayout(mState);
+ }
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/events/ReachabilityGestureListener.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/events/ReachabilityGestureListener.kt
new file mode 100644
index 000000000000..bdffcf51e7d4
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/events/ReachabilityGestureListener.kt
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.compatui.letterbox.events
+
+import android.graphics.Rect
+import android.view.GestureDetector
+import android.view.MotionEvent
+import android.window.WindowContainerToken
+import com.android.wm.shell.common.WindowContainerTransactionSupplier
+import com.android.wm.shell.transition.Transitions
+import com.android.wm.shell.transition.Transitions.TRANSIT_MOVE_LETTERBOX_REACHABILITY
+
+/**
+ * [GestureDetector.SimpleOnGestureListener] implementation which receives events from the
+ * Letterbox Input surface, understands the type of event and filter them based on the current
+ * letterbox position.
+ */
+class ReachabilityGestureListener(
+ private val taskId: Int,
+ private val token: WindowContainerToken?,
+ private val transitions: Transitions,
+ private val animationHandler: Transitions.TransitionHandler,
+ private val wctSupplier: WindowContainerTransactionSupplier
+) : GestureDetector.SimpleOnGestureListener() {
+
+ // The current letterbox bounds. Double tap events are ignored when happening in these bounds.
+ private val activityBounds = Rect()
+
+ override fun onDoubleTap(e: MotionEvent): Boolean {
+ val x = e.rawX.toInt()
+ val y = e.rawY.toInt()
+ if (!activityBounds.contains(x, y)) {
+ val wct = wctSupplier.get().apply {
+ setReachabilityOffset(token!!, taskId, x, y)
+ }
+ transitions.startTransition(
+ TRANSIT_MOVE_LETTERBOX_REACHABILITY,
+ wct,
+ animationHandler
+ )
+ return true
+ }
+ return false
+ }
+
+ /**
+ * Updates the bounds for the letterboxed activity.
+ */
+ fun updateActivityBounds(newActivityBounds: Rect) {
+ activityBounds.set(newActivityBounds)
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/events/ReachabilityGestureListenerFactory.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/events/ReachabilityGestureListenerFactory.kt
new file mode 100644
index 000000000000..5e9fe09bc840
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/events/ReachabilityGestureListenerFactory.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.compatui.letterbox.events
+
+import android.window.WindowContainerToken
+import com.android.wm.shell.common.WindowContainerTransactionSupplier
+import com.android.wm.shell.dagger.WMSingleton
+import com.android.wm.shell.transition.Transitions
+import javax.inject.Inject
+
+/**
+ * A Factory for [ReachabilityGestureListener].
+ */
+@WMSingleton
+class ReachabilityGestureListenerFactory @Inject constructor(
+ private val transitions: Transitions,
+ private val animationHandler: Transitions.TransitionHandler,
+ private val wctSupplier: WindowContainerTransactionSupplier
+) {
+ /**
+ * @return a [ReachabilityGestureListener] implementation to listen to double tap events and
+ * creating the related [WindowContainerTransaction] to handle the transition.
+ */
+ fun createReachabilityGestureListener(
+ taskId: Int,
+ token: WindowContainerToken?
+ ): ReachabilityGestureListener =
+ ReachabilityGestureListener(taskId, token, transitions, animationHandler, wctSupplier)
+}
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 35475c7ee4ce..2fd8c27d5970 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
@@ -759,7 +759,6 @@ public abstract class WMShellModule {
FocusTransitionObserver focusTransitionObserver,
DesktopModeEventLogger desktopModeEventLogger,
DesktopModeUiEventLogger desktopModeUiEventLogger,
- DesktopTilingDecorViewModel desktopTilingDecorViewModel,
DesktopWallpaperActivityTokenProvider desktopWallpaperActivityTokenProvider,
Optional<BubbleController> bubbleController,
OverviewToDesktopTransitionObserver overviewToDesktopTransitionObserver,
@@ -798,7 +797,6 @@ public abstract class WMShellModule {
mainHandler,
desktopModeEventLogger,
desktopModeUiEventLogger,
- desktopTilingDecorViewModel,
desktopWallpaperActivityTokenProvider,
bubbleController,
overviewToDesktopTransitionObserver,
@@ -990,7 +988,8 @@ public abstract class WMShellModule {
DesktopModeUiEventLogger desktopModeUiEventLogger,
WindowDecorTaskResourceLoader taskResourceLoader,
RecentsTransitionHandler recentsTransitionHandler,
- DesktopModeCompatPolicy desktopModeCompatPolicy
+ DesktopModeCompatPolicy desktopModeCompatPolicy,
+ DesktopTilingDecorViewModel desktopTilingDecorViewModel
) {
if (!DesktopModeStatus.canEnterDesktopModeOrShowAppHandle(context)) {
return Optional.empty();
@@ -1006,7 +1005,8 @@ public abstract class WMShellModule {
desktopTasksLimiter, appHandleEducationController, appToWebEducationController,
windowDecorCaptionHandleRepository, activityOrientationChangeHandler,
focusTransitionObserver, desktopModeEventLogger, desktopModeUiEventLogger,
- taskResourceLoader, recentsTransitionHandler, desktopModeCompatPolicy));
+ taskResourceLoader, recentsTransitionHandler, desktopModeCompatPolicy,
+ desktopTilingDecorViewModel));
}
@WMSingleton
@@ -1278,10 +1278,10 @@ public abstract class WMShellModule {
@WMSingleton
@Provides
static DesktopWindowingEducationTooltipController
- provideDesktopWindowingEducationTooltipController(
- Context context,
- AdditionalSystemViewContainer.Factory additionalSystemViewContainerFactory,
- DisplayController displayController) {
+ provideDesktopWindowingEducationTooltipController(
+ Context context,
+ AdditionalSystemViewContainer.Factory additionalSystemViewContainerFactory,
+ DisplayController displayController) {
return new DesktopWindowingEducationTooltipController(
context, additionalSystemViewContainerFactory, displayController);
}
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 7d80ee5f3bb6..f8b18f29c797 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
@@ -53,6 +53,7 @@ import com.android.wm.shell.pip2.phone.PipTransition;
import com.android.wm.shell.pip2.phone.PipTransitionState;
import com.android.wm.shell.pip2.phone.PipUiStateChangeController;
import com.android.wm.shell.shared.annotations.ShellMainThread;
+import com.android.wm.shell.splitscreen.SplitScreenController;
import com.android.wm.shell.sysui.ShellCommandHandler;
import com.android.wm.shell.sysui.ShellController;
import com.android.wm.shell.sysui.ShellInit;
@@ -85,11 +86,13 @@ public abstract class Pip2Module {
@NonNull PipDisplayLayoutState pipDisplayLayoutState,
@NonNull PipUiStateChangeController pipUiStateChangeController,
DisplayController displayController,
+ Optional<SplitScreenController> splitScreenControllerOptional,
PipDesktopState pipDesktopState) {
return new PipTransition(context, shellInit, shellTaskOrganizer, transitions,
pipBoundsState, null, pipBoundsAlgorithm, pipTaskListener,
pipScheduler, pipStackListenerController, pipDisplayLayoutState,
- pipUiStateChangeController, displayController, pipDesktopState);
+ pipUiStateChangeController, displayController, splitScreenControllerOptional,
+ pipDesktopState);
}
@WMSingleton
@@ -140,9 +143,10 @@ public abstract class Pip2Module {
PipBoundsState pipBoundsState,
@ShellMainThread ShellExecutor mainExecutor,
PipTransitionState pipTransitionState,
+ Optional<SplitScreenController> splitScreenControllerOptional,
PipDesktopState pipDesktopState) {
return new PipScheduler(context, pipBoundsState, mainExecutor, pipTransitionState,
- pipDesktopState);
+ splitScreenControllerOptional, pipDesktopState);
}
@WMSingleton
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandler.kt
index 6f455df6cfec..c38558d7bde9 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandler.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandler.kt
@@ -26,6 +26,7 @@ import android.provider.Settings.Global.DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERN
import android.view.Display.DEFAULT_DISPLAY
import android.view.IWindowManager
import android.view.WindowManager.TRANSIT_CHANGE
+import android.window.DesktopExperienceFlags
import android.window.WindowContainerTransaction
import com.android.internal.protolog.ProtoLog
import com.android.window.flags.Flags
@@ -62,7 +63,7 @@ class DesktopDisplayEventHandler(
private fun onInit() {
displayController.addDisplayWindowListener(this)
- if (Flags.enableMultipleDesktopsBackend()) {
+ if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue()) {
desktopTasksController.onDeskRemovedListener = this
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandler.kt
index c9b3ec0d3a11..1f7edb413908 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandler.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandler.kt
@@ -71,9 +71,31 @@ class DesktopMixedTransitionHandler(
wct: WindowContainerTransaction?,
) = freeformTaskTransitionHandler.startWindowingModeTransition(targetWindowingMode, wct)
- /** Delegates starting minimized mode transition to [FreeformTaskTransitionHandler]. */
- override fun startMinimizedModeTransition(wct: WindowContainerTransaction?): IBinder =
- freeformTaskTransitionHandler.startMinimizedModeTransition(wct)
+ /**
+ * Starts a minimize transition for [taskId], with [isLastTask] which is true if the task going
+ * to be minimized is the last visible task.
+ */
+ override fun startMinimizedModeTransition(
+ wct: WindowContainerTransaction?,
+ taskId: Int,
+ isLastTask: Boolean,
+ ): IBinder {
+ if (!DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_EXIT_BY_MINIMIZE_TRANSITION_BUGFIX.isTrue) {
+ return freeformTaskTransitionHandler.startMinimizedModeTransition(
+ wct,
+ taskId,
+ isLastTask,
+ )
+ }
+ requireNotNull(wct)
+ return transitions
+ .startTransition(Transitions.TRANSIT_MINIMIZE, wct, /* handler= */ this)
+ .also { transition ->
+ pendingMixedTransitions.add(
+ PendingMixedTransition.Minimize(transition, taskId, isLastTask)
+ )
+ }
+ }
/** Delegates starting PiP transition to [FreeformTaskTransitionHandler]. */
override fun startPipTransition(wct: WindowContainerTransaction?): IBinder =
@@ -298,7 +320,15 @@ class DesktopMixedTransitionHandler(
finishTransaction: SurfaceControl.Transaction,
finishCallback: TransitionFinishCallback,
): Boolean {
- if (!DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION.isTrue) return false
+ val shouldAnimate =
+ if (info.type == Transitions.TRANSIT_MINIMIZE) {
+ DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_EXIT_BY_MINIMIZE_TRANSITION_BUGFIX.isTrue
+ } else {
+ DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION.isTrue
+ }
+ if (!shouldAnimate) {
+ return false
+ }
val minimizeChange = findTaskChange(info, pending.minimizingTask)
if (minimizeChange == null) {
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 03bc42f08d59..0cc8a6a5c1a3 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
@@ -16,7 +16,7 @@
package com.android.wm.shell.desktopmode
-import com.android.window.flags.Flags
+import android.window.DesktopExperienceFlags
import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource.UNKNOWN
import com.android.wm.shell.sysui.ShellCommandHandler
import java.io.PrintWriter
@@ -56,7 +56,7 @@ class DesktopModeShellCommandHandler(private val controller: DesktopTasksControl
pw.println("Error: task id should be an integer")
return false
}
- if (!Flags.enableMultipleDesktopsBackend()) {
+ if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
return controller.moveTaskToDefaultDeskAndActivate(taskId, transitionSource = UNKNOWN)
}
if (args.size < 3) {
@@ -95,7 +95,7 @@ class DesktopModeShellCommandHandler(private val controller: DesktopTasksControl
}
private fun runCreateDesk(args: Array<String>, pw: PrintWriter): Boolean {
- if (!Flags.enableMultipleDesktopsBackend()) {
+ if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
pw.println("Not supported.")
return false
}
@@ -116,7 +116,7 @@ class DesktopModeShellCommandHandler(private val controller: DesktopTasksControl
}
private fun runActivateDesk(args: Array<String>, pw: PrintWriter): Boolean {
- if (!Flags.enableMultipleDesktopsBackend()) {
+ if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
pw.println("Not supported.")
return false
}
@@ -137,7 +137,7 @@ class DesktopModeShellCommandHandler(private val controller: DesktopTasksControl
}
private fun runRemoveDesk(args: Array<String>, pw: PrintWriter): Boolean {
- if (!Flags.enableMultipleDesktopsBackend()) {
+ if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
pw.println("Not supported.")
return false
}
@@ -158,7 +158,7 @@ class DesktopModeShellCommandHandler(private val controller: DesktopTasksControl
}
private fun runRemoveAllDesks(args: Array<String>, pw: PrintWriter): Boolean {
- if (!Flags.enableMultipleDesktopsBackend()) {
+ if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
pw.println("Not supported.")
return false
}
@@ -167,7 +167,7 @@ class DesktopModeShellCommandHandler(private val controller: DesktopTasksControl
}
private fun runMoveTaskToFront(args: Array<String>, pw: PrintWriter): Boolean {
- if (!Flags.enableMultipleDesktopsBackend()) {
+ if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
pw.println("Not supported.")
return false
}
@@ -188,7 +188,7 @@ class DesktopModeShellCommandHandler(private val controller: DesktopTasksControl
}
private fun runMoveTaskOutOfDesk(args: Array<String>, pw: PrintWriter): Boolean {
- if (!Flags.enableMultipleDesktopsBackend()) {
+ if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
pw.println("Not supported.")
return false
}
@@ -204,12 +204,12 @@ class DesktopModeShellCommandHandler(private val controller: DesktopTasksControl
pw.println("Error: task id should be an integer")
return false
}
- pw.println("Not implemented.")
- return false
+ controller.moveToFullscreen(taskId, transitionSource = UNKNOWN)
+ return true
}
private fun runCanCreateDesk(args: Array<String>, pw: PrintWriter): Boolean {
- if (!Flags.enableMultipleDesktopsBackend()) {
+ if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
pw.println("Not supported.")
return false
}
@@ -225,7 +225,7 @@ class DesktopModeShellCommandHandler(private val controller: DesktopTasksControl
}
private fun runGetActiveDeskId(args: Array<String>, pw: PrintWriter): Boolean {
- if (!Flags.enableMultipleDesktopsBackend()) {
+ if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
pw.println("Not supported.")
return false
}
@@ -246,7 +246,7 @@ class DesktopModeShellCommandHandler(private val controller: DesktopTasksControl
}
override fun printShellCommandHelp(pw: PrintWriter, prefix: String) {
- if (!Flags.enableMultipleDesktopsBackend()) {
+ if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
pw.println("$prefix moveTaskToDesk <taskId> ")
pw.println("$prefix Move a task with given id to desktop mode.")
pw.println("$prefix moveToNextDisplay <taskId> ")
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java
index aecbf1a23cb2..99f052832a51 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java
@@ -214,6 +214,13 @@ public class DesktopModeVisualIndicator {
return result;
}
+ /**
+ * Returns the [DragStartState] of the visual indicator.
+ */
+ DragStartState getDragStartState() {
+ return mDragStartState;
+ }
+
@VisibleForTesting
Region calculateFullscreenRegion(DisplayLayout layout, int captionHeight) {
final Region region = new Region();
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 4777e7f93bc9..eba1be517147 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
@@ -22,6 +22,7 @@ import android.util.ArrayMap
import android.util.ArraySet
import android.util.SparseArray
import android.view.Display.INVALID_DISPLAY
+import android.window.DesktopExperienceFlags
import android.window.DesktopModeFlags
import androidx.core.util.forEach
import androidx.core.util.valueIterator
@@ -137,7 +138,7 @@ class DesktopRepository(
private var desktopGestureExclusionExecutor: Executor? = null
private val desktopData: DesktopData =
- if (Flags.enableMultipleDesktopsBackend()) {
+ if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
MultiDesktopData()
} else {
SingleDesktopData()
@@ -226,10 +227,19 @@ class DesktopRepository(
desktopData.setActiveDesk(displayId = displayId, deskId = deskId)
}
+ /** Sets the given desk as inactive if it was active. */
+ fun setDeskInactive(deskId: Int) {
+ desktopData.setDeskInactive(deskId)
+ }
+
/** Returns the id of the active desk in the given display, if any. */
@VisibleForTesting
fun getActiveDeskId(displayId: Int): Int? = desktopData.getActiveDesk(displayId)?.deskId
+ /** Returns the id of the desk to which this task belongs. */
+ fun getDeskIdForTask(taskId: Int): Int? =
+ desktopData.desksSequence().find { desk -> desk.activeTasks.contains(taskId) }?.deskId
+
/**
* Adds task with [taskId] to the list of freeform tasks on [displayId]'s active desk.
*
@@ -270,20 +280,40 @@ class DesktopRepository(
@VisibleForTesting
fun removeActiveTask(taskId: Int, excludedDeskId: Int? = null) {
val affectedDisplays = mutableSetOf<Int>()
- desktopData.forAllDesks { displayId, desk ->
- if (desk.deskId != excludedDeskId && desk.activeTasks.remove(taskId)) {
- logD(
- "Removed active task=%d displayId=%d deskId=%d",
- taskId,
- displayId,
- desk.deskId,
- )
- affectedDisplays.add(displayId)
+ desktopData
+ .desksSequence()
+ .filter { desk -> desk.displayId != excludedDeskId }
+ .forEach { desk ->
+ val removed = removeActiveTaskFromDesk(desk.deskId, taskId, notifyListeners = false)
+ if (removed) {
+ logD(
+ "Removed active task=%d displayId=%d deskId=%d",
+ taskId,
+ desk.displayId,
+ desk.deskId,
+ )
+ affectedDisplays.add(desk.displayId)
+ }
}
- }
affectedDisplays.forEach { displayId -> updateActiveTasksListeners(displayId) }
}
+ private fun removeActiveTaskFromDesk(
+ deskId: Int,
+ taskId: Int,
+ notifyListeners: Boolean = true,
+ ): Boolean {
+ val desk = desktopData.getDesk(deskId) ?: return false
+ if (desk.activeTasks.remove(taskId)) {
+ logD("Removed active task=%d from deskId=%d", taskId, desk.deskId)
+ if (notifyListeners) {
+ updateActiveTasksListeners(desk.displayId)
+ }
+ return true
+ }
+ return false
+ }
+
/**
* Adds given task to the closing task list for [displayId]'s active desk.
*
@@ -322,10 +352,22 @@ class DesktopRepository(
fun isActiveTask(taskId: Int) = desksSequence().any { taskId in it.activeTasks }
+ @VisibleForTesting
+ fun isActiveTaskInDesk(taskId: Int, deskId: Int): Boolean {
+ val desk = desktopData.getDesk(deskId) ?: return false
+ return taskId in desk.activeTasks
+ }
+
fun isClosingTask(taskId: Int) = desksSequence().any { taskId in it.closingTasks }
fun isVisibleTask(taskId: Int) = desksSequence().any { taskId in it.visibleTasks }
+ @VisibleForTesting
+ fun isVisibleTaskInDesk(taskId: Int, deskId: Int): Boolean {
+ val desk = desktopData.getDesk(deskId) ?: return false
+ return taskId in desk.visibleTasks
+ }
+
fun isMinimizedTask(taskId: Int) = desksSequence().any { taskId in it.minimizedTasks }
/**
@@ -415,12 +457,19 @@ class DesktopRepository(
/** Removes task from visible tasks of all desks except [excludedDeskId]. */
private fun removeVisibleTask(taskId: Int, excludedDeskId: Int? = null) {
desktopData.forAllDesks { displayId, desk ->
- if (desk.deskId != excludedDeskId && desk.visibleTasks.remove(taskId)) {
- notifyVisibleTaskListeners(displayId, desk.visibleTasks.size)
+ if (desk.deskId != excludedDeskId) {
+ removeVisibleTaskFromDesk(deskId = desk.deskId, taskId = taskId)
}
}
}
+ private fun removeVisibleTaskFromDesk(deskId: Int, taskId: Int) {
+ val desk = desktopData.getDesk(deskId) ?: return
+ if (desk.visibleTasks.remove(taskId)) {
+ notifyVisibleTaskListeners(desk.displayId, desk.visibleTasks.size)
+ }
+ }
+
/**
* Updates visibility of a freeform task with [taskId] on [displayId] and notifies listeners.
*
@@ -576,15 +625,26 @@ class DesktopRepository(
/**
* Set whether the given task is the full-immersive task in this display's active desk.
*
- * TODO: b/389960283 - add explicit [deskId] argument.
+ * TODO: b/389960283 - consider forcing callers to use [setTaskInFullImmersiveStateInDesk] with
+ * an explicit desk id instead of using this function and defaulting to the active one.
*/
fun setTaskInFullImmersiveState(displayId: Int, taskId: Int, immersive: Boolean) {
- val desktopData = desktopData.getActiveDesk(displayId) ?: return
+ val activeDesk = desktopData.getActiveDesk(displayId) ?: return
+ setTaskInFullImmersiveStateInDesk(
+ deskId = activeDesk.deskId,
+ taskId = taskId,
+ immersive = immersive,
+ )
+ }
+
+ /** Sets whether the given task is the full-immersive task in the given desk. */
+ fun setTaskInFullImmersiveStateInDesk(deskId: Int, taskId: Int, immersive: Boolean) {
+ val desk = desktopData.getDesk(deskId) ?: return
if (immersive) {
- desktopData.fullImmersiveTaskId = taskId
+ desk.fullImmersiveTaskId = taskId
} else {
- if (desktopData.fullImmersiveTaskId == taskId) {
- desktopData.fullImmersiveTaskId = null
+ if (desk.fullImmersiveTaskId == taskId) {
+ desk.fullImmersiveTaskId = null
}
}
}
@@ -674,7 +734,8 @@ class DesktopRepository(
/**
* Minimizes the task for [taskId] and [displayId]'s active display.
*
- * TODO: b/389960283 - add explicit [deskId] argument.
+ * TODO: b/389960283 - consider forcing callers to use [minimizeTaskInDesk] with an explicit
+ * desk id instead of using this function and defaulting to the active one.
*/
fun minimizeTask(displayId: Int, taskId: Int) {
if (displayId == INVALID_DISPLAY) {
@@ -683,32 +744,41 @@ class DesktopRepository(
getDisplayIdForTask(taskId)?.let { minimizeTask(it, taskId) }
?: logW("Minimize task: No display id found for task: taskId=%d", taskId)
return
- } else {
- logD("Minimize Task: display=%d, task=%d", displayId, taskId)
- desktopData.getActiveDesk(displayId)?.minimizedTasks?.add(taskId)
- ?: logD("Minimize task: No active desk found for task: taskId=%d", taskId)
}
- updateTask(displayId, taskId, isVisible = false)
+ val deskId = desktopData.getActiveDesk(displayId)?.deskId
+ if (deskId == null) {
+ logD("Minimize task: No active desk found for task: taskId=%d", taskId)
+ return
+ }
+ minimizeTaskInDesk(displayId, deskId, taskId)
+ }
+
+ /** Minimizes the task in its desk. */
+ @VisibleForTesting
+ fun minimizeTaskInDesk(displayId: Int, deskId: Int, taskId: Int) {
+ logD("Minimize Task: displayId=%d deskId=%d, task=%d", displayId, deskId, taskId)
+ desktopData.getDesk(deskId)?.minimizedTasks?.add(taskId)
+ ?: logD("Minimize task: No active desk found for task: taskId=%d", taskId)
+ updateTaskInDesk(displayId, deskId, taskId, isVisible = false)
if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PERSISTENCE.isTrue()) {
- updatePersistentRepository(displayId)
+ updatePersistentRepositoryForDesk(deskId)
}
}
/**
* Unminimizes the task for [taskId] and [displayId].
*
- * TODO: b/389960283 - consider adding an explicit [deskId] argument.
+ * TODO: b/389960283 - consider using [unminimizeTaskFromDesk] instead.
*/
fun unminimizeTask(displayId: Int, taskId: Int) {
logD("Unminimize Task: display=%d, task=%d", displayId, taskId)
- var removed = false
- desktopData.forAllDesks(displayId) { desk ->
- if (desk.minimizedTasks.remove(taskId)) {
- removed = true
- }
- }
- if (!removed) {
- logW("Unminimize Task: display=%d, task=%d, no task data", displayId, taskId)
+ desktopData.forAllDesks(displayId) { desk -> unminimizeTaskFromDesk(desk.deskId, taskId) }
+ }
+
+ private fun unminimizeTaskFromDesk(deskId: Int, taskId: Int) {
+ logD("Unminimize Task: deskId=%d, taskId=%d", deskId, taskId)
+ if (desktopData.getDesk(deskId)?.minimizedTasks?.remove(taskId) != true) {
+ logW("Unminimize Task: deskId=%d, taskId=%d, no task data", deskId, taskId)
}
}
@@ -729,7 +799,7 @@ class DesktopRepository(
* Removes [taskId] from the respective display. If [INVALID_DISPLAY], the original display id
* will be looked up from the task id.
*
- * TODO: b/389960283 - consider adding an explicit [deskId] argument.
+ * TODO: b/389960283 - consider using [removeTaskFromDesk] instead.
*/
fun removeTask(displayId: Int, taskId: Int) {
logD("Removes freeform task: taskId=%d", taskId)
@@ -745,24 +815,33 @@ class DesktopRepository(
private fun removeTaskFromDisplay(displayId: Int, taskId: Int) {
logD("Removes freeform task: taskId=%d, displayId=%d", taskId, displayId)
desktopData.forAllDesks(displayId) { desk ->
- if (desk.freeformTasksInZOrder.remove(taskId)) {
- logD(
- "Remaining freeform tasks in desk: %d, tasks: %s",
- desk.deskId,
- desk.freeformTasksInZOrder.toDumpString(),
- )
- }
+ removeTaskFromDesk(deskId = desk.deskId, taskId = taskId)
}
+ }
+
+ /** Removes the given task from the given desk. */
+ fun removeTaskFromDesk(deskId: Int, taskId: Int) {
+ logD("removeTaskFromDesk: deskId=%d, taskId=%d", deskId, taskId)
+ // TODO: b/362720497 - consider not clearing bounds on any removal, such as when moving
+ // it between desks. It might be better to allow restoring to the previous bounds as long
+ // as they're valid (probably valid if in the same display).
boundsBeforeMaximizeByTaskId.remove(taskId)
boundsBeforeFullImmersiveByTaskId.remove(taskId)
- // Remove task from unminimized task if it is minimized.
- unminimizeTask(displayId, taskId)
+ val desk = desktopData.getDesk(deskId) ?: return
+ if (desk.freeformTasksInZOrder.remove(taskId)) {
+ logD(
+ "Remaining freeform tasks in desk: %d, tasks: %s",
+ desk.deskId,
+ desk.freeformTasksInZOrder.toDumpString(),
+ )
+ }
+ unminimizeTaskFromDesk(deskId, taskId)
// Mark task as not in immersive if it was immersive.
- setTaskInFullImmersiveState(displayId = displayId, taskId = taskId, immersive = false)
- removeActiveTask(taskId)
- removeVisibleTask(taskId)
- if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PERSISTENCE.isTrue()) {
- updatePersistentRepository(displayId)
+ setTaskInFullImmersiveStateInDesk(deskId = deskId, taskId = taskId, immersive = false)
+ removeActiveTaskFromDesk(deskId = deskId, taskId = taskId)
+ removeVisibleTaskFromDesk(deskId = deskId, taskId = taskId)
+ if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PERSISTENCE.isTrue) {
+ updatePersistentRepositoryForDesk(desk.deskId)
}
}
@@ -832,24 +911,29 @@ class DesktopRepository(
private fun updatePersistentRepository(displayId: Int) {
val desks = desktopData.desksSequence(displayId).map { desk -> desk.deepCopy() }.toList()
mainCoroutineScope.launch {
- desks.forEach { desk ->
- try {
- persistentRepository.addOrUpdateDesktop(
- // Use display id as desk id for now since only once desk per display
- // is supported.
- userId = userId,
- desktopId = desk.deskId,
- visibleTasks = desk.visibleTasks,
- minimizedTasks = desk.minimizedTasks,
- freeformTasksInZOrder = desk.freeformTasksInZOrder,
- )
- } catch (exception: Exception) {
- logE(
- "An exception occurred while updating the persistent repository \n%s",
- exception.stackTrace,
- )
- }
- }
+ desks.forEach { desk -> updatePersistentRepositoryForDesk(desk) }
+ }
+ }
+
+ private fun updatePersistentRepositoryForDesk(deskId: Int) {
+ val desk = desktopData.getDesk(deskId)?.deepCopy() ?: return
+ mainCoroutineScope.launch { updatePersistentRepositoryForDesk(desk) }
+ }
+
+ private suspend fun updatePersistentRepositoryForDesk(desk: Desk) {
+ try {
+ persistentRepository.addOrUpdateDesktop(
+ userId = userId,
+ desktopId = desk.deskId,
+ visibleTasks = desk.visibleTasks,
+ minimizedTasks = desk.minimizedTasks,
+ freeformTasksInZOrder = desk.freeformTasksInZOrder,
+ )
+ } catch (exception: Exception) {
+ logE(
+ "An exception occurred while updating the persistent repository \n%s",
+ exception.stackTrace,
+ )
}
}
@@ -866,21 +950,27 @@ class DesktopRepository(
desktopData
.desksSequence()
.groupBy { it.displayId }
- .forEach { (displayId, desks) ->
+ .map { (displayId, desks) ->
+ Triple(displayId, desktopData.getActiveDesk(displayId)?.deskId, desks)
+ }
+ .forEach { (displayId, activeDeskId, desks) ->
pw.println("${prefix}Display #$displayId:")
+ pw.println("${innerPrefix}activeDesk=$activeDeskId")
+ pw.println("${innerPrefix}desks:")
+ val desksPrefix = "$innerPrefix "
desks.forEach { desk ->
- pw.println("${innerPrefix}Desk #${desk.deskId}:")
- pw.print("$innerPrefix activeTasks=")
+ pw.println("${desksPrefix}Desk #${desk.deskId}:")
+ pw.print("$desksPrefix activeTasks=")
pw.println(desk.activeTasks.toDumpString())
- pw.print("$innerPrefix visibleTasks=")
+ pw.print("$desksPrefix visibleTasks=")
pw.println(desk.visibleTasks.toDumpString())
- pw.print("$innerPrefix freeformTasksInZOrder=")
+ pw.print("$desksPrefix freeformTasksInZOrder=")
pw.println(desk.freeformTasksInZOrder.toDumpString())
- pw.print("$innerPrefix minimizedTasks=")
+ pw.print("$desksPrefix minimizedTasks=")
pw.println(desk.minimizedTasks.toDumpString())
- pw.print("$innerPrefix fullImmersiveTaskId=")
+ pw.print("$desksPrefix fullImmersiveTaskId=")
pw.println(desk.fullImmersiveTaskId)
- pw.print("$innerPrefix topTransparentFullscreenTaskId=")
+ pw.print("$desksPrefix topTransparentFullscreenTaskId=")
pw.println(desk.topTransparentFullscreenTaskId)
}
}
@@ -910,6 +1000,9 @@ class DesktopRepository(
/** Sets the given desk as the active desk in the given display. */
fun setActiveDesk(displayId: Int, deskId: Int)
+ /** Sets the desk as inactive if it was active. */
+ fun setDeskInactive(deskId: Int)
+
/**
* Returns the default desk in the given display. Useful when the system wants to activate a
* desk but doesn't care about which one it activates (e.g. when putting a window into a
@@ -990,6 +1083,11 @@ class DesktopRepository(
// existence of visible desktop windows, among other factors.
}
+ override fun setDeskInactive(deskId: Int) {
+ // No-op, in single-desk setups, which desktop is "active" is determined by the
+ // existence of visible desktop windows, among other factors.
+ }
+
override fun getDefaultDesk(displayId: Int): Desk = getDesk(deskId = displayId)
override fun getAllActiveDesks(): Set<Desk> =
@@ -1058,6 +1156,14 @@ class DesktopRepository(
display.activeDeskId = desk.deskId
}
+ override fun setDeskInactive(deskId: Int) {
+ desktopDisplays.forEach { id, display ->
+ if (display.activeDeskId == deskId) {
+ display.activeDeskId = null
+ }
+ }
+ }
+
override fun getDefaultDesk(displayId: Int): Desk? {
val display = desktopDisplays[displayId] ?: return null
return display.orderedDesks.find { it.deskId == display.activeDeskId }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTaskChangeListener.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTaskChangeListener.kt
index 4d87b2189115..e831d5eecdc2 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTaskChangeListener.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTaskChangeListener.kt
@@ -42,6 +42,12 @@ class DesktopTaskChangeListener(private val desktopUserRepositories: DesktopUser
desktopUserRepositories.getProfile(taskInfo.userId)
if (!desktopRepository.isActiveTask(taskInfo.taskId)) return
+ // TODO: b/394281403 - with multiple desks, it's possible to have a non-freeform task
+ // inside a desk, so this should be decoupled from windowing mode.
+ // Also, changes in/out of desks are handled by the [DesksTransitionObserver], which has
+ // more specific information about the desk involved in the transition, which might be
+ // more accurate than assuming it's always the default/active desk in the display, as this
+ // method does.
// Case 1: Freeform task is changed in Desktop Mode.
if (isFreeformTask(taskInfo)) {
if (taskInfo.isVisible) {
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 f17b680f6fae..d767a0b5bd57 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
@@ -41,6 +41,7 @@ import android.os.Handler
import android.os.IBinder
import android.os.SystemProperties
import android.os.UserHandle
+import android.util.Slog
import android.view.Display.DEFAULT_DISPLAY
import android.view.DragEvent
import android.view.MotionEvent
@@ -53,6 +54,7 @@ import android.view.WindowManager.TRANSIT_OPEN
import android.view.WindowManager.TRANSIT_PIP
import android.view.WindowManager.TRANSIT_TO_FRONT
import android.widget.Toast
+import android.window.DesktopExperienceFlags
import android.window.DesktopModeFlags
import android.window.DesktopModeFlags.DISABLE_NON_RESIZABLE_APP_SNAP_RESIZE
import android.window.DesktopModeFlags.ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER
@@ -136,7 +138,6 @@ import com.android.wm.shell.sysui.UserChangeListener
import com.android.wm.shell.transition.OneShotRemoteHandler
import com.android.wm.shell.transition.Transitions
import com.android.wm.shell.transition.Transitions.TransitionFinishCallback
-import com.android.wm.shell.windowdecor.DesktopModeWindowDecoration
import com.android.wm.shell.windowdecor.DragPositioningCallbackUtility
import com.android.wm.shell.windowdecor.MoveToDesktopAnimator
import com.android.wm.shell.windowdecor.OnTaskRepositionAnimationListener
@@ -144,7 +145,7 @@ import com.android.wm.shell.windowdecor.OnTaskResizeAnimationListener
import com.android.wm.shell.windowdecor.extension.isFullscreen
import com.android.wm.shell.windowdecor.extension.isMultiWindow
import com.android.wm.shell.windowdecor.extension.requestingImmersive
-import com.android.wm.shell.windowdecor.tiling.DesktopTilingDecorViewModel
+import com.android.wm.shell.windowdecor.tiling.SnapEventHandler
import java.io.PrintWriter
import java.util.Optional
import java.util.concurrent.Executor
@@ -152,6 +153,16 @@ import java.util.concurrent.TimeUnit
import java.util.function.Consumer
import kotlin.jvm.optionals.getOrNull
+/**
+ * A callback to be invoked when a transition is started via |Transitions.startTransition| with the
+ * transition binder token that it produces.
+ *
+ * Useful when multiple components are appending WCT operations to a single transition that is
+ * started outside of their control, and each of them wants to track the transition lifecycle
+ * independently by cross-referencing the transition token with future ready-transitions.
+ */
+typealias RunOnTransitStart = (IBinder) -> Unit
+
/** Handles moving tasks in and out of desktop */
class DesktopTasksController(
private val context: Context,
@@ -184,7 +195,6 @@ class DesktopTasksController(
@ShellMainThread private val handler: Handler,
private val desktopModeEventLogger: DesktopModeEventLogger,
private val desktopModeUiEventLogger: DesktopModeUiEventLogger,
- private val desktopTilingDecorViewModel: DesktopTilingDecorViewModel,
private val desktopWallpaperActivityTokenProvider: DesktopWallpaperActivityTokenProvider,
private val bubbleController: Optional<BubbleController>,
private val overviewToDesktopTransitionObserver: OverviewToDesktopTransitionObserver,
@@ -204,7 +214,9 @@ class DesktopTasksController(
private var userId: Int
private val desktopModeShellCommandHandler: DesktopModeShellCommandHandler =
DesktopModeShellCommandHandler(this)
+
private val mOnAnimationFinishedCallback = { releaseVisualIndicator() }
+ private lateinit var snapEventHandler: SnapEventHandler
private val dragToDesktopStateListener =
object : DragToDesktopStateListener {
override fun onCommitToDesktopAnimationStart() {
@@ -269,7 +281,7 @@ class DesktopTasksController(
RecentsTransitionStateListener.stateToString(state),
)
recentsTransitionState = state
- desktopTilingDecorViewModel.onOverviewAnimationStateChange(
+ snapEventHandler.onOverviewAnimationStateChange(
RecentsTransitionStateListener.isAnimating(state)
)
}
@@ -300,6 +312,11 @@ class DesktopTasksController(
dragToDesktopTransitionHandler.setSplitScreenController(controller)
}
+ /** Setter to handle snap events */
+ fun setSnapEventHandler(handler: SnapEventHandler) {
+ snapEventHandler = handler
+ }
+
/** Returns the transition type for the given remote transition. */
private fun transitionType(remoteTransition: RemoteTransition?): Int {
if (remoteTransition == null) {
@@ -423,7 +440,7 @@ class DesktopTasksController(
/** Creates a new desk in the given display. */
fun createDesk(displayId: Int) {
- if (Flags.enableMultipleDesktopsBackend()) {
+ if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
desksOrganizer.createDesk(displayId) { deskId ->
taskRepository.addDesk(displayId = displayId, deskId = deskId)
}
@@ -450,10 +467,7 @@ class DesktopTasksController(
}
// TODO(342378842): Instead of using default display, support multiple displays
val displayId = runningTask?.displayId ?: DEFAULT_DISPLAY
- val deskId =
- checkNotNull(taskRepository.getDefaultDeskId(displayId)) {
- "Expected a default desk to exist"
- }
+ val deskId = getDefaultDeskId(displayId)
return moveTaskToDesk(
taskId = taskId,
deskId = deskId,
@@ -474,7 +488,7 @@ class DesktopTasksController(
): Boolean {
val runningTask = shellTaskOrganizer.getRunningTaskInfo(taskId)
if (runningTask != null) {
- moveRunningTaskToDesk(
+ return moveRunningTaskToDesk(
task = runningTask,
deskId = deskId,
wct = wct,
@@ -556,10 +570,10 @@ class DesktopTasksController(
transitionSource: DesktopModeTransitionSource,
remoteTransition: RemoteTransition? = null,
callback: IMoveToDesktopCallback? = null,
- ) {
+ ): Boolean {
if (desktopModeCompatPolicy.isTopActivityExemptFromDesktopWindowing(task)) {
logW("Cannot enter desktop for taskId %d, ineligible top activity found", task.taskId)
- return
+ return false
}
val displayId = taskRepository.getDisplayForDesk(deskId)
logV(
@@ -602,7 +616,7 @@ class DesktopTasksController(
addPendingMinimizeTransition(transition, it, MinimizeReason.TASK_LIMIT)
}
exitResult.asExit()?.runOnTransitionStart?.invoke(transition)
- if (Flags.enableMultipleDesktopsBackend()) {
+ if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
desksTransitionObserver.addPendingTransition(
DeskTransition.ActiveDeskWithTask(
token = transition,
@@ -614,6 +628,7 @@ class DesktopTasksController(
} else {
taskRepository.setActiveDesk(displayId = displayId, deskId = deskId)
}
+ return true
}
/**
@@ -630,7 +645,7 @@ class DesktopTasksController(
task: RunningTaskInfo,
): Int? {
val taskIdToMinimize =
- if (Flags.enableMultipleDesktopsBackend()) {
+ if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
// Activate the desk first.
prepareForDeskActivation(displayId, wct)
desksOrganizer.activateDesk(wct, deskId)
@@ -650,7 +665,7 @@ class DesktopTasksController(
// Bring other apps to front first.
bringDesktopAppsToFrontBeforeShowingNewTask(displayId, wct, task.taskId)
}
- if (Flags.enableMultipleDesktopsBackend()) {
+ if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
prepareMoveTaskToDesk(wct, task, deskId)
} else {
addMoveToDesktopChanges(wct, task)
@@ -697,10 +712,7 @@ class DesktopTasksController(
* [startDragToDesktop].
*/
private fun finalizeDragToDesktop(taskInfo: RunningTaskInfo) {
- val deskId =
- checkNotNull(taskRepository.getDefaultDeskId(taskInfo.displayId)) {
- "Expected a default desk to exist"
- }
+ val deskId = getDefaultDeskId(taskInfo.displayId)
ProtoLog.v(
WM_SHELL_DESKTOP_MODE,
"DesktopTasksController: finalizeDragToDesktop taskId=%d deskId=%d",
@@ -709,7 +721,7 @@ class DesktopTasksController(
)
val wct = WindowContainerTransaction()
exitSplitIfApplicable(wct, taskInfo)
- if (!Flags.enableMultipleDesktopsBackend()) {
+ if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
// |moveHomeTask| is also called in |bringDesktopAppsToFrontBeforeShowingNewTask|, so
// this shouldn't be necessary at all.
if (Flags.enablePerDisplayDesktopWallpaperActivity()) {
@@ -741,7 +753,7 @@ class DesktopTasksController(
addPendingMinimizeTransition(it, taskId, MinimizeReason.TASK_LIMIT)
}
exitResult.asExit()?.runOnTransitionStart?.invoke(transition)
- if (Flags.enableMultipleDesktopsBackend()) {
+ if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
desksTransitionObserver.addPendingTransition(
DeskTransition.ActiveDeskWithTask(
token = transition,
@@ -782,22 +794,44 @@ class DesktopTasksController(
wct: WindowContainerTransaction,
displayId: Int,
taskInfo: RunningTaskInfo,
- ): ((IBinder) -> Unit)? {
+ ): ((IBinder) -> Unit) {
val taskId = taskInfo.taskId
- desktopTilingDecorViewModel.removeTaskIfTiled(displayId, taskId)
- performDesktopExitCleanupIfNeeded(taskId, displayId, wct, forceToFullscreen = false)
+ val deskId = taskRepository.getDeskIdForTask(taskInfo.taskId)
+ snapEventHandler.removeTaskIfTiled(displayId, taskId)
+ val shouldExitDesktop =
+ willExitDesktop(
+ triggerTaskId = taskInfo.taskId,
+ displayId = displayId,
+ forceToFullscreen = false,
+ )
+ taskRepository.setPipShouldKeepDesktopActive(displayId, keepActive = true)
+ val desktopExitRunnable =
+ performDesktopExitCleanUp(
+ wct = wct,
+ deskId = deskId,
+ displayId = displayId,
+ willExitDesktop = shouldExitDesktop,
+ shouldEndUpAtHome = true,
+ )
+
taskRepository.addClosingTask(displayId, taskId)
taskbarDesktopTaskListener?.onTaskbarCornerRoundingUpdate(
doesAnyTaskRequireTaskbarRounding(displayId, taskId)
)
- return desktopImmersiveController
- .exitImmersiveIfApplicable(
- wct = wct,
- taskInfo = taskInfo,
- reason = DesktopImmersiveController.ExitReason.CLOSED,
- )
- .asExit()
- ?.runOnTransitionStart
+
+ val immersiveRunnable =
+ desktopImmersiveController
+ .exitImmersiveIfApplicable(
+ wct = wct,
+ taskInfo = taskInfo,
+ reason = DesktopImmersiveController.ExitReason.CLOSED,
+ )
+ .asExit()
+ ?.runOnTransitionStart
+ return { transitionToken ->
+ immersiveRunnable?.invoke(transitionToken)
+ desktopExitRunnable?.invoke(transitionToken)
+ }
}
fun minimizeTask(taskInfo: RunningTaskInfo, minimizeReason: MinimizeReason) {
@@ -831,10 +865,20 @@ class DesktopTasksController(
private fun minimizeTaskInner(taskInfo: RunningTaskInfo, minimizeReason: MinimizeReason) {
val taskId = taskInfo.taskId
+ val deskId = taskRepository.getDeskIdForTask(taskInfo.taskId)
val displayId = taskInfo.displayId
val wct = WindowContainerTransaction()
- desktopTilingDecorViewModel.removeTaskIfTiled(displayId, taskId)
- performDesktopExitCleanupIfNeeded(taskId, displayId, wct, forceToFullscreen = false)
+
+ snapEventHandler.removeTaskIfTiled(displayId, taskId)
+ taskRepository.setPipShouldKeepDesktopActive(displayId, keepActive = true)
+ val willExitDesktop = willExitDesktop(taskId, displayId, forceToFullscreen = false)
+ val desktopExitRunnable =
+ performDesktopExitCleanUp(
+ wct = wct,
+ deskId = deskId,
+ displayId = displayId,
+ willExitDesktop = willExitDesktop,
+ )
// Notify immersive handler as it might need to exit immersive state.
val exitResult =
desktopImmersiveController.exitImmersiveIfApplicable(
@@ -844,7 +888,9 @@ class DesktopTasksController(
)
wct.reorder(taskInfo.token, false)
- val transition = freeformTaskTransitionStarter.startMinimizedModeTransition(wct)
+ val isLastTask = taskRepository.isOnlyVisibleNonClosingTask(taskId, displayId)
+ val transition: IBinder =
+ freeformTaskTransitionStarter.startMinimizedModeTransition(wct, taskId, isLastTask)
desktopTasksLimiter.ifPresent {
it.addPendingMinimizeChange(
transition = transition,
@@ -854,12 +900,13 @@ class DesktopTasksController(
)
}
exitResult.asExit()?.runOnTransitionStart?.invoke(transition)
+ desktopExitRunnable?.invoke(transition)
}
/** Move a task with given `taskId` to fullscreen */
fun moveToFullscreen(taskId: Int, transitionSource: DesktopModeTransitionSource) {
shellTaskOrganizer.getRunningTaskInfo(taskId)?.let { task ->
- desktopTilingDecorViewModel.removeTaskIfTiled(task.displayId, taskId)
+ snapEventHandler.removeTaskIfTiled(task.displayId, taskId)
moveToFullscreenWithAnimation(task, task.positionInParent, transitionSource)
}
}
@@ -867,7 +914,7 @@ class DesktopTasksController(
/** Enter fullscreen by moving the focused freeform task in given `displayId` to fullscreen. */
fun enterFullscreen(displayId: Int, transitionSource: DesktopModeTransitionSource) {
getFocusedFreeformTask(displayId)?.let {
- desktopTilingDecorViewModel.removeTaskIfTiled(displayId, it.taskId)
+ snapEventHandler.removeTaskIfTiled(displayId, it.taskId)
moveToFullscreenWithAnimation(it, it.positionInParent, transitionSource)
}
}
@@ -901,7 +948,8 @@ class DesktopTasksController(
) {
logV("moveToFullscreenWithAnimation taskId=%d", task.taskId)
val wct = WindowContainerTransaction()
- addMoveToFullscreenChanges(wct, task)
+ val willExitDesktop = willExitDesktop(task.taskId, task.displayId, forceToFullscreen = true)
+ val deactivationRunnable = addMoveToFullscreenChanges(wct, task, willExitDesktop)
// We are moving a freeform task to fullscreen, put the home task under the fullscreen task.
if (!forceEnterDesktop(task.displayId)) {
@@ -909,12 +957,14 @@ class DesktopTasksController(
wct.reorder(task.token, /* onTop= */ true)
}
- exitDesktopTaskTransitionHandler.startTransition(
- transitionSource,
- wct,
- position,
- mOnAnimationFinishedCallback,
- )
+ val transition =
+ exitDesktopTaskTransitionHandler.startTransition(
+ transitionSource,
+ wct,
+ position,
+ mOnAnimationFinishedCallback,
+ )
+ deactivationRunnable?.invoke(transition)
// handles case where we are moving to full screen without closing all DW tasks.
if (!taskRepository.isOnlyVisibleNonClosingTask(task.taskId)) {
@@ -986,7 +1036,7 @@ class DesktopTasksController(
logV("moveTaskToFront taskId=%s", taskInfo.taskId)
// If a task is tiled, another task should be brought to foreground with it so let
// tiling controller handle the request.
- if (desktopTilingDecorViewModel.moveTaskToFrontIfTiled(taskInfo)) {
+ if (snapEventHandler.moveTaskToFrontIfTiled(taskInfo)) {
return
}
val wct = WindowContainerTransaction()
@@ -1173,6 +1223,8 @@ class DesktopTasksController(
wct.reorder(task.token, /* onTop= */ true, /* includingParents= */ true)
}
+ // TODO: b/394268248 - desk needs to be deactivated when moving the last task and going
+ // home.
if (Flags.enablePerDisplayDesktopWallpaperActivity()) {
performDesktopExitCleanupIfNeeded(
task.taskId,
@@ -1228,7 +1280,7 @@ class DesktopTasksController(
} else {
// Save current bounds so that task can be restored back to original bounds if necessary
// and toggle to the stable bounds.
- desktopTilingDecorViewModel.removeTaskIfTiled(taskInfo.displayId, taskInfo.taskId)
+ snapEventHandler.removeTaskIfTiled(taskInfo.displayId, taskInfo.taskId)
taskRepository.saveBoundsBeforeMaximize(taskInfo.taskId, currentTaskBounds)
destinationBounds.set(calculateMaximizeBounds(displayLayout, taskInfo))
}
@@ -1354,7 +1406,6 @@ class DesktopTasksController(
position: SnapPosition,
resizeTrigger: ResizeTrigger,
inputMethod: InputMethod,
- desktopWindowDecoration: DesktopModeWindowDecoration,
) {
desktopModeEventLogger.logTaskResizingStarted(
resizeTrigger,
@@ -1376,13 +1427,7 @@ class DesktopTasksController(
)
if (DesktopModeFlags.ENABLE_TILE_RESIZING.isTrue()) {
- val isTiled =
- desktopTilingDecorViewModel.snapToHalfScreen(
- taskInfo,
- desktopWindowDecoration,
- position,
- currentDragBounds,
- )
+ val isTiled = snapEventHandler.snapToHalfScreen(taskInfo, currentDragBounds, position)
if (isTiled) {
taskbarDesktopTaskListener?.onTaskbarCornerRoundingUpdate(true)
}
@@ -1419,7 +1464,6 @@ class DesktopTasksController(
position: SnapPosition,
resizeTrigger: ResizeTrigger,
inputMethod: InputMethod,
- desktopModeWindowDecoration: DesktopModeWindowDecoration,
) {
if (!isSnapResizingAllowed(taskInfo)) {
Toast.makeText(
@@ -1438,7 +1482,6 @@ class DesktopTasksController(
position,
resizeTrigger,
inputMethod,
- desktopModeWindowDecoration,
)
}
@@ -1450,7 +1493,6 @@ class DesktopTasksController(
currentDragBounds: Rect,
dragStartBounds: Rect,
motionEvent: MotionEvent,
- desktopModeWindowDecoration: DesktopModeWindowDecoration,
) {
releaseVisualIndicator()
if (!isSnapResizingAllowed(taskInfo)) {
@@ -1498,7 +1540,6 @@ class DesktopTasksController(
position,
resizeTrigger,
DesktopModeEventLogger.getInputMethodFromMotionEvent(motionEvent),
- desktopModeWindowDecoration,
)
}
}
@@ -1547,7 +1588,7 @@ class DesktopTasksController(
private fun prepareForDeskActivation(displayId: Int, wct: WindowContainerTransaction) {
// Move home to front, ensures that we go back home when all desktop windows are closed
val useParamDisplayId =
- Flags.enableMultipleDesktopsBackend() ||
+ DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue ||
Flags.enablePerDisplayDesktopWallpaperActivity()
moveHomeTask(displayId = if (useParamDisplayId) displayId else context.displayId, wct = wct)
// Currently, we only handle the desktop on the default display really.
@@ -1730,33 +1771,59 @@ class DesktopTasksController(
}
}
- /**
- * Remove wallpaper activity if task provided is last task and wallpaper activity token is not
- * null
- */
- private fun performDesktopExitCleanupIfNeeded(
- taskId: Int,
+ private fun willExitDesktop(
+ triggerTaskId: Int,
displayId: Int,
- wct: WindowContainerTransaction,
forceToFullscreen: Boolean,
- shouldEndUpAtHome: Boolean = true,
- ) {
- taskRepository.setPipShouldKeepDesktopActive(displayId, !forceToFullscreen)
+ ): Boolean {
if (Flags.enablePerDisplayDesktopWallpaperActivity()) {
- if (!taskRepository.isOnlyVisibleNonClosingTask(taskId, displayId)) {
- return
+ if (!taskRepository.isOnlyVisibleNonClosingTask(triggerTaskId, displayId)) {
+ return false
}
} else if (
Flags.enableDesktopWindowingPip() &&
taskRepository.isMinimizedPipPresentInDisplay(displayId) &&
!forceToFullscreen
) {
- return
+ return false
} else {
- if (!taskRepository.isOnlyVisibleNonClosingTask(taskId)) {
- return
+ if (!taskRepository.isOnlyVisibleNonClosingTask(triggerTaskId)) {
+ return false
}
}
+ return true
+ }
+
+ private fun performDesktopExitCleanupIfNeeded(
+ taskId: Int,
+ displayId: Int,
+ wct: WindowContainerTransaction,
+ forceToFullscreen: Boolean,
+ shouldEndUpAtHome: Boolean = true,
+ ): RunOnTransitStart? {
+ taskRepository.setPipShouldKeepDesktopActive(displayId, keepActive = !forceToFullscreen)
+ if (!willExitDesktop(taskId, displayId, forceToFullscreen)) {
+ return null
+ }
+ // TODO: b/394268248 - update remaining callers to pass in a |deskId| and apply the
+ // |RunOnTransitStart| when the transition is started.
+ return performDesktopExitCleanUp(
+ wct = wct,
+ deskId = null,
+ displayId = displayId,
+ willExitDesktop = true,
+ shouldEndUpAtHome = shouldEndUpAtHome,
+ )
+ }
+
+ private fun performDesktopExitCleanUp(
+ wct: WindowContainerTransaction,
+ deskId: Int?,
+ displayId: Int,
+ willExitDesktop: Boolean,
+ shouldEndUpAtHome: Boolean = true,
+ ): RunOnTransitStart? {
+ if (!willExitDesktop) return null
desktopModeEnterExitTransitionListener?.onExitDesktopModeTransitionStarted(
FULLSCREEN_ANIMATION_DURATION
)
@@ -1766,6 +1833,7 @@ class DesktopTasksController(
// intent.
addLaunchHomePendingIntent(wct, displayId)
}
+ return prepareDeskDeactivationIfNeeded(wct, deskId)
}
fun releaseVisualIndicator() {
@@ -1976,8 +2044,10 @@ class DesktopTasksController(
unminimizeReason = UnminimizeReason.APP_HANDLE_MENU_BUTTON,
)
} else {
- moveBackgroundTaskToDesktop(
+ val deskId = getDefaultDeskId(callingTask.displayId)
+ moveTaskToDesk(
requestedTaskId,
+ deskId,
WindowContainerTransaction(),
DesktopModeTransitionSource.APP_HANDLE_MENU_BUTTON,
)
@@ -2095,7 +2165,16 @@ class DesktopTasksController(
): WindowContainerTransaction? {
logV("DesktopTasksController: handleMidRecentsFreeformTaskLaunch")
val wct = WindowContainerTransaction()
- addMoveToFullscreenChanges(wct, task)
+ addMoveToFullscreenChanges(
+ wct = wct,
+ taskInfo = task,
+ willExitDesktop =
+ willExitDesktop(
+ triggerTaskId = task.taskId,
+ displayId = task.displayId,
+ forceToFullscreen = true,
+ ),
+ )
wct.reorder(task.token, true)
return wct
}
@@ -2119,7 +2198,16 @@ class DesktopTasksController(
// launched. We should make this task go to fullscreen instead of freeform. Note
// that this means any re-launch of a freeform window outside of desktop will be in
// fullscreen as long as default-desktop flag is disabled.
- addMoveToFullscreenChanges(wct, task)
+ addMoveToFullscreenChanges(
+ wct = wct,
+ taskInfo = task,
+ willExitDesktop =
+ willExitDesktop(
+ triggerTaskId = task.taskId,
+ displayId = task.displayId,
+ forceToFullscreen = true,
+ ),
+ )
return wct
}
bringDesktopAppsToFrontBeforeShowingNewTask(task.displayId, wct, task.taskId)
@@ -2169,7 +2257,7 @@ class DesktopTasksController(
return wct
}
if (!wct.isEmpty) {
- desktopTilingDecorViewModel.removeTaskIfTiled(task.displayId, task.taskId)
+ snapEventHandler.removeTaskIfTiled(task.displayId, task.taskId)
return wct
}
return null
@@ -2215,7 +2303,16 @@ class DesktopTasksController(
// changes we do for similar transitions. The task not having WINDOWING_MODE_UNDEFINED
// set when needed can interfere with future split / multi-instance transitions.
return WindowContainerTransaction().also { wct ->
- addMoveToFullscreenChanges(wct, task)
+ addMoveToFullscreenChanges(
+ wct = wct,
+ taskInfo = task,
+ willExitDesktop =
+ willExitDesktop(
+ triggerTaskId = task.taskId,
+ displayId = task.displayId,
+ forceToFullscreen = true,
+ ),
+ )
}
}
return null
@@ -2243,10 +2340,25 @@ class DesktopTasksController(
}
// Already fullscreen, no-op.
if (task.isFullscreen) return null
- return WindowContainerTransaction().also { wct -> addMoveToFullscreenChanges(wct, task) }
+ return WindowContainerTransaction().also { wct ->
+ addMoveToFullscreenChanges(
+ wct = wct,
+ taskInfo = task,
+ willExitDesktop =
+ willExitDesktop(
+ triggerTaskId = task.taskId,
+ displayId = task.displayId,
+ forceToFullscreen = true,
+ ),
+ )
+ }
}
- /** Handle task closing by removing wallpaper activity if it's the last active task */
+ /**
+ * Handle task closing by removing wallpaper activity if it's the last active task.
+ *
+ * TODO: b/394268248 - desk needs to be deactivated.
+ */
private fun handleTaskClosing(
task: RunningTaskInfo,
transition: IBinder,
@@ -2265,7 +2377,7 @@ class DesktopTasksController(
if (!DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION.isTrue()) {
taskRepository.addClosingTask(task.displayId, task.taskId)
- desktopTilingDecorViewModel.removeTaskIfTiled(task.displayId, task.taskId)
+ snapEventHandler.removeTaskIfTiled(task.displayId, task.taskId)
}
taskbarDesktopTaskListener?.onTaskbarCornerRoundingUpdate(
@@ -2313,7 +2425,7 @@ class DesktopTasksController(
taskInfo: RunningTaskInfo,
deskId: Int,
) {
- if (!Flags.enableMultipleDesktopsBackend()) return
+ if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) return
val displayId = taskRepository.getDisplayForDesk(deskId)
val displayLayout = displayController.getDisplayLayout(displayId) ?: return
val initialBounds = getInitialBounds(displayLayout, taskInfo, displayId)
@@ -2397,10 +2509,15 @@ class DesktopTasksController(
return bounds
}
+ /**
+ * Applies the changes needed to enter fullscreen and returns the id of the desk that needs to
+ * be deactivated.
+ */
private fun addMoveToFullscreenChanges(
wct: WindowContainerTransaction,
taskInfo: RunningTaskInfo,
- ) {
+ willExitDesktop: Boolean,
+ ): RunOnTransitStart? {
val tdaInfo = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(taskInfo.displayId)!!
val tdaWindowingMode = tdaInfo.configuration.windowConfiguration.windowingMode
val targetWindowingMode =
@@ -2415,12 +2532,16 @@ class DesktopTasksController(
if (useDesktopOverrideDensity()) {
wct.setDensityDpi(taskInfo.token, getDefaultDensityDpi())
}
-
- performDesktopExitCleanupIfNeeded(
- taskInfo.taskId,
- taskInfo.displayId,
- wct,
- forceToFullscreen = true,
+ if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
+ wct.reparent(taskInfo.token, tdaInfo.token, /* onTop= */ true)
+ }
+ taskRepository.setPipShouldKeepDesktopActive(taskInfo.displayId, keepActive = false)
+ val deskId = taskRepository.getDeskIdForTask(taskInfo.taskId)
+ return performDesktopExitCleanUp(
+ wct = wct,
+ deskId = deskId,
+ displayId = taskInfo.displayId,
+ willExitDesktop = willExitDesktop,
shouldEndUpAtHome = false,
)
}
@@ -2445,6 +2566,8 @@ class DesktopTasksController(
/**
* Adds split screen changes to a transaction. Note that bounds are not reset here due to
* animation; see {@link onDesktopSplitSelectAnimComplete}
+ *
+ * TODO: b/394268248 - desk needs to be deactivated.
*/
private fun addMoveToSplitChanges(wct: WindowContainerTransaction, taskInfo: RunningTaskInfo) {
// This windowing mode is to get the transition animation started; once we complete
@@ -2534,10 +2657,7 @@ class DesktopTasksController(
displayId: Int,
remoteTransition: RemoteTransition? = null,
) {
- val deskId =
- checkNotNull(taskRepository.getDefaultDeskId(displayId)) {
- "Expected a default desk to exist"
- }
+ val deskId = getDefaultDeskId(displayId)
activateDesk(deskId, remoteTransition)
}
@@ -2545,7 +2665,7 @@ class DesktopTasksController(
fun activateDesk(deskId: Int, remoteTransition: RemoteTransition? = null) {
val displayId = taskRepository.getDisplayForDesk(deskId)
val wct = WindowContainerTransaction()
- if (Flags.enableMultipleDesktopsBackend()) {
+ if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
prepareForDeskActivation(displayId, wct)
desksOrganizer.activateDesk(wct, deskId)
if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PERSISTENCE.isTrue()) {
@@ -2566,7 +2686,7 @@ class DesktopTasksController(
val transition = transitions.startTransition(transitionType, wct, handler)
handler?.setTransition(transition)
- if (Flags.enableMultipleDesktopsBackend()) {
+ if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
desksTransitionObserver.addPendingTransition(
DeskTransition.ActivateDesk(
token = transition,
@@ -2581,16 +2701,35 @@ class DesktopTasksController(
)
}
+ /**
+ * TODO: b/393978539 - Deactivation should not happen in desktop-first devices when going home.
+ */
+ private fun prepareDeskDeactivationIfNeeded(
+ wct: WindowContainerTransaction,
+ deskId: Int?,
+ ): RunOnTransitStart? {
+ if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) return null
+ if (deskId == null) return null
+ desksOrganizer.deactivateDesk(wct, deskId)
+ return { transition ->
+ desksTransitionObserver.addPendingTransition(
+ DeskTransition.DeactivateDesk(token = transition, deskId = deskId)
+ )
+ }
+ }
+
/** Removes the default desk in the given display. */
@Deprecated("Deprecated with multi-desks.", ReplaceWith("removeDesk()"))
fun removeDefaultDeskInDisplay(displayId: Int) {
- val deskId =
- checkNotNull(taskRepository.getDefaultDeskId(displayId)) {
- "Expected a default desk to exist"
- }
+ val deskId = getDefaultDeskId(displayId)
removeDesk(displayId = displayId, deskId = deskId)
}
+ private fun getDefaultDeskId(displayId: Int) =
+ checkNotNull(taskRepository.getDefaultDeskId(displayId)) {
+ "Expected a default desk to exist in display: $displayId"
+ }
+
/** Removes the given desk. */
fun removeDesk(deskId: Int) {
val displayId = taskRepository.getDisplayForDesk(deskId)
@@ -2602,7 +2741,7 @@ class DesktopTasksController(
logV("removeDesk deskId=%d from displayId=%d", deskId, displayId)
val tasksToRemove =
- if (Flags.enableMultipleDesktopsBackend()) {
+ if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
taskRepository.getActiveTaskIdsInDesk(deskId)
} else {
// TODO: 362720497 - make sure minimized windows are also removed in WM
@@ -2611,7 +2750,7 @@ class DesktopTasksController(
}
val wct = WindowContainerTransaction()
- if (!Flags.enableMultipleDesktopsBackend()) {
+ if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
tasksToRemove.forEach {
val task = shellTaskOrganizer.getRunningTaskInfo(it)
if (task != null) {
@@ -2624,9 +2763,9 @@ class DesktopTasksController(
// TODO: 362720497 - double check background tasks are also removed.
desksOrganizer.removeDesk(wct, deskId)
}
- if (!Flags.enableMultipleDesktopsBackend() && wct.isEmpty) return
+ if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue && wct.isEmpty) return
val transition = transitions.startTransition(TRANSIT_CLOSE, wct, /* handler= */ null)
- if (Flags.enableMultipleDesktopsBackend()) {
+ if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
desksTransitionObserver.addPendingTransition(
DeskTransition.RemoveDesk(
token = transition,
@@ -2730,7 +2869,7 @@ class DesktopTasksController(
taskBounds: Rect,
) {
if (taskInfo.windowingMode != WINDOWING_MODE_FREEFORM) return
- desktopTilingDecorViewModel.removeTaskIfTiled(taskInfo.displayId, taskInfo.taskId)
+ snapEventHandler.removeTaskIfTiled(taskInfo.displayId, taskInfo.taskId)
updateVisualIndicator(
taskInfo,
taskSurface,
@@ -2747,6 +2886,12 @@ class DesktopTasksController(
taskTop: Float,
dragStartState: DragStartState,
): DesktopModeVisualIndicator.IndicatorType {
+ // If the visual indicator has the wrong start state, it was never cleared from a previous
+ // drag event and needs to be cleared
+ if (visualIndicator != null && visualIndicator?.dragStartState != dragStartState) {
+ Slog.e(TAG, "Visual indicator from previous motion event was never released")
+ releaseVisualIndicator()
+ }
// If the visual indicator does not exist, create it.
val indicator =
visualIndicator
@@ -2790,7 +2935,6 @@ class DesktopTasksController(
validDragArea: Rect,
dragStartBounds: Rect,
motionEvent: MotionEvent,
- desktopModeWindowDecoration: DesktopModeWindowDecoration,
) {
if (taskInfo.configuration.windowConfiguration.windowingMode != WINDOWING_MODE_FREEFORM) {
return
@@ -2829,7 +2973,6 @@ class DesktopTasksController(
currentDragBounds,
dragStartBounds,
motionEvent,
- desktopModeWindowDecoration,
)
}
IndicatorType.TO_SPLIT_RIGHT_INDICATOR -> {
@@ -2844,7 +2987,6 @@ class DesktopTasksController(
currentDragBounds,
dragStartBounds,
motionEvent,
- desktopModeWindowDecoration,
)
}
IndicatorType.NO_INDICATOR,
@@ -3130,7 +3272,7 @@ class DesktopTasksController(
logV("onUserChanged previousUserId=%d, newUserId=%d", userId, newUserId)
userId = newUserId
taskRepository = userRepositories.getProfile(userId)
- desktopTilingDecorViewModel.onUserChange()
+ snapEventHandler.onUserChange()
}
/** Called when a task's info changes. */
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 fc29498291da..0929ae15e668 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
@@ -251,14 +251,15 @@ sealed class DragToDesktopTransitionHandler(
(cancelState == CancelState.CANCEL_BUBBLE_LEFT ||
cancelState == CancelState.CANCEL_BUBBLE_RIGHT)
) {
- if (!bubbleController.isPresent) {
+ if (bubbleController.isEmpty || state !is TransitionState.FromFullscreen) {
+ // TODO(b/388853233): add support for dragging split task to bubble
startCancelAnimation()
} else {
// Animation is handled by BubbleController
val wct = WindowContainerTransaction()
restoreWindowOrder(wct, state)
- // TODO(b/388851898): pass along information about left or right side
- requestBubbleFromScaledTask(wct)
+ val onLeft = cancelState == CancelState.CANCEL_BUBBLE_LEFT
+ requestBubbleFromScaledTask(wct, onLeft)
}
} else {
// There's no dragged task, this can happen when the "cancel" happened too quickly
@@ -318,23 +319,27 @@ sealed class DragToDesktopTransitionHandler(
splitScreenController.requestEnterSplitSelect(taskInfo, wct, splitPosition, taskBounds)
}
- private fun requestBubbleFromScaledTask(wct: WindowContainerTransaction) {
+ private fun requestBubbleFromScaledTask(wct: WindowContainerTransaction, onLeft: Boolean) {
// TODO(b/391928049): update density once we can drag from desktop to bubble
val state = requireTransitionState()
val taskInfo = state.draggedTaskChange?.taskInfo ?: error("Expected non-null taskInfo")
val taskBounds = getAnimatedTaskBounds()
state.dragAnimator.cancelAnimator()
- requestBubble(wct, taskInfo, taskBounds)
+ requestBubble(wct, taskInfo, onLeft, taskBounds)
}
private fun requestBubble(
wct: WindowContainerTransaction,
taskInfo: RunningTaskInfo,
+ onLeft: Boolean,
taskBounds: Rect = Rect(taskInfo.configuration.windowConfiguration.bounds),
) {
val controller =
bubbleController.orElseThrow { IllegalStateException("BubbleController not set") }
- controller.expandStackAndSelectBubble(taskInfo, BubbleTransitions.DragData(taskBounds, wct))
+ controller.expandStackAndSelectBubble(
+ taskInfo,
+ BubbleTransitions.DragData(taskBounds, wct, onLeft),
+ )
}
override fun startAnimation(
@@ -493,12 +498,17 @@ sealed class DragToDesktopTransitionHandler(
state.cancelState == CancelState.CANCEL_BUBBLE_LEFT ||
state.cancelState == CancelState.CANCEL_BUBBLE_RIGHT
) {
+ if (bubbleController.isEmpty || state !is TransitionState.FromFullscreen) {
+ // TODO(b/388853233): add support for dragging split task to bubble
+ startCancelDragToDesktopTransition()
+ return true
+ }
val taskInfo =
state.draggedTaskChange?.taskInfo ?: error("Expected non-null task info.")
val wct = WindowContainerTransaction()
restoreWindowOrder(wct)
- // TODO(b/388851898): pass along information about left or right side
- requestBubble(wct, taskInfo)
+ val onLeft = state.cancelState == CancelState.CANCEL_BUBBLE_LEFT
+ requestBubble(wct, taskInfo, onLeft)
}
return true
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandler.java
index 5ae1fca73d4e..95cc1e68ac11 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandler.java
@@ -106,7 +106,7 @@ public class ExitDesktopTaskTransitionHandler implements Transitions.TransitionH
* @param position Position of the task when transition is started
* @param onAnimationEndCallback to be called after animation
*/
- public void startTransition(@NonNull DesktopModeTransitionSource transitionSource,
+ public IBinder startTransition(@NonNull DesktopModeTransitionSource transitionSource,
@NonNull WindowContainerTransaction wct, Point position,
Function0<Unit> onAnimationEndCallback) {
mPosition = position;
@@ -114,6 +114,7 @@ public class ExitDesktopTaskTransitionHandler implements Transitions.TransitionH
final IBinder token = mTransitions.startTransition(getExitTransitionType(transitionSource),
wct, this);
mPendingTransitionTokens.add(token);
+ return token;
}
@Override
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 2a8a3475c2a5..b5490cb4b595 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
@@ -20,7 +20,7 @@ import android.util.SparseArray
import android.util.SparseBooleanArray
import android.view.Display.DEFAULT_DISPLAY
import android.window.WindowContainerToken
-import androidx.core.util.forEach
+import androidx.core.util.keyIterator
import com.android.internal.protolog.ProtoLog
import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE
@@ -45,11 +45,13 @@ class DesktopWallpaperActivityTokenProvider {
}
fun removeToken(token: WindowContainerToken) {
- wallpaperActivityTokenByDisplayId.forEach { displayId, value ->
- if (value == token) {
- logV("Remove desktop wallpaper activity token for display %s", displayId)
- wallpaperActivityTokenByDisplayId.delete(displayId)
+ val displayId =
+ wallpaperActivityTokenByDisplayId.keyIterator().asSequence().find {
+ wallpaperActivityTokenByDisplayId[it] == token
}
+ if (displayId != null) {
+ logV("Remove desktop wallpaper activity token for display %s", displayId)
+ wallpaperActivityTokenByDisplayId.delete(displayId)
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DeskTransition.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DeskTransition.kt
index 8c4fd9db050f..9dec96933ee5 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DeskTransition.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DeskTransition.kt
@@ -42,4 +42,7 @@ sealed class DeskTransition {
val deskId: Int,
val enterTaskId: Int,
) : DeskTransition()
+
+ /** A transition to deactivate a desk. */
+ data class DeactivateDesk(override val token: IBinder, val deskId: Int) : DeskTransition()
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksOrganizer.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksOrganizer.kt
index 547890a6200a..0f2f3711a9a3 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksOrganizer.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksOrganizer.kt
@@ -27,6 +27,9 @@ interface DesksOrganizer {
/** Activates the given desk, making it visible in its display. */
fun activateDesk(wct: WindowContainerTransaction, deskId: Int)
+ /** Deactivates the given desk, removing it as the default launch container for new tasks. */
+ fun deactivateDesk(wct: WindowContainerTransaction, deskId: Int)
+
/** Removes the given desk and its desktop windows. */
fun removeDesk(wct: WindowContainerTransaction, deskId: Int)
@@ -37,6 +40,9 @@ interface DesksOrganizer {
task: ActivityManager.RunningTaskInfo,
)
+ /** Whether the change is for the given desk id. */
+ fun isDeskChange(change: TransitionInfo.Change, deskId: Int): Boolean
+
/**
* Returns the desk id in which the task in the given change is located at the end of a
* transition, if any.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksTransitionObserver.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksTransitionObserver.kt
index 6d88c3310a63..e57b56378fb3 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksTransitionObserver.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksTransitionObserver.kt
@@ -17,9 +17,11 @@ package com.android.wm.shell.desktopmode.multidesks
import android.os.IBinder
import android.view.WindowManager.TRANSIT_CLOSE
+import android.window.DesktopExperienceFlags
import android.window.TransitionInfo
-import com.android.window.flags.Flags
+import com.android.internal.protolog.ProtoLog
import com.android.wm.shell.desktopmode.DesktopUserRepositories
+import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE
/**
* Observer of desk-related transitions, such as adding, removing or activating a whole desk. It
@@ -33,7 +35,7 @@ class DesksTransitionObserver(
/** Adds a pending desk transition to be tracked. */
fun addPendingTransition(transition: DeskTransition) {
- if (!Flags.enableMultipleDesktopsBackend()) return
+ if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) return
deskTransitions[transition.token] = transition
}
@@ -42,8 +44,9 @@ class DesksTransitionObserver(
* observer.
*/
fun onTransitionReady(transition: IBinder, info: TransitionInfo) {
- if (!Flags.enableMultipleDesktopsBackend()) return
+ if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) return
val deskTransition = deskTransitions.remove(transition) ?: return
+ logD("Desk transition ready: %s", deskTransition)
val desktopRepository = desktopUserRepositories.current
when (deskTransition) {
is DeskTransition.RemoveDesk -> {
@@ -88,6 +91,42 @@ class DesksTransitionObserver(
)
}
}
+ is DeskTransition.DeactivateDesk -> {
+ var visibleDeactivation = false
+ for (change in info.changes) {
+ val isDeskChange = desksOrganizer.isDeskChange(change, deskTransition.deskId)
+ if (isDeskChange) {
+ visibleDeactivation = true
+ continue
+ }
+ val taskId = change.taskInfo?.taskId ?: continue
+ val removedFromDesk =
+ desktopRepository.getDeskIdForTask(taskId) == deskTransition.deskId &&
+ desksOrganizer.getDeskAtEnd(change) == null
+ if (removedFromDesk) {
+ desktopRepository.removeTaskFromDesk(
+ deskId = deskTransition.deskId,
+ taskId = taskId,
+ )
+ }
+ }
+ // Always deactivate even if there's no change that confirms the desk was
+ // deactivated. Some interactions, such as the desk deactivating because it's
+ // occluded by a fullscreen task result in a transition change, but others, such
+ // as transitioning from an empty desk to home may not.
+ if (!visibleDeactivation) {
+ logD("Deactivating desk without transition change")
+ }
+ desktopRepository.setDeskInactive(deskId = deskTransition.deskId)
+ }
}
}
+
+ private fun logD(msg: String, vararg arguments: Any?) {
+ ProtoLog.d(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments)
+ }
+
+ private companion object {
+ private const val TAG = "DesksTransitionObserver"
+ }
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizer.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizer.kt
index 5cda76e2f3e0..339932cabd2c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizer.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizer.kt
@@ -23,12 +23,12 @@ import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED
import android.util.SparseArray
import android.view.SurfaceControl
import android.view.WindowManager.TRANSIT_TO_FRONT
+import android.window.DesktopExperienceFlags
import android.window.TransitionInfo
import android.window.WindowContainerTransaction
import androidx.core.util.forEach
import com.android.internal.annotations.VisibleForTesting
import com.android.internal.protolog.ProtoLog
-import com.android.window.flags.Flags
import com.android.wm.shell.ShellTaskOrganizer
import com.android.wm.shell.desktopmode.multidesks.DesksOrganizer.OnCreateCallback
import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE
@@ -47,7 +47,7 @@ class RootTaskDesksOrganizer(
@VisibleForTesting val roots = SparseArray<DeskRoot>()
init {
- if (Flags.enableMultipleDesktopsBackend()) {
+ if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
shellInit.addInitCallback(
{ shellCommandHandler.addDumpCallback(this::dump, this) },
this,
@@ -83,6 +83,16 @@ class RootTaskDesksOrganizer(
)
}
+ override fun deactivateDesk(wct: WindowContainerTransaction, deskId: Int) {
+ logV("deactivateDesk %d", deskId)
+ val root = checkNotNull(roots[deskId]) { "Root not found for desk: $deskId" }
+ wct.setLaunchRoot(
+ /* container= */ root.taskInfo.token,
+ /* windowingModes= */ null,
+ /* activityTypes= */ null,
+ )
+ }
+
override fun moveTaskToDesk(
wct: WindowContainerTransaction,
deskId: Int,
@@ -93,6 +103,9 @@ class RootTaskDesksOrganizer(
wct.reparent(task.token, root.taskInfo.token, /* onTop= */ true)
}
+ override fun isDeskChange(change: TransitionInfo.Change, deskId: Int): Boolean =
+ roots.contains(deskId) && change.taskInfo?.taskId == deskId
+
override fun getDeskAtEnd(change: TransitionInfo.Change): Int? =
change.taskInfo?.parentTaskId?.takeIf { it in roots }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializerImpl.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializerImpl.kt
index 5a89451ffdbc..0507e59c06e1 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializerImpl.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializerImpl.kt
@@ -17,8 +17,8 @@
package com.android.wm.shell.desktopmode.persistence
import android.content.Context
+import android.window.DesktopExperienceFlags
import android.window.DesktopModeFlags
-import com.android.window.flags.Flags
import com.android.wm.shell.desktopmode.DesktopRepository
import com.android.wm.shell.desktopmode.DesktopUserRepositories
import com.android.wm.shell.shared.annotations.ShellMainThread
@@ -58,7 +58,7 @@ class DesktopRepositoryInitializerImpl(
repository.addDesk(
displayId = persistentDesktop.displayId,
deskId =
- if (Flags.enableMultipleDesktopsBackend()) {
+ if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
persistentDesktop.desktopId
} else {
// When disabled, desk ids are always the display id.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionHandler.java
index 31715f0444a9..b60fb5e7bfdd 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionHandler.java
@@ -93,7 +93,8 @@ public class FreeformTaskTransitionHandler
}
@Override
- public IBinder startMinimizedModeTransition(WindowContainerTransaction wct) {
+ public IBinder startMinimizedModeTransition(
+ WindowContainerTransaction wct, int taskId, boolean isLastTask) {
final int type = Transitions.TRANSIT_MINIMIZE;
final IBinder token = mTransitions.startTransition(type, wct, this);
mPendingTransitionTokens.add(token);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionStarter.java b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionStarter.java
index a874a5be426d..822934c1e646 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionStarter.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionStarter.java
@@ -38,10 +38,13 @@ public interface FreeformTaskTransitionStarter {
* Starts window minimization transition
*
* @param wct the {@link WindowContainerTransaction} that changes the windowing mode
+ * @param taskId the task id of the task being minimized
+ * @param isLastTask true if the task being minimized is the last visible task
*
* @return the started transition
*/
- IBinder startMinimizedModeTransition(WindowContainerTransaction wct);
+ IBinder startMinimizedModeTransition(
+ WindowContainerTransaction wct, int taskId, boolean isLastTask);
/**
* Starts close window transition
@@ -60,4 +63,4 @@ public interface FreeformTaskTransitionStarter {
* @return the started transition
*/
IBinder startPipTransition(WindowContainerTransaction wct);
-} \ No newline at end of file
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitionHandler.java
index c0a0f469add4..d666126b91ba 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitionHandler.java
@@ -22,7 +22,6 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
import static android.service.dreams.Flags.dismissDreamOnKeyguardDismiss;
import static android.view.WindowManager.KEYGUARD_VISIBILITY_TRANSIT_FLAGS;
-import static android.view.WindowManager.TRANSIT_FLAG_AOD_APPEARING;
import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_APPEARING;
import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY;
import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_LOCKED;
@@ -201,8 +200,7 @@ public class KeyguardTransitionHandler
transition, info, startTransaction, finishTransaction, finishCallback);
}
- if ((info.getFlags() & TRANSIT_FLAG_KEYGUARD_APPEARING) != 0
- || (info.getFlags() & TRANSIT_FLAG_AOD_APPEARING) != 0) {
+ if ((info.getFlags() & TRANSIT_FLAG_KEYGUARD_APPEARING) != 0) {
return startAnimation(mAppearTransition, "appearing",
transition, info, startTransaction, finishTransaction, finishCallback);
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java
index da3181096d98..cef18f55b86d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java
@@ -145,7 +145,7 @@ public abstract class PipTransitionController implements Transitions.TransitionH
/**
* Called when the Shell wants to start an exit-via-expand from Pip transition/animation.
*/
- public void startExpandTransition(WindowContainerTransaction out) {
+ public void startExpandTransition(WindowContainerTransaction out, boolean toSplit) {
// Default implementation does nothing.
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipDismissTargetHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipDismissTargetHandler.java
index 71697596afd3..a837e7d308eb 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipDismissTargetHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipDismissTargetHandler.java
@@ -296,6 +296,7 @@ public class PipDismissTargetHandler implements ViewTreeObserver.OnPreDrawListen
return;
}
+ mMagneticTarget.updateLocationOnScreen();
createOrUpdateDismissTarget();
if (mTargetViewContainer.getVisibility() != View.VISIBLE) {
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 e17587ff18bc..df7a25af8376 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
@@ -35,6 +35,10 @@ import com.android.wm.shell.pip.PipTransitionController;
import com.android.wm.shell.pip2.PipSurfaceTransactionHelper;
import com.android.wm.shell.pip2.animation.PipAlphaAnimator;
import com.android.wm.shell.protolog.ShellProtoLogGroup;
+import com.android.wm.shell.shared.split.SplitScreenConstants;
+import com.android.wm.shell.splitscreen.SplitScreenController;
+
+import java.util.Optional;
/**
* Scheduler for Shell initiated PiP transitions and animations.
@@ -47,6 +51,7 @@ public class PipScheduler {
private final ShellExecutor mMainExecutor;
private final PipTransitionState mPipTransitionState;
private final PipDesktopState mPipDesktopState;
+ private final Optional<SplitScreenController> mSplitScreenControllerOptional;
private PipTransitionController mPipTransitionController;
private PipSurfaceTransactionHelper.SurfaceControlTransactionFactory
mSurfaceControlTransactionFactory;
@@ -59,12 +64,14 @@ public class PipScheduler {
PipBoundsState pipBoundsState,
ShellExecutor mainExecutor,
PipTransitionState pipTransitionState,
+ Optional<SplitScreenController> splitScreenControllerOptional,
PipDesktopState pipDesktopState) {
mContext = context;
mPipBoundsState = pipBoundsState;
mMainExecutor = mainExecutor;
mPipTransitionState = pipTransitionState;
mPipDesktopState = pipDesktopState;
+ mSplitScreenControllerOptional = splitScreenControllerOptional;
mSurfaceControlTransactionFactory =
new PipSurfaceTransactionHelper.VsyncSurfaceControlTransactionFactory();
@@ -96,10 +103,23 @@ public class PipScheduler {
public void scheduleExitPipViaExpand() {
mMainExecutor.execute(() -> {
if (!mPipTransitionState.isInPip()) return;
- WindowContainerTransaction wct = getExitPipViaExpandTransaction();
- if (wct != null) {
- mPipTransitionController.startExpandTransition(wct);
- }
+
+ final WindowContainerTransaction expandWct = getExitPipViaExpandTransaction();
+ if (expandWct == null) return;
+
+ final WindowContainerTransaction wct = new WindowContainerTransaction();
+ mSplitScreenControllerOptional.ifPresent(splitScreenController -> {
+ int lastParentTaskId = mPipTransitionState.getPipTaskInfo()
+ .lastParentTaskIdBeforePip;
+ if (splitScreenController.isTaskInSplitScreen(lastParentTaskId)) {
+ splitScreenController.prepareEnterSplitScreen(wct,
+ null /* taskInfo */, SplitScreenConstants.SPLIT_POSITION_UNDEFINED);
+ }
+ });
+
+ boolean toSplit = !wct.isEmpty();
+ wct.merge(expandWct, true /* transfer */);
+ mPipTransitionController.startExpandTransition(wct, toSplit);
});
}
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 a57b4b948b42..035c93db7ee4 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
@@ -16,7 +16,6 @@
package com.android.wm.shell.pip2.phone;
-import static android.app.WindowConfiguration.ROTATION_UNDEFINED;
import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
import static android.view.Surface.ROTATION_0;
@@ -29,7 +28,13 @@ import static android.view.WindowManager.TRANSIT_PIP;
import static android.view.WindowManager.TRANSIT_TO_BACK;
import static android.view.WindowManager.TRANSIT_TO_FRONT;
+import static com.android.wm.shell.pip2.phone.transition.PipTransitionUtils.getChangeByToken;
+import static com.android.wm.shell.pip2.phone.transition.PipTransitionUtils.getFixedRotationDelta;
+import static com.android.wm.shell.pip2.phone.transition.PipTransitionUtils.getLeash;
+import static com.android.wm.shell.pip2.phone.transition.PipTransitionUtils.getPipChange;
+import static com.android.wm.shell.pip2.phone.transition.PipTransitionUtils.getPipParams;
import static com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP;
+import static com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP_TO_SPLIT;
import static com.android.wm.shell.transition.Transitions.TRANSIT_REMOVE_PIP;
import static com.android.wm.shell.transition.Transitions.TRANSIT_RESIZE_PIP;
import static com.android.wm.shell.transition.Transitions.transitTypeToString;
@@ -45,7 +50,6 @@ import android.graphics.PointF;
import android.graphics.Rect;
import android.os.Bundle;
import android.os.IBinder;
-import android.view.Surface;
import android.view.SurfaceControl;
import android.view.WindowManager;
import android.window.TransitionInfo;
@@ -70,11 +74,14 @@ import com.android.wm.shell.pip.PipTransitionController;
import com.android.wm.shell.pip2.PipSurfaceTransactionHelper;
import com.android.wm.shell.pip2.animation.PipAlphaAnimator;
import com.android.wm.shell.pip2.animation.PipEnterAnimator;
-import com.android.wm.shell.pip2.animation.PipExpandAnimator;
+import com.android.wm.shell.pip2.phone.transition.PipExpandHandler;
import com.android.wm.shell.shared.TransitionUtil;
+import com.android.wm.shell.splitscreen.SplitScreenController;
import com.android.wm.shell.sysui.ShellInit;
import com.android.wm.shell.transition.Transitions;
+import java.util.Optional;
+
/**
* Implementation of transitions for PiP on phone.
*/
@@ -130,6 +137,7 @@ public class PipTransition extends PipTransitionController implements
//
// Internal state and relevant cached info
//
+ private final PipExpandHandler mExpandHandler;
private Transitions.TransitionFinishCallback mFinishCallback;
@@ -151,6 +159,7 @@ public class PipTransition extends PipTransitionController implements
PipDisplayLayoutState pipDisplayLayoutState,
PipUiStateChangeController pipUiStateChangeController,
DisplayController displayController,
+ Optional<SplitScreenController> splitScreenControllerOptional,
PipDesktopState pipDesktopState) {
super(shellInit, shellTaskOrganizer, transitions, pipBoundsState, pipMenuController,
pipBoundsAlgorithm);
@@ -165,6 +174,9 @@ public class PipTransition extends PipTransitionController implements
mDisplayController = displayController;
mPipSurfaceTransactionHelper = new PipSurfaceTransactionHelper(mContext);
mPipDesktopState = pipDesktopState;
+
+ mExpandHandler = new PipExpandHandler(mContext, pipBoundsState, pipBoundsAlgorithm,
+ pipTransitionState, pipDisplayLayoutState, splitScreenControllerOptional);
}
@Override
@@ -184,10 +196,11 @@ public class PipTransition extends PipTransitionController implements
//
@Override
- public void startExpandTransition(WindowContainerTransaction out) {
+ public void startExpandTransition(WindowContainerTransaction out, boolean toSplit) {
if (out == null) return;
mPipTransitionState.setState(PipTransitionState.EXITING_PIP);
- mExitViaExpandTransition = mTransitions.startTransition(TRANSIT_EXIT_PIP, out, this);
+ mExitViaExpandTransition = mTransitions.startTransition(toSplit ? TRANSIT_EXIT_PIP_TO_SPLIT
+ : TRANSIT_EXIT_PIP, out, this);
}
@Override
@@ -239,10 +252,11 @@ public class PipTransition extends PipTransitionController implements
@NonNull SurfaceControl.Transaction finishT,
@NonNull IBinder mergeTarget,
@NonNull Transitions.TransitionFinishCallback finishCallback) {
- // Just jump-cut the current animation if any, but do not merge.
if (info.getType() == TRANSIT_EXIT_PIP) {
end();
}
+ mExpandHandler.mergeAnimation(transition, info, startT, finishT, mergeTarget,
+ finishCallback);
}
@Override
@@ -290,7 +304,8 @@ public class PipTransition extends PipTransitionController implements
finishCallback);
} else if (transition == mExitViaExpandTransition) {
mExitViaExpandTransition = null;
- return startExpandAnimation(info, startTransaction, finishTransaction, finishCallback);
+ return mExpandHandler.startAnimation(transition, info, startTransaction,
+ finishTransaction, finishCallback);
} else if (transition == mResizeTransition) {
mResizeTransition = null;
return startResizeAnimation(info, startTransaction, finishTransaction, finishCallback);
@@ -300,6 +315,9 @@ public class PipTransition extends PipTransitionController implements
mPipTransitionState.setState(PipTransitionState.EXITING_PIP);
return startRemoveAnimation(info, startTransaction, finishTransaction, finishCallback);
}
+ // For any unhandled transition, make sure the PiP surface is properly updated,
+ // i.e. corner and shadow radius.
+ syncPipSurfaceState(info, startTransaction, finishTransaction);
return false;
}
@@ -433,7 +451,7 @@ public class PipTransition extends PipTransitionController implements
(destinationBounds.height() - overlaySize) / 2f);
}
- final int delta = getFixedRotationDelta(info, pipChange);
+ final int delta = getFixedRotationDelta(info, pipChange, mPipDisplayLayoutState);
if (delta != ROTATION_0) {
// Update transition target changes in place to prepare for fixed rotation.
handleBoundsEnterFixedRotation(info, pipChange, pipActivityChange);
@@ -493,7 +511,7 @@ public class PipTransition extends PipTransitionController implements
final Rect adjustedSourceRectHint = getAdjustedSourceRectHint(info, pipChange,
pipActivityChange);
- final int delta = getFixedRotationDelta(info, pipChange);
+ final int delta = getFixedRotationDelta(info, pipChange, mPipDisplayLayoutState);
if (delta != ROTATION_0) {
// Update transition target changes in place to prepare for fixed rotation.
handleBoundsEnterFixedRotation(info, pipChange, pipActivityChange);
@@ -582,27 +600,6 @@ public class PipTransition extends PipTransitionController implements
endBounds.top + activityEndOffset.y);
}
- private void handleExpandFixedRotation(TransitionInfo.Change outPipTaskChange, int delta) {
- final Rect endBounds = outPipTaskChange.getEndAbsBounds();
- final int width = endBounds.width();
- final int height = endBounds.height();
- final int left = endBounds.left;
- final int top = endBounds.top;
- int newTop, newLeft;
-
- if (delta == Surface.ROTATION_90) {
- newLeft = top;
- newTop = -(left + width);
- } else {
- newLeft = -(height + top);
- newTop = left;
- }
- // Modify the endBounds, rotating and placing them potentially off-screen, so that
- // as we translate and rotate around the origin, we place them right into the target.
- endBounds.set(newLeft, newTop, newLeft + height, newTop + width);
- }
-
-
private boolean startAlphaTypeEnterAnimation(@NonNull TransitionInfo info,
@NonNull SurfaceControl.Transaction startTransaction,
@NonNull SurfaceControl.Transaction finishTransaction,
@@ -630,83 +627,6 @@ public class PipTransition extends PipTransitionController implements
return true;
}
- private boolean startExpandAnimation(@NonNull TransitionInfo info,
- @NonNull SurfaceControl.Transaction startTransaction,
- @NonNull SurfaceControl.Transaction finishTransaction,
- @NonNull Transitions.TransitionFinishCallback finishCallback) {
- WindowContainerToken pipToken = mPipTransitionState.getPipTaskToken();
-
- TransitionInfo.Change pipChange = getChangeByToken(info, pipToken);
- if (pipChange == null) {
- // pipChange is null, check to see if we've reparented the PIP activity for
- // the multi activity case. If so we should use the activity leash instead
- for (TransitionInfo.Change change : info.getChanges()) {
- if (change.getTaskInfo() == null
- && change.getLastParent() != null
- && change.getLastParent().equals(pipToken)) {
- pipChange = change;
- break;
- }
- }
-
- // failsafe
- if (pipChange == null) {
- return false;
- }
- }
- mFinishCallback = finishCallback;
-
- // The parent change if we were in a multi-activity PiP; null if single activity PiP.
- final TransitionInfo.Change parentBeforePip = pipChange.getTaskInfo() == null
- ? getChangeByToken(info, pipChange.getParent()) : null;
- if (parentBeforePip != null) {
- // For multi activity, we need to manually set the leash layer
- startTransaction.setLayer(parentBeforePip.getLeash(), Integer.MAX_VALUE - 1);
- }
-
- final Rect startBounds = pipChange.getStartAbsBounds();
- final Rect endBounds = pipChange.getEndAbsBounds();
- final SurfaceControl pipLeash = getLeash(pipChange);
-
- PictureInPictureParams params = null;
- if (pipChange.getTaskInfo() != null) {
- // single activity
- params = getPipParams(pipChange);
- } else if (parentBeforePip != null && parentBeforePip.getTaskInfo() != null) {
- // multi activity
- params = getPipParams(parentBeforePip);
- }
- final Rect sourceRectHint = PipBoundsAlgorithm.getValidSourceHintRect(params, endBounds,
- startBounds);
-
- // We define delta = startRotation - endRotation, so we need to flip the sign.
- final int delta = -getFixedRotationDelta(info, pipChange);
- if (delta != ROTATION_0) {
- // Update PiP target change in place to prepare for fixed rotation;
- handleExpandFixedRotation(pipChange, delta);
- }
-
- PipExpandAnimator animator = new PipExpandAnimator(mContext, pipLeash,
- startTransaction, finishTransaction, endBounds, startBounds, endBounds,
- sourceRectHint, delta);
- animator.setAnimationEndCallback(() -> {
- if (parentBeforePip != null) {
- // TODO b/377362511: Animate local leash instead to also handle letterbox case.
- // For multi-activity, set the crop to be null
- finishTransaction.setCrop(pipLeash, null);
- }
- finishTransition();
- });
- cacheAndStartTransitionAnimator(animator);
-
- // Save the PiP bounds in case, we re-enter the PiP with the same component.
- float snapFraction = mPipBoundsAlgorithm.getSnapFraction(
- mPipBoundsState.getBounds());
- mPipBoundsState.saveReentryState(snapFraction);
-
- return true;
- }
-
private boolean startRemoveAnimation(@NonNull TransitionInfo info,
@NonNull SurfaceControl.Transaction startTransaction,
@NonNull SurfaceControl.Transaction finishTransaction,
@@ -740,29 +660,6 @@ public class PipTransition extends PipTransitionController implements
// Various helpers to resolve transition requests and infos
//
- @Nullable
- private TransitionInfo.Change getPipChange(TransitionInfo info) {
- for (TransitionInfo.Change change : info.getChanges()) {
- if (change.getTaskInfo() != null
- && change.getTaskInfo().getWindowingMode() == WINDOWING_MODE_PINNED) {
- return change;
- }
- }
- return null;
- }
-
- @Nullable
- private TransitionInfo.Change getChangeByToken(TransitionInfo info,
- WindowContainerToken token) {
- for (TransitionInfo.Change change : info.getChanges()) {
- if (change.getTaskInfo() != null
- && change.getTaskInfo().getToken().equals(token)) {
- return change;
- }
- }
- return null;
- }
-
@NonNull
private Rect getAdjustedSourceRectHint(@NonNull TransitionInfo info,
@NonNull TransitionInfo.Change pipTaskChange,
@@ -786,8 +683,8 @@ public class PipTransition extends PipTransitionController implements
Rect cutoutInsets = parentBeforePip != null
? parentBeforePip.getTaskInfo().displayCutoutInsets
: pipTaskChange.getTaskInfo().displayCutoutInsets;
- if (cutoutInsets != null
- && getFixedRotationDelta(info, pipTaskChange) == ROTATION_90) {
+ if (cutoutInsets != null && getFixedRotationDelta(info, pipTaskChange,
+ mPipDisplayLayoutState) == ROTATION_90) {
adjustedSourceRectHint.offset(cutoutInsets.left, cutoutInsets.top);
}
if (mPipDesktopState.isDesktopWindowingPipEnabled()) {
@@ -804,25 +701,6 @@ public class PipTransition extends PipTransitionController implements
return adjustedSourceRectHint;
}
- @Surface.Rotation
- private int getFixedRotationDelta(@NonNull TransitionInfo info,
- @NonNull TransitionInfo.Change pipChange) {
- TransitionInfo.Change fixedRotationChange = findFixedRotationChange(info);
- int startRotation = pipChange.getStartRotation();
- if (pipChange.getEndRotation() != ROTATION_UNDEFINED
- && startRotation != pipChange.getEndRotation()) {
- // If PiP change was collected along with the display change and the orientation change
- // happened in sync with the PiP change, then do not treat this as fixed-rotation case.
- return ROTATION_0;
- }
-
- int endRotation = fixedRotationChange != null
- ? fixedRotationChange.getEndFixedRotation() : mPipDisplayLayoutState.getRotation();
- int delta = endRotation == ROTATION_UNDEFINED ? ROTATION_0
- : startRotation - endRotation;
- return delta;
- }
-
private void prepareOtherTargetTransforms(TransitionInfo info,
SurfaceControl.Transaction startTransaction,
SurfaceControl.Transaction finishTransaction) {
@@ -850,7 +728,8 @@ public class PipTransition extends PipTransitionController implements
// If PiP is enabled on Connected Displays, update PipDisplayLayoutState to have the correct
// display info that PiP is entering in.
- if (mPipDesktopState.isConnectedDisplaysPipEnabled()) {
+ if (mPipDesktopState.isConnectedDisplaysPipEnabled()
+ && pipTask.displayId != mPipDisplayLayoutState.getDisplayId()) {
final DisplayLayout displayLayout = mDisplayController.getDisplayLayout(
pipTask.displayId);
if (displayLayout != null) {
@@ -1009,20 +888,6 @@ public class PipTransition extends PipTransitionController implements
mTransitionAnimator.start();
}
- @NonNull
- private static PictureInPictureParams getPipParams(@NonNull TransitionInfo.Change pipChange) {
- return pipChange.getTaskInfo().pictureInPictureParams != null
- ? pipChange.getTaskInfo().pictureInPictureParams
- : new PictureInPictureParams.Builder().build();
- }
-
- @NonNull
- private static SurfaceControl getLeash(TransitionInfo.Change change) {
- SurfaceControl leash = change.getLeash();
- Preconditions.checkNotNull(leash, "Leash is null for change=" + change);
- return leash;
- }
-
//
// Miscellaneous callbacks and listeners
//
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransitionState.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransitionState.java
index 8805cbb0dfbd..18c9a705dcf7 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransitionState.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransitionState.java
@@ -314,7 +314,8 @@ public class PipTransitionState {
mSwipePipToHomeAppBounds.setEmpty();
}
- @Nullable WindowContainerToken getPipTaskToken() {
+ @Nullable
+ public WindowContainerToken getPipTaskToken() {
return mPipTaskInfo != null ? mPipTaskInfo.getToken() : null;
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/transition/PipExpandHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/transition/PipExpandHandler.java
new file mode 100644
index 000000000000..db4942b2fb95
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/transition/PipExpandHandler.java
@@ -0,0 +1,331 @@
+/*
+ * 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.pip2.phone.transition;
+
+import static android.view.Surface.ROTATION_0;
+
+import static com.android.wm.shell.pip2.phone.transition.PipTransitionUtils.getChangeByToken;
+import static com.android.wm.shell.pip2.phone.transition.PipTransitionUtils.getFixedRotationDelta;
+import static com.android.wm.shell.pip2.phone.transition.PipTransitionUtils.getLeash;
+import static com.android.wm.shell.pip2.phone.transition.PipTransitionUtils.getPipParams;
+import static com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP;
+import static com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP_TO_SPLIT;
+
+import android.animation.ValueAnimator;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.PictureInPictureParams;
+import android.content.Context;
+import android.graphics.Rect;
+import android.os.IBinder;
+import android.view.Surface;
+import android.view.SurfaceControl;
+import android.window.TransitionInfo;
+import android.window.TransitionRequestInfo;
+import android.window.WindowContainerToken;
+import android.window.WindowContainerTransaction;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.protolog.ProtoLog;
+import com.android.wm.shell.common.pip.PipBoundsAlgorithm;
+import com.android.wm.shell.common.pip.PipBoundsState;
+import com.android.wm.shell.common.pip.PipDisplayLayoutState;
+import com.android.wm.shell.pip2.animation.PipExpandAnimator;
+import com.android.wm.shell.pip2.phone.PipTransitionState;
+import com.android.wm.shell.protolog.ShellProtoLogGroup;
+import com.android.wm.shell.splitscreen.SplitScreenController;
+import com.android.wm.shell.transition.Transitions;
+
+import java.util.Optional;
+
+public class PipExpandHandler implements Transitions.TransitionHandler {
+ private final Context mContext;
+ private final PipBoundsState mPipBoundsState;
+ private final PipBoundsAlgorithm mPipBoundsAlgorithm;
+ private final PipTransitionState mPipTransitionState;
+ private final PipDisplayLayoutState mPipDisplayLayoutState;
+ private final Optional<SplitScreenController> mSplitScreenControllerOptional;
+
+ @Nullable
+ private Transitions.TransitionFinishCallback mFinishCallback;
+ @Nullable
+ private ValueAnimator mTransitionAnimator;
+
+ private PipExpandAnimatorSupplier mPipExpandAnimatorSupplier;
+
+ public PipExpandHandler(Context context,
+ PipBoundsState pipBoundsState,
+ PipBoundsAlgorithm pipBoundsAlgorithm,
+ PipTransitionState pipTransitionState,
+ PipDisplayLayoutState pipDisplayLayoutState,
+ Optional<SplitScreenController> splitScreenControllerOptional) {
+ mContext = context;
+ mPipBoundsState = pipBoundsState;
+ mPipBoundsAlgorithm = pipBoundsAlgorithm;
+ mPipTransitionState = pipTransitionState;
+ mPipDisplayLayoutState = pipDisplayLayoutState;
+ mSplitScreenControllerOptional = splitScreenControllerOptional;
+
+ mPipExpandAnimatorSupplier = PipExpandAnimator::new;
+ }
+
+ @Override
+ public WindowContainerTransaction handleRequest(@NonNull IBinder transition,
+ @NonNull TransitionRequestInfo request) {
+ // All Exit-via-Expand from PiP transitions are Shell initiated.
+ return null;
+ }
+
+ @Override
+ public boolean startAnimation(@NonNull IBinder transition,
+ @NonNull TransitionInfo info,
+ @NonNull SurfaceControl.Transaction startTransaction,
+ @NonNull SurfaceControl.Transaction finishTransaction,
+ @NonNull Transitions.TransitionFinishCallback finishCallback) {
+ switch (info.getType()) {
+ case TRANSIT_EXIT_PIP:
+ return startExpandAnimation(info, startTransaction, finishTransaction,
+ finishCallback);
+ case TRANSIT_EXIT_PIP_TO_SPLIT:
+ return startExpandToSplitAnimation(info, startTransaction, finishTransaction,
+ finishCallback);
+ }
+ return false;
+ }
+
+ @Override
+ public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
+ @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget,
+ @NonNull Transitions.TransitionFinishCallback finishCallback) {
+ end();
+ }
+
+ /**
+ * Ends the animation if such is running in the context of expanding out of PiP.
+ */
+ public void end() {
+ if (mTransitionAnimator != null && mTransitionAnimator.isRunning()) {
+ mTransitionAnimator.end();
+ mTransitionAnimator = null;
+ }
+ }
+
+ private boolean startExpandAnimation(@NonNull TransitionInfo info,
+ @NonNull SurfaceControl.Transaction startTransaction,
+ @NonNull SurfaceControl.Transaction finishTransaction,
+ @NonNull Transitions.TransitionFinishCallback finishCallback) {
+ WindowContainerToken pipToken = mPipTransitionState.getPipTaskToken();
+
+ TransitionInfo.Change pipChange = getChangeByToken(info, pipToken);
+ if (pipChange == null) {
+ // pipChange is null, check to see if we've reparented the PIP activity for
+ // the multi activity case. If so we should use the activity leash instead
+ for (TransitionInfo.Change change : info.getChanges()) {
+ if (change.getTaskInfo() == null
+ && change.getLastParent() != null
+ && change.getLastParent().equals(pipToken)) {
+ pipChange = change;
+ break;
+ }
+ }
+
+ // failsafe
+ if (pipChange == null) {
+ return false;
+ }
+ }
+ mFinishCallback = finishCallback;
+
+ // The parent change if we were in a multi-activity PiP; null if single activity PiP.
+ final TransitionInfo.Change parentBeforePip = pipChange.getTaskInfo() == null
+ ? getChangeByToken(info, pipChange.getParent()) : null;
+ if (parentBeforePip != null) {
+ // For multi activity, we need to manually set the leash layer
+ startTransaction.setLayer(parentBeforePip.getLeash(), Integer.MAX_VALUE - 1);
+ }
+
+ final Rect startBounds = pipChange.getStartAbsBounds();
+ final Rect endBounds = pipChange.getEndAbsBounds();
+ final SurfaceControl pipLeash = getLeash(pipChange);
+
+ PictureInPictureParams params = null;
+ if (pipChange.getTaskInfo() != null) {
+ // single activity
+ params = getPipParams(pipChange);
+ } else if (parentBeforePip != null && parentBeforePip.getTaskInfo() != null) {
+ // multi activity
+ params = getPipParams(parentBeforePip);
+ }
+ final Rect sourceRectHint = PipBoundsAlgorithm.getValidSourceHintRect(params, endBounds,
+ startBounds);
+
+ // We define delta = startRotation - endRotation, so we need to flip the sign.
+ final int delta = -getFixedRotationDelta(info, pipChange, mPipDisplayLayoutState);
+ if (delta != ROTATION_0) {
+ // Update PiP target change in place to prepare for fixed rotation;
+ handleExpandFixedRotation(pipChange, delta);
+ }
+
+ PipExpandAnimator animator = mPipExpandAnimatorSupplier.get(mContext, pipLeash,
+ startTransaction, finishTransaction, endBounds, startBounds, endBounds,
+ sourceRectHint, delta);
+ animator.setAnimationEndCallback(() -> {
+ if (parentBeforePip != null) {
+ // TODO b/377362511: Animate local leash instead to also handle letterbox case.
+ // For multi-activity, set the crop to be null
+ finishTransaction.setCrop(pipLeash, null);
+ }
+ finishTransition();
+ });
+ cacheAndStartTransitionAnimator(animator);
+ saveReentryState();
+ return true;
+ }
+
+ private boolean startExpandToSplitAnimation(@NonNull TransitionInfo info,
+ @NonNull SurfaceControl.Transaction startTransaction,
+ @NonNull SurfaceControl.Transaction finishTransaction,
+ @NonNull Transitions.TransitionFinishCallback finishCallback) {
+ WindowContainerToken pipToken = mPipTransitionState.getPipTaskToken();
+
+ // Expanding PiP to Split-screen makes sense only if we are dealing with multi-activity PiP
+ // and the lastParentBeforePip is still in one of the split-stages.
+ //
+ // This means we should be animating the PiP activity leash, since we do the reparenting
+ // of the PiP activity back to its original task in startWCT.
+ TransitionInfo.Change pipChange = null;
+ for (TransitionInfo.Change change : info.getChanges()) {
+ if (change.getTaskInfo() == null
+ && change.getLastParent() != null
+ && change.getLastParent().equals(pipToken)) {
+ pipChange = change;
+ break;
+ }
+ }
+ // failsafe
+ if (pipChange == null || pipChange.getLeash() == null) {
+ return false;
+ }
+ mFinishCallback = finishCallback;
+
+ // Get the original parent before PiP. If original task hosting the PiP activity was
+ // already visible, then it's not participating in this transition; in that case,
+ // parentBeforePip would be null.
+ final TransitionInfo.Change parentBeforePip = getChangeByToken(info, pipChange.getParent());
+
+ final Rect startBounds = pipChange.getStartAbsBounds();
+ final Rect endBounds = pipChange.getEndAbsBounds();
+ if (parentBeforePip != null) {
+ // Since we have the parent task amongst the targets, all PiP activity
+ // leash translations will be relative to the original task, NOT the root leash.
+ startBounds.offset(-parentBeforePip.getStartAbsBounds().left,
+ -parentBeforePip.getStartAbsBounds().top);
+ endBounds.offset(-parentBeforePip.getEndAbsBounds().left,
+ -parentBeforePip.getEndAbsBounds().top);
+ }
+
+ final SurfaceControl pipLeash = pipChange.getLeash();
+ PipExpandAnimator animator = mPipExpandAnimatorSupplier.get(mContext, pipLeash,
+ startTransaction, finishTransaction, endBounds, startBounds, endBounds,
+ null /* srcRectHint */, ROTATION_0 /* delta */);
+
+
+ mSplitScreenControllerOptional.ifPresent(splitController -> {
+ splitController.finishEnterSplitScreen(finishTransaction);
+ });
+
+ animator.setAnimationEndCallback(() -> {
+ if (parentBeforePip == null) {
+ // After PipExpandAnimator is done modifying finishTransaction, we need to make
+ // sure PiP activity leash is offset at origin relative to its task as we reparent
+ // targets back from the transition root leash.
+ finishTransaction.setPosition(pipLeash, 0, 0);
+ }
+ finishTransition();
+ });
+ cacheAndStartTransitionAnimator(animator);
+ saveReentryState();
+ return true;
+ }
+
+ private void finishTransition() {
+ final int currentState = mPipTransitionState.getState();
+ if (currentState != PipTransitionState.EXITING_PIP) {
+ ProtoLog.e(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "Unexpected state %s as we are finishing an exit-via-expand transition",
+ mPipTransitionState);
+ }
+ mPipTransitionState.setState(PipTransitionState.EXITED_PIP);
+
+ if (mFinishCallback != null) {
+ // Need to unset mFinishCallback first because onTransitionFinished can re-enter this
+ // handler if there is a pending PiP animation.
+ final Transitions.TransitionFinishCallback finishCallback = mFinishCallback;
+ mFinishCallback = null;
+ finishCallback.onTransitionFinished(null /* finishWct */);
+ }
+ }
+
+ private void handleExpandFixedRotation(TransitionInfo.Change outPipTaskChange, int delta) {
+ final Rect endBounds = outPipTaskChange.getEndAbsBounds();
+ final int width = endBounds.width();
+ final int height = endBounds.height();
+ final int left = endBounds.left;
+ final int top = endBounds.top;
+ int newTop, newLeft;
+
+ if (delta == Surface.ROTATION_90) {
+ newLeft = top;
+ newTop = -(left + width);
+ } else {
+ newLeft = -(height + top);
+ newTop = left;
+ }
+ // Modify the endBounds, rotating and placing them potentially off-screen, so that
+ // as we translate and rotate around the origin, we place them right into the target.
+ endBounds.set(newLeft, newTop, newLeft + height, newTop + width);
+ }
+
+ private void saveReentryState() {
+ float snapFraction = mPipBoundsAlgorithm.getSnapFraction(
+ mPipBoundsState.getBounds());
+ mPipBoundsState.saveReentryState(snapFraction);
+ }
+
+ private void cacheAndStartTransitionAnimator(@NonNull ValueAnimator animator) {
+ mTransitionAnimator = animator;
+ mTransitionAnimator.start();
+ }
+
+ @VisibleForTesting
+ interface PipExpandAnimatorSupplier {
+ PipExpandAnimator get(Context context,
+ @NonNull SurfaceControl leash,
+ SurfaceControl.Transaction startTransaction,
+ SurfaceControl.Transaction finishTransaction,
+ @NonNull Rect baseBounds,
+ @NonNull Rect startBounds,
+ @NonNull Rect endBounds,
+ @Nullable Rect sourceRectHint,
+ @Surface.Rotation int rotation);
+ }
+
+ @VisibleForTesting
+ void setPipExpandAnimatorSupplier(@NonNull PipExpandAnimatorSupplier supplier) {
+ mPipExpandAnimatorSupplier = supplier;
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/transition/PipTransitionUtils.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/transition/PipTransitionUtils.java
new file mode 100644
index 000000000000..01cda6c91108
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/transition/PipTransitionUtils.java
@@ -0,0 +1,133 @@
+/*
+ * 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.pip2.phone.transition;
+
+import static android.app.WindowConfiguration.ROTATION_UNDEFINED;
+import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
+import static android.view.Surface.ROTATION_0;
+
+import android.annotation.NonNull;
+import android.app.PictureInPictureParams;
+import android.view.Surface;
+import android.view.SurfaceControl;
+import android.window.TransitionInfo;
+import android.window.WindowContainerToken;
+
+import androidx.annotation.Nullable;
+
+import com.android.internal.util.Preconditions;
+import com.android.wm.shell.common.pip.PipDisplayLayoutState;
+
+/**
+ * A set of utility methods to help resolve PiP transitions.
+ */
+public class PipTransitionUtils {
+
+ /**
+ * @return change for a pinned mode task; null if no such task is in the list of changes.
+ */
+ @Nullable
+ public static TransitionInfo.Change getPipChange(TransitionInfo info) {
+ for (TransitionInfo.Change change : info.getChanges()) {
+ if (change.getTaskInfo() != null
+ && change.getTaskInfo().getWindowingMode() == WINDOWING_MODE_PINNED) {
+ return change;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * @return change for a task with the provided token; null if no task with such token found.
+ */
+ @Nullable
+ public static TransitionInfo.Change getChangeByToken(TransitionInfo info,
+ WindowContainerToken token) {
+ for (TransitionInfo.Change change : info.getChanges()) {
+ if (change.getTaskInfo() != null
+ && change.getTaskInfo().getToken().equals(token)) {
+ return change;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * @return the leash to interact with the container this change represents.
+ * @throws NullPointerException if the leash is null.
+ */
+ @NonNull
+ public static SurfaceControl getLeash(TransitionInfo.Change change) {
+ SurfaceControl leash = change.getLeash();
+ Preconditions.checkNotNull(leash, "Leash is null for change=" + change);
+ return leash;
+ }
+
+ /**
+ * Get the rotation delta in a potential fixed rotation transition.
+ *
+ * Whenever PiP participates in fixed rotation, its actual orientation isn't updated
+ * in the initial transition as per the async rotation convention.
+ *
+ * @param pipChange PiP change to verify that PiP task's rotation wasn't updated already.
+ * @param pipDisplayLayoutState display layout state that PiP component keeps track of.
+ */
+ @Surface.Rotation
+ public static int getFixedRotationDelta(@NonNull TransitionInfo info,
+ @NonNull TransitionInfo.Change pipChange,
+ @NonNull PipDisplayLayoutState pipDisplayLayoutState) {
+ TransitionInfo.Change fixedRotationChange = findFixedRotationChange(info);
+ int startRotation = pipChange.getStartRotation();
+ if (pipChange.getEndRotation() != ROTATION_UNDEFINED
+ && startRotation != pipChange.getEndRotation()) {
+ // If PiP change was collected along with the display change and the orientation change
+ // happened in sync with the PiP change, then do not treat this as fixed-rotation case.
+ return ROTATION_0;
+ }
+
+ int endRotation = fixedRotationChange != null
+ ? fixedRotationChange.getEndFixedRotation() : pipDisplayLayoutState.getRotation();
+ int delta = endRotation == ROTATION_UNDEFINED ? ROTATION_0
+ : startRotation - endRotation;
+ return delta;
+ }
+
+ /**
+ * Gets a change amongst the transition targets that is in a different final orientation than
+ * the display, signalling a potential fixed rotation transition.
+ */
+ @Nullable
+ public static TransitionInfo.Change findFixedRotationChange(@NonNull TransitionInfo info) {
+ for (int i = info.getChanges().size() - 1; i >= 0; --i) {
+ final TransitionInfo.Change change = info.getChanges().get(i);
+ if (change.getEndFixedRotation() != ROTATION_UNDEFINED) {
+ return change;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * @return {@link PictureInPictureParams} provided by the client from the PiP change.
+ */
+ @NonNull
+ public static PictureInPictureParams getPipParams(@NonNull TransitionInfo.Change pipChange) {
+ return pipChange.getTaskInfo().pictureInPictureParams != null
+ ? pipChange.getTaskInfo().pictureInPictureParams
+ : new PictureInPictureParams.Builder().build();
+ }
+}
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 a969845fb8e8..847a0383e7d0 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
@@ -796,7 +796,8 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler,
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
" unhandled root taskId=%d", taskInfo.taskId);
}
- } else if (TransitionUtil.isDividerBar(change)) {
+ } else if (TransitionUtil.isDividerBar(change)
+ || TransitionUtil.isDimLayer(change)) {
final RemoteAnimationTarget target = TransitionUtil.newTarget(change,
belowLayers - i, info, t, mLeashMap);
// Add this as a app and we will separate them on launcher side by window type.
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 a799b7f2580e..73b42d6f007c 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
@@ -40,11 +40,13 @@ import static com.android.wm.shell.Flags.enableFlexibleSplit;
import static com.android.wm.shell.Flags.enableFlexibleTwoAppSplit;
import static com.android.wm.shell.common.split.SplitLayout.PARALLAX_ALIGN_CENTER;
import static com.android.wm.shell.common.split.SplitLayout.PARALLAX_FLEX;
+import static com.android.wm.shell.common.split.SplitLayout.RESTING_DIM_LAYER;
import static com.android.wm.shell.common.split.SplitScreenUtils.reverseSplitPosition;
import static com.android.wm.shell.common.split.SplitScreenUtils.splitFailureMessage;
import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN;
import static com.android.wm.shell.shared.TransitionUtil.isClosingType;
import static com.android.wm.shell.shared.TransitionUtil.isOpeningType;
+import static com.android.wm.shell.shared.split.SplitScreenConstants.FLAG_IS_DIM_LAYER;
import static com.android.wm.shell.shared.split.SplitScreenConstants.FLAG_IS_DIVIDER_BAR;
import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_10_90;
import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_50_50;
@@ -1824,6 +1826,14 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
// Ensure divider surface are re-parented back into the hierarchy at the end of the
// transition. See Transition#buildFinishTransaction for more detail.
finishT.reparent(mSplitLayout.getDividerLeash(), mRootTaskLeash);
+ if (Flags.enableFlexibleSplit()) {
+ mStageOrderOperator.getActiveStages().forEach(stage -> {
+ finishT.reparent(stage.mDimLayer, stage.mRootLeash);
+ });
+ } else if (Flags.enableFlexibleTwoAppSplit()) {
+ finishT.reparent(mMainStage.mDimLayer, mMainStage.mRootLeash);
+ finishT.reparent(mSideStage.mDimLayer, mSideStage.mRootLeash);
+ }
updateSurfaceBounds(mSplitLayout, finishT, false /* applyResizingOffset */);
finishT.show(mRootTaskLeash);
@@ -3540,6 +3550,9 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
finishEnterSplitScreen(finishT);
addDividerBarToTransition(info, true /* show */);
+ if (Flags.enableFlexibleTwoAppSplit()) {
+ addAllDimLayersToTransition(info, true /* show */);
+ }
return true;
}
@@ -3790,6 +3803,9 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
}
addDividerBarToTransition(info, false /* show */);
+ if (Flags.enableFlexibleTwoAppSplit()) {
+ addAllDimLayersToTransition(info, false /* show */);
+ }
}
/** Call this when the recents animation canceled during split-screen. */
@@ -3836,6 +3852,19 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
returnToApp);
mPausingTasks.clear();
if (returnToApp) {
+ // Reparent auxiliary surfaces (divider bar and dim layers) back onto their
+ // original roots.
+ if (Flags.enableFlexibleSplit()) {
+ mStageOrderOperator.getActiveStages().forEach(stage -> {
+ finishT.reparent(stage.mDimLayer, stage.mRootLeash);
+ finishT.setLayer(stage.mDimLayer, RESTING_DIM_LAYER);
+ });
+ } else if (Flags.enableFlexibleTwoAppSplit()) {
+ finishT.reparent(mMainStage.mDimLayer, mMainStage.mRootLeash);
+ finishT.reparent(mSideStage.mDimLayer, mSideStage.mRootLeash);
+ finishT.setLayer(mMainStage.mDimLayer, RESTING_DIM_LAYER);
+ finishT.setLayer(mSideStage.mDimLayer, RESTING_DIM_LAYER);
+ }
updateSurfaceBounds(mSplitLayout, finishT,
false /* applyResizingOffset */);
finishT.reparent(mSplitLayout.getDividerLeash(), mRootTaskLeash);
@@ -3902,6 +3931,39 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
info.addChange(barChange);
}
+ /** Add dim layers to the transition, so that they can be hidden/shown when animation starts. */
+ private void addAllDimLayersToTransition(@NonNull TransitionInfo info, boolean show) {
+ if (Flags.enableFlexibleSplit()) {
+ List<StageTaskListener> stages = mStageOrderOperator.getActiveStages();
+ for (int i = 0; i < stages.size(); i++) {
+ final StageTaskListener stage = stages.get(i);
+ mSplitState.getCurrentLayout().get(i).roundOut(mTempRect1);
+ addDimLayerToTransition(info, show, stage, mTempRect1);
+ }
+ } else {
+ addDimLayerToTransition(info, show, mMainStage, getMainStageBounds());
+ addDimLayerToTransition(info, show, mSideStage, getSideStageBounds());
+ }
+ }
+
+ /** Adds a single dim layer to the given TransitionInfo. */
+ private void addDimLayerToTransition(@NonNull TransitionInfo info, boolean show,
+ StageTaskListener stage, Rect bounds) {
+ final SurfaceControl dimLayer = stage.mDimLayer;
+ if (dimLayer == null || !dimLayer.isValid()) {
+ Slog.w(TAG, "addDimLayerToTransition but leash was released or not created");
+ } else {
+ final TransitionInfo.Change change =
+ new TransitionInfo.Change(null /* token */, dimLayer);
+ change.setParent(mRootTaskInfo.token);
+ change.setStartAbsBounds(bounds);
+ change.setEndAbsBounds(bounds);
+ change.setMode(show ? TRANSIT_TO_FRONT : TRANSIT_TO_BACK);
+ change.setFlags(FLAG_IS_DIM_LAYER);
+ info.addChange(change);
+ }
+ }
+
@NeverCompile
@Override
public void dump(@NonNull PrintWriter pw, String prefix) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java
index 7aa00370ff58..dd5439a8aa10 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java
@@ -389,7 +389,9 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel, FocusT
} else if (id == R.id.back_button) {
mTaskOperations.injectBackKey(mDisplayId);
} else if (id == R.id.minimize_window) {
- mTaskOperations.minimizeTask(mTaskToken);
+ // This minimize button uses the same effect for any minimization. The last argument
+ // doesn't matter.
+ mTaskOperations.minimizeTask(mTaskToken, mTaskId, /* isLastTask= */ false);
} else if (id == R.id.maximize_window) {
RunningTaskInfo taskInfo = mTaskOrganizer.getRunningTaskInfo(mTaskId);
final DisplayAreaInfo rootDisplayAreaInfo =
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 1cc04b421132..5a6ea214e561 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
@@ -149,6 +149,8 @@ import com.android.wm.shell.windowdecor.common.viewhost.WindowDecorViewHost;
import com.android.wm.shell.windowdecor.common.viewhost.WindowDecorViewHostSupplier;
import com.android.wm.shell.windowdecor.extension.InsetsStateKt;
import com.android.wm.shell.windowdecor.extension.TaskInfoKt;
+import com.android.wm.shell.windowdecor.tiling.DesktopTilingDecorViewModel;
+import com.android.wm.shell.windowdecor.tiling.SnapEventHandler;
import com.android.wm.shell.windowdecor.viewholder.AppHeaderViewHolder;
import kotlin.Pair;
@@ -173,7 +175,7 @@ import java.util.function.Supplier;
*/
public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel,
- FocusTransitionListener {
+ FocusTransitionListener, SnapEventHandler {
private static final String TAG = "DesktopModeWindowDecorViewModel";
private final DesktopModeWindowDecoration.Factory mDesktopModeWindowDecorFactory;
@@ -255,6 +257,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel,
private final WindowDecorTaskResourceLoader mTaskResourceLoader;
private final RecentsTransitionHandler mRecentsTransitionHandler;
private final DesktopModeCompatPolicy mDesktopModeCompatPolicy;
+ private final DesktopTilingDecorViewModel mDesktopTilingDecorViewModel;
public DesktopModeWindowDecorViewModel(
Context context,
@@ -292,7 +295,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel,
DesktopModeUiEventLogger desktopModeUiEventLogger,
WindowDecorTaskResourceLoader taskResourceLoader,
RecentsTransitionHandler recentsTransitionHandler,
- DesktopModeCompatPolicy desktopModeCompatPolicy) {
+ DesktopModeCompatPolicy desktopModeCompatPolicy,
+ DesktopTilingDecorViewModel desktopTilingDecorViewModel) {
this(
context,
shellExecutor,
@@ -335,7 +339,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel,
desktopModeUiEventLogger,
taskResourceLoader,
recentsTransitionHandler,
- desktopModeCompatPolicy);
+ desktopModeCompatPolicy,
+ desktopTilingDecorViewModel);
}
@VisibleForTesting
@@ -381,7 +386,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel,
DesktopModeUiEventLogger desktopModeUiEventLogger,
WindowDecorTaskResourceLoader taskResourceLoader,
RecentsTransitionHandler recentsTransitionHandler,
- DesktopModeCompatPolicy desktopModeCompatPolicy) {
+ DesktopModeCompatPolicy desktopModeCompatPolicy,
+ DesktopTilingDecorViewModel desktopTilingDecorViewModel) {
mContext = context;
mMainExecutor = shellExecutor;
mMainHandler = mainHandler;
@@ -452,7 +458,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel,
mTaskResourceLoader = taskResourceLoader;
mRecentsTransitionHandler = recentsTransitionHandler;
mDesktopModeCompatPolicy = desktopModeCompatPolicy;
-
+ mDesktopTilingDecorViewModel = desktopTilingDecorViewModel;
+ mDesktopTasksController.setSnapEventHandler(this);
shellInit.addInitCallback(this::onInit, this);
}
@@ -723,8 +730,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel,
decoration.mTaskInfo,
left ? SnapPosition.LEFT : SnapPosition.RIGHT,
left ? ResizeTrigger.SNAP_LEFT_MENU : ResizeTrigger.SNAP_RIGHT_MENU,
- inputMethod,
- decoration);
+ inputMethod);
decoration.closeHandleMenu();
decoration.closeMaximizeMenu();
@@ -885,6 +891,33 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel,
return snapshotList;
}
+ @Override
+ public boolean snapToHalfScreen(@NonNull RunningTaskInfo taskInfo,
+ @NonNull Rect currentDragBounds, @NonNull SnapPosition position) {
+ return mDesktopTilingDecorViewModel.snapToHalfScreen(taskInfo,
+ mWindowDecorByTaskId.get(taskInfo.taskId), position, currentDragBounds);
+ }
+
+ @Override
+ public void removeTaskIfTiled(int displayId, int taskId) {
+ mDesktopTilingDecorViewModel.removeTaskIfTiled(displayId, taskId);
+ }
+
+ @Override
+ public void onUserChange() {
+ mDesktopTilingDecorViewModel.onUserChange();
+ }
+
+ @Override
+ public void onOverviewAnimationStateChange(boolean running) {
+ mDesktopTilingDecorViewModel.onOverviewAnimationStateChange(running);
+ }
+
+ @Override
+ public boolean moveTaskToFrontIfTiled(@NonNull RunningTaskInfo taskInfo) {
+ return mDesktopTilingDecorViewModel.moveTaskToFrontIfTiled(taskInfo);
+ }
+
private class DesktopModeTouchEventListener extends GestureDetector.SimpleOnGestureListener
implements View.OnClickListener, View.OnTouchListener, View.OnLongClickListener,
View.OnGenericMotionListener, DragDetector.MotionEventHandler {
@@ -951,7 +984,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel,
mDesktopTasksController.onDesktopWindowClose(
wct, mDisplayId, decoration.mTaskInfo);
final IBinder transition = mTaskOperations.closeTask(mTaskToken, wct);
- if (transition != null && runOnTransitionStart != null) {
+ if (transition != null) {
runOnTransitionStart.invoke(transition);
}
}
@@ -1238,8 +1271,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel,
taskInfo, decoration.mTaskSurface,
new PointF(e.getRawX(dragPointerIdx), e.getRawY(dragPointerIdx)),
newTaskBounds, decoration.calculateValidDragArea(),
- new Rect(mOnDragStartInitialBounds), e,
- mWindowDecorByTaskId.get(taskInfo.taskId));
+ new Rect(mOnDragStartInitialBounds), e);
if (touchingButton) {
// We need the input event to not be consumed here to end the ripple
// effect on the touched button. We will reset drag state in the ensuing
@@ -1444,16 +1476,13 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel,
relevantDecor.mTaskInfo.configuration.windowConfiguration.getBounds());
boolean dragFromStatusBarAllowed = false;
final int windowingMode = relevantDecor.mTaskInfo.getWindowingMode();
- if (DesktopModeStatus.canEnterDesktopMode(mContext)) {
+ if (DesktopModeStatus.canEnterDesktopMode(mContext)
+ || BubbleAnythingFlagHelper.enableBubbleToFullscreen()) {
// In proto2 any full screen or multi-window task can be dragged to
// freeform.
dragFromStatusBarAllowed = windowingMode == WINDOWING_MODE_FULLSCREEN
|| windowingMode == WINDOWING_MODE_MULTI_WINDOW;
}
- if (BubbleAnythingFlagHelper.enableBubbleToFullscreen()) {
- // TODO(b/388851898): add support for split screen (multi-window wm mode)
- dragFromStatusBarAllowed = windowingMode == WINDOWING_MODE_FULLSCREEN;
- }
final boolean shouldStartTransitionDrag =
relevantDecor.checkTouchEventInFocusedCaptionHandle(ev)
|| DesktopModeFlags.ENABLE_HANDLE_INPUT_FIX.isTrue();
@@ -1502,7 +1531,11 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel,
// Do not create an indicator at all if we're not past transition height.
DisplayLayout layout = mDisplayController
.getDisplayLayout(relevantDecor.mTaskInfo.displayId);
- if (ev.getRawY() < 2 * layout.stableInsets().top
+ // It's possible task is not at the top of the screen (e.g. bottom of vertical
+ // Splitscreen)
+ final int taskTop = relevantDecor.mTaskInfo.configuration.windowConfiguration
+ .getBounds().top;
+ if (ev.getRawY() < 2 * layout.stableInsets().top + taskTop
&& mMoveToDesktopAnimator == null) {
return;
}
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 3fb94630eab3..271dead467b4 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
@@ -803,8 +803,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
if (!mTaskInfo.isVisible()) {
closeMaximizeMenu();
} else {
- final int menuWidth = calculateMaximizeMenuWidth();
- mMaximizeMenu.positionMenu(calculateMaximizeMenuPosition(menuWidth), startT);
+ mMaximizeMenu.positionMenu(startT);
}
}
@@ -1069,27 +1068,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
return Resources.ID_NULL;
}
- private int calculateMaximizeMenuWidth() {
- final boolean showImmersive = DesktopModeFlags.ENABLE_FULLY_IMMERSIVE_IN_DESKTOP.isTrue()
- && TaskInfoKt.getRequestingImmersive(mTaskInfo);
- final boolean showMaximize = true;
- final boolean showSnaps = mTaskInfo.isResizeable;
- int showCount = 0;
- if (showImmersive) showCount++;
- if (showMaximize) showCount++;
- if (showSnaps) showCount++;
- return switch (showCount) {
- case 1 -> loadDimensionPixelSize(mContext.getResources(),
- R.dimen.desktop_mode_maximize_menu_width_one_options);
- case 2 -> loadDimensionPixelSize(mContext.getResources(),
- R.dimen.desktop_mode_maximize_menu_width_two_options);
- case 3 -> loadDimensionPixelSize(mContext.getResources(),
- R.dimen.desktop_mode_maximize_menu_width_three_options);
- default -> throw new IllegalArgumentException("");
- };
- }
-
- private PointF calculateMaximizeMenuPosition(int menuWidth) {
+ private PointF calculateMaximizeMenuPosition(int menuWidth, int menuHeight) {
final PointF position = new PointF();
final Resources resources = mContext.getResources();
final DisplayLayout displayLayout =
@@ -1105,9 +1084,6 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
final int[] maximizeButtonLocation = new int[2];
maximizeWindowButton.getLocationInWindow(maximizeButtonLocation);
- final int menuHeight = loadDimensionPixelSize(
- resources, R.dimen.desktop_mode_maximize_menu_height);
-
float menuLeft = (mPositionInParent.x + maximizeButtonLocation[0] - ((float) (menuWidth
- maximizeWindowButton.getWidth()) / 2));
float menuTop = (mPositionInParent.y + captionHeight);
@@ -1294,17 +1270,16 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
* Create and display maximize menu window
*/
void createMaximizeMenu() {
- final int menuWidth = calculateMaximizeMenuWidth();
mMaximizeMenu = mMaximizeMenuFactory.create(mSyncQueue, mRootTaskDisplayAreaOrganizer,
mDisplayController, mTaskInfo, mContext,
- calculateMaximizeMenuPosition(menuWidth), mSurfaceControlTransactionSupplier);
+ (width, height) -> calculateMaximizeMenuPosition(width, height),
+ mSurfaceControlTransactionSupplier);
mMaximizeMenu.show(
/* isTaskInImmersiveMode= */
DesktopModeFlags.ENABLE_FULLY_IMMERSIVE_IN_DESKTOP.isTrue()
&& mDesktopUserRepositories.getProfile(mTaskInfo.userId)
.isTaskInFullImmersiveState(mTaskInfo.taskId),
- /* menuWidth= */ menuWidth,
/* showImmersiveOption= */
DesktopModeFlags.ENABLE_FULLY_IMMERSIVE_IN_DESKTOP.isTrue()
&& TaskInfoKt.getRequestingImmersive(mTaskInfo),
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt
index 38accce82999..ad3525af3f94 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt
@@ -90,7 +90,7 @@ class MaximizeMenu(
private val displayController: DisplayController,
private val taskInfo: RunningTaskInfo,
private val decorWindowContext: Context,
- private val menuPosition: PointF,
+ private val positionSupplier: (Int, Int) -> PointF,
private val transactionSupplier: Supplier<Transaction> = Supplier { Transaction() }
) {
private var maximizeMenu: AdditionalViewHostViewContainer? = null
@@ -100,19 +100,19 @@ class MaximizeMenu(
private val cornerRadius = loadDimensionPixelSize(
R.dimen.desktop_mode_maximize_menu_corner_radius
).toFloat()
- private val menuHeight = loadDimensionPixelSize(R.dimen.desktop_mode_maximize_menu_height)
+ private lateinit var menuPosition: PointF
private val menuPadding = loadDimensionPixelSize(R.dimen.desktop_mode_menu_padding)
/** Position the menu relative to the caption's position. */
- fun positionMenu(position: PointF, t: Transaction) {
- menuPosition.set(position)
+ fun positionMenu(t: Transaction) {
+ menuPosition = positionSupplier(maximizeMenuView?.measureWidth() ?: 0,
+ maximizeMenuView?.measureHeight() ?: 0)
t.setPosition(leash, menuPosition.x, menuPosition.y)
}
/** Creates and shows the maximize window. */
fun show(
isTaskInImmersiveMode: Boolean,
- menuWidth: Int,
showImmersiveOption: Boolean,
showSnapOptions: Boolean,
onMaximizeOrRestoreClickListener: () -> Unit,
@@ -125,7 +125,6 @@ class MaximizeMenu(
if (maximizeMenu != null) return
createMaximizeMenu(
isTaskInImmersiveMode = isTaskInImmersiveMode,
- menuWidth = menuWidth,
showImmersiveOption = showImmersiveOption,
showSnapOptions = showSnapOptions,
onMaximizeClickListener = onMaximizeOrRestoreClickListener,
@@ -161,7 +160,6 @@ class MaximizeMenu(
/** Create a maximize menu that is attached to the display area. */
private fun createMaximizeMenu(
isTaskInImmersiveMode: Boolean,
- menuWidth: Int,
showImmersiveOption: Boolean,
showSnapOptions: Boolean,
onMaximizeClickListener: () -> Unit,
@@ -178,16 +176,6 @@ class MaximizeMenu(
.setName("Maximize Menu")
.setContainerLayer()
.build()
- val lp = WindowManager.LayoutParams(
- menuWidth,
- menuHeight,
- WindowManager.LayoutParams.TYPE_APPLICATION,
- WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
- or WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH,
- PixelFormat.TRANSPARENT
- )
- lp.title = "Maximize Menu for Task=" + taskInfo.taskId
- lp.setTrustedOverlay()
val windowManager = WindowlessWindowManager(
taskInfo.configuration,
leash,
@@ -207,7 +195,6 @@ class MaximizeMenu(
MaximizeMenuView.ImmersiveConfig.Hidden
},
showSnapOptions = showSnapOptions,
- menuHeight = menuHeight,
menuPadding = menuPadding,
).also { menuView ->
menuView.bind(taskInfo)
@@ -217,6 +204,19 @@ class MaximizeMenu(
menuView.onRightSnapClickListener = onRightSnapClickListener
menuView.onMenuHoverListener = onHoverListener
menuView.onOutsideTouchListener = onOutsideTouchListener
+ val menuWidth = menuView.measureWidth()
+ val menuHeight = menuView.measureHeight()
+ menuPosition = positionSupplier(menuWidth, menuHeight)
+ val lp = WindowManager.LayoutParams(
+ menuWidth.toInt(),
+ menuHeight.toInt(),
+ WindowManager.LayoutParams.TYPE_APPLICATION,
+ WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
+ or WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH,
+ PixelFormat.TRANSPARENT
+ )
+ lp.title = "Maximize Menu for Task=" + taskInfo.taskId
+ lp.setTrustedOverlay()
viewHost.setView(menuView.rootView, lp)
}
@@ -268,7 +268,6 @@ class MaximizeMenu(
private val sizeToggleDirection: SizeToggleDirection,
immersiveConfig: ImmersiveConfig,
showSnapOptions: Boolean,
- private val menuHeight: Int,
private val menuPadding: Int
) {
val rootView = LayoutInflater.from(context)
@@ -583,7 +582,7 @@ class MaximizeMenu(
// the menu.
val value = animatedValue as Float
val topPadding = menuPadding -
- ((1 - value) * menuHeight).toInt()
+ ((1 - value) * measureHeight()).toInt()
container.setPadding(menuPadding, topPadding,
menuPadding, menuPadding)
}
@@ -604,7 +603,7 @@ class MaximizeMenu(
}
},
ObjectAnimator.ofFloat(rootView, TRANSLATION_Y,
- (STARTING_MENU_HEIGHT_SCALE - 1) * menuHeight, 0f).apply {
+ (STARTING_MENU_HEIGHT_SCALE - 1) * measureHeight(), 0f).apply {
duration = OPEN_MENU_HEIGHT_ANIMATION_DURATION_MS
interpolator = EMPHASIZED_DECELERATE
},
@@ -667,7 +666,7 @@ class MaximizeMenu(
// the menu.
val value = animatedValue as Float
val topPadding = menuPadding -
- ((1 - value) * menuHeight).toInt()
+ ((1 - value) * measureHeight()).toInt()
container.setPadding(menuPadding, topPadding,
menuPadding, menuPadding)
}
@@ -688,7 +687,7 @@ class MaximizeMenu(
}
},
ObjectAnimator.ofFloat(rootView, TRANSLATION_Y,
- 0f, (STARTING_MENU_HEIGHT_SCALE - 1) * menuHeight).apply {
+ 0f, (STARTING_MENU_HEIGHT_SCALE - 1) * measureHeight()).apply {
duration = CLOSE_MENU_HEIGHT_ANIMATION_DURATION_MS
interpolator = FAST_OUT_LINEAR_IN
},
@@ -792,6 +791,18 @@ class MaximizeMenu(
)
}
+ /** Measure width of the root view of this menu. */
+ fun measureWidth() : Int {
+ rootView.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
+ return rootView.getMeasuredWidth()
+ }
+
+ /** Measure height of the root view of this menu. */
+ fun measureHeight() : Int {
+ rootView.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
+ return rootView.getMeasuredHeight()
+ }
+
private fun deactivateSnapOptions() {
// TODO(b/346440693): the background/colorStateList set on these buttons is overridden
// to a static resource & color on manually tracked hover events, which defeats the
@@ -1036,7 +1047,7 @@ interface MaximizeMenuFactory {
displayController: DisplayController,
taskInfo: RunningTaskInfo,
decorWindowContext: Context,
- menuPosition: PointF,
+ positionSupplier: (Int, Int) -> PointF,
transactionSupplier: Supplier<Transaction>
): MaximizeMenu
}
@@ -1049,7 +1060,7 @@ object DefaultMaximizeMenuFactory : MaximizeMenuFactory {
displayController: DisplayController,
taskInfo: RunningTaskInfo,
decorWindowContext: Context,
- menuPosition: PointF,
+ positionSupplier: (Int, Int) -> PointF,
transactionSupplier: Supplier<Transaction>
): MaximizeMenu {
return MaximizeMenu(
@@ -1058,7 +1069,7 @@ object DefaultMaximizeMenuFactory : MaximizeMenuFactory {
displayController,
taskInfo,
decorWindowContext,
- menuPosition,
+ positionSupplier,
transactionSupplier
)
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskOperations.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskOperations.java
index bc85d2b40748..45ba4413814c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskOperations.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskOperations.java
@@ -86,14 +86,18 @@ class TaskOperations {
return null;
}
- IBinder minimizeTask(WindowContainerToken taskToken) {
- return minimizeTask(taskToken, new WindowContainerTransaction());
+ IBinder minimizeTask(WindowContainerToken taskToken, int taskId, boolean isLastTask) {
+ return minimizeTask(taskToken, taskId, isLastTask, new WindowContainerTransaction());
}
- IBinder minimizeTask(WindowContainerToken taskToken, WindowContainerTransaction wct) {
+ IBinder minimizeTask(
+ WindowContainerToken taskToken,
+ int taskId,
+ boolean isLastTask,
+ WindowContainerTransaction wct) {
wct.reorder(taskToken, false);
if (Transitions.ENABLE_SHELL_TRANSITIONS) {
- return mTransitionStarter.startMinimizedModeTransition(wct);
+ return mTransitionStarter.startMinimizedModeTransition(wct, taskId, isLastTask);
} else {
mSyncQueue.queue(wct);
return null;
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 25dadfde274d..4002dc572897 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
@@ -361,6 +361,8 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer>
}
outResult.mRootView = rootView;
+ final boolean fontScaleChanged = mWindowDecorConfig != null
+ && mWindowDecorConfig.fontScale != mTaskInfo.configuration.fontScale;
final int oldDensityDpi = mWindowDecorConfig != null
? mWindowDecorConfig.densityDpi : DENSITY_DPI_UNDEFINED;
final int oldNightMode = mWindowDecorConfig != null
@@ -375,7 +377,8 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer>
|| mDisplay.getDisplayId() != mTaskInfo.displayId
|| oldLayoutResId != mLayoutResId
|| oldNightMode != newNightMode
- || mDecorWindowContext == null) {
+ || mDecorWindowContext == null
+ || fontScaleChanged) {
releaseViews(wct);
if (!obtainDisplayOrRegisterListener()) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/viewhost/ReusableWindowDecorViewHost.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/viewhost/ReusableWindowDecorViewHost.kt
index 4a09614029dc..a5592f81a39e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/viewhost/ReusableWindowDecorViewHost.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/viewhost/ReusableWindowDecorViewHost.kt
@@ -50,9 +50,8 @@ class ReusableWindowDecorViewHost(
@VisibleForTesting
val viewHostAdapter: SurfaceControlViewHostAdapter =
SurfaceControlViewHostAdapter(context, display),
+ private val rootView: FrameLayout = FrameLayout(context)
) : WindowDecorViewHost, Warmable {
- @VisibleForTesting val rootView = FrameLayout(context)
-
private var currentUpdateJob: Job? = null
override val surfaceControl: SurfaceControl
@@ -131,8 +130,10 @@ class ReusableWindowDecorViewHost(
Trace.beginSection("ReusableWindowDecorViewHost#updateViewHost")
viewHostAdapter.prepareViewHost(configuration, touchableRegion)
onDrawTransaction?.let { viewHostAdapter.applyTransactionOnDraw(it) }
- rootView.removeAllViews()
- rootView.addView(view)
+ if (view.parent != rootView) {
+ rootView.removeAllViews()
+ rootView.addView(view)
+ }
viewHostAdapter.updateView(rootView, attrs)
Trace.endSection()
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/SnapEventHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/SnapEventHandler.kt
new file mode 100644
index 000000000000..52e24d6fe0d0
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/SnapEventHandler.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.windowdecor.tiling
+
+import android.app.ActivityManager.RunningTaskInfo
+import android.graphics.Rect
+import com.android.wm.shell.desktopmode.DesktopTasksController.SnapPosition
+
+/** Interface for handling snap to half screen events. */
+interface SnapEventHandler {
+ /** Snaps an app to half the screen for tiling. */
+ fun snapToHalfScreen(
+ taskInfo: RunningTaskInfo,
+ currentDragBounds: Rect,
+ position: SnapPosition,
+ ): Boolean
+
+ /** Removes a task from tiling if it's tiled, for example on task exiting. */
+ fun removeTaskIfTiled(displayId: Int, taskId: Int)
+
+ /** Notifies the tiling handler of user switch. */
+ fun onUserChange()
+
+ /** Notifies the tiling handler of overview animation state change. */
+ fun onOverviewAnimationStateChange(running: Boolean)
+
+ /** If a task is tiled, delegate moving to front to tiling infrastructure. */
+ fun moveTaskToFrontIfTiled(taskInfo: RunningTaskInfo): Boolean
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleTransitionsTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleTransitionsTest.java
index 87ee4f58bfdd..42310caba1c6 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleTransitionsTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleTransitionsTest.java
@@ -215,7 +215,7 @@ public class BubbleTransitionsTest extends ShellTestCase {
pendingWct.reorder(pendingDragOpToken, /* onTop= */ false);
BubbleTransitions.DragData dragData = new BubbleTransitions.DragData(
- draggedTaskBounds, pendingWct
+ draggedTaskBounds, pendingWct, /* releasedOnLeft= */ false
);
final BubbleTransitions.BubbleTransition bt = mBubbleTransitions.startConvertToBubble(
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 d3de0f7c09b4..3d5e9495e29d 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
@@ -16,26 +16,52 @@
package com.android.wm.shell.common;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
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.res.Configuration;
+import android.graphics.RectF;
import android.hardware.display.DisplayManager;
+import android.hardware.display.DisplayTopology;
+import android.os.RemoteException;
+import android.platform.test.annotations.EnableFlags;
+import android.testing.TestableContext;
+import android.util.SparseArray;
+import android.view.Display;
+import android.view.DisplayAdjustments;
+import android.view.IDisplayWindowListener;
import android.view.IWindowManager;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
+import com.android.dx.mockito.inline.extended.ExtendedMockito;
+import com.android.dx.mockito.inline.extended.StaticMockitoSession;
+import com.android.window.flags.Flags;
import com.android.wm.shell.ShellTestCase;
+import com.android.wm.shell.TestSyncExecutor;
+import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
import com.android.wm.shell.sysui.ShellInit;
+import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
+import org.mockito.quality.Strictness;
+
+import java.util.function.Consumer;
/**
* Tests for the display controller.
@@ -46,23 +72,163 @@ import org.mockito.MockitoAnnotations;
@SmallTest
@RunWith(AndroidJUnit4.class)
public class DisplayControllerTests extends ShellTestCase {
-
- private @Mock Context mContext;
- private @Mock IWindowManager mWM;
- private @Mock ShellInit mShellInit;
- private @Mock ShellExecutor mMainExecutor;
- private @Mock DisplayManager mDisplayManager;
+ @Mock private IWindowManager mWM;
+ @Mock private ShellInit mShellInit;
+ @Mock private DisplayManager mDisplayManager;
+ @Mock private DisplayTopology mMockTopology;
+ @Mock private DisplayController.OnDisplaysChangedListener mListener;
+ private StaticMockitoSession mMockitoSession;
+ private TestSyncExecutor mMainExecutor;
+ private IDisplayWindowListener mDisplayContainerListener;
+ private Consumer<DisplayTopology> mCapturedTopologyListener;
+ private Display mMockDisplay;
private DisplayController mController;
+ private static final int DISPLAY_ID_0 = 0;
+ private static final int DISPLAY_ID_1 = 1;
+ private static final RectF DISPLAY_ABS_BOUNDS_0 = new RectF(10, 10, 20, 20);
+ private static final RectF DISPLAY_ABS_BOUNDS_1 = new RectF(11, 11, 22, 22);
@Before
- public void setUp() {
- MockitoAnnotations.initMocks(this);
+ public void setUp() throws RemoteException {
+ mMockitoSession =
+ ExtendedMockito.mockitoSession()
+ .initMocks(this)
+ .mockStatic(DesktopModeStatus.class)
+ .strictness(Strictness.LENIENT)
+ .startMocking();
+
+ mContext = spy(new TestableContext(
+ androidx.test.platform.app.InstrumentationRegistry.getInstrumentation()
+ .getContext(), null));
+
+ mMainExecutor = new TestSyncExecutor();
mController = new DisplayController(
mContext, mWM, mShellInit, mMainExecutor, mDisplayManager);
+
+ mMockDisplay = mock(Display.class);
+ when(mMockDisplay.getDisplayAdjustments()).thenReturn(
+ new DisplayAdjustments(new Configuration()));
+ when(mDisplayManager.getDisplay(anyInt())).thenReturn(mMockDisplay);
+ when(mDisplayManager.getDisplayTopology()).thenReturn(mMockTopology);
+ doAnswer(invocation -> {
+ mDisplayContainerListener = invocation.getArgument(0);
+ return new int[]{DISPLAY_ID_0};
+ }).when(mWM).registerDisplayWindowListener(any());
+ doAnswer(invocation -> {
+ mCapturedTopologyListener = invocation.getArgument(1);
+ return null;
+ }).when(mDisplayManager).registerTopologyListener(any(), any());
+ SparseArray<RectF> absoluteBounds = new SparseArray<>();
+ absoluteBounds.put(DISPLAY_ID_0, DISPLAY_ABS_BOUNDS_0);
+ absoluteBounds.put(DISPLAY_ID_1, DISPLAY_ABS_BOUNDS_1);
+ when(mMockTopology.getAbsoluteBounds()).thenReturn(absoluteBounds);
+ }
+
+ @After
+ public void tearDown() {
+ if (mMockitoSession != null) {
+ mMockitoSession.finishMocking();
+ }
}
@Test
public void instantiateController_addInitCallback() {
verify(mShellInit, times(1)).addInitCallback(any(), eq(mController));
}
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_CONNECTED_DISPLAYS_WINDOW_DRAG)
+ public void onInit_canEnterDesktopMode_registerListeners() throws RemoteException {
+ ExtendedMockito.doReturn(true)
+ .when(() -> DesktopModeStatus.canEnterDesktopMode(any()));
+
+ mController.onInit();
+
+ assertNotNull(mController.getDisplayContext(DISPLAY_ID_0));
+ verify(mWM).registerDisplayWindowListener(any());
+ verify(mDisplayManager).registerTopologyListener(eq(mMainExecutor), any());
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_CONNECTED_DISPLAYS_WINDOW_DRAG)
+ public void onInit_canNotEnterDesktopMode_onlyRegisterDisplayWindowListener()
+ throws RemoteException {
+ ExtendedMockito.doReturn(false)
+ .when(() -> DesktopModeStatus.canEnterDesktopMode(any()));
+
+ mController.onInit();
+
+ assertNotNull(mController.getDisplayContext(DISPLAY_ID_0));
+ verify(mWM).registerDisplayWindowListener(any());
+ verify(mDisplayManager, never()).registerTopologyListener(any(), any());
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_CONNECTED_DISPLAYS_WINDOW_DRAG)
+ public void addDisplayWindowListener_notifiesExistingDisplaysAndTopology() {
+ ExtendedMockito.doReturn(true)
+ .when(() -> DesktopModeStatus.canEnterDesktopMode(any()));
+
+ mController.onInit();
+ mController.addDisplayWindowListener(mListener);
+
+ verify(mListener).onDisplayAdded(eq(DISPLAY_ID_0));
+ verify(mListener).onTopologyChanged(eq(mMockTopology));
+ }
+
+ @Test
+ public void onDisplayAddedAndRemoved_updatesDisplayContexts() throws RemoteException {
+ mController.onInit();
+ mController.addDisplayWindowListener(mListener);
+
+ mDisplayContainerListener.onDisplayAdded(DISPLAY_ID_1);
+
+ verify(mListener).onDisplayAdded(eq(DISPLAY_ID_0));
+ verify(mListener).onDisplayAdded(eq(DISPLAY_ID_1));
+ assertNotNull(mController.getDisplayContext(DISPLAY_ID_1));
+ verify(mContext).createDisplayContext(eq(mMockDisplay));
+
+ mDisplayContainerListener.onDisplayRemoved(DISPLAY_ID_1);
+
+ assertNull(mController.getDisplayContext(DISPLAY_ID_1));
+ verify(mListener).onDisplayRemoved(eq(DISPLAY_ID_1));
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_CONNECTED_DISPLAYS_WINDOW_DRAG)
+ public void onDisplayTopologyChanged_updateDisplayLayout() throws RemoteException {
+ ExtendedMockito.doReturn(true)
+ .when(() -> DesktopModeStatus.canEnterDesktopMode(any()));
+ mController.onInit();
+ mController.addDisplayWindowListener(mListener);
+ mDisplayContainerListener.onDisplayAdded(DISPLAY_ID_1);
+
+ mCapturedTopologyListener.accept(mMockTopology);
+
+ assertEquals(DISPLAY_ABS_BOUNDS_0, mController.getDisplayLayout(DISPLAY_ID_0)
+ .globalBoundsDp());
+ assertEquals(DISPLAY_ABS_BOUNDS_1, mController.getDisplayLayout(DISPLAY_ID_1)
+ .globalBoundsDp());
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_CONNECTED_DISPLAYS_WINDOW_DRAG)
+ public void onDisplayTopologyChanged_topologyBeforeDisplayAdded_appliesBoundsOnAdd()
+ throws RemoteException {
+ ExtendedMockito.doReturn(true)
+ .when(() -> DesktopModeStatus.canEnterDesktopMode(any()));
+ mController.onInit();
+ mController.addDisplayWindowListener(mListener);
+
+ mCapturedTopologyListener.accept(mMockTopology);
+
+ assertNull(mController.getDisplayLayout(DISPLAY_ID_1));
+
+ mDisplayContainerListener.onDisplayAdded(DISPLAY_ID_1);
+
+ assertEquals(DISPLAY_ABS_BOUNDS_0,
+ mController.getDisplayLayout(DISPLAY_ID_0).globalBoundsDp());
+ assertEquals(DISPLAY_ABS_BOUNDS_1,
+ mController.getDisplayLayout(DISPLAY_ID_1).globalBoundsDp());
+ }
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/WindowContainerTransactionSupplierTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/WindowContainerTransactionSupplierTest.kt
new file mode 100644
index 000000000000..c91ef5e6b868
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/WindowContainerTransactionSupplierTest.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.common
+
+import android.testing.AndroidTestingRunner
+import android.window.WindowContainerTransaction
+import androidx.test.filters.SmallTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/**
+ * Tests for [WindowContainerTransactionSupplier].
+ *
+ * Build/Install/Run:
+ * atest WMShellUnitTests:WindowContainerTransactionSupplierTest
+ */
+@RunWith(AndroidTestingRunner::class)
+@SmallTest
+class WindowContainerTransactionSupplierTest {
+
+ @Test
+ fun `WindowContainerTransactionSupplier supplies a WindowContainerTransaction`() {
+ val supplier = WindowContainerTransactionSupplier()
+ SuppliersUtilsTest.assertSupplierProvidesValue(supplier) {
+ it is WindowContainerTransaction
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PhonePipKeepClearAlgorithmTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PhonePipKeepClearAlgorithmTest.java
index e3798e92c092..a6c35f1bd93c 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PhonePipKeepClearAlgorithmTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PhonePipKeepClearAlgorithmTest.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2022 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.wm.shell.pip.phone;
+package com.android.wm.shell.common.pip;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
@@ -29,9 +29,6 @@ import android.testing.TestableLooper;
import androidx.test.filters.SmallTest;
import com.android.wm.shell.ShellTestCase;
-import com.android.wm.shell.common.pip.PhonePipKeepClearAlgorithm;
-import com.android.wm.shell.common.pip.PipBoundsAlgorithm;
-import com.android.wm.shell.common.pip.PipBoundsState;
import org.junit.Before;
import org.junit.Test;
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PhoneSizeSpecSourceTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PhoneSizeSpecSourceTest.java
index 85f1da5322ea..737735c9efcd 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PhoneSizeSpecSourceTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PhoneSizeSpecSourceTest.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2023 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.wm.shell.pip.phone;
+package com.android.wm.shell.common.pip;
import static org.mockito.Mockito.when;
@@ -27,9 +27,6 @@ import android.view.DisplayInfo;
import com.android.wm.shell.R;
import com.android.wm.shell.ShellTestCase;
import com.android.wm.shell.common.DisplayLayout;
-import com.android.wm.shell.common.pip.PhoneSizeSpecSource;
-import com.android.wm.shell.common.pip.PipDisplayLayoutState;
-import com.android.wm.shell.common.pip.SizeSpecSource;
import org.junit.Assert;
import org.junit.Before;
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsAlgorithmTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PipBoundsAlgorithmTest.java
index 080b0ae006ea..6bda2259b44c 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsAlgorithmTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PipBoundsAlgorithmTest.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2020 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.wm.shell.pip;
+package com.android.wm.shell.common.pip;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
@@ -32,13 +32,6 @@ import androidx.test.filters.SmallTest;
import com.android.wm.shell.R;
import com.android.wm.shell.ShellTestCase;
import com.android.wm.shell.common.DisplayLayout;
-import com.android.wm.shell.common.pip.PhoneSizeSpecSource;
-import com.android.wm.shell.common.pip.PipBoundsAlgorithm;
-import com.android.wm.shell.common.pip.PipBoundsState;
-import com.android.wm.shell.common.pip.PipDisplayLayoutState;
-import com.android.wm.shell.common.pip.PipKeepClearAlgorithmInterface;
-import com.android.wm.shell.common.pip.PipSnapAlgorithm;
-import com.android.wm.shell.common.pip.SizeSpecSource;
import org.junit.Before;
import org.junit.Test;
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsStateTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PipBoundsStateTest.java
index 304da75f870c..ad664acfdc37 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsStateTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PipBoundsStateTest.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2020 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.wm.shell.pip;
+package com.android.wm.shell.common.pip;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
@@ -36,10 +36,6 @@ import androidx.test.filters.SmallTest;
import com.android.internal.util.function.TriConsumer;
import com.android.wm.shell.R;
import com.android.wm.shell.ShellTestCase;
-import com.android.wm.shell.common.pip.PhoneSizeSpecSource;
-import com.android.wm.shell.common.pip.PipBoundsState;
-import com.android.wm.shell.common.pip.PipDisplayLayoutState;
-import com.android.wm.shell.common.pip.SizeSpecSource;
import org.junit.Before;
import org.junit.Test;
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipDoubleTapHelperTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PipDoubleTapHelperTest.java
index b583acda1c9a..1756aad8fc9b 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipDoubleTapHelperTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PipDoubleTapHelperTest.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2022 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.wm.shell.pip.phone;
+package com.android.wm.shell.common.pip;
import static com.android.wm.shell.common.pip.PipDoubleTapHelper.SIZE_SPEC_CUSTOM;
import static com.android.wm.shell.common.pip.PipDoubleTapHelper.SIZE_SPEC_DEFAULT;
@@ -29,8 +29,6 @@ import android.graphics.Rect;
import android.testing.AndroidTestingRunner;
import com.android.wm.shell.ShellTestCase;
-import com.android.wm.shell.common.pip.PipBoundsState;
-import com.android.wm.shell.common.pip.PipDoubleTapHelper;
import org.junit.Assert;
import org.junit.Before;
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipSnapAlgorithmTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PipSnapAlgorithmTest.java
index ac13d7ffcd61..3e71ab3e1ad4 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipSnapAlgorithmTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PipSnapAlgorithmTest.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2020 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.wm.shell.pip;
+package com.android.wm.shell.common.pip;
import static org.junit.Assert.assertEquals;
@@ -25,8 +25,6 @@ import android.testing.TestableLooper;
import androidx.test.filters.SmallTest;
import com.android.wm.shell.ShellTestCase;
-import com.android.wm.shell.common.pip.PipBoundsState;
-import com.android.wm.shell.common.pip.PipSnapAlgorithm;
import org.junit.Before;
import org.junit.Test;
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/FlexParallaxSpecTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/FlexParallaxSpecTests.java
new file mode 100644
index 000000000000..22a85fc49a4b
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/FlexParallaxSpecTests.java
@@ -0,0 +1,401 @@
+/*
+ * 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.common.split;
+
+import static android.view.WindowManager.DOCKED_INVALID;
+import static android.view.WindowManager.DOCKED_LEFT;
+import static android.view.WindowManager.DOCKED_RIGHT;
+
+import static com.android.wm.shell.common.split.ResizingEffectPolicy.DEFAULT_OFFSCREEN_DIM;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.when;
+
+import android.graphics.Point;
+import android.graphics.Rect;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.wm.shell.common.split.DividerSnapAlgorithm.SnapTarget;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@RunWith(AndroidJUnit4.class)
+public class FlexParallaxSpecTests {
+ ParallaxSpec mFlexSpec = new FlexParallaxSpec();
+
+ Rect mDisplayBounds = new Rect(0, 0, 1000, 1000);
+ Rect mRetreatingSurface = new Rect(0, 0, 1000, 1000);
+ Rect mRetreatingContent = new Rect(0, 0, 1000, 1000);
+ Rect mAdvancingSurface = new Rect(0, 0, 1000, 1000);
+ Rect mAdvancingContent = new Rect(0, 0, 1000, 1000);
+ boolean mIsLeftRightSplit;
+ boolean mTopLeftShrink;
+
+ int mDimmingSide;
+ float mDimValue;
+ Point mRetreatingParallax = new Point(0, 0);
+ Point mAdvancingParallax = new Point(0, 0);
+
+ @Mock DividerSnapAlgorithm mockSnapAlgorithm;
+ @Mock SnapTarget mockStartEdge;
+ @Mock SnapTarget mockFirstTarget;
+ @Mock SnapTarget mockMiddleTarget;
+ @Mock SnapTarget mockLastTarget;
+ @Mock SnapTarget mockEndEdge;
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.initMocks(this);
+ when(mockSnapAlgorithm.getDismissStartTarget()).thenReturn(mockStartEdge);
+ when(mockSnapAlgorithm.getFirstSplitTarget()).thenReturn(mockFirstTarget);
+ when(mockSnapAlgorithm.getMiddleTarget()).thenReturn(mockMiddleTarget);
+ when(mockSnapAlgorithm.getLastSplitTarget()).thenReturn(mockLastTarget);
+ when(mockSnapAlgorithm.getDismissEndTarget()).thenReturn(mockEndEdge);
+
+ when(mockStartEdge.getPosition()).thenReturn(0);
+ when(mockFirstTarget.getPosition()).thenReturn(250);
+ when(mockMiddleTarget.getPosition()).thenReturn(500);
+ when(mockLastTarget.getPosition()).thenReturn(750);
+ when(mockEndEdge.getPosition()).thenReturn(1000);
+ }
+
+ @Test
+ public void testHorizontalDragFromCenter() {
+ mIsLeftRightSplit = true;
+ simulateDragFromCenterToLeft(125);
+ assertThat(mDimmingSide).isEqualTo(DOCKED_LEFT);
+ assertThat(mDimValue).isGreaterThan(DEFAULT_OFFSCREEN_DIM);
+ assertThat(mDimValue).isLessThan(1f);
+ assertThat(mRetreatingParallax.x).isGreaterThan(0);
+ assertThat(mRetreatingParallax.y).isEqualTo(0);
+ assertThat(mAdvancingParallax.x).isEqualTo(0);
+ assertThat(mAdvancingParallax.y).isEqualTo(0);
+
+ simulateDragFromCenterToLeft(250);
+ assertThat(mDimmingSide).isEqualTo(DOCKED_LEFT);
+ assertThat(mDimValue).isEqualTo(DEFAULT_OFFSCREEN_DIM);
+ assertThat(mRetreatingParallax.x).isGreaterThan(0);
+ assertThat(mRetreatingParallax.y).isEqualTo(0);
+ assertThat(mAdvancingParallax.x).isEqualTo(0);
+ assertThat(mAdvancingParallax.y).isEqualTo(0);
+
+ simulateDragFromCenterToLeft(375);
+ assertThat(mDimmingSide).isEqualTo(DOCKED_LEFT);
+ assertThat(mDimValue).isGreaterThan(0f);
+ assertThat(mDimValue).isLessThan(DEFAULT_OFFSCREEN_DIM);
+ assertThat(mRetreatingParallax.x).isGreaterThan(0);
+ assertThat(mRetreatingParallax.y).isEqualTo(0);
+ assertThat(mAdvancingParallax.x).isEqualTo(0);
+ assertThat(mAdvancingParallax.y).isEqualTo(0);
+
+ simulateDragFromCenterToRight(500);
+ assertThat(mDimmingSide).isEqualTo(DOCKED_INVALID);
+ assertThat(mDimValue).isEqualTo(0f);
+ assertThat(mRetreatingParallax.x).isEqualTo(0);
+ assertThat(mRetreatingParallax.y).isEqualTo(0);
+ assertThat(mAdvancingParallax.x).isEqualTo(0);
+ assertThat(mAdvancingParallax.y).isEqualTo(0);
+
+ simulateDragFromCenterToRight(625);
+ assertThat(mDimmingSide).isEqualTo(DOCKED_RIGHT);
+ assertThat(mDimValue).isGreaterThan(0f);
+ assertThat(mDimValue).isLessThan(DEFAULT_OFFSCREEN_DIM);
+ assertThat(mRetreatingParallax.x).isEqualTo(0);
+ assertThat(mRetreatingParallax.y).isEqualTo(0);
+ assertThat(mAdvancingParallax.x).isEqualTo(0);
+ assertThat(mAdvancingParallax.y).isEqualTo(0);
+
+ simulateDragFromCenterToRight(750);
+ assertThat(mDimmingSide).isEqualTo(DOCKED_RIGHT);
+ assertThat(mDimValue).isEqualTo(DEFAULT_OFFSCREEN_DIM);
+ assertThat(mRetreatingParallax.x).isEqualTo(0);
+ assertThat(mRetreatingParallax.y).isEqualTo(0);
+ assertThat(mAdvancingParallax.x).isEqualTo(0);
+ assertThat(mAdvancingParallax.y).isEqualTo(0);
+
+ simulateDragFromCenterToRight(875);
+ assertThat(mDimmingSide).isEqualTo(DOCKED_RIGHT);
+ assertThat(mDimValue).isGreaterThan(DEFAULT_OFFSCREEN_DIM);
+ assertThat(mDimValue).isLessThan(1f);
+ assertThat(mRetreatingParallax.x).isEqualTo(0);
+ assertThat(mRetreatingParallax.y).isEqualTo(0);
+ assertThat(mAdvancingParallax.x).isEqualTo(0);
+ assertThat(mAdvancingParallax.y).isEqualTo(0);
+ }
+
+ @Test
+ public void testHorizontalDragFromLeft() {
+ mIsLeftRightSplit = true;
+ simulateDragFromLeftToLeft(125);
+ assertThat(mDimmingSide).isEqualTo(DOCKED_LEFT);
+ assertThat(mDimValue).isGreaterThan(DEFAULT_OFFSCREEN_DIM);
+ assertThat(mDimValue).isLessThan(1f);
+ assertThat(mRetreatingParallax.x).isGreaterThan(0);
+ assertThat(mRetreatingParallax.y).isEqualTo(0);
+ assertThat(mAdvancingParallax.x).isEqualTo(0);
+ assertThat(mAdvancingParallax.y).isEqualTo(0);
+
+ simulateDragFromLeftToLeft(250);
+ assertThat(mDimmingSide).isEqualTo(DOCKED_LEFT);
+ assertThat(mDimValue).isEqualTo(DEFAULT_OFFSCREEN_DIM);
+ assertThat(mRetreatingParallax.x).isEqualTo(0);
+ assertThat(mRetreatingParallax.y).isEqualTo(0);
+ assertThat(mAdvancingParallax.x).isEqualTo(0);
+ assertThat(mAdvancingParallax.y).isEqualTo(0);
+
+ simulateDragFromLeftToCenter(375);
+ assertThat(mDimmingSide).isEqualTo(DOCKED_LEFT);
+ assertThat(mDimValue).isGreaterThan(0f);
+ assertThat(mDimValue).isLessThan(DEFAULT_OFFSCREEN_DIM);
+ assertThat(mRetreatingParallax.x).isLessThan(0);
+ assertThat(mRetreatingParallax.y).isEqualTo(0);
+ assertThat(mAdvancingParallax.x).isEqualTo(0);
+ assertThat(mAdvancingParallax.y).isEqualTo(0);
+
+ simulateDragFromLeftToCenter(500);
+ assertThat(mDimmingSide).isEqualTo(DOCKED_INVALID);
+ assertThat(mDimValue).isEqualTo(0f);
+ assertThat(mRetreatingParallax.x).isLessThan(0);
+ assertThat(mRetreatingParallax.y).isEqualTo(0);
+ assertThat(mAdvancingParallax.x).isEqualTo(0);
+ assertThat(mAdvancingParallax.y).isEqualTo(0);
+
+ simulateDragFromLeftToRight(625);
+ assertThat(mDimmingSide).isEqualTo(DOCKED_RIGHT);
+ assertThat(mDimValue).isGreaterThan(0f);
+ assertThat(mDimValue).isLessThan(DEFAULT_OFFSCREEN_DIM);
+ assertThat(mRetreatingParallax.x).isLessThan(0);
+ assertThat(mRetreatingParallax.y).isEqualTo(0);
+ assertThat(mAdvancingParallax.x).isEqualTo(0);
+ assertThat(mAdvancingParallax.y).isEqualTo(0);
+
+ simulateDragFromLeftToRight(750);
+ assertThat(mDimmingSide).isEqualTo(DOCKED_RIGHT);
+ assertThat(mDimValue).isEqualTo(DEFAULT_OFFSCREEN_DIM);
+ assertThat(mRetreatingParallax.x).isLessThan(0);
+ assertThat(mRetreatingParallax.y).isEqualTo(0);
+ assertThat(mAdvancingParallax.x).isEqualTo(0);
+ assertThat(mAdvancingParallax.y).isEqualTo(0);
+
+ simulateDragFromLeftToRight(875);
+ assertThat(mDimmingSide).isEqualTo(DOCKED_RIGHT);
+ assertThat(mDimValue).isGreaterThan(DEFAULT_OFFSCREEN_DIM);
+ assertThat(mDimValue).isLessThan(1f);
+ assertThat(mRetreatingParallax.x).isLessThan(0);
+ assertThat(mRetreatingParallax.y).isEqualTo(0);
+ assertThat(mAdvancingParallax.x).isGreaterThan(0);
+ assertThat(mAdvancingParallax.y).isEqualTo(0);
+ }
+
+ @Test
+ public void testHorizontalDragFromRight() {
+ mIsLeftRightSplit = true;
+
+ simulateDragFromRightToLeft(125);
+ assertThat(mDimmingSide).isEqualTo(DOCKED_LEFT);
+ assertThat(mDimValue).isGreaterThan(DEFAULT_OFFSCREEN_DIM);
+ assertThat(mDimValue).isLessThan(1f);
+ assertThat(mRetreatingParallax.x).isGreaterThan(0);
+ assertThat(mRetreatingParallax.y).isEqualTo(0);
+ assertThat(mAdvancingParallax.x).isEqualTo(0);
+ assertThat(mAdvancingParallax.y).isEqualTo(0);
+
+ simulateDragFromRightToLeft(250);
+ assertThat(mDimmingSide).isEqualTo(DOCKED_LEFT);
+ assertThat(mDimValue).isEqualTo(DEFAULT_OFFSCREEN_DIM);
+ assertThat(mRetreatingParallax.x).isGreaterThan(0);
+ assertThat(mRetreatingParallax.y).isEqualTo(0);
+ assertThat(mAdvancingParallax.x).isEqualTo(0);
+ assertThat(mAdvancingParallax.y).isEqualTo(0);
+
+ simulateDragFromRightToLeft(375);
+ assertThat(mDimmingSide).isEqualTo(DOCKED_LEFT);
+ assertThat(mDimValue).isGreaterThan(0f);
+ assertThat(mDimValue).isLessThan(DEFAULT_OFFSCREEN_DIM);
+ assertThat(mRetreatingParallax.x).isGreaterThan(0);
+ assertThat(mRetreatingParallax.y).isEqualTo(0);
+ assertThat(mAdvancingParallax.x).isEqualTo(0);
+ assertThat(mAdvancingParallax.y).isEqualTo(0);
+
+ simulateDragFromRightToCenter(500);
+ assertThat(mDimmingSide).isEqualTo(DOCKED_INVALID);
+ assertThat(mDimValue).isEqualTo(0f);
+ assertThat(mRetreatingParallax.x).isLessThan(0);
+ assertThat(mRetreatingParallax.y).isEqualTo(0);
+ assertThat(mAdvancingParallax.x).isEqualTo(0);
+ assertThat(mAdvancingParallax.y).isEqualTo(0);
+
+ simulateDragFromRightToCenter(625);
+ assertThat(mDimmingSide).isEqualTo(DOCKED_RIGHT);
+ assertThat(mDimValue).isGreaterThan(0f);
+ assertThat(mDimValue).isLessThan(DEFAULT_OFFSCREEN_DIM);
+ assertThat(mRetreatingParallax.x).isLessThan(0);
+ assertThat(mRetreatingParallax.y).isEqualTo(0);
+ assertThat(mAdvancingParallax.x).isEqualTo(0);
+ assertThat(mAdvancingParallax.y).isEqualTo(0);
+
+ simulateDragFromRightToRight(750);
+ assertThat(mDimmingSide).isEqualTo(DOCKED_RIGHT);
+ assertThat(mDimValue).isEqualTo(DEFAULT_OFFSCREEN_DIM);
+ assertThat(mRetreatingParallax.x).isEqualTo(0);
+ assertThat(mRetreatingParallax.y).isEqualTo(0);
+ assertThat(mAdvancingParallax.x).isEqualTo(0);
+ assertThat(mAdvancingParallax.y).isEqualTo(0);
+
+ simulateDragFromRightToRight(875);
+ assertThat(mDimmingSide).isEqualTo(DOCKED_RIGHT);
+ assertThat(mDimValue).isGreaterThan(DEFAULT_OFFSCREEN_DIM);
+ assertThat(mDimValue).isLessThan(1f);
+ assertThat(mRetreatingParallax.x).isEqualTo(0);
+ assertThat(mRetreatingParallax.y).isEqualTo(0);
+ assertThat(mAdvancingParallax.x).isEqualTo(0);
+ assertThat(mAdvancingParallax.y).isEqualTo(0);
+ }
+
+ private void simulateDragFromCenterToLeft(int to) {
+ int from = 500;
+
+ mRetreatingSurface = flexOffscreenAppLeft(to);
+ mRetreatingContent = onscreenAppLeft(from);
+ mAdvancingSurface = onscreenAppRight(to);
+ mAdvancingContent = onscreenAppRight(from);
+
+ calculateDimAndParallax(from, to);
+ }
+
+ private void simulateDragFromCenterToRight(int to) {
+ int from = 500;
+
+ mRetreatingSurface = flexOffscreenAppRight(to);
+ mRetreatingContent = onscreenAppRight(from);
+ mAdvancingSurface = onscreenAppLeft(to);
+ mAdvancingContent = onscreenAppLeft(from);
+
+ calculateDimAndParallax(from, to);
+ }
+
+ private void simulateDragFromLeftToLeft(int to) {
+ int from = 250;
+
+ mRetreatingSurface = flexOffscreenAppLeft(to);
+ mRetreatingContent = fullOffscreenAppLeft(from);
+ mAdvancingSurface = onscreenAppRight(to);
+ mAdvancingContent = onscreenAppRight(from);
+
+ calculateDimAndParallax(from, to);
+ }
+
+ private void simulateDragFromLeftToCenter(int to) {
+ int from = 250;
+
+ mRetreatingSurface = onscreenAppRight(to);
+ mRetreatingContent = onscreenAppRight(from);
+ mAdvancingSurface = fullOffscreenAppLeft(to);
+ mAdvancingContent = fullOffscreenAppLeft(from);
+
+ calculateDimAndParallax(from, to);
+ }
+
+ private void simulateDragFromLeftToRight(int to) {
+ int from = 250;
+
+ mRetreatingSurface = flexOffscreenAppRight(to);
+ mRetreatingContent = onscreenAppRight(from);
+ mAdvancingSurface = fullOffscreenAppLeft(to);
+ mAdvancingContent = fullOffscreenAppLeft(from);
+
+ calculateDimAndParallax(from, to);
+ }
+
+ private void simulateDragFromRightToLeft(int to) {
+ int from = 750;
+
+ mRetreatingSurface = flexOffscreenAppLeft(to);
+ mRetreatingContent = onscreenAppLeft(from);
+ mAdvancingSurface = fullOffscreenAppRight(to);
+ mAdvancingContent = fullOffscreenAppRight(from);
+
+ calculateDimAndParallax(from, to);
+ }
+
+ private void simulateDragFromRightToCenter(int to) {
+ int from = 750;
+
+ mRetreatingSurface = onscreenAppLeft(to);
+ mRetreatingContent = onscreenAppLeft(from);
+ mAdvancingSurface = fullOffscreenAppRight(to);
+ mAdvancingContent = fullOffscreenAppRight(from);
+
+ calculateDimAndParallax(from, to);
+ }
+
+ private void simulateDragFromRightToRight(int to) {
+ int from = 750;
+
+ mRetreatingSurface = flexOffscreenAppRight(to);
+ mRetreatingContent = fullOffscreenAppRight(from);
+ mAdvancingSurface = onscreenAppLeft(to);
+ mAdvancingContent = onscreenAppLeft(from);
+
+ calculateDimAndParallax(from, to);
+ }
+
+ private Rect flexOffscreenAppLeft(int pos) {
+ return new Rect(pos - (1000 - pos), 0, pos, 1000);
+ }
+
+ private Rect onscreenAppLeft(int pos) {
+ return new Rect(0, 0, pos, 1000);
+ }
+
+ private Rect fullOffscreenAppLeft(int pos) {
+ return new Rect(Math.min(0, pos - 750), 0, pos, 1000);
+ }
+
+ private Rect flexOffscreenAppRight(int pos) {
+ return new Rect(pos, 0, pos * 2, 1000);
+ }
+
+ private Rect onscreenAppRight(int pos) {
+ return new Rect(pos, 0, 1000, 1000);
+ }
+
+ private Rect fullOffscreenAppRight(int pos) {
+ return new Rect(pos, 0, Math.max(pos + 750, 1000), 1000);
+ }
+
+ private void calculateDimAndParallax(int from, int to) {
+ resetParallax();
+ mTopLeftShrink = to < from;
+ mDimmingSide = mFlexSpec.getDimmingSide(to, mockSnapAlgorithm, mIsLeftRightSplit);
+ mDimValue = mFlexSpec.getDimValue(to, mockSnapAlgorithm);
+ mFlexSpec.getParallax(mRetreatingParallax, mAdvancingParallax, to, mockSnapAlgorithm,
+ mIsLeftRightSplit, mDisplayBounds, mRetreatingSurface, mRetreatingContent,
+ mAdvancingSurface, mAdvancingContent, mDimmingSide, mTopLeftShrink);
+ }
+
+ private void resetParallax() {
+ mRetreatingParallax.set(0, 0);
+ mAdvancingParallax.set(0, 0);
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/events/ReachabilityGestureListenerFactoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/events/ReachabilityGestureListenerFactoryTest.kt
new file mode 100644
index 000000000000..a5f6ced20dc0
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/events/ReachabilityGestureListenerFactoryTest.kt
@@ -0,0 +1,131 @@
+/*
+ * Copyright 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.compatui.letterbox.events
+
+import android.graphics.Rect
+import android.testing.AndroidTestingRunner
+import android.window.WindowContainerToken
+import android.window.WindowContainerTransaction
+import androidx.test.filters.SmallTest
+import com.android.wm.shell.ShellTestCase
+import com.android.wm.shell.common.WindowContainerTransactionSupplier
+import com.android.wm.shell.compatui.letterbox.LetterboxEvents.motionEventAt
+import com.android.wm.shell.transition.Transitions
+import com.android.wm.shell.transition.Transitions.TRANSIT_MOVE_LETTERBOX_REACHABILITY
+import java.util.function.Consumer
+import kotlin.test.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.verify
+
+/**
+ * Tests for [ReachabilityGestureListenerFactory].
+ *
+ * Build/Install/Run:
+ * atest WMShellUnitTests:ReachabilityGestureListenerFactoryTest
+ */
+@RunWith(AndroidTestingRunner::class)
+@SmallTest
+class ReachabilityGestureListenerFactoryTest : ShellTestCase() {
+
+ @Test
+ fun `When invoked a ReachabilityGestureListenerFactory is created`() {
+ runTestScenario { r ->
+ r.invokeCreate()
+
+ r.checkReachabilityGestureListenerCreated()
+ }
+ }
+
+ @Test
+ fun `Right parameters are used for creation`() {
+ runTestScenario { r ->
+ r.invokeCreate()
+
+ r.checkRightParamsAreUsed()
+ }
+ }
+
+ /**
+ * Runs a test scenario providing a Robot.
+ */
+ fun runTestScenario(consumer: Consumer<ReachabilityGestureListenerFactoryRobotTest>) {
+ val robot = ReachabilityGestureListenerFactoryRobotTest()
+ consumer.accept(robot)
+ }
+
+ class ReachabilityGestureListenerFactoryRobotTest {
+
+ companion object {
+ @JvmStatic
+ private val TASK_ID = 1
+
+ @JvmStatic
+ private val TOKEN = mock<WindowContainerToken>()
+ }
+
+ private val transitions: Transitions
+ private val animationHandler: Transitions.TransitionHandler
+ private val factory: ReachabilityGestureListenerFactory
+ private val wctSupplier: WindowContainerTransactionSupplier
+ private val wct: WindowContainerTransaction
+ private lateinit var obtainedResult: Any
+
+ init {
+ transitions = mock<Transitions>()
+ animationHandler = mock<Transitions.TransitionHandler>()
+ wctSupplier = mock<WindowContainerTransactionSupplier>()
+ wct = mock<WindowContainerTransaction>()
+ doReturn(wct).`when`(wctSupplier).get()
+ factory = ReachabilityGestureListenerFactory(transitions, animationHandler, wctSupplier)
+ }
+
+ fun invokeCreate(taskId: Int = TASK_ID, token: WindowContainerToken? = TOKEN) {
+ obtainedResult = factory.createReachabilityGestureListener(taskId, token)
+ }
+
+ fun checkReachabilityGestureListenerCreated(expected: Boolean = true) {
+ assertEquals(expected, obtainedResult is ReachabilityGestureListener)
+ }
+
+ fun checkRightParamsAreUsed(taskId: Int = TASK_ID, token: WindowContainerToken? = TOKEN) {
+ with(obtainedResult as ReachabilityGestureListener) {
+ // Click outside the bounds
+ updateActivityBounds(Rect(0, 0, 10, 20))
+ onDoubleTap(motionEventAt(50f, 100f))
+ // WindowContainerTransactionSupplier is invoked to create a
+ // WindowContainerTransaction
+ verify(wctSupplier).get()
+ // Verify the right params are passed to startAppCompatReachability()
+ verify(wct).setReachabilityOffset(
+ token!!,
+ taskId,
+ 50,
+ 100
+ )
+ // startTransition() is invoked on Transitions with the right parameters
+ verify(transitions).startTransition(
+ TRANSIT_MOVE_LETTERBOX_REACHABILITY,
+ wct,
+ animationHandler
+ )
+ }
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/events/ReachabilityGestureListenerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/events/ReachabilityGestureListenerTest.kt
new file mode 100644
index 000000000000..bc10ea578ffb
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/events/ReachabilityGestureListenerTest.kt
@@ -0,0 +1,146 @@
+/*
+ * Copyright 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.compatui.letterbox.events
+
+import android.graphics.Rect
+import android.testing.AndroidTestingRunner
+import android.window.WindowContainerToken
+import android.window.WindowContainerTransaction
+import androidx.test.filters.SmallTest
+import com.android.wm.shell.ShellTestCase
+import com.android.wm.shell.common.WindowContainerTransactionSupplier
+import com.android.wm.shell.compatui.letterbox.LetterboxEvents.motionEventAt
+import com.android.wm.shell.compatui.letterbox.asMode
+import com.android.wm.shell.transition.Transitions
+import com.android.wm.shell.transition.Transitions.TRANSIT_MOVE_LETTERBOX_REACHABILITY
+import java.util.function.Consumer
+import kotlin.test.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.verify
+
+/**
+ * Tests for [ReachabilityGestureListener].
+ *
+ * Build/Install/Run:
+ * atest WMShellUnitTests:ReachabilityGestureListenerTest
+ */
+@RunWith(AndroidTestingRunner::class)
+@SmallTest
+class ReachabilityGestureListenerTest : ShellTestCase() {
+
+ @Test
+ fun `Only events outside the bounds are handled`() {
+ runTestScenario { r ->
+ r.updateActivityBounds(Rect(0, 0, 100, 200))
+ r.sendMotionEvent(50, 100)
+
+ r.verifyReachabilityTransitionCreated(expected = false, 50, 100)
+ r.verifyReachabilityTransitionStarted(expected = false)
+ r.verifyEventIsHandled(expected = false)
+
+ r.updateActivityBounds(Rect(0, 0, 10, 50))
+ r.sendMotionEvent(50, 100)
+
+ r.verifyReachabilityTransitionCreated(expected = true, 50, 100)
+ r.verifyReachabilityTransitionStarted(expected = true)
+ r.verifyEventIsHandled(expected = true)
+ }
+ }
+
+ /**
+ * Runs a test scenario providing a Robot.
+ */
+ fun runTestScenario(consumer: Consumer<ReachabilityGestureListenerRobotTest>) {
+ val robot = ReachabilityGestureListenerRobotTest()
+ consumer.accept(robot)
+ }
+
+ class ReachabilityGestureListenerRobotTest(
+ taskId: Int = TASK_ID,
+ token: WindowContainerToken? = TOKEN
+ ) {
+
+ companion object {
+ @JvmStatic
+ private val TASK_ID = 1
+
+ @JvmStatic
+ private val TOKEN = mock<WindowContainerToken>()
+ }
+
+ private val reachabilityListener: ReachabilityGestureListener
+ private val transitions: Transitions
+ private val animationHandler: Transitions.TransitionHandler
+ private val wctSupplier: WindowContainerTransactionSupplier
+ private val wct: WindowContainerTransaction
+ private var eventHandled = false
+
+ init {
+ transitions = mock<Transitions>()
+ animationHandler = mock<Transitions.TransitionHandler>()
+ wctSupplier = mock<WindowContainerTransactionSupplier>()
+ wct = mock<WindowContainerTransaction>()
+ doReturn(wct).`when`(wctSupplier).get()
+ reachabilityListener =
+ ReachabilityGestureListener(
+ taskId,
+ token,
+ transitions,
+ animationHandler,
+ wctSupplier
+ )
+ }
+
+ fun updateActivityBounds(activityBounds: Rect) {
+ reachabilityListener.updateActivityBounds(activityBounds)
+ }
+
+ fun sendMotionEvent(x: Int, y: Int) {
+ eventHandled = reachabilityListener.onDoubleTap(motionEventAt(x.toFloat(), y.toFloat()))
+ }
+
+ fun verifyReachabilityTransitionCreated(
+ expected: Boolean,
+ x: Int,
+ y: Int,
+ taskId: Int = TASK_ID,
+ token: WindowContainerToken? = TOKEN
+ ) {
+ verify(wct, expected.asMode()).setReachabilityOffset(
+ token!!,
+ taskId,
+ x,
+ y
+ )
+ }
+
+ fun verifyReachabilityTransitionStarted(expected: Boolean = true) {
+ verify(transitions, expected.asMode()).startTransition(
+ TRANSIT_MOVE_LETTERBOX_REACHABILITY,
+ wct,
+ animationHandler
+ )
+ }
+
+ fun verifyEventIsHandled(expected: Boolean) {
+ assertEquals(expected, eventHandled)
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt
index 0b41952a89d7..e9f92cfd7c56 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt
@@ -58,6 +58,7 @@ import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyBoolean
import org.mockito.ArgumentMatchers.anyInt
import org.mockito.Mock
import org.mockito.Mockito
@@ -126,17 +127,6 @@ class DesktopMixedTransitionHandlerTest : ShellTestCase() {
}
@Test
- fun startMinimizedModeTransition_callsFreeformTaskTransitionHandler() {
- val wct = WindowContainerTransaction()
- whenever(freeformTaskTransitionHandler.startMinimizedModeTransition(any()))
- .thenReturn(mock())
-
- mixedHandler.startMinimizedModeTransition(wct)
-
- verify(freeformTaskTransitionHandler).startMinimizedModeTransition(wct)
- }
-
- @Test
@DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_EXIT_TRANSITIONS_BUGFIX)
fun startRemoveTransition_callsFreeformTaskTransitionHandler() {
val wct = WindowContainerTransaction()
@@ -531,6 +521,131 @@ class DesktopMixedTransitionHandlerTest : ShellTestCase() {
}
@Test
+ @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_EXIT_BY_MINIMIZE_TRANSITION_BUGFIX)
+ fun startMinimizedModeTransition_exitByMinimizeTransitionFlagsDisabled_doesNotUseMixedHandler() {
+ val wct = WindowContainerTransaction()
+ val task = createTask(WINDOWING_MODE_FREEFORM)
+ whenever(
+ freeformTaskTransitionHandler.startMinimizedModeTransition(
+ any(),
+ anyInt(),
+ anyBoolean(),
+ )
+ )
+ .thenReturn(mock())
+
+ mixedHandler.startMinimizedModeTransition(
+ wct = wct,
+ taskId = task.taskId,
+ isLastTask = true,
+ )
+
+ verify(freeformTaskTransitionHandler)
+ .startMinimizedModeTransition(eq(wct), eq(task.taskId), eq(true))
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_EXIT_BY_MINIMIZE_TRANSITION_BUGFIX)
+ fun startMinimizedModeTransition_exitByMinimizeTransitionFlagsEnabled_notLastTask_callsMinimizationHandler() {
+ val wct = WindowContainerTransaction()
+ val minimizingTask = createTask(WINDOWING_MODE_FREEFORM)
+ val minimizingTaskChange = createChange(minimizingTask)
+ val transition = Binder()
+ whenever(
+ transitions.startTransition(eq(Transitions.TRANSIT_MINIMIZE), eq(wct), anyOrNull())
+ )
+ .thenReturn(transition)
+ whenever(
+ desktopMinimizationTransitionHandler.startAnimation(
+ any(),
+ any(),
+ any(),
+ any(),
+ any(),
+ )
+ )
+ .thenReturn(true)
+
+ mixedHandler.startMinimizedModeTransition(
+ wct = wct,
+ taskId = minimizingTask.taskId,
+ isLastTask = false,
+ )
+ val started =
+ mixedHandler.startAnimation(
+ transition = transition,
+ info =
+ createCloseTransitionInfo(
+ Transitions.TRANSIT_MINIMIZE,
+ listOf(minimizingTaskChange),
+ ),
+ startTransaction = mock(),
+ finishTransaction = mock(),
+ finishCallback = {},
+ )
+
+ assertTrue("Should delegate animation to minimization transition handler", started)
+ verify(desktopMinimizationTransitionHandler)
+ .startAnimation(
+ eq(transition),
+ argThat { info -> info.changes.contains(minimizingTaskChange) },
+ any(),
+ any(),
+ any(),
+ )
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_EXIT_BY_MINIMIZE_TRANSITION_BUGFIX)
+ fun startMinimizedModeTransition_exitByMinimizeTransitionFlagsEnabled_withMinimizingLastTask_dispatchesTransition() {
+ val wct = WindowContainerTransaction()
+ val minimizingTask = createTask(WINDOWING_MODE_FREEFORM)
+ val minimizingTaskChange = createChange(minimizingTask)
+ val transition = Binder()
+ whenever(
+ transitions.startTransition(eq(Transitions.TRANSIT_MINIMIZE), eq(wct), anyOrNull())
+ )
+ .thenReturn(transition)
+ whenever(
+ desktopMinimizationTransitionHandler.startAnimation(
+ any(),
+ any(),
+ any(),
+ any(),
+ any(),
+ )
+ )
+ .thenReturn(true)
+
+ mixedHandler.startMinimizedModeTransition(
+ wct = wct,
+ taskId = minimizingTask.taskId,
+ isLastTask = true,
+ )
+ mixedHandler.startAnimation(
+ transition = transition,
+ info =
+ createCloseTransitionInfo(
+ Transitions.TRANSIT_MINIMIZE,
+ listOf(minimizingTaskChange),
+ ),
+ startTransaction = mock(),
+ finishTransaction = mock(),
+ finishCallback = {},
+ )
+
+ verify(transitions)
+ .dispatchTransition(
+ eq(transition),
+ argThat { info -> info.changes.contains(minimizingTaskChange) },
+ any(),
+ any(),
+ any(),
+ eq(mixedHandler),
+ )
+ }
+
+ @Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS_BUGFIX)
fun addPendingAndAnimateLaunchTransition_noMinimizeChange_doesNotReparentMinimizeChange() {
val wct = WindowContainerTransaction()
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 8510441c0557..ed9b97d264f7 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
@@ -55,6 +55,8 @@ import org.mockito.Mock
import org.mockito.Mockito.inOrder
import org.mockito.Mockito.spy
import org.mockito.kotlin.any
+import org.mockito.kotlin.clearInvocations
+import org.mockito.kotlin.eq
import org.mockito.kotlin.never
import org.mockito.kotlin.times
import org.mockito.kotlin.verify
@@ -894,12 +896,12 @@ class DesktopRepositoryTest(flags: FlagsParameterization) : ShellTestCase() {
val taskId = 1
val listener = TestListener()
repo.addActiveTaskListener(listener)
- repo.addTask(DEFAULT_DISPLAY, taskId, isVisible = true)
+ repo.addTask(THIRD_DISPLAY, taskId, isVisible = true)
repo.removeTask(THIRD_DISPLAY, taskId)
assertThat(repo.isActiveTask(taskId)).isFalse()
- assertThat(listener.activeChangesOnDefaultDisplay).isEqualTo(2)
+ assertThat(listener.activeChangesOnThirdDisplay).isEqualTo(2)
}
@Test
@@ -917,7 +919,7 @@ class DesktopRepositoryTest(flags: FlagsParameterization) : ShellTestCase() {
fun removeTask_updatesTaskVisibility() {
repo.addDesk(displayId = THIRD_DISPLAY, deskId = THIRD_DISPLAY)
val taskId = 1
- repo.addTask(DEFAULT_DISPLAY, taskId, isVisible = true)
+ repo.addTask(THIRD_DISPLAY, taskId, isVisible = true)
repo.removeTask(THIRD_DISPLAY, taskId)
@@ -1106,6 +1108,30 @@ class DesktopRepositoryTest(flags: FlagsParameterization) : ShellTestCase() {
}
@Test
+ @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun setTaskInFullImmersiveState_inDesk_savedAsInImmersiveState() {
+ repo.addDesk(DEFAULT_DISPLAY, deskId = 6)
+ repo.addTaskToDesk(DEFAULT_DISPLAY, deskId = 6, taskId = 10, isVisible = true)
+ assertThat(repo.isTaskInFullImmersiveState(6)).isFalse()
+
+ repo.setTaskInFullImmersiveStateInDesk(deskId = 6, taskId = 10, immersive = true)
+
+ assertThat(repo.isTaskInFullImmersiveState(taskId = 10)).isTrue()
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun removeTaskInFullImmersiveState_inDesk_removedAsInImmersiveState() {
+ repo.addDesk(DEFAULT_DISPLAY, deskId = 6)
+ repo.addTaskToDesk(DEFAULT_DISPLAY, deskId = 6, taskId = 10, isVisible = true)
+ repo.setTaskInFullImmersiveStateInDesk(deskId = 6, taskId = 10, immersive = true)
+
+ repo.setTaskInFullImmersiveStateInDesk(deskId = 6, taskId = 10, immersive = false)
+
+ assertThat(repo.isTaskInFullImmersiveState(taskId = 10)).isFalse()
+ }
+
+ @Test
fun removeTaskInFullImmersiveState_otherWasImmersive_otherRemainsImmersive() {
repo.setTaskInFullImmersiveState(DEFAULT_DISPLAY, taskId = 1, immersive = true)
@@ -1274,14 +1300,146 @@ class DesktopRepositoryTest(flags: FlagsParameterization) : ShellTestCase() {
assertEquals(SECOND_DISPLAY, repo.getDisplayForDesk(deskId = 8))
}
+ @Test
+ @EnableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun setDeskActive() {
+ repo.addDesk(DEFAULT_DISPLAY, deskId = 6)
+
+ repo.setActiveDesk(DEFAULT_DISPLAY, deskId = 6)
+
+ assertThat(repo.getActiveDeskId(DEFAULT_DISPLAY)).isEqualTo(6)
+ }
+
+ @Test
+ @EnableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun setDeskInactive() {
+ repo.addDesk(DEFAULT_DISPLAY, deskId = 6)
+ repo.setActiveDesk(DEFAULT_DISPLAY, deskId = 6)
+
+ repo.setDeskInactive(deskId = 6)
+
+ assertThat(repo.getActiveDeskId(DEFAULT_DISPLAY)).isNull()
+ }
+
+ @Test
+ @EnableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun getDeskIdForTask() {
+ repo.addDesk(DEFAULT_DISPLAY, deskId = 6)
+ repo.addTaskToDesk(DEFAULT_DISPLAY, deskId = 6, taskId = 10, isVisible = true)
+
+ assertThat(repo.getDeskIdForTask(10)).isEqualTo(6)
+ }
+
+ @Test
+ @EnableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun removeTaskFromDesk_clearsBoundsBeforeMaximize() {
+ repo.addDesk(DEFAULT_DISPLAY, deskId = 6)
+ repo.addTaskToDesk(DEFAULT_DISPLAY, deskId = 6, taskId = 10, isVisible = true)
+ repo.saveBoundsBeforeMaximize(taskId = 10, bounds = Rect(10, 10, 100, 100))
+
+ repo.removeTaskFromDesk(deskId = 6, taskId = 10)
+
+ assertThat(repo.removeBoundsBeforeMaximize(taskId = 10)).isNull()
+ }
+
+ @Test
+ @EnableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun removeTaskFromDesk_clearsBoundsBeforeImmersive() {
+ repo.addDesk(DEFAULT_DISPLAY, deskId = 6)
+ repo.addTaskToDesk(DEFAULT_DISPLAY, deskId = 6, taskId = 10, isVisible = true)
+ repo.saveBoundsBeforeFullImmersive(taskId = 10, bounds = Rect(10, 10, 100, 100))
+
+ repo.removeTaskFromDesk(deskId = 6, taskId = 10)
+
+ assertThat(repo.removeBoundsBeforeFullImmersive(taskId = 10)).isNull()
+ }
+
+ @Test
+ @EnableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun removeTaskFromDesk_removesFromZOrderList() {
+ repo.addDesk(DEFAULT_DISPLAY, deskId = 6)
+ repo.addTaskToDesk(DEFAULT_DISPLAY, deskId = 6, taskId = 10, isVisible = true)
+
+ repo.removeTaskFromDesk(deskId = 6, taskId = 10)
+
+ assertThat(repo.getFreeformTasksIdsInDeskInZOrder(deskId = 6)).doesNotContain(10)
+ }
+
+ @Test
+ @EnableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun removeTaskFromDesk_removesFromMinimized() {
+ repo.addDesk(DEFAULT_DISPLAY, deskId = 6)
+ repo.addTaskToDesk(DEFAULT_DISPLAY, deskId = 6, taskId = 10, isVisible = true)
+ repo.minimizeTaskInDesk(DEFAULT_DISPLAY, deskId = 6, taskId = 10)
+
+ repo.removeTaskFromDesk(deskId = 6, taskId = 10)
+
+ assertThat(repo.getMinimizedTaskIdsInDesk(deskId = 6)).doesNotContain(10)
+ }
+
+ @Test
+ @EnableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun removeTaskFromDesk_removesFromImmersive() {
+ repo.addDesk(DEFAULT_DISPLAY, deskId = 6)
+ repo.addTaskToDesk(DEFAULT_DISPLAY, deskId = 6, taskId = 10, isVisible = true)
+ repo.setTaskInFullImmersiveStateInDesk(deskId = 6, taskId = 10, immersive = true)
+
+ repo.removeTaskFromDesk(deskId = 6, taskId = 10)
+
+ assertThat(repo.isTaskInFullImmersiveState(taskId = 10)).isFalse()
+ }
+
+ @Test
+ @EnableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun removeTaskFromDesk_removesFromActiveTasks() {
+ repo.addDesk(DEFAULT_DISPLAY, deskId = 6)
+ repo.addTaskToDesk(DEFAULT_DISPLAY, deskId = 6, taskId = 10, isVisible = true)
+
+ repo.removeTaskFromDesk(deskId = 6, taskId = 10)
+
+ assertThat(repo.isActiveTaskInDesk(taskId = 10, deskId = 6)).isFalse()
+ }
+
+ @Test
+ @EnableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun removeTaskFromDesk_removesFromVisibleTasks() {
+ repo.addDesk(DEFAULT_DISPLAY, deskId = 6)
+ repo.addTaskToDesk(DEFAULT_DISPLAY, deskId = 6, taskId = 10, isVisible = true)
+
+ repo.removeTaskFromDesk(deskId = 6, taskId = 10)
+
+ assertThat(repo.isVisibleTaskInDesk(taskId = 10, deskId = 6)).isFalse()
+ }
+
+ @Test
+ @EnableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, FLAG_ENABLE_DESKTOP_WINDOWING_PERSISTENCE)
+ fun removeTaskFromDesk_updatesPersistence() = runTest {
+ repo.addDesk(DEFAULT_DISPLAY, deskId = 6)
+ repo.addTaskToDesk(DEFAULT_DISPLAY, deskId = 6, taskId = 10, isVisible = true)
+ clearInvocations(persistentRepository)
+
+ repo.removeTaskFromDesk(deskId = 6, taskId = 10)
+
+ verify(persistentRepository)
+ .addOrUpdateDesktop(
+ userId = eq(DEFAULT_USER_ID),
+ desktopId = eq(6),
+ visibleTasks = any(),
+ minimizedTasks = any(),
+ freeformTasksInZOrder = any(),
+ )
+ }
+
class TestListener : DesktopRepository.ActiveTasksListener {
var activeChangesOnDefaultDisplay = 0
var activeChangesOnSecondaryDisplay = 0
+ var activeChangesOnThirdDisplay = 0
override fun onActiveTasksChanged(displayId: Int) {
when (displayId) {
DEFAULT_DISPLAY -> activeChangesOnDefaultDisplay++
SECOND_DISPLAY -> activeChangesOnSecondaryDisplay++
+ THIRD_DISPLAY -> activeChangesOnThirdDisplay++
else -> fail("Active task listener received unexpected display id: $displayId")
}
}
@@ -1290,9 +1448,11 @@ class DesktopRepositoryTest(flags: FlagsParameterization) : ShellTestCase() {
class TestVisibilityListener : DesktopRepository.VisibleTasksListener {
var visibleTasksCountOnDefaultDisplay = 0
var visibleTasksCountOnSecondaryDisplay = 0
+ var visibleTasksCountOnThirdDisplay = 0
var visibleChangesOnDefaultDisplay = 0
var visibleChangesOnSecondaryDisplay = 0
+ var visibleChangesOnThirdDisplay = 0
override fun onTasksVisibilityChanged(displayId: Int, visibleTasksCount: Int) {
when (displayId) {
@@ -1304,6 +1464,10 @@ class DesktopRepositoryTest(flags: FlagsParameterization) : ShellTestCase() {
visibleTasksCountOnSecondaryDisplay = visibleTasksCount
visibleChangesOnSecondaryDisplay++
}
+ THIRD_DISPLAY -> {
+ visibleTasksCountOnThirdDisplay = visibleTasksCount
+ visibleChangesOnThirdDisplay++
+ }
else -> fail("Visible task listener received unexpected display id: $displayId")
}
}
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 3ee9501dd8dd..e2c3dda0d927 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
@@ -151,8 +151,7 @@ import com.android.wm.shell.transition.Transitions
import com.android.wm.shell.transition.Transitions.ENABLE_SHELL_TRANSITIONS
import com.android.wm.shell.transition.Transitions.TransitionHandler
import com.android.wm.shell.windowdecor.DesktopModeWindowDecorViewModelTestsBase.Companion.HOME_LAUNCHER_PACKAGE_NAME
-import com.android.wm.shell.windowdecor.DesktopModeWindowDecoration
-import com.android.wm.shell.windowdecor.tiling.DesktopTilingDecorViewModel
+import com.android.wm.shell.windowdecor.tiling.SnapEventHandler
import com.google.common.truth.Truth.assertThat
import com.google.common.truth.Truth.assertWithMessage
import java.util.Optional
@@ -179,6 +178,7 @@ import org.mockito.ArgumentMatchers.isA
import org.mockito.ArgumentMatchers.isNull
import org.mockito.Mock
import org.mockito.Mockito
+import org.mockito.Mockito.anyBoolean
import org.mockito.Mockito.anyInt
import org.mockito.Mockito.clearInvocations
import org.mockito.Mockito.mock
@@ -233,6 +233,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
@Mock lateinit var multiInstanceHelper: MultiInstanceHelper
@Mock lateinit var desktopModeVisualIndicator: DesktopModeVisualIndicator
@Mock lateinit var recentTasksController: RecentTasksController
+ @Mock lateinit var snapEventHandler: SnapEventHandler
@Mock private lateinit var mockInteractionJankMonitor: InteractionJankMonitor
@Mock private lateinit var mockSurface: SurfaceControl
@Mock private lateinit var taskbarDesktopTaskListener: TaskbarDesktopTaskListener
@@ -245,9 +246,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
@Mock lateinit var repositoryInitializer: DesktopRepositoryInitializer
@Mock private lateinit var mockToast: Toast
private lateinit var mockitoSession: StaticMockitoSession
- @Mock private lateinit var desktopTilingDecorViewModel: DesktopTilingDecorViewModel
@Mock private lateinit var bubbleController: BubbleController
- @Mock private lateinit var desktopWindowDecoration: DesktopModeWindowDecoration
@Mock private lateinit var resources: Resources
@Mock
lateinit var desktopModeEnterExitTransitionListener: DesktopModeEntryExitTransitionListener
@@ -328,8 +327,10 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
desktopModeCompatPolicy = spy(DesktopModeCompatPolicy(spyContext))
whenever(shellTaskOrganizer.getRunningTasks(anyInt())).thenAnswer { runningTasks }
- whenever(transitions.startTransition(anyInt(), any(), isNull())).thenAnswer { Binder() }
+ whenever(transitions.startTransition(anyInt(), any(), anyOrNull())).thenAnswer { Binder() }
whenever(enterDesktopTransitionHandler.moveToDesktop(any(), any())).thenAnswer { Binder() }
+ whenever(exitDesktopTransitionHandler.startTransition(any(), any(), any(), any()))
+ .thenReturn(Binder())
whenever(displayController.getDisplayLayout(anyInt())).thenReturn(displayLayout)
whenever(displayController.getDisplayContext(anyInt())).thenReturn(mockDisplayContext)
whenever(displayController.getDisplay(anyInt())).thenReturn(display)
@@ -379,6 +380,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
recentsTransitionStateListener = captor.firstValue
controller.taskbarDesktopTaskListener = taskbarDesktopTaskListener
+ controller.setSnapEventHandler(snapEventHandler)
assumeTrue(ENABLE_SHELL_TRANSITIONS)
@@ -422,7 +424,6 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
mockHandler,
desktopModeEventLogger,
desktopModeUiEventLogger,
- desktopTilingDecorViewModel,
desktopWallpaperActivityTokenProvider,
Optional.of(bubbleController),
overviewToDesktopTransitionObserver,
@@ -918,7 +919,10 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
}
@Test
- @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+ @DisableFlags(
+ Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
+ Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND,
+ )
fun showDesktopApps_twoDisplays_bringsToFrontOnlyOneDisplay_desktopWallpaperDisabled() {
taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY)
val homeTaskDefaultDisplay = setUpHomeTask(DEFAULT_DISPLAY)
@@ -2040,6 +2044,30 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
}
@Test
+ @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun moveToFullscreen_fromDesk_reparentsToTaskDisplayArea() {
+ val task = setUpFreeformTask()
+ val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!!
+
+ controller.moveToFullscreen(task.taskId, transitionSource = UNKNOWN)
+
+ val wct = getLatestExitDesktopWct()
+ wct.assertHop(ReparentPredicate(token = task.token, parentToken = tda.token, toTop = true))
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun moveToFullscreen_fromDesk_deactivatesDesk() {
+ val task = setUpFreeformTask()
+ val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!!
+
+ controller.moveToFullscreen(task.taskId, transitionSource = UNKNOWN)
+
+ val wct = getLatestExitDesktopWct()
+ verify(desksOrganizer).deactivateDesk(wct, deskId = 0)
+ }
+
+ @Test
fun moveToFullscreen_tdaFullscreen_windowingModeSetToUndefined() {
val task = setUpFreeformTask()
val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!!
@@ -2054,6 +2082,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
@Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER)
+ @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
fun moveToFullscreen_tdaFullscreen_windowingModeUndefined_removesWallpaperActivity() {
whenever(DesktopModeStatus.enterDesktopByDefaultOnFreeformDisplay(context)).thenReturn(true)
val homeTask = setUpHomeTask()
@@ -2079,6 +2108,60 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
}
@Test
+ @EnableFlags(
+ Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER,
+ Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND,
+ )
+ fun moveToFullscreen_tdaFullscreen_windowingModeUndefined_removesWallpaperActivity_multiDesksEnabled() {
+ whenever(DesktopModeStatus.enterDesktopByDefaultOnFreeformDisplay(context)).thenReturn(true)
+ setUpHomeTask()
+ val task = setUpFreeformTask()
+ assertNotNull(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY))
+ .configuration
+ .windowConfiguration
+ .windowingMode = WINDOWING_MODE_FULLSCREEN
+
+ controller.moveToFullscreen(task.taskId, transitionSource = UNKNOWN)
+
+ val wct = getLatestExitDesktopWct()
+ val taskChange = assertNotNull(wct.changes[task.token.asBinder()])
+ verify(desktopModeEnterExitTransitionListener)
+ .onExitDesktopModeTransitionStarted(FULLSCREEN_ANIMATION_DURATION)
+ assertThat(taskChange.windowingMode).isEqualTo(WINDOWING_MODE_UNDEFINED)
+ // Removes wallpaper activity when leaving desktop
+ wct.assertReorder(wallpaperToken, toTop = false)
+ }
+
+ @Test
+ @EnableFlags(
+ Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER,
+ Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND,
+ )
+ fun moveToFullscreen_tdaFullscreen_windowingModeUndefined_homeBehindFullscreen_multiDesksEnabled() {
+ whenever(DesktopModeStatus.enterDesktopByDefaultOnFreeformDisplay(context)).thenReturn(true)
+ val homeTask = setUpHomeTask()
+ val task = setUpFreeformTask()
+ assertNotNull(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY))
+ .configuration
+ .windowConfiguration
+ .windowingMode = WINDOWING_MODE_FULLSCREEN
+
+ controller.moveToFullscreen(task.taskId, transitionSource = UNKNOWN)
+
+ val wct = getLatestExitDesktopWct()
+ val taskChange = assertNotNull(wct.changes[task.token.asBinder()])
+ verify(desktopModeEnterExitTransitionListener)
+ .onExitDesktopModeTransitionStarted(FULLSCREEN_ANIMATION_DURATION)
+ assertThat(taskChange.windowingMode).isEqualTo(WINDOWING_MODE_UNDEFINED)
+ // Moves home task behind the fullscreen task
+ val homeReorderIndex = wct.indexOfReorder(homeTask, toTop = true)
+ val fullscreenReorderIndex = wct.indexOfReorder(task, toTop = true)
+ assertThat(homeReorderIndex).isNotEqualTo(-1)
+ assertThat(fullscreenReorderIndex).isNotEqualTo(-1)
+ assertThat(fullscreenReorderIndex).isGreaterThan(homeReorderIndex)
+ }
+
+ @Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER)
fun moveToFullscreen_tdaFreeform_enforcedDesktop_doesNotReorderHome() {
whenever(DesktopModeStatus.enterDesktopByDefaultOnFreeformDisplay(context)).thenReturn(true)
@@ -2094,9 +2177,9 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
val wct = getLatestExitDesktopWct()
verify(desktopModeEnterExitTransitionListener)
.onExitDesktopModeTransitionStarted(FULLSCREEN_ANIMATION_DURATION)
- assertThat(wct.hierarchyOps).hasSize(1)
// Removes wallpaper activity when leaving desktop but doesn't reorder home or the task
- wct.assertReorderAt(index = 0, wallpaperToken, toTop = false)
+ wct.assertReorder(wallpaperToken, toTop = false)
+ wct.assertWithoutHop(ReorderPredicate(homeTask.token, toTop = null))
}
@Test
@@ -2114,6 +2197,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
@Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER)
+ @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
fun moveToFullscreen_tdaFreeform_windowingModeFullscreen_removesWallpaperActivity() {
val homeTask = setUpHomeTask()
val task = setUpFreeformTask()
@@ -2139,6 +2223,61 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
}
@Test
+ @EnableFlags(
+ Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER,
+ Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND,
+ )
+ fun moveToFullscreen_tdaFreeform_windowingModeFullscreen_removesWallpaperActivity_multiDesksEnabled() {
+ val homeTask = setUpHomeTask()
+ val task = setUpFreeformTask()
+
+ assertNotNull(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY))
+ .configuration
+ .windowConfiguration
+ .windowingMode = WINDOWING_MODE_FREEFORM
+
+ controller.moveToFullscreen(task.taskId, transitionSource = UNKNOWN)
+
+ val wct = getLatestExitDesktopWct()
+ val taskChange = assertNotNull(wct.changes[task.token.asBinder()])
+ assertThat(taskChange.windowingMode).isEqualTo(WINDOWING_MODE_FULLSCREEN)
+ verify(desktopModeEnterExitTransitionListener)
+ .onExitDesktopModeTransitionStarted(FULLSCREEN_ANIMATION_DURATION)
+ // Removes wallpaper activity when leaving desktop
+ wct.assertReorder(wallpaperToken, toTop = false)
+ }
+
+ @Test
+ @EnableFlags(
+ Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER,
+ Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND,
+ )
+ fun moveToFullscreen_tdaFreeform_windowingModeFullscreen_homeBehindFullscreen_multiDesksEnabled() {
+ val homeTask = setUpHomeTask()
+ val task = setUpFreeformTask()
+
+ assertNotNull(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY))
+ .configuration
+ .windowConfiguration
+ .windowingMode = WINDOWING_MODE_FREEFORM
+
+ controller.moveToFullscreen(task.taskId, transitionSource = UNKNOWN)
+
+ val wct = getLatestExitDesktopWct()
+ val taskChange = assertNotNull(wct.changes[task.token.asBinder()])
+ assertThat(taskChange.windowingMode).isEqualTo(WINDOWING_MODE_FULLSCREEN)
+ verify(desktopModeEnterExitTransitionListener)
+ .onExitDesktopModeTransitionStarted(FULLSCREEN_ANIMATION_DURATION)
+ // Moves home task behind the fullscreen task
+ val homeReorderIndex = wct.indexOfReorder(homeTask, toTop = true)
+ val fullscreenReorderIndex = wct.indexOfReorder(task, toTop = true)
+ assertThat(homeReorderIndex).isNotEqualTo(-1)
+ assertThat(fullscreenReorderIndex).isNotEqualTo(-1)
+ assertThat(fullscreenReorderIndex).isGreaterThan(homeReorderIndex)
+ }
+
+ @Test
+ @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
fun moveToFullscreen_multipleVisibleNonMinimizedTasks_doesNotRemoveWallpaperActivity() {
val homeTask = setUpHomeTask()
val task1 = setUpFreeformTask()
@@ -2165,6 +2304,29 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
}
@Test
+ @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun moveToFullscreen_multipleVisibleNonMinimizedTasks_doesNotRemoveWallpaperActivity_multiDesksEnabled() {
+ val homeTask = setUpHomeTask()
+ val task1 = setUpFreeformTask()
+ // Setup task2
+ setUpFreeformTask()
+
+ val tdaInfo = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)
+ assertNotNull(tdaInfo).configuration.windowConfiguration.windowingMode =
+ WINDOWING_MODE_FULLSCREEN
+
+ controller.moveToFullscreen(task1.taskId, transitionSource = UNKNOWN)
+
+ val wct = getLatestExitDesktopWct()
+ val task1Change = assertNotNull(wct.changes[task1.token.asBinder()])
+ assertThat(task1Change.windowingMode).isEqualTo(WINDOWING_MODE_UNDEFINED)
+ verify(desktopModeEnterExitTransitionListener)
+ .onExitDesktopModeTransitionStarted(FULLSCREEN_ANIMATION_DURATION)
+ // Does not remove wallpaper activity, as desktop still has a visible desktop task
+ wct.assertWithoutHop(ReorderPredicate(wallpaperToken, toTop = false))
+ }
+
+ @Test
fun moveToFullscreen_nonExistentTask_doesNothing() {
controller.moveToFullscreen(999, transitionSource = UNKNOWN)
verifyExitDesktopWCTNotExecuted()
@@ -2760,22 +2922,97 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
}
@Test
+ @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun onDesktopWindowClose_lastWindow_deactivatesDesk() {
+ val task = setUpFreeformTask()
+ val wct = WindowContainerTransaction()
+
+ controller.onDesktopWindowClose(wct, displayId = DEFAULT_DISPLAY, task)
+
+ verify(desksOrganizer).deactivateDesk(wct, deskId = 0)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun onDesktopWindowClose_lastWindow_addsPendingDeactivateTransition() {
+ val task = setUpFreeformTask()
+ val wct = WindowContainerTransaction()
+
+ val transition = Binder()
+ val runOnTransitStart =
+ controller.onDesktopWindowClose(wct, displayId = DEFAULT_DISPLAY, task)
+ runOnTransitStart(transition)
+
+ verify(desksTransitionsObserver)
+ .addPendingTransition(DeskTransition.DeactivateDesk(transition, deskId = 0))
+ }
+
+ @Test
fun onDesktopWindowMinimize_noActiveTask_doesntRemoveWallpaper() {
val task = setUpFreeformTask(active = false)
val transition = Binder()
- whenever(freeformTaskTransitionStarter.startMinimizedModeTransition(any()))
+ whenever(
+ freeformTaskTransitionStarter.startMinimizedModeTransition(
+ any(),
+ anyInt(),
+ anyBoolean(),
+ )
+ )
.thenReturn(transition)
controller.minimizeTask(task, MinimizeReason.MINIMIZE_BUTTON)
val captor = argumentCaptor<WindowContainerTransaction>()
- verify(freeformTaskTransitionStarter).startMinimizedModeTransition(captor.capture())
+ verify(freeformTaskTransitionStarter)
+ .startMinimizedModeTransition(captor.capture(), eq(task.taskId), eq(false))
captor.firstValue.hierarchyOps.none { hop ->
hop.type == HIERARCHY_OP_TYPE_REMOVE_TASK && hop.container == wallpaperToken.asBinder()
}
}
@Test
+ @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun onDesktopWindowMinimize_lastWindow_deactivatesDesk() {
+ val task = setUpFreeformTask()
+ val transition = Binder()
+ whenever(
+ freeformTaskTransitionStarter.startMinimizedModeTransition(
+ any(),
+ anyInt(),
+ anyBoolean(),
+ )
+ )
+ .thenReturn(transition)
+
+ controller.minimizeTask(task, MinimizeReason.MINIMIZE_BUTTON)
+
+ val captor = argumentCaptor<WindowContainerTransaction>()
+ verify(freeformTaskTransitionStarter)
+ .startMinimizedModeTransition(captor.capture(), eq(task.taskId), eq(true))
+ verify(desksOrganizer).deactivateDesk(captor.firstValue, deskId = 0)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun onDesktopWindowMinimize_lastWindow_addsPendingDeactivateTransition() {
+ val task = setUpFreeformTask()
+ val transition = Binder()
+ whenever(
+ freeformTaskTransitionStarter.startMinimizedModeTransition(
+ any(),
+ anyInt(),
+ anyBoolean(),
+ )
+ )
+ .thenReturn(transition)
+
+ controller.minimizeTask(task, MinimizeReason.MINIMIZE_BUTTON)
+
+ verify(desksTransitionsObserver)
+ .addPendingTransition(DeskTransition.DeactivateDesk(token = transition, deskId = 0))
+ }
+
+ @Test
fun onPipTaskMinimize_autoEnterEnabled_startPipTransition() {
val task = setUpPipTask(autoEnterEnabled = true)
val handler = mock(TransitionHandler::class.java)
@@ -2785,18 +3022,26 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
controller.minimizeTask(task, MinimizeReason.MINIMIZE_BUTTON)
verify(freeformTaskTransitionStarter).startPipTransition(any())
- verify(freeformTaskTransitionStarter, never()).startMinimizedModeTransition(any())
+ verify(freeformTaskTransitionStarter, never())
+ .startMinimizedModeTransition(any(), anyInt(), anyBoolean())
}
@Test
fun onPipTaskMinimize_autoEnterDisabled_startMinimizeTransition() {
val task = setUpPipTask(autoEnterEnabled = false)
- whenever(freeformTaskTransitionStarter.startMinimizedModeTransition(any()))
+ whenever(
+ freeformTaskTransitionStarter.startMinimizedModeTransition(
+ any(),
+ anyInt(),
+ anyBoolean(),
+ )
+ )
.thenReturn(Binder())
controller.minimizeTask(task, MinimizeReason.MINIMIZE_BUTTON)
- verify(freeformTaskTransitionStarter).startMinimizedModeTransition(any())
+ verify(freeformTaskTransitionStarter)
+ .startMinimizedModeTransition(any(), eq(task.taskId), anyBoolean())
verify(freeformTaskTransitionStarter, never()).startPipTransition(any())
}
@@ -2820,13 +3065,20 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
fun onDesktopWindowMinimize_singleActiveTask_noWallpaperActivityToken_doesntRemoveWallpaper() {
val task = setUpFreeformTask(active = true)
val transition = Binder()
- whenever(freeformTaskTransitionStarter.startMinimizedModeTransition(any()))
+ whenever(
+ freeformTaskTransitionStarter.startMinimizedModeTransition(
+ any(),
+ anyInt(),
+ anyBoolean(),
+ )
+ )
.thenReturn(transition)
controller.minimizeTask(task, MinimizeReason.MINIMIZE_BUTTON)
val captor = argumentCaptor<WindowContainerTransaction>()
- verify(freeformTaskTransitionStarter).startMinimizedModeTransition(captor.capture())
+ verify(freeformTaskTransitionStarter)
+ .startMinimizedModeTransition(captor.capture(), eq(task.taskId), eq(true))
captor.firstValue.hierarchyOps.none { hop -> hop.type == HIERARCHY_OP_TYPE_REMOVE_TASK }
}
@@ -2835,14 +3087,21 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
fun onTaskMinimize_singleActiveTask_hasWallpaperActivityToken_removesWallpaper() {
val task = setUpFreeformTask()
val transition = Binder()
- whenever(freeformTaskTransitionStarter.startMinimizedModeTransition(any()))
+ whenever(
+ freeformTaskTransitionStarter.startMinimizedModeTransition(
+ any(),
+ anyInt(),
+ anyBoolean(),
+ )
+ )
.thenReturn(transition)
// The only active task is being minimized.
controller.minimizeTask(task, MinimizeReason.MINIMIZE_BUTTON)
val captor = argumentCaptor<WindowContainerTransaction>()
- verify(freeformTaskTransitionStarter).startMinimizedModeTransition(captor.capture())
+ verify(freeformTaskTransitionStarter)
+ .startMinimizedModeTransition(captor.capture(), eq(task.taskId), eq(true))
// Adds remove wallpaper operation
captor.firstValue.assertReorderAt(index = 0, wallpaperToken, toTop = false)
}
@@ -2851,7 +3110,13 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
fun onDesktopWindowMinimize_singleActiveTask_alreadyMinimized_doesntRemoveWallpaper() {
val task = setUpFreeformTask()
val transition = Binder()
- whenever(freeformTaskTransitionStarter.startMinimizedModeTransition(any()))
+ whenever(
+ freeformTaskTransitionStarter.startMinimizedModeTransition(
+ any(),
+ anyInt(),
+ anyBoolean(),
+ )
+ )
.thenReturn(transition)
taskRepository.minimizeTask(DEFAULT_DISPLAY, task.taskId)
@@ -2859,7 +3124,8 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
controller.minimizeTask(task, MinimizeReason.MINIMIZE_BUTTON)
val captor = argumentCaptor<WindowContainerTransaction>()
- verify(freeformTaskTransitionStarter).startMinimizedModeTransition(captor.capture())
+ verify(freeformTaskTransitionStarter)
+ .startMinimizedModeTransition(captor.capture(), eq(task.taskId), eq(false))
captor.firstValue.hierarchyOps.none { hop ->
hop.type == HIERARCHY_OP_TYPE_REMOVE_TASK && hop.container == wallpaperToken.asBinder()
}
@@ -2870,13 +3136,20 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
val task1 = setUpFreeformTask(active = true)
setUpFreeformTask(active = true)
val transition = Binder()
- whenever(freeformTaskTransitionStarter.startMinimizedModeTransition(any()))
+ whenever(
+ freeformTaskTransitionStarter.startMinimizedModeTransition(
+ any(),
+ anyInt(),
+ anyBoolean(),
+ )
+ )
.thenReturn(transition)
controller.minimizeTask(task1, MinimizeReason.MINIMIZE_BUTTON)
val captor = argumentCaptor<WindowContainerTransaction>()
- verify(freeformTaskTransitionStarter).startMinimizedModeTransition(captor.capture())
+ verify(freeformTaskTransitionStarter)
+ .startMinimizedModeTransition(captor.capture(), eq(task1.taskId), eq(false))
captor.firstValue.hierarchyOps.none { hop ->
hop.type == HIERARCHY_OP_TYPE_REMOVE_TASK && hop.container == wallpaperToken.asBinder()
}
@@ -2888,7 +3161,13 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
val task1 = setUpFreeformTask(active = true)
val task2 = setUpFreeformTask(active = true)
val transition = Binder()
- whenever(freeformTaskTransitionStarter.startMinimizedModeTransition(any()))
+ whenever(
+ freeformTaskTransitionStarter.startMinimizedModeTransition(
+ any(),
+ anyInt(),
+ anyBoolean(),
+ )
+ )
.thenReturn(transition)
taskRepository.minimizeTask(DEFAULT_DISPLAY, task2.taskId)
@@ -2896,7 +3175,8 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
controller.minimizeTask(task1, MinimizeReason.MINIMIZE_BUTTON)
// Adds remove wallpaper operation
val captor = argumentCaptor<WindowContainerTransaction>()
- verify(freeformTaskTransitionStarter).startMinimizedModeTransition(captor.capture())
+ verify(freeformTaskTransitionStarter)
+ .startMinimizedModeTransition(captor.capture(), eq(task1.taskId), eq(true))
// Adds remove wallpaper operation
captor.firstValue.assertReorderAt(index = 0, wallpaperToken, toTop = false)
}
@@ -2905,7 +3185,13 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
fun onDesktopWindowMinimize_triesToExitImmersive() {
val task = setUpFreeformTask()
val transition = Binder()
- whenever(freeformTaskTransitionStarter.startMinimizedModeTransition(any()))
+ whenever(
+ freeformTaskTransitionStarter.startMinimizedModeTransition(
+ any(),
+ anyInt(),
+ anyBoolean(),
+ )
+ )
.thenReturn(transition)
controller.minimizeTask(task, MinimizeReason.MINIMIZE_BUTTON)
@@ -2918,7 +3204,13 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
val task = setUpFreeformTask()
val transition = Binder()
val runOnTransit = RunOnStartTransitionCallback()
- whenever(freeformTaskTransitionStarter.startMinimizedModeTransition(any()))
+ whenever(
+ freeformTaskTransitionStarter.startMinimizedModeTransition(
+ any(),
+ anyInt(),
+ anyBoolean(),
+ )
+ )
.thenReturn(transition)
whenever(mMockDesktopImmersiveController.exitImmersiveIfApplicable(any(), eq(task), any()))
.thenReturn(
@@ -3972,10 +4264,11 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
val taskChange = assertNotNull(wct.changes[task2.token.asBinder()])
assertThat(taskChange.windowingMode)
.isEqualTo(WINDOWING_MODE_UNDEFINED) // inherited FULLSCREEN
- wct.assertReorderAt(index = 0, wallpaperToken, toTop = false)
+ wct.assertReorder(wallpaperToken, toTop = false)
}
@Test
+ @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
fun moveFocusedTaskToFullscreen_multipleVisibleTasks_doesNotRemoveWallpaperActivity() {
val homeTask = setUpHomeTask()
val task1 = setUpFreeformTask()
@@ -3999,6 +4292,52 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
}
@Test
+ @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun moveFocusedTaskToFullscreen_multipleVisibleTasks_doesNotRemoveWallpaperActivity_multiDesksEnabled() {
+ val homeTask = setUpHomeTask()
+ val task1 = setUpFreeformTask()
+ val task2 = setUpFreeformTask()
+ val task3 = setUpFreeformTask()
+
+ task1.isFocused = false
+ task2.isFocused = true
+ task3.isFocused = false
+ controller.enterFullscreen(DEFAULT_DISPLAY, transitionSource = UNKNOWN)
+
+ val wct = getLatestExitDesktopWct()
+ val taskChange = assertNotNull(wct.changes[task2.token.asBinder()])
+ assertThat(taskChange.windowingMode)
+ .isEqualTo(WINDOWING_MODE_UNDEFINED) // inherited FULLSCREEN
+ // Does not remove wallpaper activity
+ wct.assertWithoutHop(ReorderPredicate(wallpaperToken, toTop = null))
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun moveFocusedTaskToFullscreen_multipleVisibleTasks_fullscreenOverHome_multiDesksEnabled() {
+ val homeTask = setUpHomeTask()
+ val task1 = setUpFreeformTask()
+ val task2 = setUpFreeformTask()
+ val task3 = setUpFreeformTask()
+
+ task1.isFocused = false
+ task2.isFocused = true
+ task3.isFocused = false
+ controller.enterFullscreen(DEFAULT_DISPLAY, transitionSource = UNKNOWN)
+
+ val wct = getLatestExitDesktopWct()
+ val taskChange = assertNotNull(wct.changes[task2.token.asBinder()])
+ assertThat(taskChange.windowingMode)
+ .isEqualTo(WINDOWING_MODE_UNDEFINED) // inherited FULLSCREEN
+ // Moves home task behind the fullscreen task
+ val homeReorderIndex = wct.indexOfReorder(homeTask, toTop = true)
+ val fullscreenReorderIndex = wct.indexOfReorder(task2, toTop = true)
+ assertThat(homeReorderIndex).isNotEqualTo(-1)
+ assertThat(fullscreenReorderIndex).isNotEqualTo(-1)
+ assertThat(fullscreenReorderIndex).isGreaterThan(homeReorderIndex)
+ }
+
+ @Test
@EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PIP)
fun moveFocusedTaskToFullscreen_minimizedPipPresent_removeWallpaperActivity() {
val freeformTask = setUpFreeformTask()
@@ -4420,7 +4759,6 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
validDragArea = Rect(0, 50, 2000, 2000),
dragStartBounds = Rect(),
motionEvent,
- desktopWindowDecoration,
)
val rectAfterEnd = Rect(100, 50, 500, 1150)
verify(transitions)
@@ -4458,7 +4796,6 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
validDragArea = Rect(0, 50, 2000, 2000),
dragStartBounds = Rect(),
motionEvent,
- desktopWindowDecoration,
)
verify(transitions)
@@ -4498,7 +4835,6 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
validDragArea = Rect(0, 50, 2000, 2000),
dragStartBounds = Rect(),
motionEvent,
- desktopWindowDecoration,
)
verify(transitions)
@@ -4539,7 +4875,6 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
validDragArea = Rect(0, 50, 2000, 2000),
dragStartBounds = Rect(),
motionEvent,
- desktopWindowDecoration,
)
// Assert the task exits desktop mode
@@ -4577,7 +4912,6 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
validDragArea = Rect(0, 50, 2000, 2000),
dragStartBounds = Rect(),
motionEvent,
- desktopWindowDecoration,
)
// Assert bounds set to stable bounds
@@ -4633,7 +4967,6 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
validDragArea = Rect(0, 50, 2000, 2000),
dragStartBounds = Rect(),
motionEvent,
- desktopWindowDecoration,
)
// Assert that task is NOT updated via WCT
@@ -5053,7 +5386,6 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
SnapPosition.LEFT,
ResizeTrigger.SNAP_LEFT_MENU,
InputMethod.TOUCH,
- desktopWindowDecoration,
)
// Assert bounds set to stable bounds
val wct = getLatestToggleResizeDesktopTaskWct(currentDragBounds)
@@ -5099,7 +5431,6 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
SnapPosition.LEFT,
ResizeTrigger.SNAP_LEFT_MENU,
InputMethod.TOUCH,
- desktopWindowDecoration,
)
// Assert that task is NOT updated via WCT
verify(toggleResizeDesktopTaskTransitionHandler, never()).startTransition(any(), any())
@@ -5143,7 +5474,6 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
currentDragBounds,
preDragBounds,
motionEvent,
- desktopWindowDecoration,
)
val wct = getLatestToggleResizeDesktopTaskWct(currentDragBounds)
assertThat(findBoundsChange(wct, task)).isEqualTo(expectedBounds)
@@ -5173,7 +5503,6 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
currentDragBounds,
preDragBounds,
motionEvent,
- desktopWindowDecoration,
)
verify(mReturnToDragStartAnimator)
.start(
@@ -5198,7 +5527,6 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
SnapPosition.LEFT,
ResizeTrigger.SNAP_LEFT_MENU,
InputMethod.MOUSE,
- desktopWindowDecoration,
)
// Assert that task is NOT updated via WCT
@@ -5225,7 +5553,6 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
SnapPosition.LEFT,
ResizeTrigger.SNAP_LEFT_MENU,
InputMethod.MOUSE,
- desktopWindowDecoration,
)
// Assert bounds set to half of the stable bounds
@@ -6278,15 +6605,46 @@ private fun WindowContainerTransaction.assertWithoutHop(
assertThat(hierarchyOps.none(predicate)).isTrue()
}
-private fun WindowContainerTransaction.assertReorder(
+private fun WindowContainerTransaction.indexOfReorder(
task: RunningTaskInfo,
toTop: Boolean? = null,
-) {
- assertHop { hop ->
+): Int {
+ val hop = hierarchyOps.singleOrNull(ReorderPredicate(task.token, toTop)) ?: return -1
+ return hierarchyOps.indexOf(hop)
+}
+
+private class ReorderPredicate(val token: WindowContainerToken, val toTop: Boolean? = null) :
+ ((WindowContainerTransaction.HierarchyOp) -> Boolean) {
+ override fun invoke(hop: WindowContainerTransaction.HierarchyOp): Boolean =
hop.type == HIERARCHY_OP_TYPE_REORDER &&
(toTop == null || hop.toTop == toTop) &&
- hop.container == task.token.asBinder()
- }
+ hop.container == token.asBinder()
+}
+
+private class ReparentPredicate(
+ val token: WindowContainerToken,
+ val parentToken: WindowContainerToken,
+ val toTop: Boolean? = null,
+) : ((WindowContainerTransaction.HierarchyOp) -> Boolean) {
+ override fun invoke(hop: WindowContainerTransaction.HierarchyOp): Boolean =
+ hop.isReparent &&
+ (toTop == null || hop.toTop == toTop) &&
+ hop.container == token.asBinder() &&
+ hop.newParent == parentToken.asBinder()
+}
+
+private fun WindowContainerTransaction.assertReorder(
+ task: RunningTaskInfo,
+ toTop: Boolean? = null,
+) {
+ assertReorder(task.token, toTop)
+}
+
+private fun WindowContainerTransaction.assertReorder(
+ token: WindowContainerToken,
+ toTop: Boolean? = null,
+) {
+ assertHop(ReorderPredicate(token, toTop))
}
private fun WindowContainerTransaction.assertReorderAt(
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 ba26d1df94f6..85f6cd36992d 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
@@ -51,6 +51,7 @@ import org.mockito.ArgumentMatchers.anyInt
import org.mockito.ArgumentMatchers.eq
import org.mockito.Mock
import org.mockito.MockitoSession
+import org.mockito.kotlin.argThat
import org.mockito.kotlin.mock
import org.mockito.kotlin.never
import org.mockito.kotlin.times
@@ -180,10 +181,11 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() {
defaultHandler,
DragToDesktopTransitionHandler.CancelState.CANCEL_BUBBLE_LEFT,
)
- verify(bubbleController).expandStackAndSelectBubble(
- any<RunningTaskInfo>(),
- any<BubbleTransitions.DragData>()
- )
+ verify(bubbleController)
+ .expandStackAndSelectBubble(
+ any<RunningTaskInfo>(),
+ argThat<BubbleTransitions.DragData> { isReleasedOnLeft },
+ )
}
@Test
@@ -192,10 +194,11 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() {
defaultHandler,
DragToDesktopTransitionHandler.CancelState.CANCEL_BUBBLE_RIGHT,
)
- verify(bubbleController).expandStackAndSelectBubble(
- any<RunningTaskInfo>(),
- any<BubbleTransitions.DragData>()
- )
+ verify(bubbleController)
+ .expandStackAndSelectBubble(
+ any<RunningTaskInfo>(),
+ argThat<BubbleTransitions.DragData> { !isReleasedOnLeft },
+ )
}
@Test
@@ -382,10 +385,11 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() {
)
// Verify the request went through bubble controller.
- verify(bubbleController).expandStackAndSelectBubble(
- any<RunningTaskInfo>(),
- any<BubbleTransitions.DragData>()
- )
+ verify(bubbleController)
+ .expandStackAndSelectBubble(
+ any<RunningTaskInfo>(),
+ argThat<BubbleTransitions.DragData> { isReleasedOnLeft },
+ )
}
@Test
@@ -398,10 +402,11 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() {
)
// Verify the request went through bubble controller.
- verify(bubbleController).expandStackAndSelectBubble(
- any<RunningTaskInfo>(),
- any<BubbleTransitions.DragData>()
- )
+ verify(bubbleController)
+ .expandStackAndSelectBubble(
+ any<RunningTaskInfo>(),
+ argThat<BubbleTransitions.DragData> { !isReleasedOnLeft },
+ )
}
@Test
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/desktopwallpaperactivity/DesktopWallpaperActivityTokenProviderTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/desktopwallpaperactivity/DesktopWallpaperActivityTokenProviderTest.kt
new file mode 100644
index 000000000000..aa4e9aaf248e
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/desktopwallpaperactivity/DesktopWallpaperActivityTokenProviderTest.kt
@@ -0,0 +1,128 @@
+/*
+ * 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.desktopmode.desktopwallpaperactivity
+
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper.RunWithLooper
+import android.view.Display.DEFAULT_DISPLAY
+import androidx.test.filters.SmallTest
+import com.android.wm.shell.MockToken
+import com.android.wm.shell.ShellTestCase
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/**
+ * Test class for [DesktopWallpaperActivityTokenProvider]
+ *
+ * Usage: atest WMShellUnitTests:DesktopWallpaperActivityTokenProviderTest
+ */
+@SmallTest
+@RunWithLooper
+@RunWith(AndroidTestingRunner::class)
+class DesktopWallpaperActivityTokenProviderTest : ShellTestCase() {
+
+ private lateinit var provider: DesktopWallpaperActivityTokenProvider
+ private val DEFAULT_DISPLAY = 0
+ private val SECONDARY_DISPLAY = 1
+
+ @Before
+ fun setUp() {
+ provider = DesktopWallpaperActivityTokenProvider()
+ }
+
+ @Test
+ fun setToken_setsTokenForDisplay() {
+ val token = MockToken().token()
+
+ provider.setToken(token, DEFAULT_DISPLAY)
+
+ assertThat(provider.getToken(DEFAULT_DISPLAY)).isEqualTo(token)
+ }
+
+ @Test
+ fun setToken_overwritesExistingTokenForDisplay() {
+ val token1 = MockToken().token()
+ val token2 = MockToken().token()
+
+ provider.setToken(token1, DEFAULT_DISPLAY)
+ provider.setToken(token2, DEFAULT_DISPLAY)
+
+ assertThat(provider.getToken(DEFAULT_DISPLAY)).isEqualTo(token2)
+ }
+
+ @Test
+ fun getToken_returnsNullForNonExistentDisplay() {
+ assertThat(provider.getToken(SECONDARY_DISPLAY)).isNull()
+ }
+
+ @Test
+ fun removeToken_removesTokenForDisplay() {
+ val token = MockToken().token()
+
+ provider.setToken(token, DEFAULT_DISPLAY)
+ provider.removeToken(DEFAULT_DISPLAY)
+
+ assertThat(provider.getToken(DEFAULT_DISPLAY)).isNull()
+ }
+
+ @Test
+ fun removeToken_withToken_removesTokenForDisplay() {
+ val token = MockToken().token()
+
+ provider.setToken(token, DEFAULT_DISPLAY)
+ provider.removeToken(token)
+
+ assertThat(provider.getToken(DEFAULT_DISPLAY)).isNull()
+ }
+
+ @Test
+ fun removeToken_doesNothingForNonExistentDisplay() {
+ provider.removeToken(SECONDARY_DISPLAY)
+
+ assertThat(provider.getToken(SECONDARY_DISPLAY)).isNull()
+ }
+
+ @Test
+ fun removeToken_withNonExistentToken_doesNothing() {
+ val token1 = MockToken().token()
+ val token2 = MockToken().token()
+
+ provider.setToken(token1, DEFAULT_DISPLAY)
+ provider.removeToken(token2)
+
+ assertThat(provider.getToken(DEFAULT_DISPLAY)).isEqualTo(token1)
+ }
+
+ @Test
+ fun multipleDisplays_tokensAreIndependent() {
+ val token1 = MockToken().token()
+ val token2 = MockToken().token()
+
+ provider.setToken(token1, DEFAULT_DISPLAY)
+ provider.setToken(token2, SECONDARY_DISPLAY)
+
+ assertThat(provider.getToken(DEFAULT_DISPLAY)).isEqualTo(token1)
+ assertThat(provider.getToken(SECONDARY_DISPLAY)).isEqualTo(token2)
+
+ provider.removeToken(DEFAULT_DISPLAY)
+
+ assertThat(provider.getToken(DEFAULT_DISPLAY)).isNull()
+ assertThat(provider.getToken(SECONDARY_DISPLAY)).isEqualTo(token2)
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/DesksTransitionObserverTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/DesksTransitionObserverTest.kt
index 9f09e3f57927..4dcf669f4d25 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/DesksTransitionObserverTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/DesksTransitionObserverTest.kt
@@ -20,6 +20,7 @@ import android.platform.test.annotations.EnableFlags
import android.platform.test.flag.junit.SetFlagsRule
import android.testing.AndroidTestingRunner
import android.view.Display.DEFAULT_DISPLAY
+import android.view.WindowManager.TRANSIT_CHANGE
import android.view.WindowManager.TRANSIT_CLOSE
import android.view.WindowManager.TRANSIT_TO_FRONT
import android.window.TransitionInfo
@@ -177,4 +178,70 @@ class DesksTransitionObserverTest : ShellTestCase() {
assertThat(repository.getActiveDeskId(DEFAULT_DISPLAY)).isEqualTo(deskId)
assertThat(repository.getActiveTaskIdsInDesk(deskId)).contains(task.taskId)
}
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun onTransitionReady_deactivateDesk_updatesRepository() {
+ val transition = Binder()
+ val deskChange = Change(mock(), mock())
+ whenever(mockDesksOrganizer.isDeskChange(deskChange, deskId = 5)).thenReturn(true)
+ val deactivateTransition = DeskTransition.DeactivateDesk(transition, deskId = 5)
+ repository.addDesk(DEFAULT_DISPLAY, deskId = 5)
+ repository.setActiveDesk(DEFAULT_DISPLAY, deskId = 5)
+
+ observer.addPendingTransition(deactivateTransition)
+ observer.onTransitionReady(
+ transition = transition,
+ info = TransitionInfo(TRANSIT_CHANGE, /* flags= */ 0).apply { addChange(deskChange) },
+ )
+
+ assertThat(repository.getActiveDeskId(DEFAULT_DISPLAY)).isNull()
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun onTransitionReady_deactivateDeskWithExitingTask_updatesRepository() {
+ val transition = Binder()
+ val exitingTask = createFreeformTask(DEFAULT_DISPLAY)
+ val exitingTaskChange = Change(mock(), mock()).apply { taskInfo = exitingTask }
+ whenever(mockDesksOrganizer.getDeskAtEnd(exitingTaskChange)).thenReturn(null)
+ val deactivateTransition = DeskTransition.DeactivateDesk(transition, deskId = 5)
+ repository.addDesk(DEFAULT_DISPLAY, deskId = 5)
+ repository.setActiveDesk(DEFAULT_DISPLAY, deskId = 5)
+ repository.addTaskToDesk(
+ displayId = DEFAULT_DISPLAY,
+ deskId = 5,
+ taskId = exitingTask.taskId,
+ isVisible = true,
+ )
+ assertThat(repository.isActiveTaskInDesk(deskId = 5, taskId = exitingTask.taskId)).isTrue()
+
+ observer.addPendingTransition(deactivateTransition)
+ observer.onTransitionReady(
+ transition = transition,
+ info =
+ TransitionInfo(TRANSIT_CHANGE, /* flags= */ 0).apply {
+ addChange(exitingTaskChange)
+ },
+ )
+
+ assertThat(repository.isActiveTaskInDesk(deskId = 5, taskId = exitingTask.taskId)).isFalse()
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun onTransitionReady_deactivateDeskWithoutVisibleChange_updatesRepository() {
+ val transition = Binder()
+ val deactivateTransition = DeskTransition.DeactivateDesk(transition, deskId = 5)
+ repository.addDesk(DEFAULT_DISPLAY, deskId = 5)
+ repository.setActiveDesk(DEFAULT_DISPLAY, deskId = 5)
+
+ observer.addPendingTransition(deactivateTransition)
+ observer.onTransitionReady(
+ transition = transition,
+ info = TransitionInfo(TRANSIT_CHANGE, /* flags= */ 0),
+ )
+
+ assertThat(repository.getActiveDeskId(DEFAULT_DISPLAY)).isNull()
+ }
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizerTest.kt
index 4d4b15389eca..8b10ca1a2a70 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizerTest.kt
@@ -23,11 +23,13 @@ import android.view.WindowManager.TRANSIT_TO_FRONT
import android.window.TransitionInfo
import android.window.WindowContainerTransaction
import android.window.WindowContainerTransaction.HierarchyOp
+import android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_SET_LAUNCH_ROOT
import androidx.test.filters.SmallTest
import com.android.wm.shell.ShellTaskOrganizer
import com.android.wm.shell.ShellTestCase
import com.android.wm.shell.TestShellExecutor
import com.android.wm.shell.desktopmode.DesktopTestHelpers.createFreeformTask
+import com.android.wm.shell.desktopmode.multidesks.RootTaskDesksOrganizer.DeskRoot
import com.android.wm.shell.sysui.ShellCommandHandler
import com.android.wm.shell.sysui.ShellInit
import com.google.common.truth.Truth.assertThat
@@ -104,54 +106,45 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
@Test
fun testOnTaskVanished_removesRoot() {
- val callback = FakeOnCreateCallback()
- organizer.createDesk(Display.DEFAULT_DISPLAY, callback)
- val freeformRoot = createFreeformTask().apply { parentTaskId = -1 }
- organizer.onTaskAppeared(freeformRoot, SurfaceControl())
+ val desk = createDesk()
- organizer.onTaskVanished(freeformRoot)
+ organizer.onTaskVanished(desk.taskInfo)
- assertThat(organizer.roots.contains(freeformRoot.taskId)).isFalse()
+ assertThat(organizer.roots.contains(desk.deskId)).isFalse()
}
@Test
fun testDesktopWindowAppearsInDesk() {
- organizer.createDesk(Display.DEFAULT_DISPLAY, FakeOnCreateCallback())
- val freeformRoot = createFreeformTask().apply { parentTaskId = -1 }
- organizer.onTaskAppeared(freeformRoot, SurfaceControl())
- val child = createFreeformTask().apply { parentTaskId = freeformRoot.taskId }
+ val desk = createDesk()
+ val child = createFreeformTask().apply { parentTaskId = desk.deskId }
organizer.onTaskAppeared(child, SurfaceControl())
- assertThat(organizer.roots[freeformRoot.taskId].children).contains(child.taskId)
+ assertThat(desk.children).contains(child.taskId)
}
@Test
fun testDesktopWindowDisappearsFromDesk() {
- organizer.createDesk(Display.DEFAULT_DISPLAY, FakeOnCreateCallback())
- val freeformRoot = createFreeformTask().apply { parentTaskId = -1 }
- organizer.onTaskAppeared(freeformRoot, SurfaceControl())
- val child = createFreeformTask().apply { parentTaskId = freeformRoot.taskId }
+ val desk = createDesk()
+ val child = createFreeformTask().apply { parentTaskId = desk.deskId }
organizer.onTaskAppeared(child, SurfaceControl())
organizer.onTaskVanished(child)
- assertThat(organizer.roots[freeformRoot.taskId].children).doesNotContain(child.taskId)
+ assertThat(desk.children).doesNotContain(child.taskId)
}
@Test
fun testRemoveDesk() {
- organizer.createDesk(Display.DEFAULT_DISPLAY, FakeOnCreateCallback())
- val freeformRoot = createFreeformTask().apply { parentTaskId = -1 }
- organizer.onTaskAppeared(freeformRoot, SurfaceControl())
+ val desk = createDesk()
val wct = WindowContainerTransaction()
- organizer.removeDesk(wct, freeformRoot.taskId)
+ organizer.removeDesk(wct, desk.deskId)
assertThat(
wct.hierarchyOps.any { hop ->
hop.type == HierarchyOp.HIERARCHY_OP_TYPE_REMOVE_ROOT_TASK &&
- hop.container == freeformRoot.token.asBinder()
+ hop.container == desk.taskInfo.token.asBinder()
}
)
.isTrue()
@@ -167,25 +160,23 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
@Test
fun testActivateDesk() {
- organizer.createDesk(Display.DEFAULT_DISPLAY, FakeOnCreateCallback())
- val freeformRoot = createFreeformTask().apply { parentTaskId = -1 }
- organizer.onTaskAppeared(freeformRoot, SurfaceControl())
+ val desk = createDesk()
val wct = WindowContainerTransaction()
- organizer.activateDesk(wct, freeformRoot.taskId)
+ organizer.activateDesk(wct, desk.deskId)
assertThat(
wct.hierarchyOps.any { hop ->
hop.type == HierarchyOp.HIERARCHY_OP_TYPE_REORDER &&
hop.toTop &&
- hop.container == freeformRoot.token.asBinder()
+ hop.container == desk.taskInfo.token.asBinder()
}
)
.isTrue()
assertThat(
wct.hierarchyOps.any { hop ->
hop.type == HierarchyOp.HIERARCHY_OP_TYPE_SET_LAUNCH_ROOT &&
- hop.container == freeformRoot.token.asBinder()
+ hop.container == desk.taskInfo.token.asBinder()
}
)
.isTrue()
@@ -201,20 +192,18 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
@Test
fun testMoveTaskToDesk() {
- organizer.createDesk(Display.DEFAULT_DISPLAY, FakeOnCreateCallback())
- val freeformRoot = createFreeformTask().apply { parentTaskId = -1 }
- organizer.onTaskAppeared(freeformRoot, SurfaceControl())
+ val desk = createDesk()
val desktopTask = createFreeformTask().apply { parentTaskId = -1 }
val wct = WindowContainerTransaction()
- organizer.moveTaskToDesk(wct, freeformRoot.taskId, desktopTask)
+ organizer.moveTaskToDesk(wct, desk.deskId, desktopTask)
assertThat(
wct.hierarchyOps.any { hop ->
hop.isReparent &&
hop.toTop &&
hop.container == desktopTask.token.asBinder() &&
- hop.newParent == freeformRoot.token.asBinder()
+ hop.newParent == desk.taskInfo.token.asBinder()
}
)
.isTrue()
@@ -240,17 +229,15 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
@Test
fun testGetDeskAtEnd() {
- organizer.createDesk(Display.DEFAULT_DISPLAY, FakeOnCreateCallback())
- val freeformRoot = createFreeformTask().apply { parentTaskId = -1 }
- organizer.onTaskAppeared(freeformRoot, SurfaceControl())
+ val desk = createDesk()
- val task = createFreeformTask().apply { parentTaskId = freeformRoot.taskId }
+ val task = createFreeformTask().apply { parentTaskId = desk.deskId }
val endDesk =
organizer.getDeskAtEnd(
TransitionInfo.Change(task.token, SurfaceControl()).apply { taskInfo = task }
)
- assertThat(endDesk).isEqualTo(freeformRoot.taskId)
+ assertThat(endDesk).isEqualTo(desk.deskId)
}
@Test
@@ -273,6 +260,47 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
assertThat(isActive).isTrue()
}
+ @Test
+ fun deactivateDesk_clearsLaunchRoot() {
+ val wct = WindowContainerTransaction()
+ val desk = createDesk()
+ organizer.activateDesk(wct, desk.deskId)
+
+ organizer.deactivateDesk(wct, desk.deskId)
+
+ assertThat(
+ wct.hierarchyOps.any { hop ->
+ hop.type == HIERARCHY_OP_TYPE_SET_LAUNCH_ROOT &&
+ hop.container == desk.taskInfo.token.asBinder() &&
+ hop.windowingModes == null &&
+ hop.activityTypes == null
+ }
+ )
+ .isTrue()
+ }
+
+ @Test
+ fun isDeskChange() {
+ val desk = createDesk()
+
+ assertThat(
+ organizer.isDeskChange(
+ TransitionInfo.Change(desk.taskInfo.token, desk.leash).apply {
+ taskInfo = desk.taskInfo
+ },
+ desk.deskId,
+ )
+ )
+ .isTrue()
+ }
+
+ private fun createDesk(): DeskRoot {
+ organizer.createDesk(Display.DEFAULT_DISPLAY, FakeOnCreateCallback())
+ val freeformRoot = createFreeformTask().apply { parentTaskId = -1 }
+ organizer.onTaskAppeared(freeformRoot, SurfaceControl())
+ return organizer.roots[freeformRoot.taskId]
+ }
+
private class FakeOnCreateCallback : DesksOrganizer.OnCreateCallback {
var deskId: Int? = null
val created: Boolean
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 8e0381e4f933..0c1952910d1a 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
@@ -19,6 +19,7 @@ package com.android.wm.shell.pip2.phone;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.when;
@@ -26,6 +27,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.ActivityManager;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Matrix;
@@ -44,15 +46,19 @@ import com.android.wm.shell.common.pip.PipDesktopState;
import com.android.wm.shell.pip.PipTransitionController;
import com.android.wm.shell.pip2.PipSurfaceTransactionHelper;
import com.android.wm.shell.pip2.animation.PipAlphaAnimator;
+import com.android.wm.shell.splitscreen.SplitScreenController;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
+import org.mockito.ArgumentMatchers;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
+import java.util.Optional;
+
/**
* Unit test against {@link PipScheduler}
*/
@@ -77,6 +83,8 @@ public class PipSchedulerTest {
@Mock private PipSurfaceTransactionHelper.SurfaceControlTransactionFactory mMockFactory;
@Mock private SurfaceControl.Transaction mMockTransaction;
@Mock private PipAlphaAnimator mMockAlphaAnimator;
+ @Mock private SplitScreenController mMockSplitScreenController;
+
@Captor private ArgumentCaptor<Runnable> mRunnableArgumentCaptor;
@Captor private ArgumentCaptor<WindowContainerTransaction> mWctArgumentCaptor;
@@ -93,7 +101,8 @@ public class PipSchedulerTest {
.thenReturn(mMockTransaction);
mPipScheduler = new PipScheduler(mMockContext, mMockPipBoundsState, mMockMainExecutor,
- mMockPipTransitionState, mMockPipDesktopState);
+ mMockPipTransitionState, Optional.of(mMockSplitScreenController),
+ mMockPipDesktopState);
mPipScheduler.setPipTransitionController(mMockPipTransitionController);
mPipScheduler.setSurfaceControlTransactionFactory(mMockFactory);
mPipScheduler.setPipAlphaAnimatorSupplier((context, leash, startTx, finishTx, direction) ->
@@ -119,12 +128,18 @@ public class PipSchedulerTest {
assertNotNull(mRunnableArgumentCaptor.getValue());
mRunnableArgumentCaptor.getValue().run();
- verify(mMockPipTransitionController, never()).startExpandTransition(any());
+ verify(mMockPipTransitionController, never()).startExpandTransition(any(), anyBoolean());
}
@Test
- public void scheduleExitPipViaExpand_exitTransitionCalled() {
+ public void scheduleExitPipViaExpand_noSplit_expandTransitionCalled() {
setMockPipTaskToken();
+ ActivityManager.RunningTaskInfo pipTaskInfo = getTaskInfoWithLastParentBeforePip(1);
+ when(mMockPipTransitionState.getPipTaskInfo()).thenReturn(pipTaskInfo);
+
+ // Make sure task with the id = 1 isn't in split-screen.
+ when(mMockSplitScreenController.isTaskInSplitScreen(
+ ArgumentMatchers.eq(1))).thenReturn(false);
mPipScheduler.scheduleExitPipViaExpand();
@@ -132,7 +147,29 @@ public class PipSchedulerTest {
assertNotNull(mRunnableArgumentCaptor.getValue());
mRunnableArgumentCaptor.getValue().run();
- verify(mMockPipTransitionController, times(1)).startExpandTransition(any());
+ verify(mMockPipTransitionController, times(1)).startExpandTransition(any(), anyBoolean());
+ }
+
+ @Test
+ public void scheduleExitPipViaExpand_lastParentInSplit_prepareSplitAndExpand() {
+ setMockPipTaskToken();
+ ActivityManager.RunningTaskInfo pipTaskInfo = getTaskInfoWithLastParentBeforePip(1);
+ when(mMockPipTransitionState.getPipTaskInfo()).thenReturn(pipTaskInfo);
+
+ // Make sure task with the id = 1 is in split-screen.
+ when(mMockSplitScreenController.isTaskInSplitScreen(
+ ArgumentMatchers.eq(1))).thenReturn(true);
+
+ mPipScheduler.scheduleExitPipViaExpand();
+
+ verify(mMockMainExecutor, times(1)).execute(mRunnableArgumentCaptor.capture());
+ assertNotNull(mRunnableArgumentCaptor.getValue());
+ mRunnableArgumentCaptor.getValue().run();
+
+ // We need to both prepare the split screen with the last parent and start expanding.
+ verify(mMockSplitScreenController,
+ times(1)).prepareEnterSplitScreen(any(), any(), anyInt());
+ verify(mMockPipTransitionController, times(1)).startExpandTransition(any(), anyBoolean());
}
@Test
@@ -259,4 +296,10 @@ public class PipSchedulerTest {
private void setMockPipTaskToken() {
when(mMockPipTransitionState.getPipTaskToken()).thenReturn(mMockPipTaskToken);
}
+
+ private ActivityManager.RunningTaskInfo getTaskInfoWithLastParentBeforePip(int lastParentId) {
+ final ActivityManager.RunningTaskInfo taskInfo = new ActivityManager.RunningTaskInfo();
+ taskInfo.lastParentTaskIdBeforePip = lastParentId;
+ return taskInfo;
+ }
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipTouchStateTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipTouchStateTest.java
new file mode 100644
index 000000000000..2e389b7dd151
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipTouchStateTest.java
@@ -0,0 +1,148 @@
+/*
+ * Copyright (C) 2017 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.pip2.phone;
+
+import static android.view.MotionEvent.ACTION_BUTTON_PRESS;
+import static android.view.MotionEvent.ACTION_DOWN;
+import static android.view.MotionEvent.ACTION_MOVE;
+import static android.view.MotionEvent.ACTION_UP;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import android.os.SystemClock;
+import android.testing.AndroidTestingRunner;
+import android.view.MotionEvent;
+import android.view.ViewConfiguration;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.wm.shell.ShellTestCase;
+import com.android.wm.shell.TestShellExecutor;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.concurrent.CountDownLatch;
+
+@RunWith(AndroidTestingRunner.class)
+@SmallTest
+public class PipTouchStateTest extends ShellTestCase {
+
+ private PipTouchState mTouchState;
+ private CountDownLatch mDoubleTapCallbackTriggeredLatch;
+ private CountDownLatch mHoverExitCallbackTriggeredLatch;
+ private TestShellExecutor mMainExecutor;
+
+ @Before
+ public void setUp() throws Exception {
+ mMainExecutor = new TestShellExecutor();
+ mDoubleTapCallbackTriggeredLatch = new CountDownLatch(1);
+ mHoverExitCallbackTriggeredLatch = new CountDownLatch(1);
+ mTouchState = new PipTouchState(ViewConfiguration.get(getContext()),
+ mDoubleTapCallbackTriggeredLatch::countDown,
+ mHoverExitCallbackTriggeredLatch::countDown,
+ mMainExecutor);
+ assertFalse(mTouchState.isDoubleTap());
+ assertFalse(mTouchState.isWaitingForDoubleTap());
+ }
+
+ @Test
+ public void testDoubleTapLongSingleTap_notDoubleTapAndNotWaiting() {
+ final long currentTime = SystemClock.uptimeMillis();
+
+ mTouchState.onTouchEvent(createMotionEvent(ACTION_DOWN, currentTime, 0, 0));
+ mTouchState.onTouchEvent(createMotionEvent(ACTION_UP,
+ currentTime + PipTouchState.DOUBLE_TAP_TIMEOUT + 10, 0, 0));
+ assertFalse(mTouchState.isDoubleTap());
+ assertFalse(mTouchState.isWaitingForDoubleTap());
+ assertTrue(mTouchState.getDoubleTapTimeoutCallbackDelay() == -1);
+ }
+
+ @Test
+ public void testDoubleTapTimeout_timeoutCallbackCalled() throws Exception {
+ final long currentTime = SystemClock.uptimeMillis();
+
+ mTouchState.onTouchEvent(createMotionEvent(ACTION_DOWN, currentTime, 0, 0));
+ mTouchState.onTouchEvent(createMotionEvent(ACTION_UP,
+ currentTime + PipTouchState.DOUBLE_TAP_TIMEOUT - 10, 0, 0));
+ assertFalse(mTouchState.isDoubleTap());
+ assertTrue(mTouchState.isWaitingForDoubleTap());
+
+ assertTrue(mTouchState.getDoubleTapTimeoutCallbackDelay() == 10);
+ mTouchState.scheduleDoubleTapTimeoutCallback();
+
+ mMainExecutor.flushAll();
+ assertTrue(mDoubleTapCallbackTriggeredLatch.getCount() == 0);
+ }
+
+ @Test
+ public void testDoubleTapDrag_doubleTapCanceled() {
+ final long currentTime = SystemClock.uptimeMillis();
+
+ mTouchState.onTouchEvent(createMotionEvent(ACTION_DOWN, currentTime, 0, 0));
+ mTouchState.onTouchEvent(createMotionEvent(ACTION_MOVE, currentTime + 10, 500, 500));
+ mTouchState.onTouchEvent(createMotionEvent(ACTION_UP, currentTime + 20, 500, 500));
+ assertTrue(mTouchState.isDragging());
+ assertFalse(mTouchState.isDoubleTap());
+ assertFalse(mTouchState.isWaitingForDoubleTap());
+ assertTrue(mTouchState.getDoubleTapTimeoutCallbackDelay() == -1);
+ }
+
+ @Test
+ public void testDoubleTap_doubleTapRegistered() {
+ final long currentTime = SystemClock.uptimeMillis();
+
+ mTouchState.onTouchEvent(createMotionEvent(ACTION_DOWN, currentTime, 0, 0));
+ mTouchState.onTouchEvent(createMotionEvent(ACTION_UP, currentTime + 10, 0, 0));
+ mTouchState.onTouchEvent(createMotionEvent(ACTION_DOWN,
+ currentTime + PipTouchState.DOUBLE_TAP_TIMEOUT - 20, 0, 0));
+ mTouchState.onTouchEvent(createMotionEvent(ACTION_UP,
+ currentTime + PipTouchState.DOUBLE_TAP_TIMEOUT - 10, 0, 0));
+ assertTrue(mTouchState.isDoubleTap());
+ assertFalse(mTouchState.isWaitingForDoubleTap());
+ assertTrue(mTouchState.getDoubleTapTimeoutCallbackDelay() == -1);
+ }
+
+ @Test
+ public void testHoverExitTimeout_timeoutCallbackCalled() throws Exception {
+ mTouchState.scheduleHoverExitTimeoutCallback();
+ mMainExecutor.flushAll();
+ assertTrue(mHoverExitCallbackTriggeredLatch.getCount() == 0);
+ }
+
+ @Test
+ public void testHoverExitTimeout_timeoutCallbackNotCalled() throws Exception {
+ mTouchState.scheduleHoverExitTimeoutCallback();
+ assertTrue(mHoverExitCallbackTriggeredLatch.getCount() == 1);
+ }
+
+ @Test
+ public void testHoverExitTimeout_timeoutCallbackNotCalled_ifButtonPress() throws Exception {
+ mTouchState.scheduleHoverExitTimeoutCallback();
+ mTouchState.onTouchEvent(createMotionEvent(ACTION_BUTTON_PRESS, SystemClock.uptimeMillis(),
+ 0, 0));
+ mMainExecutor.flushAll();
+ assertTrue(mHoverExitCallbackTriggeredLatch.getCount() == 1);
+ }
+
+ private MotionEvent createMotionEvent(int action, long eventTime, float x, float y) {
+ return MotionEvent.obtain(0, eventTime, action, x, y, 0);
+ }
+
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/transition/PipExpandHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/transition/PipExpandHandlerTest.java
new file mode 100644
index 000000000000..2a22842eda1a
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/transition/PipExpandHandlerTest.java
@@ -0,0 +1,188 @@
+/*
+ * 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.pip2.phone.transition;
+
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.view.WindowManager.TRANSIT_CHANGE;
+
+import static com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP;
+import static com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP_TO_SPLIT;
+
+import static org.junit.Assert.assertNull;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.mockito.kotlin.VerificationKt.times;
+import static org.mockito.kotlin.VerificationKt.verify;
+
+import android.annotation.Nullable;
+import android.app.ActivityManager;
+import android.app.PictureInPictureParams;
+import android.app.WindowConfiguration;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Rect;
+import android.os.IBinder;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+import android.view.Surface;
+import android.view.SurfaceControl;
+import android.view.WindowManager;
+import android.window.TransitionInfo;
+import android.window.TransitionRequestInfo;
+import android.window.WindowContainerToken;
+import android.window.WindowContainerTransaction;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.wm.shell.common.pip.PipBoundsAlgorithm;
+import com.android.wm.shell.common.pip.PipBoundsState;
+import com.android.wm.shell.common.pip.PipDisplayLayoutState;
+import com.android.wm.shell.pip2.animation.PipExpandAnimator;
+import com.android.wm.shell.pip2.phone.PipTransitionState;
+import com.android.wm.shell.splitscreen.SplitScreenController;
+import com.android.wm.shell.transition.TransitionInfoBuilder;
+import com.android.wm.shell.util.StubTransaction;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.Optional;
+
+/**
+ * Unit test against {@link PipExpandHandler}
+ */
+
+@SmallTest
+@TestableLooper.RunWithLooper
+@RunWith(AndroidTestingRunner.class)
+public class PipExpandHandlerTest {
+ @Mock private Context mMockContext;
+ @Mock private PipBoundsState mMockPipBoundsState;
+ @Mock private PipBoundsAlgorithm mMockPipBoundsAlgorithm;
+ @Mock private PipTransitionState mMockPipTransitionState;
+ @Mock private PipDisplayLayoutState mMockPipDisplayLayoutState;
+ @Mock private SplitScreenController mMockSplitScreenController;
+
+ @Mock private IBinder mMockTransitionToken;
+ @Mock private TransitionRequestInfo mMockRequestInfo;
+ @Mock private StubTransaction mStartT;
+ @Mock private StubTransaction mFinishT;
+ @Mock private SurfaceControl mPipLeash;
+
+ @Mock private PipExpandAnimator mMockPipExpandAnimator;
+
+ @Surface.Rotation
+ private static final int DISPLAY_ROTATION = Surface.ROTATION_0;
+
+ private static final float SNAP_FRACTION = 1.5f;
+ private static final Rect PIP_BOUNDS = new Rect(0, 0, 100, 100);
+ private static final Rect DISPLAY_BOUNDS = new Rect(0, 0, 1000, 1000);
+ private static final Rect RIGHT_HALF_DISPLAY_BOUNDS = new Rect(500, 0, 1000, 1000);
+
+ private PipExpandHandler mPipExpandHandler;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ when(mMockPipBoundsState.getBounds()).thenReturn(PIP_BOUNDS);
+ when(mMockPipBoundsAlgorithm.getSnapFraction(eq(PIP_BOUNDS))).thenReturn(SNAP_FRACTION);
+ when(mMockPipDisplayLayoutState.getRotation()).thenReturn(DISPLAY_ROTATION);
+
+ mPipExpandHandler = new PipExpandHandler(mMockContext, mMockPipBoundsState,
+ mMockPipBoundsAlgorithm, mMockPipTransitionState, mMockPipDisplayLayoutState,
+ Optional.of(mMockSplitScreenController));
+ mPipExpandHandler.setPipExpandAnimatorSupplier((context, leash, startTransaction,
+ finishTransaction, baseBounds, startBounds, endBounds,
+ sourceRectHint, rotation) -> mMockPipExpandAnimator);
+ }
+
+ @Test
+ public void handleRequest_returnNull() {
+ // All expand from PiP transitions are started in Shell, so handleRequest shouldn't be
+ // returning any non-null WCT
+ WindowContainerTransaction wct = mPipExpandHandler.handleRequest(
+ mMockTransitionToken, mMockRequestInfo);
+ assertNull(wct);
+ }
+
+ @Test
+ public void startAnimation_transitExit_startExpandAnimator() {
+ final ActivityManager.RunningTaskInfo pipTaskInfo = createPipTaskInfo(
+ 1, WINDOWING_MODE_FULLSCREEN, new PictureInPictureParams.Builder().build());
+
+ final TransitionInfo info = getExpandFromPipTransitionInfo(
+ TRANSIT_EXIT_PIP, pipTaskInfo, null /* lastParent */, false /* toSplit */);
+ final WindowContainerToken pipToken = pipTaskInfo.getToken();
+ when(mMockPipTransitionState.getPipTaskToken()).thenReturn(pipToken);
+
+ mPipExpandHandler.startAnimation(mMockTransitionToken, info, mStartT, mFinishT,
+ (wct) -> {});
+
+ verify(mMockPipExpandAnimator, times(1)).start();
+ verify(mMockPipBoundsState, times(1)).saveReentryState(SNAP_FRACTION);
+ }
+
+ @Test
+ public void startAnimation_transitExitToSplit_startExpandAnimator() {
+ // The task info of the task that was pinned while we were in PiP.
+ final WindowContainerToken pipToken = createPipTaskInfo(1, WINDOWING_MODE_FULLSCREEN,
+ new PictureInPictureParams.Builder().build()).getToken();
+ when(mMockPipTransitionState.getPipTaskToken()).thenReturn(pipToken);
+
+ // Change representing the ActivityRecord we are animating in the multi-activity PiP case;
+ // make sure change's taskInfo=null as this is an activity, but let lastParent be PiP token.
+ final TransitionInfo info = getExpandFromPipTransitionInfo(
+ TRANSIT_EXIT_PIP_TO_SPLIT, null /* taskInfo */, pipToken, true /* toSplit */);
+
+ mPipExpandHandler.startAnimation(mMockTransitionToken, info, mStartT, mFinishT,
+ (wct) -> {});
+
+ verify(mMockSplitScreenController, times(1)).finishEnterSplitScreen(eq(mFinishT));
+ verify(mMockPipExpandAnimator, times(1)).start();
+ verify(mMockPipBoundsState, times(1)).saveReentryState(SNAP_FRACTION);
+ }
+
+ private TransitionInfo getExpandFromPipTransitionInfo(@WindowManager.TransitionType int type,
+ @Nullable ActivityManager.RunningTaskInfo pipTaskInfo,
+ @Nullable WindowContainerToken lastParent, boolean toSplit) {
+ final TransitionInfo info = new TransitionInfoBuilder(type)
+ .addChange(TRANSIT_CHANGE, pipTaskInfo).build();
+ final TransitionInfo.Change pipChange = info.getChanges().getFirst();
+ pipChange.setRotation(DISPLAY_ROTATION,
+ WindowConfiguration.ROTATION_UNDEFINED);
+ pipChange.setStartAbsBounds(PIP_BOUNDS);
+ pipChange.setEndAbsBounds(toSplit ? RIGHT_HALF_DISPLAY_BOUNDS : DISPLAY_BOUNDS);
+ pipChange.setLeash(mPipLeash);
+ pipChange.setLastParent(lastParent);
+ return info;
+ }
+
+ private static ActivityManager.RunningTaskInfo createPipTaskInfo(int taskId,
+ int windowingMode, PictureInPictureParams params) {
+ ActivityManager.RunningTaskInfo taskInfo = new ActivityManager.RunningTaskInfo();
+ taskInfo.taskId = taskId;
+ taskInfo.configuration.windowConfiguration.setWindowingMode(windowingMode);
+ taskInfo.token = mock(WindowContainerToken.class);
+ taskInfo.baseIntent = mock(Intent.class);
+ taskInfo.pictureInPictureParams = params;
+ return taskInfo;
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatusTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatusTest.kt
index 391d46287498..4082ffd4ac0a 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatusTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatusTest.kt
@@ -22,8 +22,6 @@ import android.platform.test.annotations.DisableFlags
import android.platform.test.annotations.EnableFlags
import android.platform.test.annotations.Presubmit
import android.platform.test.flag.junit.SetFlagsRule
-import android.provider.Settings
-import android.provider.Settings.Global.DEVELOPMENT_OVERRIDE_DESKTOP_MODE_FEATURES
import android.window.DesktopModeFlags
import androidx.test.filters.SmallTest
import com.android.internal.R
@@ -63,14 +61,12 @@ class DesktopModeStatusTest : ShellTestCase() {
doReturn(context.contentResolver).whenever(mockContext).contentResolver
resetDesktopModeFlagsCache()
resetEnforceDeviceRestriction()
- resetFlagOverride()
}
@After
fun tearDown() {
resetDesktopModeFlagsCache()
resetEnforceDeviceRestriction()
- resetFlagOverride()
}
@DisableFlags(
@@ -157,23 +153,40 @@ class DesktopModeStatusTest : ShellTestCase() {
}
@Test
- fun isInternalDisplayEligibleToHostDesktops_configDEModeOn_returnsTrue() {
+ fun isDeviceEligibleForDesktopMode_configDEModeOnAndIntDispHostsDesktop_returnsTrue() {
+ doReturn(true).whenever(mockResources).getBoolean(eq(R.bool.config_isDesktopModeSupported))
doReturn(true).whenever(mockResources).getBoolean(eq(R.bool.config_canInternalDisplayHostDesktops))
- assertThat(DesktopModeStatus.isInternalDisplayEligibleToHostDesktops(mockContext)).isTrue()
+ assertThat(DesktopModeStatus.isDeviceEligibleForDesktopMode(mockContext)).isTrue()
+ }
+
+ @Test
+ fun isDeviceEligibleForDesktopMode_configDEModeOffAndIntDispHostsDesktop_returnsFalse() {
+ doReturn(false).whenever(mockResources).getBoolean(eq(R.bool.config_isDesktopModeSupported))
+ doReturn(true).whenever(mockResources).getBoolean(eq(R.bool.config_canInternalDisplayHostDesktops))
+
+ assertThat(DesktopModeStatus.isDeviceEligibleForDesktopMode(mockContext)).isFalse()
+ }
+
+ @Test
+ fun isDeviceEligibleForDesktopMode_configDEModeOnAndIntDispHostsDesktopOff_returnsFalse() {
+ doReturn(true).whenever(mockResources).getBoolean(eq(R.bool.config_isDesktopModeSupported))
+ doReturn(false).whenever(mockResources).getBoolean(eq(R.bool.config_canInternalDisplayHostDesktops))
+
+ assertThat(DesktopModeStatus.isDeviceEligibleForDesktopMode(mockContext)).isFalse()
}
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
@DisableFlags(Flags.FLAG_ENABLE_DESKTOP_MODE_THROUGH_DEV_OPTION)
@Test
fun isInternalDisplayEligibleToHostDesktops_supportFlagOff_returnsFalse() {
- assertThat(DesktopModeStatus.isInternalDisplayEligibleToHostDesktops(mockContext)).isFalse()
+ assertThat(DesktopModeStatus.isDeviceEligibleForDesktopMode(mockContext)).isFalse()
}
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_MODE_THROUGH_DEV_OPTION)
@Test
fun isInternalDisplayEligibleToHostDesktops_supportFlagOn_returnsFalse() {
- assertThat(DesktopModeStatus.isInternalDisplayEligibleToHostDesktops(mockContext)).isFalse()
+ assertThat(DesktopModeStatus.isDeviceEligibleForDesktopMode(mockContext)).isFalse()
}
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_MODE_THROUGH_DEV_OPTION)
@@ -183,7 +196,7 @@ class DesktopModeStatusTest : ShellTestCase() {
eq(R.bool.config_isDesktopModeDevOptionSupported)
)
- assertThat(DesktopModeStatus.isInternalDisplayEligibleToHostDesktops(mockContext)).isTrue()
+ assertThat(DesktopModeStatus.isDeviceEligibleForDesktopMode(mockContext)).isTrue()
}
@DisableFlags(Flags.FLAG_SHOW_DESKTOP_EXPERIENCE_DEV_OPTION)
@@ -229,18 +242,11 @@ class DesktopModeStatusTest : ShellTestCase() {
cachedToggleOverride.set(null, null)
}
- private fun resetFlagOverride() {
- Settings.Global.putString(
- context.contentResolver,
- DEVELOPMENT_OVERRIDE_DESKTOP_MODE_FEATURES, null
- )
- }
-
private fun setFlagOverride(override: DesktopModeFlags.ToggleOverride) {
- Settings.Global.putInt(
- context.contentResolver,
- DEVELOPMENT_OVERRIDE_DESKTOP_MODE_FEATURES, override.setting
- )
+ val cachedToggleOverride =
+ DesktopModeFlags::class.java.getDeclaredField("sCachedToggleOverride")
+ cachedToggleOverride.isAccessible = true
+ cachedToggleOverride.set(null, override)
}
private fun setDeviceEligibleForDesktopMode(eligible: Boolean) {
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 da41a23f066c..d8d45c02b364 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
@@ -484,7 +484,6 @@ class DesktopModeWindowDecorViewModelTests : DesktopModeWindowDecorViewModelTest
eq(SnapPosition.LEFT),
eq(ResizeTrigger.SNAP_LEFT_MENU),
eq(InputMethod.UNKNOWN_INPUT_METHOD),
- eq(decor)
)
}
@@ -520,7 +519,6 @@ class DesktopModeWindowDecorViewModelTests : DesktopModeWindowDecorViewModelTest
eq(SnapPosition.LEFT),
eq(ResizeTrigger.SNAP_LEFT_MENU),
eq(InputMethod.UNKNOWN_INPUT_METHOD),
- eq(decor),
)
}
@@ -542,7 +540,6 @@ class DesktopModeWindowDecorViewModelTests : DesktopModeWindowDecorViewModelTest
eq(decor.mTaskInfo), any(), eq(currentBounds), eq(SnapPosition.LEFT),
eq(ResizeTrigger.MAXIMIZE_BUTTON),
eq(InputMethod.UNKNOWN_INPUT_METHOD),
- eq(decor),
)
}
@@ -562,7 +559,6 @@ class DesktopModeWindowDecorViewModelTests : DesktopModeWindowDecorViewModelTest
eq(SnapPosition.RIGHT),
eq(ResizeTrigger.SNAP_RIGHT_MENU),
eq(InputMethod.UNKNOWN_INPUT_METHOD),
- eq(decor),
)
}
@@ -598,7 +594,6 @@ class DesktopModeWindowDecorViewModelTests : DesktopModeWindowDecorViewModelTest
eq(SnapPosition.RIGHT),
eq(ResizeTrigger.SNAP_RIGHT_MENU),
eq(InputMethod.UNKNOWN_INPUT_METHOD),
- eq(decor),
)
}
@@ -620,7 +615,6 @@ class DesktopModeWindowDecorViewModelTests : DesktopModeWindowDecorViewModelTest
eq(decor.mTaskInfo), any(), eq(currentBounds), eq(SnapPosition.RIGHT),
eq(ResizeTrigger.MAXIMIZE_BUTTON),
eq(InputMethod.UNKNOWN_INPUT_METHOD),
- eq(decor),
)
}
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 e40034b09f39..8cccdb2b6120 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
@@ -81,6 +81,7 @@ import com.android.wm.shell.windowdecor.DesktopModeWindowDecorViewModel.DesktopM
import com.android.wm.shell.windowdecor.common.WindowDecorTaskResourceLoader
import com.android.wm.shell.windowdecor.common.viewhost.WindowDecorViewHost
import com.android.wm.shell.windowdecor.common.viewhost.WindowDecorViewHostSupplier
+import com.android.wm.shell.windowdecor.tiling.DesktopTilingDecorViewModel
import com.android.wm.shell.windowdecor.viewholder.AppHeaderViewHolder
import org.junit.After
import org.mockito.Mockito
@@ -147,6 +148,7 @@ open class DesktopModeWindowDecorViewModelTestsBase : ShellTestCase() {
protected val mockCaptionHandleRepository = mock<WindowDecorCaptionHandleRepository>()
protected val mockDesktopRepository: DesktopRepository = mock<DesktopRepository>()
protected val mockRecentsTransitionHandler = mock<RecentsTransitionHandler>()
+ protected val mockTilingWindowDecoration = mock<DesktopTilingDecorViewModel>()
protected val motionEvent = mock<MotionEvent>()
private val displayLayout = mock<DisplayLayout>()
private val display = mock<Display>()
@@ -226,6 +228,7 @@ open class DesktopModeWindowDecorViewModelTestsBase : ShellTestCase() {
mock<WindowDecorTaskResourceLoader>(),
mockRecentsTransitionHandler,
desktopModeCompatPolicy,
+ mockTilingWindowDecoration,
)
desktopModeWindowDecorViewModel.setSplitScreenController(mockSplitScreenController)
whenever(mockDisplayController.getDisplayLayout(any())).thenReturn(mockDisplayLayout)
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 71c821dd9b71..c4f70ac2297f 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
@@ -120,6 +120,7 @@ import com.android.wm.shell.windowdecor.viewholder.AppHeaderViewHolder;
import kotlin.Unit;
import kotlin.jvm.functions.Function0;
import kotlin.jvm.functions.Function1;
+import kotlin.jvm.functions.Function2;
import kotlinx.coroutines.CoroutineScope;
import kotlinx.coroutines.MainCoroutineDispatcher;
@@ -998,8 +999,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {
createMaximizeMenu(decoration);
- verify(menu).show(anyBoolean(), anyInt(), anyBoolean(), anyBoolean(), any(), any(), any(),
- any(), mOnMaxMenuHoverChangeListener.capture(), any());
+ verify(menu).show(anyBoolean(), anyBoolean(), anyBoolean(), any(), any(), any(), any(),
+ mOnMaxMenuHoverChangeListener.capture(), any());
assertTrue(decoration.isMaximizeMenuActive());
}
@@ -1011,8 +1012,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {
new FakeMaximizeMenuFactory(menu));
decoration.setAppHeaderMaximizeButtonHovered(false);
createMaximizeMenu(decoration);
- verify(menu).show(anyBoolean(), anyInt(), anyBoolean(), anyBoolean(), any(), any(), any(),
- any(), mOnMaxMenuHoverChangeListener.capture(), any());
+ verify(menu).show(anyBoolean(), anyBoolean(), anyBoolean(), any(), any(), any(), any(),
+ mOnMaxMenuHoverChangeListener.capture(), any());
mOnMaxMenuHoverChangeListener.getValue().invoke(false);
@@ -1050,8 +1051,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {
final DesktopModeWindowDecoration decoration = createWindowDecoration(taskInfo,
new FakeMaximizeMenuFactory(menu));
createMaximizeMenu(decoration);
- verify(menu).show(anyBoolean(), anyInt(), anyBoolean(), anyBoolean(), any(), any(), any(),
- any(), mOnMaxMenuHoverChangeListener.capture(), any());
+ verify(menu).show(anyBoolean(), anyBoolean(), anyBoolean(), any(), any(), any(), any(),
+ mOnMaxMenuHoverChangeListener.capture(), any());
mOnMaxMenuHoverChangeListener.getValue().invoke(true);
@@ -1065,8 +1066,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {
final DesktopModeWindowDecoration decoration = createWindowDecoration(taskInfo,
new FakeMaximizeMenuFactory(menu));
createMaximizeMenu(decoration);
- verify(menu).show(anyBoolean(), anyInt(), anyBoolean(), anyBoolean(), any(), any(), any(),
- any(), mOnMaxMenuHoverChangeListener.capture(), any());
+ verify(menu).show(anyBoolean(), anyBoolean(), anyBoolean(), any(), any(), any(), any(),
+ mOnMaxMenuHoverChangeListener.capture(), any());
decoration.setAppHeaderMaximizeButtonHovered(true);
@@ -1086,7 +1087,6 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {
verify(menu).show(
anyBoolean(),
- anyInt(),
/* showImmersiveOption= */ eq(true),
anyBoolean(),
any(),
@@ -1111,7 +1111,6 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {
verify(menu).show(
anyBoolean(),
- anyInt(),
/* showImmersiveOption= */ eq(false),
anyBoolean(),
any(),
@@ -1136,7 +1135,6 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {
verify(menu).show(
anyBoolean(),
- anyInt(),
anyBoolean(),
/* showSnapOptions= */ eq(true),
any(),
@@ -1161,7 +1159,6 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {
verify(menu).show(
anyBoolean(),
- anyInt(),
anyBoolean(),
/* showSnapOptions= */ eq(false),
any(),
@@ -1766,7 +1763,9 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {
@NonNull RootTaskDisplayAreaOrganizer rootTdaOrganizer,
@NonNull DisplayController displayController,
@NonNull ActivityManager.RunningTaskInfo taskInfo,
- @NonNull Context decorWindowContext, @NonNull PointF menuPosition,
+ @NonNull Context decorWindowContext,
+ @NonNull Function2<? super Integer,? super Integer,? extends PointF>
+ positionSupplier,
@NonNull Supplier<SurfaceControl.Transaction> transactionSupplier) {
return mMaximizeMenu;
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java
index aa1f82e3e4d8..af0162334440 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java
@@ -40,6 +40,7 @@ import static org.mockito.ArgumentMatchers.anyFloat;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.argThat;
+import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.inOrder;
@@ -386,6 +387,49 @@ public class WindowDecorationTests extends ShellTestCase {
verify(mMockWindowDecorViewHost).updateView(same(mMockView), any(), any(), any(), any());
}
+
+ @Test
+ public void testReinflateViewsOnFontScaleChange() {
+ final Display defaultDisplay = mock(Display.class);
+ doReturn(defaultDisplay).when(mMockDisplayController)
+ .getDisplay(Display.DEFAULT_DISPLAY);
+
+ final ActivityManager.RunningTaskInfo taskInfo = new TestRunningTaskInfoBuilder()
+ .setVisible(true)
+ .setDisplayId(Display.DEFAULT_DISPLAY)
+ .build();
+ final TestWindowDecoration windowDecor = spy(createWindowDecoration(taskInfo));
+ windowDecor.relayout(taskInfo, true /* hasGlobalFocus */, Region.obtain());
+ clearInvocations(windowDecor);
+ final ActivityManager.RunningTaskInfo taskInfo2 = new TestRunningTaskInfoBuilder()
+ .setVisible(true)
+ .setDisplayId(Display.DEFAULT_DISPLAY)
+ .build();
+ taskInfo2.configuration.fontScale = taskInfo.configuration.fontScale + 1;
+ windowDecor.relayout(taskInfo2, true /* hasGlobalFocus */, Region.obtain());
+ // WindowDecoration#releaseViews should be called since the font scale has changed.
+ verify(windowDecor).releaseViews(any());
+ }
+
+ @Test
+ public void testViewNotReinflatedWhenFontScaleNotChanged() {
+ final Display defaultDisplay = mock(Display.class);
+ doReturn(defaultDisplay).when(mMockDisplayController)
+ .getDisplay(Display.DEFAULT_DISPLAY);
+
+ final ActivityManager.RunningTaskInfo taskInfo = new TestRunningTaskInfoBuilder()
+ .setVisible(true)
+ .setDisplayId(Display.DEFAULT_DISPLAY)
+ .build();
+ final TestWindowDecoration windowDecor = spy(createWindowDecoration(taskInfo));
+ windowDecor.relayout(taskInfo, true /* hasGlobalFocus */, Region.obtain());
+ clearInvocations(windowDecor);
+ windowDecor.relayout(taskInfo, true /* hasGlobalFocus */, Region.obtain());
+ // WindowDecoration#releaseViews should be called since task info (and therefore the
+ // fontScale) has not changed.
+ verify(windowDecor, never()).releaseViews(any());
+ }
+
@Test
public void testAddViewHostViewContainer() {
final Display defaultDisplay = mock(Display.class);
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/common/viewhost/ReusableWindowDecorViewHostTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/common/viewhost/ReusableWindowDecorViewHostTest.kt
index d99a4825e580..c86730ed1dc7 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/common/viewhost/ReusableWindowDecorViewHostTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/common/viewhost/ReusableWindowDecorViewHostTest.kt
@@ -20,6 +20,7 @@ import android.testing.TestableLooper
import android.view.SurfaceControl
import android.view.View
import android.view.WindowManager
+import android.widget.FrameLayout
import androidx.test.filters.SmallTest
import com.android.wm.shell.ShellTestCase
import com.google.common.truth.Truth.assertThat
@@ -30,6 +31,9 @@ import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.mock
+import org.mockito.Mockito.times
+import org.mockito.kotlin.clearInvocations
+import org.mockito.kotlin.never
import org.mockito.kotlin.spy
import org.mockito.kotlin.verify
@@ -47,24 +51,46 @@ class ReusableWindowDecorViewHostTest : ShellTestCase() {
fun update_differentView_replacesView() = runTest {
val view = View(context)
val lp = WindowManager.LayoutParams()
- val reusableVH = createReusableViewHost()
- reusableVH.updateView(view, lp, context.resources.configuration, null)
+ val rootView = FrameLayout(context)
+ val reusableVH = createReusableViewHost(rootView)
+ reusableVH.updateView(view, lp, context.resources.configuration)
- assertThat(reusableVH.rootView.childCount).isEqualTo(1)
- assertThat(reusableVH.rootView.getChildAt(0)).isEqualTo(view)
+ assertThat(rootView.childCount).isEqualTo(1)
+ assertThat(rootView.getChildAt(0)).isEqualTo(view)
val newView = View(context)
val newLp = WindowManager.LayoutParams()
- reusableVH.updateView(newView, newLp, context.resources.configuration, null)
+ reusableVH.updateView(newView, newLp, context.resources.configuration)
- assertThat(reusableVH.rootView.childCount).isEqualTo(1)
- assertThat(reusableVH.rootView.getChildAt(0)).isEqualTo(newView)
+ assertThat(rootView.childCount).isEqualTo(1)
+ assertThat(rootView.getChildAt(0)).isEqualTo(newView)
+ }
+
+ @Test
+ fun update_sameView_doesNotReplaceView() = runTest {
+ val view = View(context)
+ val lp = WindowManager.LayoutParams()
+ val spyRootView = spy(FrameLayout(context))
+ val reusableVH = createReusableViewHost(spyRootView)
+ reusableVH.updateView(view, lp, context.resources.configuration)
+
+ verify(spyRootView, times(1)).removeAllViews()
+ assertThat(spyRootView.childCount).isEqualTo(1)
+ assertThat(spyRootView.getChildAt(0)).isEqualTo(view)
+
+ reusableVH.updateView(view, lp, context.resources.configuration)
+
+ clearInvocations(spyRootView)
+ verify(spyRootView, never()).removeAllViews()
+ assertThat(spyRootView.childCount).isEqualTo(1)
+ assertThat(spyRootView.getChildAt(0)).isEqualTo(view)
}
@OptIn(ExperimentalCoroutinesApi::class)
@Test
fun updateView_clearsPendingAsyncJob() = runTest {
- val reusableVH = createReusableViewHost()
+ val rootView = FrameLayout(context)
+ val reusableVH = createReusableViewHost(rootView)
val asyncView = View(context)
val syncView = View(context)
val asyncAttrs = WindowManager.LayoutParams(100, 100)
@@ -83,7 +109,6 @@ class ReusableWindowDecorViewHostTest : ShellTestCase() {
view = syncView,
attrs = syncAttrs,
configuration = context.resources.configuration,
- onDrawTransaction = null,
)
// Would run coroutine if it hadn't been cancelled.
@@ -91,7 +116,7 @@ class ReusableWindowDecorViewHostTest : ShellTestCase() {
assertThat(reusableVH.viewHostAdapter.isInitialized()).isTrue()
// View host view/attrs should match the ones from the sync call.
- assertThat(reusableVH.rootView.getChildAt(0)).isEqualTo(syncView)
+ assertThat(rootView.getChildAt(0)).isEqualTo(syncView)
assertThat(reusableVH.view()!!.layoutParams.width).isEqualTo(syncAttrs.width)
}
@@ -118,7 +143,8 @@ class ReusableWindowDecorViewHostTest : ShellTestCase() {
@OptIn(ExperimentalCoroutinesApi::class)
@Test
fun updateViewAsync_clearsPendingAsyncJob() = runTest {
- val reusableVH = createReusableViewHost()
+ val rootView = FrameLayout(context)
+ val reusableVH = createReusableViewHost(rootView)
val view = View(context)
reusableVH.updateViewAsync(
@@ -136,7 +162,7 @@ class ReusableWindowDecorViewHostTest : ShellTestCase() {
advanceUntilIdle()
assertThat(reusableVH.viewHostAdapter.isInitialized()).isTrue()
- assertThat(reusableVH.rootView.getChildAt(0)).isEqualTo(otherView)
+ assertThat(rootView.getChildAt(0)).isEqualTo(otherView)
}
@Test
@@ -148,7 +174,6 @@ class ReusableWindowDecorViewHostTest : ShellTestCase() {
view = view,
attrs = WindowManager.LayoutParams(100, 100),
configuration = context.resources.configuration,
- onDrawTransaction = null,
)
val t = mock(SurfaceControl.Transaction::class.java)
@@ -159,19 +184,23 @@ class ReusableWindowDecorViewHostTest : ShellTestCase() {
@Test
fun warmUp_addsRootView() = runTest {
- val reusableVH = createReusableViewHost().apply { warmUp() }
+ val rootView = FrameLayout(context)
+ val reusableVH = createReusableViewHost(rootView).apply { warmUp() }
assertThat(reusableVH.viewHostAdapter.isInitialized()).isTrue()
- assertThat(reusableVH.view()).isEqualTo(reusableVH.rootView)
+ assertThat(reusableVH.view()).isEqualTo(rootView)
}
- private fun CoroutineScope.createReusableViewHost() =
+ private fun CoroutineScope.createReusableViewHost(
+ rootView: FrameLayout = FrameLayout(context)
+ ) =
ReusableWindowDecorViewHost(
context = context,
mainScope = this,
display = context.display,
id = 1,
viewHostAdapter = spy(SurfaceControlViewHostAdapter(context, context.display)),
+ rootView
)
private fun ReusableWindowDecorViewHost.view(): View? = viewHostAdapter.viewHost?.view
diff --git a/libs/androidfw/ApkAssets.cpp b/libs/androidfw/ApkAssets.cpp
index dbb891455ddd..e693fcfd3918 100644
--- a/libs/androidfw/ApkAssets.cpp
+++ b/libs/androidfw/ApkAssets.cpp
@@ -162,10 +162,13 @@ const std::string& ApkAssets::GetDebugName() const {
return assets_provider_->GetDebugName();
}
-bool ApkAssets::IsUpToDate() const {
+UpToDate ApkAssets::IsUpToDate() const {
// Loaders are invalidated by the app, not the system, so assume they are up to date.
- return IsLoader() || ((!loaded_idmap_ || loaded_idmap_->IsUpToDate())
- && assets_provider_->IsUpToDate());
+ 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(); });
}
} // namespace android
diff --git a/libs/androidfw/AssetManager2.cpp b/libs/androidfw/AssetManager2.cpp
index 0fa31c7a832e..f5e10d94452f 100644
--- a/libs/androidfw/AssetManager2.cpp
+++ b/libs/androidfw/AssetManager2.cpp
@@ -1467,8 +1467,6 @@ base::expected<uint32_t, NullOrIOError> AssetManager2::GetResourceId(
}
const StringPiece16 kAttr16 = u"attr";
- const static std::u16string kAttrPrivate16 = u"^attr-private";
-
for (const PackageGroup& package_group : package_groups_) {
for (const ConfiguredPackage& package_impl : package_group.packages_) {
const LoadedPackage* package = package_impl.loaded_package_;
@@ -1480,12 +1478,13 @@ base::expected<uint32_t, NullOrIOError> AssetManager2::GetResourceId(
base::expected<uint32_t, NullOrIOError> resid = package->FindEntryByName(type16, entry16);
if (UNLIKELY(IsIOError(resid))) {
return base::unexpected(resid.error());
- }
+ }
if (!resid.has_value() && kAttr16 == type16) {
// Private attributes in libraries (such as the framework) are sometimes encoded
// under the type '^attr-private' in order to leave the ID space of public 'attr'
// free for future additions. Check '^attr-private' for the same name.
+ const static std::u16string kAttrPrivate16 = u"^attr-private";
resid = package->FindEntryByName(kAttrPrivate16, entry16);
}
diff --git a/libs/androidfw/AssetsProvider.cpp b/libs/androidfw/AssetsProvider.cpp
index 2d3c06506a1f..808509120462 100644
--- a/libs/androidfw/AssetsProvider.cpp
+++ b/libs/androidfw/AssetsProvider.cpp
@@ -24,9 +24,27 @@
#include <ziparchive/zip_archive.h>
namespace android {
-namespace {
-constexpr const char* kEmptyDebugString = "<empty>";
-} // namespace
+
+static constexpr std::string_view kEmptyDebugString = "<empty>";
+
+std::unique_ptr<AssetsProvider> AssetsProvider::CreateWithOverride(
+ std::unique_ptr<AssetsProvider> provider, std::unique_ptr<AssetsProvider> override) {
+ if (provider == nullptr) {
+ return {};
+ }
+ if (override == nullptr) {
+ return provider;
+ }
+ return MultiAssetsProvider::Create(std::move(override), std::move(provider));
+}
+
+std::unique_ptr<AssetsProvider> AssetsProvider::CreateFromNullable(
+ std::unique_ptr<AssetsProvider> nullable) {
+ if (nullable) {
+ return nullable;
+ }
+ return EmptyAssetsProvider::Create();
+}
std::unique_ptr<Asset> AssetsProvider::Open(const std::string& path, Asset::AccessMode mode,
bool* file_exists) const {
@@ -86,11 +104,9 @@ void ZipAssetsProvider::ZipCloser::operator()(ZipArchive* a) const {
}
ZipAssetsProvider::ZipAssetsProvider(ZipArchiveHandle handle, PathOrDebugName&& path,
- package_property_t flags, time_t last_mod_time)
- : zip_handle_(handle),
- name_(std::move(path)),
- flags_(flags),
- last_mod_time_(last_mod_time) {}
+ package_property_t flags, ModDate 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,
@@ -104,10 +120,10 @@ std::unique_ptr<ZipAssetsProvider> ZipAssetsProvider::Create(std::string path,
return {};
}
- struct stat sb{.st_mtime = -1};
+ ModDate mod_date = kInvalidModDate;
// Skip all up-to-date checks if the file won't ever change.
- if (!isReadonlyFilesystem(path.c_str())) {
- if ((released_fd < 0 ? stat(path.c_str(), &sb) : fstat(released_fd, &sb)) < 0) {
+ if (isKnownWritablePath(path.c_str()) || !isReadonlyFilesystem(GetFileDescriptor(handle))) {
+ if (mod_date = getFileModDate(GetFileDescriptor(handle)); mod_date == kInvalidModDate) {
// 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.
@@ -116,7 +132,7 @@ std::unique_ptr<ZipAssetsProvider> ZipAssetsProvider::Create(std::string path,
}
return std::unique_ptr<ZipAssetsProvider>(
- new ZipAssetsProvider(handle, PathOrDebugName::Path(std::move(path)), flags, sb.st_mtime));
+ new ZipAssetsProvider(handle, PathOrDebugName::Path(std::move(path)), flags, mod_date));
}
std::unique_ptr<ZipAssetsProvider> ZipAssetsProvider::Create(base::unique_fd fd,
@@ -137,10 +153,10 @@ std::unique_ptr<ZipAssetsProvider> ZipAssetsProvider::Create(base::unique_fd fd,
return {};
}
- struct stat sb{.st_mtime = -1};
+ ModDate mod_date = kInvalidModDate;
// Skip all up-to-date checks if the file won't ever change.
if (!isReadonlyFilesystem(released_fd)) {
- if (fstat(released_fd, &sb) < 0) {
+ if (mod_date = getFileModDate(released_fd); mod_date == kInvalidModDate) {
// 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.
@@ -150,7 +166,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, sb.st_mtime));
+ handle, PathOrDebugName::DebugName(std::move(friendly_name)), flags, mod_date));
}
std::unique_ptr<Asset> ZipAssetsProvider::OpenInternal(const std::string& path,
@@ -282,21 +298,16 @@ const std::string& ZipAssetsProvider::GetDebugName() const {
return name_.GetDebugName();
}
-bool ZipAssetsProvider::IsUpToDate() const {
- if (last_mod_time_ == -1) {
- return true;
+UpToDate ZipAssetsProvider::IsUpToDate() const {
+ if (last_mod_time_ == kInvalidModDate) {
+ return UpToDate::Always;
}
- 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 last_mod_time_ == sb.st_mtime;
+ return fromBool(last_mod_time_ == getFileModDate(GetFileDescriptor(zip_handle_.get())));
}
-DirectoryAssetsProvider::DirectoryAssetsProvider(std::string&& path, time_t last_mod_time)
- : dir_(std::move(path)), last_mod_time_(last_mod_time) {}
+DirectoryAssetsProvider::DirectoryAssetsProvider(std::string&& path, ModDate 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;
@@ -317,7 +328,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 ? -1 : sb.st_mtime));
+ new DirectoryAssetsProvider(std::move(path), isReadonly ? kInvalidModDate : getModDate(sb)));
}
std::unique_ptr<Asset> DirectoryAssetsProvider::OpenInternal(const std::string& path,
@@ -346,17 +357,11 @@ const std::string& DirectoryAssetsProvider::GetDebugName() const {
return dir_;
}
-bool DirectoryAssetsProvider::IsUpToDate() const {
- if (last_mod_time_ == -1) {
- return true;
+UpToDate DirectoryAssetsProvider::IsUpToDate() const {
+ if (last_mod_time_ == kInvalidModDate) {
+ return UpToDate::Always;
}
- 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 last_mod_time_ == sb.st_mtime;
+ return fromBool(last_mod_time_ == getFileModDate(dir_.c_str()));
}
MultiAssetsProvider::MultiAssetsProvider(std::unique_ptr<AssetsProvider>&& primary,
@@ -397,8 +402,8 @@ const std::string& MultiAssetsProvider::GetDebugName() const {
return debug_name_;
}
-bool MultiAssetsProvider::IsUpToDate() const {
- return primary_->IsUpToDate() && secondary_->IsUpToDate();
+UpToDate MultiAssetsProvider::IsUpToDate() const {
+ return combine(primary_->IsUpToDate(), [this] { return secondary_->IsUpToDate(); });
}
EmptyAssetsProvider::EmptyAssetsProvider(std::optional<std::string>&& path) :
@@ -438,12 +443,12 @@ const std::string& EmptyAssetsProvider::GetDebugName() const {
if (path_.has_value()) {
return *path_;
}
- const static std::string kEmpty = kEmptyDebugString;
+ constexpr static std::string kEmpty{kEmptyDebugString};
return kEmpty;
}
-bool EmptyAssetsProvider::IsUpToDate() const {
- return true;
+UpToDate EmptyAssetsProvider::IsUpToDate() const {
+ return UpToDate::Always;
}
} // namespace android
diff --git a/libs/androidfw/Idmap.cpp b/libs/androidfw/Idmap.cpp
index 095be57a5dc8..f0ef97e5bdcc 100644
--- a/libs/androidfw/Idmap.cpp
+++ b/libs/androidfw/Idmap.cpp
@@ -22,9 +22,10 @@
#include "android-base/logging.h"
#include "android-base/stringprintf.h"
#include "android-base/utf8.h"
-#include "androidfw/misc.h"
+#include "androidfw/AssetManager.h"
#include "androidfw/ResourceTypes.h"
#include "androidfw/Util.h"
+#include "androidfw/misc.h"
#include "utils/ByteOrder.h"
#include "utils/Trace.h"
@@ -280,11 +281,16 @@ 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_(getFileModDate(idmap_fd_.get())) {
+ 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_);
+ }
}
std::unique_ptr<LoadedIdmap> LoadedIdmap::Load(StringPiece idmap_path, StringPiece idmap_data) {
@@ -405,8 +411,11 @@ std::unique_ptr<LoadedIdmap> LoadedIdmap::Load(StringPiece idmap_path, StringPie
std::move(idmap_string_pool),*overlay_path, *target_path));
}
-bool LoadedIdmap::IsUpToDate() const {
- return idmap_last_mod_time_ == getFileModDate(idmap_fd_.get());
+UpToDate LoadedIdmap::IsUpToDate() const {
+ if (idmap_last_mod_time_ == kInvalidModDate) {
+ return UpToDate::Always;
+ }
+ return fromBool(idmap_last_mod_time_ == getFileModDate(idmap_fd_.get()));
}
} // namespace android
diff --git a/libs/androidfw/LocaleDataLookup.cpp b/libs/androidfw/LocaleDataLookup.cpp
index ea9e9a2d4280..9aacdcb9ca92 100644
--- a/libs/androidfw/LocaleDataLookup.cpp
+++ b/libs/androidfw/LocaleDataLookup.cpp
@@ -14871,12 +14871,22 @@ static uint32_t findLatnParent(uint32_t packed_lang_region) {
case 0x656E4154u: // en-AT -> en-150
case 0x656E4245u: // en-BE -> en-150
case 0x656E4348u: // en-CH -> en-150
+ case 0x656E435Au: // en-CZ -> en-150
case 0x656E4445u: // en-DE -> en-150
case 0x656E444Bu: // en-DK -> en-150
+ case 0x656E4553u: // en-ES -> en-150
case 0x656E4649u: // en-FI -> en-150
+ case 0x656E4652u: // en-FR -> en-150
+ case 0x656E4855u: // en-HU -> en-150
+ case 0x656E4954u: // en-IT -> en-150
case 0x656E4E4Cu: // en-NL -> en-150
+ case 0x656E4E4Fu: // en-NO -> en-150
+ case 0x656E504Cu: // en-PL -> en-150
+ case 0x656E5054u: // en-PT -> en-150
+ case 0x656E524Fu: // en-RO -> en-150
case 0x656E5345u: // en-SE -> en-150
case 0x656E5349u: // en-SI -> en-150
+ case 0x656E534Bu: // en-SK -> en-150
return 0x656E80A1u;
case 0x65734152u: // es-AR -> es-419
case 0x6573424Fu: // es-BO -> es-419
diff --git a/libs/androidfw/ResourceTypes.cpp b/libs/androidfw/ResourceTypes.cpp
index 978bc768cd3d..a18c5f5f92f6 100644
--- a/libs/androidfw/ResourceTypes.cpp
+++ b/libs/androidfw/ResourceTypes.cpp
@@ -152,12 +152,11 @@ static void fill9patchOffsets(Res_png_9patch* patch) {
patch->colorsOffset = patch->yDivsOffset + (patch->numYDivs * sizeof(int32_t));
}
-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_value::copyFrom_dtoh_slow(const Res_value& src) {
+ size = dtohs(src.size);
+ res0 = src.res0;
+ dataType = src.dataType;
+ data = dtohl(src.data);
}
void Res_png_9patch::deviceToFile()
@@ -2035,16 +2034,6 @@ 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) {
@@ -2109,34 +2098,33 @@ size_t ResTable_config::unpackRegion(char region[4]) const {
return unpackLanguageOrRegion(this->country, '0', region);
}
-
-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);
+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);
}
/* static */ inline int compareLocales(const ResTable_config &l, const ResTable_config &r) {
@@ -2149,7 +2137,7 @@ void ResTable_config::swapHtoD() {
// systems should happen very infrequently (if at all.)
// The comparison code relies on memcmp low-level optimizations that make it
// more efficient than strncmp.
- const char emptyScript[sizeof(l.localeScript)] = {'\0', '\0', '\0', '\0'};
+ static constexpr 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 be55fe8b4bb6..86c459fb4647 100644
--- a/libs/androidfw/Util.cpp
+++ b/libs/androidfw/Util.cpp
@@ -32,13 +32,18 @@ namespace android {
namespace util {
void ReadUtf16StringFromDevice(const uint16_t* src, size_t len, std::string* out) {
- 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;
+ 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;
+ }
}
}
@@ -63,8 +68,10 @@ std::string Utf16ToUtf8(StringPiece16 utf16) {
}
std::string utf8;
- utf8.resize(utf8_length);
- utf16_to_utf8(utf16.data(), utf16.length(), &*utf8.begin(), utf8_length + 1);
+ utf8.resize_and_overwrite(utf8_length, [&utf16](char* data, size_t size) {
+ utf16_to_utf8(utf16.data(), utf16.length(), data, size + 1);
+ return size;
+ });
return utf8;
}
diff --git a/libs/androidfw/include/androidfw/ApkAssets.h b/libs/androidfw/include/androidfw/ApkAssets.h
index 231808beb718..3f6f4661f2f7 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();
}
- bool IsUpToDate() const;
+ UpToDate 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 d33c325ff369..037f684f5b78 100644
--- a/libs/androidfw/include/androidfw/AssetsProvider.h
+++ b/libs/androidfw/include/androidfw/AssetsProvider.h
@@ -14,8 +14,7 @@
* limitations under the License.
*/
-#ifndef ANDROIDFW_ASSETSPROVIDER_H
-#define ANDROIDFW_ASSETSPROVIDER_H
+#pragma once
#include <memory>
#include <string>
@@ -37,6 +36,12 @@ namespace android {
struct AssetsProvider {
static constexpr off64_t kUnknownLength = -1;
+ static std::unique_ptr<AssetsProvider> CreateWithOverride(
+ std::unique_ptr<AssetsProvider> provider, std::unique_ptr<AssetsProvider> override);
+
+ static std::unique_ptr<AssetsProvider> CreateFromNullable(
+ std::unique_ptr<AssetsProvider> nullable);
+
// Opens a file for reading. If `file_exists` is not null, it will be set to `true` if the file
// exists. This is useful for determining if the file exists but was unable to be opened due to
// an I/O error.
@@ -58,7 +63,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 bool IsUpToDate() const = 0;
+ WARN_UNUSED virtual UpToDate IsUpToDate() const = 0;
// Creates an Asset from a file on disk.
static std::unique_ptr<Asset> CreateAssetFromFile(const std::string& path);
@@ -95,7 +100,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 bool IsUpToDate() const override;
+ WARN_UNUSED UpToDate IsUpToDate() const override;
WARN_UNUSED std::optional<uint32_t> GetCrc(std::string_view path) const;
~ZipAssetsProvider() override = default;
@@ -106,7 +111,7 @@ struct ZipAssetsProvider : public AssetsProvider {
private:
struct PathOrDebugName;
ZipAssetsProvider(ZipArchive* handle, PathOrDebugName&& path, package_property_t flags,
- time_t last_mod_time);
+ ModDate last_mod_time);
struct PathOrDebugName {
static PathOrDebugName Path(std::string value) {
@@ -135,7 +140,7 @@ struct ZipAssetsProvider : public AssetsProvider {
std::unique_ptr<ZipArchive, ZipCloser> zip_handle_;
PathOrDebugName name_;
package_property_t flags_;
- time_t last_mod_time_;
+ ModDate last_mod_time_;
};
// Supplies assets from a root directory.
@@ -147,7 +152,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 bool IsUpToDate() const override;
+ WARN_UNUSED UpToDate IsUpToDate() const override;
~DirectoryAssetsProvider() override = default;
protected:
@@ -156,9 +161,9 @@ struct DirectoryAssetsProvider : public AssetsProvider {
bool* file_exists) const override;
private:
- explicit DirectoryAssetsProvider(std::string&& path, time_t last_mod_time);
+ explicit DirectoryAssetsProvider(std::string&& path, ModDate last_mod_time);
std::string dir_;
- time_t last_mod_time_;
+ ModDate last_mod_time_;
};
// Supplies assets from a `primary` asset provider and falls back to supplying assets from the
@@ -172,7 +177,7 @@ struct MultiAssetsProvider : public AssetsProvider {
WARN_UNUSED std::optional<std::string_view> GetPath() const override;
WARN_UNUSED const std::string& GetDebugName() const override;
- WARN_UNUSED bool IsUpToDate() const override;
+ WARN_UNUSED UpToDate IsUpToDate() const override;
~MultiAssetsProvider() override = default;
protected:
@@ -199,7 +204,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 bool IsUpToDate() const override;
+ WARN_UNUSED UpToDate IsUpToDate() const override;
~EmptyAssetsProvider() override = default;
protected:
@@ -212,5 +217,3 @@ 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 d1db13f53069..0c0856315d8f 100644
--- a/libs/androidfw/include/androidfw/Idmap.h
+++ b/libs/androidfw/include/androidfw/Idmap.h
@@ -14,8 +14,7 @@
* limitations under the License.
*/
-#ifndef IDMAP_H_
-#define IDMAP_H_
+#pragma once
#include <memory>
#include <string>
@@ -32,6 +31,31 @@
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;
@@ -197,7 +221,7 @@ class LoadedIdmap {
// Returns whether the idmap file on disk has not been modified since the construction of this
// LoadedIdmap.
- bool IsUpToDate() const;
+ UpToDate IsUpToDate() const;
protected:
// Exposed as protected so that tests can subclass and mock this class out.
@@ -237,5 +261,3 @@ class LoadedIdmap {
};
} // namespace android
-
-#endif // IDMAP_H_
diff --git a/libs/androidfw/include/androidfw/ResourceTypes.h b/libs/androidfw/include/androidfw/ResourceTypes.h
index 8b2871c21a1e..30594dcfa939 100644
--- a/libs/androidfw/include/androidfw/ResourceTypes.h
+++ b/libs/androidfw/include/androidfw/ResourceTypes.h
@@ -47,6 +47,8 @@
namespace android {
+constexpr const bool kDeviceEndiannessSame = dtohs(0x1001) == 0x1001;
+
constexpr const uint32_t kIdmapMagic = 0x504D4449u;
constexpr const uint32_t kIdmapCurrentVersion = 0x0000000Bu;
@@ -408,7 +410,16 @@ struct Res_value
typedef uint32_t data_type;
data_type data;
- void copyFrom_dtoh(const Res_value& src);
+ 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);
};
/**
@@ -1254,11 +1265,32 @@ struct ResTable_config
// Varies in length from 3 to 8 chars. Zero-filled value.
char localeNumberingSystem[8];
- void copyFromDeviceNoSwap(const ResTable_config& o);
-
- void copyFromDtoH(const ResTable_config& o);
-
- void swapHtoD();
+ 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();
+ }
+ }
int compare(const ResTable_config& o) const;
int compareLogical(const ResTable_config& o) const;
@@ -1384,6 +1416,10 @@ 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 c9ba8a01a5e9..d8ca64a174a2 100644
--- a/libs/androidfw/include/androidfw/misc.h
+++ b/libs/androidfw/include/androidfw/misc.h
@@ -15,6 +15,7 @@
*/
#pragma once
+#include <sys/stat.h>
#include <time.h>
//
@@ -64,10 +65,15 @@ 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 32f3624a3aee..26eb320805c9 100644
--- a/libs/androidfw/misc.cpp
+++ b/libs/androidfw/misc.cpp
@@ -16,10 +16,10 @@
#define LOG_TAG "misc"
-//
-// Miscellaneous utility functions.
-//
-#include <androidfw/misc.h>
+#include "androidfw/misc.h"
+
+#include <errno.h>
+#include <sys/stat.h>
#include "android-base/logging.h"
@@ -28,9 +28,7 @@
#include <sys/vfs.h>
#endif // __linux__
-#include <errno.h>
-#include <sys/stat.h>
-
+#include <array>
#include <cstdio>
#include <cstring>
#include <tuple>
@@ -40,28 +38,26 @@ 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;
@@ -75,7 +71,7 @@ FileType getFileType(const char* fileName)
}
}
-static ModDate getModDate(const struct stat& st) {
+ModDate getModDate(const struct stat& st) {
#ifdef _WIN32
return st.st_mtime;
#elif defined(__APPLE__)
@@ -113,8 +109,14 @@ 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";
@@ -131,6 +133,13 @@ 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 cb2e56f5f5e4..22b9e69500d9 100644
--- a/libs/androidfw/tests/Idmap_test.cpp
+++ b/libs/androidfw/tests/Idmap_test.cpp
@@ -218,10 +218,11 @@ TEST_F(IdmapTest, OverlayAssetsIsUpToDate) {
auto apk_assets = ApkAssets::LoadOverlay(temp_file.path);
ASSERT_NE(nullptr, apk_assets);
- ASSERT_TRUE(apk_assets->IsUpToDate());
+ ASSERT_TRUE(apk_assets->IsOverlay());
+ ASSERT_EQ(UpToDate::True, apk_assets->IsUpToDate());
unlink(temp_file.path);
- ASSERT_FALSE(apk_assets->IsUpToDate());
+ ASSERT_EQ(UpToDate::False, apk_assets->IsUpToDate());
const auto sleep_duration =
std::chrono::nanoseconds(std::max(kModDateResolutionNs, 1'000'000ull));
@@ -230,7 +231,27 @@ TEST_F(IdmapTest, OverlayAssetsIsUpToDate) {
base::WriteStringToFile("hello", temp_file.path);
std::this_thread::sleep_for(sleep_duration);
- ASSERT_FALSE(apk_assets->IsUpToDate());
+ 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));
}
} // namespace
diff --git a/libs/hwui/aconfig/hwui_flags.aconfig b/libs/hwui/aconfig/hwui_flags.aconfig
index 62fd7d358123..d3fc91b65829 100644
--- a/libs/hwui/aconfig/hwui_flags.aconfig
+++ b/libs/hwui/aconfig/hwui_flags.aconfig
@@ -137,6 +137,14 @@ flag {
}
flag {
+ name: "shader_color_space"
+ is_exported: true
+ namespace: "core_graphics"
+ description: "API to set the working colorspace of a Shader or ColorFilter"
+ bug: "299670828"
+}
+
+flag {
name: "query_global_priority"
namespace: "core_graphics"
description: "Attempt to query whether the vulkan driver supports the requested global priority before queue creation."
@@ -174,7 +182,7 @@ flag {
flag {
name: "early_preload_gl_context"
namespace: "core_graphics"
- description: "Initialize GL context and GraphicBufferAllocater init on renderThread preload. This improves app startup time for apps using GL."
+ description: "Preload GL context on renderThread preload. This improves app startup time for apps using GL."
bug: "383612849"
}
@@ -187,4 +195,12 @@ flag {
metadata {
purpose: PURPOSE_BUGFIX
}
+}
+
+flag {
+ name: "early_preinit_buffer_allocator"
+ namespace: "core_graphics"
+ description: "Initialize GraphicBufferAllocater on ViewRootImpl init, to avoid blocking on init during buffer allocation, improving app launch latency."
+ bug: "389908734"
+ is_fixed_read_only: true
} \ No newline at end of file
diff --git a/libs/hwui/jni/Shader.cpp b/libs/hwui/jni/Shader.cpp
index eadb9dea566f..45f0fe0288a4 100644
--- a/libs/hwui/jni/Shader.cpp
+++ b/libs/hwui/jni/Shader.cpp
@@ -266,11 +266,17 @@ static jlong RuntimeShader_getNativeFinalizer(JNIEnv*, jobject) {
return static_cast<jlong>(reinterpret_cast<uintptr_t>(&SkRuntimeShaderBuilder_delete));
}
-static jlong RuntimeShader_create(JNIEnv* env, jobject, jlong shaderBuilder, jlong matrixPtr) {
+static jlong RuntimeShader_create(JNIEnv* env, jobject, jlong shaderBuilder, jlong matrixPtr,
+ jlong colorSpacePtr) {
SkRuntimeShaderBuilder* builder = reinterpret_cast<SkRuntimeShaderBuilder*>(shaderBuilder);
const SkMatrix* matrix = reinterpret_cast<const SkMatrix*>(matrixPtr);
+ auto colorSpace = GraphicsJNI::getNativeColorSpace(colorSpacePtr);
sk_sp<SkShader> shader = builder->makeShader(matrix);
ThrowIAE_IfNull(env, shader);
+ if (colorSpace) {
+ shader = shader->makeWithWorkingColorSpace(colorSpace);
+ ThrowIAE_IfNull(env, shader);
+ }
return reinterpret_cast<jlong>(shader.release());
}
@@ -350,6 +356,10 @@ static void RuntimeShader_updateChild(JNIEnv* env, jobject, jlong shaderBuilder,
UpdateChild(env, builder, name.c_str(), childEffect);
}
+static void RuntimeShader_no(JNIEnv* env) {
+ jniThrowRuntimeException(env, "Not supported");
+}
+
///////////////////////////////////////////////////////////////////////////////////////////////
static const JNINativeMethod gShaderMethods[] = {
@@ -379,7 +389,8 @@ static const JNINativeMethod gComposeShaderMethods[] = {
static const JNINativeMethod gRuntimeShaderMethods[] = {
{"nativeGetFinalizer", "()J", (void*)RuntimeShader_getNativeFinalizer},
- {"nativeCreateShader", "(JJ)J", (void*)RuntimeShader_create},
+ {"nativeCreateShader", "(JJ)J", (void*)RuntimeShader_no},
+ {"nativeCreateShader", "(JJJ)J", (void*)RuntimeShader_create},
{"nativeCreateBuilder", "(Ljava/lang/String;)J", (void*)RuntimeShader_createShaderBuilder},
{"nativeUpdateUniforms", "(JLjava/lang/String;[FZ)V",
(void*)RuntimeShader_updateFloatArrayUniforms},
diff --git a/libs/hwui/jni/android_graphics_HardwareRenderer.cpp b/libs/hwui/jni/android_graphics_HardwareRenderer.cpp
index df9f83036709..99e7740d66d2 100644
--- a/libs/hwui/jni/android_graphics_HardwareRenderer.cpp
+++ b/libs/hwui/jni/android_graphics_HardwareRenderer.cpp
@@ -52,6 +52,9 @@
#include <renderthread/RenderThread.h>
#include <src/image/SkImage_Base.h>
#include <thread/CommonPool.h>
+#ifdef __ANDROID__
+#include <ui/GraphicBufferAllocator.h>
+#endif
#include <utils/Color.h>
#include <utils/RefBase.h>
#include <utils/StrongPointer.h>
@@ -849,6 +852,17 @@ static void android_view_ThreadedRenderer_preload(JNIEnv*, jclass) {
RenderProxy::preload();
}
+static void android_view_ThreadedRenderer_preInitBufferAllocator(JNIEnv*, jclass) {
+#ifdef __ANDROID__
+ CommonPool::async([] {
+ ATRACE_NAME("preInitBufferAllocator:GraphicBufferAllocator");
+ // This involves several binder calls which we do not want blocking
+ // critical path of the activity that is launching.
+ GraphicBufferAllocator::getInstance();
+ });
+#endif
+}
+
static void android_view_ThreadedRenderer_setRtAnimationsEnabled(JNIEnv* env, jobject clazz,
jboolean enabled) {
RenderProxy::setRtAnimationsEnabled(enabled);
@@ -1040,6 +1054,8 @@ static const JNINativeMethod gMethods[] = {
(void*)android_view_ThreadedRenderer_setDisplayDensityDpi},
{"nInitDisplayInfo", "(IIFIJJZZZ)V", (void*)android_view_ThreadedRenderer_initDisplayInfo},
{"preload", "()V", (void*)android_view_ThreadedRenderer_preload},
+ {"preInitBufferAllocator", "()V",
+ (void*)android_view_ThreadedRenderer_preInitBufferAllocator},
{"isWebViewOverlaysEnabled", "()Z",
(void*)android_view_ThreadedRenderer_isWebViewOverlaysEnabled},
{"nSetDrawingEnabled", "(Z)V", (void*)android_view_ThreadedRenderer_setDrawingEnabled},
diff --git a/media/java/android/media/AudioDeviceVolumeManager.java b/media/java/android/media/AudioDeviceVolumeManager.java
index e1fbfea19235..892a8612d74a 100644
--- a/media/java/android/media/AudioDeviceVolumeManager.java
+++ b/media/java/android/media/AudioDeviceVolumeManager.java
@@ -86,10 +86,10 @@ public class AudioDeviceVolumeManager {
/**
* @hide
* Interface to receive volume changes on a device that behaves in absolute volume mode.
- * @see #setDeviceAbsoluteMultiVolumeBehavior(AudioDeviceAttributes, List, Executor,
- * OnAudioDeviceVolumeChangeListener)
- * @see #setDeviceAbsoluteVolumeBehavior(AudioDeviceAttributes, VolumeInfo, Executor,
- * OnAudioDeviceVolumeChangeListener)
+ * @see #setDeviceAbsoluteMultiVolumeBehavior(AudioDeviceAttributes, List, boolean, Executor,
+ * OnAudioDeviceVolumeChangedListener)
+ * @see #setDeviceAbsoluteVolumeBehavior(AudioDeviceAttributes, VolumeInfo, boolean, Executor,
+ * OnAudioDeviceVolumeChangedListener)
*/
public interface OnAudioDeviceVolumeChangedListener {
/**
@@ -203,6 +203,9 @@ public class AudioDeviceVolumeManager {
* volume updates to apply on that device
* @param device the audio device set to absolute volume mode
* @param volume the type of volume this device responds to
+ * @param handlesVolumeAdjustment whether the controller handles volume adjustments separately
+ * from volume changes. If true, adjustments from {@link AudioManager#adjustStreamVolume}
+ * will be sent via {@link OnAudioDeviceVolumeChangedListener#onAudioDeviceVolumeAdjusted}.
* @param executor the Executor used for receiving volume updates through the listener
* @param vclistener the callback for volume updates
*/
@@ -211,13 +214,13 @@ public class AudioDeviceVolumeManager {
public void setDeviceAbsoluteVolumeBehavior(
@NonNull AudioDeviceAttributes device,
@NonNull VolumeInfo volume,
+ boolean handlesVolumeAdjustment,
@NonNull @CallbackExecutor Executor executor,
- @NonNull OnAudioDeviceVolumeChangedListener vclistener,
- boolean handlesVolumeAdjustment) {
+ @NonNull OnAudioDeviceVolumeChangedListener vclistener) {
final ArrayList<VolumeInfo> volumes = new ArrayList<>(1);
volumes.add(volume);
- setDeviceAbsoluteMultiVolumeBehavior(device, volumes, executor, vclistener,
- handlesVolumeAdjustment);
+ setDeviceAbsoluteMultiVolumeBehavior(device, volumes, handlesVolumeAdjustment, executor,
+ vclistener);
}
/**
@@ -226,20 +229,20 @@ public class AudioDeviceVolumeManager {
* registers a listener for receiving volume updates to apply on that device
* @param device the audio device set to absolute multi-volume mode
* @param volumes the list of volumes the given device responds to
+ * @param handlesVolumeAdjustment whether the controller handles volume adjustments separately
+ * from volume changes. If true, adjustments from {@link AudioManager#adjustStreamVolume}
+ * will be sent via {@link OnAudioDeviceVolumeChangedListener#onAudioDeviceVolumeAdjusted}.
* @param executor the Executor used for receiving volume updates through the listener
* @param vclistener the callback for volume updates
- * @param handlesVolumeAdjustment whether the controller handles volume adjustments separately
- * from volume changes. If true, adjustments from {@link AudioManager#adjustStreamVolume}
- * will be sent via {@link OnAudioDeviceVolumeChangedListener#onAudioDeviceVolumeAdjusted}.
*/
@RequiresPermission(anyOf = { android.Manifest.permission.MODIFY_AUDIO_ROUTING,
android.Manifest.permission.BLUETOOTH_PRIVILEGED })
public void setDeviceAbsoluteMultiVolumeBehavior(
@NonNull AudioDeviceAttributes device,
@NonNull List<VolumeInfo> volumes,
+ boolean handlesVolumeAdjustment,
@NonNull @CallbackExecutor Executor executor,
- @NonNull OnAudioDeviceVolumeChangedListener vclistener,
- boolean handlesVolumeAdjustment) {
+ @NonNull OnAudioDeviceVolumeChangedListener vclistener) {
baseSetDeviceAbsoluteMultiVolumeBehavior(device, volumes, executor, vclistener,
handlesVolumeAdjustment, AudioManager.DEVICE_VOLUME_BEHAVIOR_ABSOLUTE);
}
@@ -249,11 +252,14 @@ public class AudioDeviceVolumeManager {
* Configures a device to use absolute volume model, and registers a listener for receiving
* volume updates to apply on that device.
*
- * Should be used instead of {@link #setDeviceAbsoluteVolumeBehavior} when there is no reliable
- * way to set the device's volume to a percentage.
+ * <p>Should be used instead of {@link #setDeviceAbsoluteVolumeBehavior} when there is no
+ * reliable way to set the device's volume to a percentage.
*
* @param device the audio device set to absolute volume mode
* @param volume the type of volume this device responds to
+ * @param handlesVolumeAdjustment whether the controller handles volume adjustments separately
+ * from volume changes. If true, adjustments from {@link AudioManager#adjustStreamVolume}
+ * will be sent via {@link OnAudioDeviceVolumeChangedListener#onAudioDeviceVolumeAdjusted}.
* @param executor the Executor used for receiving volume updates through the listener
* @param vclistener the callback for volume updates
*/
@@ -262,13 +268,13 @@ public class AudioDeviceVolumeManager {
public void setDeviceAbsoluteVolumeAdjustOnlyBehavior(
@NonNull AudioDeviceAttributes device,
@NonNull VolumeInfo volume,
+ boolean handlesVolumeAdjustment,
@NonNull @CallbackExecutor Executor executor,
- @NonNull OnAudioDeviceVolumeChangedListener vclistener,
- boolean handlesVolumeAdjustment) {
+ @NonNull OnAudioDeviceVolumeChangedListener vclistener) {
final ArrayList<VolumeInfo> volumes = new ArrayList<>(1);
volumes.add(volume);
- setDeviceAbsoluteMultiVolumeAdjustOnlyBehavior(device, volumes, executor, vclistener,
- handlesVolumeAdjustment);
+ setDeviceAbsoluteMultiVolumeAdjustOnlyBehavior(device, volumes, handlesVolumeAdjustment,
+ executor, vclistener);
}
/**
@@ -276,11 +282,14 @@ public class AudioDeviceVolumeManager {
* Configures a device to use absolute volume model applied to different volume types, and
* registers a listener for receiving volume updates to apply on that device.
*
- * Should be used instead of {@link #setDeviceAbsoluteMultiVolumeBehavior} when there is
+ * <p>Should be used instead of {@link #setDeviceAbsoluteMultiVolumeBehavior} when there is
* no reliable way to set the device's volume to a percentage.
*
* @param device the audio device set to absolute multi-volume mode
* @param volumes the list of volumes the given device responds to
+ * @param handlesVolumeAdjustment whether the controller handles volume adjustments separately
+ * from volume changes. If true, adjustments from {@link AudioManager#adjustStreamVolume}
+ * will be sent via {@link OnAudioDeviceVolumeChangedListener#onAudioDeviceVolumeAdjusted}.
* @param executor the Executor used for receiving volume updates through the listener
* @param vclistener the callback for volume updates
*/
@@ -289,16 +298,16 @@ public class AudioDeviceVolumeManager {
public void setDeviceAbsoluteMultiVolumeAdjustOnlyBehavior(
@NonNull AudioDeviceAttributes device,
@NonNull List<VolumeInfo> volumes,
+ boolean handlesVolumeAdjustment,
@NonNull @CallbackExecutor Executor executor,
- @NonNull OnAudioDeviceVolumeChangedListener vclistener,
- boolean handlesVolumeAdjustment) {
+ @NonNull OnAudioDeviceVolumeChangedListener vclistener) {
baseSetDeviceAbsoluteMultiVolumeBehavior(device, volumes, executor, vclistener,
handlesVolumeAdjustment, AudioManager.DEVICE_VOLUME_BEHAVIOR_ABSOLUTE_ADJUST_ONLY);
}
/**
* Base method for configuring a device to use absolute volume behavior, or one of its variants.
- * See {@link AudioManager#AbsoluteDeviceVolumeBehavior} for a list of allowed behaviors.
+ * See {@link AudioManager.AbsoluteDeviceVolumeBehavior} for a list of allowed behaviors.
*
* @param behavior the variant of absolute device volume behavior to adopt
*/
diff --git a/media/java/android/media/AudioSystem.java b/media/java/android/media/AudioSystem.java
index 12d7f33a0d51..e01cb928e369 100644
--- a/media/java/android/media/AudioSystem.java
+++ b/media/java/android/media/AudioSystem.java
@@ -1754,13 +1754,21 @@ public class AudioSystem
@UnsupportedAppUsage
public static int setDeviceConnectionState(AudioDeviceAttributes attributes, int state,
int codecFormat) {
+ return setDeviceConnectionState(attributes, state, codecFormat, false /*deviceSwitch*/);
+ }
+
+ /**
+ * @hide
+ */
+ public static int setDeviceConnectionState(AudioDeviceAttributes attributes, int state,
+ int codecFormat, boolean deviceSwitch) {
android.media.audio.common.AudioPort port =
AidlConversion.api2aidl_AudioDeviceAttributes_AudioPort(attributes);
Parcel parcel = Parcel.obtain();
port.writeToParcel(parcel, 0);
parcel.setDataPosition(0);
try {
- return setDeviceConnectionState(state, parcel, codecFormat);
+ return setDeviceConnectionState(state, parcel, codecFormat, deviceSwitch);
} finally {
parcel.recycle();
}
@@ -1769,7 +1777,10 @@ public class AudioSystem
* @hide
*/
@UnsupportedAppUsage
- public static native int setDeviceConnectionState(int state, Parcel parcel, int codecFormat);
+ public static native int setDeviceConnectionState(int state, Parcel parcel, int codecFormat,
+ boolean deviceSwitch);
+
+
/** @hide */
@UnsupportedAppUsage
public static native int getDeviceConnectionState(int device, String device_address);
diff --git a/media/java/android/media/MediaCodec.java b/media/java/android/media/MediaCodec.java
index fb1b5b57cce6..15c832392a22 100644
--- a/media/java/android/media/MediaCodec.java
+++ b/media/java/android/media/MediaCodec.java
@@ -4060,6 +4060,7 @@ final public class MediaCodec {
* Finish building a queue request and queue the buffers with tunings.
*/
public void queue() {
+ Trace.traceBegin(Trace.TRACE_TAG_VIDEO, "MediaCodec::queueRequest-queue#java");
if (!isAccessible()) {
throw new IllegalStateException("The request is stale");
}
@@ -4088,6 +4089,7 @@ final public class MediaCodec {
mTuningKeys, mTuningValues);
}
clear();
+ Trace.traceEnd(Trace.TRACE_TAG_VIDEO);
}
@NonNull QueueRequest clear() {
diff --git a/media/java/android/media/MediaRoute2ProviderService.java b/media/java/android/media/MediaRoute2ProviderService.java
index 3104f9d42891..e94fb7d9e52b 100644
--- a/media/java/android/media/MediaRoute2ProviderService.java
+++ b/media/java/android/media/MediaRoute2ProviderService.java
@@ -55,6 +55,7 @@ import java.util.ArrayList;
import java.util.Collection;
import java.util.Deque;
import java.util.List;
+import java.util.Objects;
import java.util.concurrent.atomic.AtomicBoolean;
/**
@@ -96,12 +97,10 @@ public abstract class MediaRoute2ProviderService extends Service {
* system media, as described by {@link MediaRoute2Info#getSupportedRoutingTypes()}.
*
* @see #onCreateSystemRoutingSession
- * @hide
*/
- // TODO: b/362507305 - Unhide once the implementation and CTS are in place.
@FlaggedApi(Flags.FLAG_ENABLE_MIRRORING_IN_MEDIA_ROUTER_2)
@SdkConstant(SdkConstant.SdkConstantType.INTENT_CATEGORY)
- public static final String SERVICE_INTERFACE_SYSTEM_MEDIA =
+ public static final String CATEGORY_SYSTEM_MEDIA =
"android.media.MediaRoute2ProviderService.SYSTEM_MEDIA";
/**
@@ -165,9 +164,7 @@ public abstract class MediaRoute2ProviderService extends Service {
* The request has failed because the requested operation is not implemented by the provider.
*
* @see #notifyRequestFailed
- * @hide
*/
- // TODO: b/362507305 - Unhide once the implementation and CTS are in place.
@FlaggedApi(Flags.FLAG_ENABLE_MIRRORING_IN_MEDIA_ROUTER_2)
public static final int REASON_UNIMPLEMENTED = 5;
@@ -175,9 +172,7 @@ public abstract class MediaRoute2ProviderService extends Service {
* The request has failed because the provider has failed to route system media.
*
* @see #notifyRequestFailed
- * @hide
*/
- // TODO: b/362507305 - Unhide once the implementation and CTS are in place.
@FlaggedApi(Flags.FLAG_ENABLE_MIRRORING_IN_MEDIA_ROUTER_2)
public static final int REASON_FAILED_TO_REROUTE_SYSTEM_MEDIA = 6;
@@ -217,7 +212,7 @@ public abstract class MediaRoute2ProviderService extends Service {
* package (for example, if they affect the entire system).
*/
@GuardedBy("mRequestIdsLock")
- private final LongSparseArray<Integer> mSystemMediaSessionCreationRequests =
+ private final LongSparseArray<Integer> mSystemRoutingSessionCreationRequests =
new LongSparseArray<>();
@GuardedBy("mSessionLock")
@@ -350,7 +345,7 @@ public abstract class MediaRoute2ProviderService extends Service {
/**
* Notifies the system of the successful creation of a system media routing session.
*
- * <p>This method can only be called as the result of a prior call to {@link
+ * <p>This method must only be called as the result of a prior call to {@link
* #onCreateSystemRoutingSession}.
*
* @param requestId the ID of the {@link #onCreateSystemRoutingSession} request which this call
@@ -365,13 +360,13 @@ public abstract class MediaRoute2ProviderService extends Service {
* where you can clean up this session. {@link AudioRecord#startRecording()} must be called
* immediately on {@link MediaStreams#getAudioRecord()} after calling this method, in order
* to start streaming audio to the receiver.
- * @hide
+ * @throws IllegalStateException If the provided {@code requestId} doesn't correspond to a
+ * previous call to {@link #onCreateSystemRoutingSession}.
*/
- // TODO: b/362507305 - Unhide once the implementation and CTS are in place.
@FlaggedApi(Flags.FLAG_ENABLE_MIRRORING_IN_MEDIA_ROUTER_2)
@RequiresPermission(Manifest.permission.MODIFY_AUDIO_ROUTING)
@Nullable
- public final MediaStreams notifySystemMediaSessionCreated(
+ public final MediaStreams notifySystemRoutingSessionCreated(
long requestId,
@NonNull RoutingSessionInfo sessionInfo,
@NonNull MediaStreamsFormats formats) {
@@ -380,7 +375,7 @@ public abstract class MediaRoute2ProviderService extends Service {
if (DEBUG) {
Log.d(
TAG,
- "notifySystemMediaSessionCreated: Creating a session. requestId="
+ "notifySystemRoutingSessionCreated: Creating a session. requestId="
+ requestId
+ ", sessionInfo="
+ sessionInfo);
@@ -388,8 +383,8 @@ public abstract class MediaRoute2ProviderService extends Service {
Integer uid;
synchronized (mRequestIdsLock) {
- uid = mSystemMediaSessionCreationRequests.get(requestId);
- mSystemMediaSessionCreationRequests.remove(requestId);
+ uid = mSystemRoutingSessionCreationRequests.get(requestId);
+ mSystemRoutingSessionCreationRequests.remove(requestId);
}
if (uid == null) {
@@ -656,37 +651,34 @@ public abstract class MediaRoute2ProviderService extends Service {
/**
* Called when the service receives a request to create a system routing session.
*
- * <p>This method will only be called for routes that support routing of the system media, as
- * described by {@link MediaRoute2Info#getSupportedRoutingTypes()}.
+ * <p>This method must be overridden by subclasses that support routes that support routing
+ * {@link MediaRoute2Info#getSupportedRoutingTypes() system media}. The provided {@code routeId}
+ * will always correspond to a route that supports routing of the system media, as per {@link
+ * MediaRoute2Info#getSupportedRoutingTypes()}.
*
- * <p>Implementors of this method must call {@link #notifySystemMediaSessionCreated} with the
+ * <p>Implementors of this method must call {@link #notifySystemRoutingSessionCreated} with the
* given {@code requestId} to indicate a successful session creation. If the session creation
* fails (for example, if the connection to the receiver device fails), the implementor must
* call {@link #notifyRequestFailed}, passing the {@code requestId}.
*
* <p>Unlike {@link #onCreateSession}, system sessions route the system media (for example,
* audio and/or video) which is to be retrieved by calling {@link
- * #notifySystemMediaSessionCreated}.
+ * #notifySystemRoutingSessionCreated}.
*
* <p>Changes to the session can be notified by calling {@link #notifySessionUpdated}.
*
* @param requestId the ID of this request
- * @param packageName the package name of the application whose media to route.
* @param routeId the ID of the route initially being {@link
* RoutingSessionInfo#getSelectedRoutes() selected}.
- * @param sessionHints an optional bundle of arguments sent by {@link MediaRouter2}, or null if
- * none.
+ * @param parameters {@link SystemRoutingSessionParams} for the session creation.
* @see RoutingSessionInfo.Builder
- * @see #notifySystemMediaSessionCreated
- * @hide
+ * @see #notifySystemRoutingSessionCreated
*/
- // TODO: b/362507305 - Unhide once the implementation and CTS are in place.
@FlaggedApi(Flags.FLAG_ENABLE_MIRRORING_IN_MEDIA_ROUTER_2)
public void onCreateSystemRoutingSession(
long requestId,
- @NonNull String packageName,
@NonNull String routeId,
- @Nullable Bundle sessionHints) {
+ @NonNull SystemRoutingSessionParams parameters) {
mHandler.post(() -> notifyRequestFailed(requestId, REASON_UNIMPLEMENTED));
}
@@ -974,24 +966,29 @@ public abstract class MediaRoute2ProviderService extends Service {
int uid,
String packageName,
String routeId,
- @Nullable Bundle sessionHints) {
- if (!checkCallerIsSystem()) {
+ @Nullable Bundle extras) {
+ if (!Flags.enableMirroringInMediaRouter2() || !checkCallerIsSystem()) {
return;
}
if (!checkRouteIdIsValid(routeId, "requestCreateSession")) {
return;
}
synchronized (mRequestIdsLock) {
- mSystemMediaSessionCreationRequests.put(requestId, uid);
+ mSystemRoutingSessionCreationRequests.put(requestId, uid);
}
+ var sessionParamsBuilder =
+ new SystemRoutingSessionParams.Builder().setPackageName(packageName);
+ if (extras != null) {
+ sessionParamsBuilder.setExtras(extras);
+ }
+ var sessionParams = sessionParamsBuilder.build();
mHandler.sendMessage(
obtainMessage(
MediaRoute2ProviderService::onCreateSystemRoutingSession,
MediaRoute2ProviderService.this,
requestId,
- packageName,
routeId,
- sessionHints));
+ sessionParams));
}
@Override
@@ -1072,14 +1069,12 @@ public abstract class MediaRoute2ProviderService extends Service {
}
/**
- * Holds the streams to be routed as part of a system media routing session.
- *
- * <p>The encoded data format matches the {@link MediaStreamsFormats} passed to {@link
- * #notifySystemMediaSessionCreated}.
+ * Holds the streams to be routed as part of a {@link #onCreateSystemRoutingSession system media
+ * routing session}.
*
- * @hide
+ * <p>The encoded data format will match the {@link MediaStreamsFormats} passed to {@link
+ * #notifySystemRoutingSessionCreated}.
*/
- // TODO: b/362507305 - Unhide once the implementation and CTS are in place.
@FlaggedApi(Flags.FLAG_ENABLE_MIRRORING_IN_MEDIA_ROUTER_2)
public static final class MediaStreams {
@@ -1088,8 +1083,6 @@ public abstract class MediaRoute2ProviderService extends Service {
/**
* Holds the last {@link RoutingSessionInfo} associated with these streams.
- *
- * @hide
*/
@NonNull
// Access guarded by mSessionsLock, but it's not convenient to enforce through @GuardedBy.
@@ -1147,15 +1140,91 @@ public abstract class MediaRoute2ProviderService extends Service {
}
}
+ /**
+ * Holds parameters associated with a {@link #onCreateSystemRoutingSession session creation
+ * request}.
+ */
+ @FlaggedApi(Flags.FLAG_ENABLE_MIRRORING_IN_MEDIA_ROUTER_2)
+ public static final class SystemRoutingSessionParams {
+
+ private final String mPackageName;
+ private final Bundle mExtras;
+
+ private SystemRoutingSessionParams(Builder builder) {
+ this.mPackageName = builder.mPackageName;
+ this.mExtras = builder.mExtras;
+ }
+
+ /**
+ * Returns the name of the package associated with the session, or an empty string if not
+ * applicable.
+ *
+ * <p>The package name is not applicable if the session is not associated with a specific
+ * package, for example is the session affects the entire system.
+ */
+ @FlaggedApi(Flags.FLAG_ENABLE_MIRRORING_IN_MEDIA_ROUTER_2)
+ @NonNull
+ public String getPackageName() {
+ return mPackageName;
+ }
+
+ /** Returns a bundle provided by the client that triggered the session creation request. */
+ @FlaggedApi(Flags.FLAG_ENABLE_MIRRORING_IN_MEDIA_ROUTER_2)
+ @NonNull
+ public Bundle getExtras() {
+ return mExtras;
+ }
+
+ /** A builder for {@link SystemRoutingSessionParams}. */
+ public static final class Builder {
+ private String mPackageName;
+ private Bundle mExtras;
+
+ /** Constructor. */
+ public Builder() {
+ mPackageName = "";
+ mExtras = Bundle.EMPTY;
+ }
+
+ /**
+ * Sets the {@link #getExtras() extras}.
+ *
+ * <p>The default value is an empty {@link Bundle}.
+ *
+ * <p>Note that this bundle is not copied, so avoiding mutating the given {@link Bundle}
+ * after passing it to this method.
+ */
+ @NonNull
+ public Builder setExtras(@NonNull Bundle extras) {
+ mExtras = Objects.requireNonNull(extras);
+ return this;
+ }
+
+ /**
+ * Sets the {@link #getPackageName()}.
+ *
+ * <p>The default value is an empty string.
+ */
+ @NonNull
+ public Builder setPackageName(@NonNull String packageName) {
+ mPackageName = Objects.requireNonNull(packageName);
+ return this;
+ }
+
+ /** Returns a new {@link SystemRoutingSessionParams} instance. */
+ @NonNull
+ public SystemRoutingSessionParams build() {
+ return new SystemRoutingSessionParams(this);
+ }
+ }
+ }
/**
* Holds the formats to encode media data to be read from {@link MediaStreams}.
*
* @see MediaStreams
- * @see #notifySystemMediaSessionCreated
- * @hide
+ * @see #notifySystemRoutingSessionCreated
*/
- // TODO: b/362507305 - Unhide once the implementation and CTS are in place.
@FlaggedApi(Flags.FLAG_ENABLE_MIRRORING_IN_MEDIA_ROUTER_2)
public static final class MediaStreamsFormats {
@@ -1169,29 +1238,25 @@ public abstract class MediaRoute2ProviderService extends Service {
/**
* Returns the audio format to use for creating the {@link MediaStreams#getAudioRecord} to
- * return from {@link #notifySystemMediaSessionCreated}.
- *
- * @hide
+ * return from {@link #notifySystemRoutingSessionCreated}. May be null if the session
+ * doesn't support system audio.
*/
- // TODO: b/362507305 - Unhide once the implementation and CTS are in place.
@FlaggedApi(Flags.FLAG_ENABLE_MIRRORING_IN_MEDIA_ROUTER_2)
+ @Nullable
public AudioFormat getAudioFormat() {
return mAudioFormat;
}
/**
* Builder for {@link MediaStreamsFormats}
- *
- * @hide
*/
- // TODO: b/362507305 - Unhide once the implementation and CTS are in place.
@FlaggedApi(Flags.FLAG_ENABLE_MIRRORING_IN_MEDIA_ROUTER_2)
public static final class Builder {
private AudioFormat mAudioFormat;
/**
* Sets the audio format to use for creating the {@link MediaStreams#getAudioRecord} to
- * return from {@link #notifySystemMediaSessionCreated}.
+ * return from {@link #notifySystemRoutingSessionCreated}.
*
* @param audioFormat the audio format
* @return this builder
diff --git a/media/java/android/media/RingtoneManager.java b/media/java/android/media/RingtoneManager.java
index 0f24654879cd..021348153bb8 100644
--- a/media/java/android/media/RingtoneManager.java
+++ b/media/java/android/media/RingtoneManager.java
@@ -60,6 +60,7 @@ import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
+import java.util.Objects;
/**
* RingtoneManager provides access to ringtones, notification, and other types
@@ -810,9 +811,7 @@ public class RingtoneManager {
// Don't set the stream type
Ringtone ringtone = getRingtone(context, ringtoneUri, -1 /* streamType */,
volumeShaperConfig, false);
- if (Flags.enableRingtoneHapticsCustomization()
- && Utils.isRingtoneVibrationSettingsSupported(context)
- && Utils.hasVibration(ringtoneUri) && hasHapticChannels(ringtoneUri)) {
+ if (muteHapticChannelForVibration(context, ringtoneUri)) {
audioAttributes = new AudioAttributes.Builder(
audioAttributes).setHapticChannelsMuted(true).build();
}
@@ -1305,4 +1304,19 @@ public class RingtoneManager {
default: throw new IllegalArgumentException();
}
}
+
+ private static boolean muteHapticChannelForVibration(Context context, Uri ringtoneUri) {
+ final Uri vibrationUri = Utils.getVibrationUri(ringtoneUri);
+ // No vibration is specified
+ if (vibrationUri == null) {
+ return false;
+ }
+ // The user specified the synchronized pattern
+ if (Objects.equals(vibrationUri.toString(), Utils.SYNCHRONIZED_VIBRATION)) {
+ return false;
+ }
+ return Flags.enableRingtoneHapticsCustomization()
+ && Utils.isRingtoneVibrationSettingsSupported(context)
+ && hasHapticChannels(ringtoneUri);
+ }
}
diff --git a/media/java/android/media/Utils.java b/media/java/android/media/Utils.java
index 11bd221ec696..d6e27b0ffa75 100644
--- a/media/java/android/media/Utils.java
+++ b/media/java/android/media/Utils.java
@@ -66,6 +66,8 @@ public class Utils {
public static final String VIBRATION_URI_PARAM = "vibration_uri";
+ public static final String SYNCHRONIZED_VIBRATION = "synchronized";
+
/**
* Sorts distinct (non-intersecting) range array in ascending order.
* @throws java.lang.IllegalArgumentException if ranges are not distinct
@@ -757,8 +759,8 @@ public class Utils {
return null;
}
String filePath = vibrationUri.getPath();
- if (filePath == null) {
- Log.w(TAG, "The file path is null.");
+ if (filePath == null || filePath.equals(Utils.SYNCHRONIZED_VIBRATION)) {
+ Log.w(TAG, "Ignore the vibration parsing for file:" + filePath);
return null;
}
File vibrationFile = new File(filePath);
diff --git a/packages/SettingsLib/CollapsingToolbarBaseActivity/Android.bp b/packages/SettingsLib/CollapsingToolbarBaseActivity/Android.bp
index b56b944955d7..af8e856a5ad6 100644
--- a/packages/SettingsLib/CollapsingToolbarBaseActivity/Android.bp
+++ b/packages/SettingsLib/CollapsingToolbarBaseActivity/Android.bp
@@ -35,5 +35,6 @@ android_library {
"com.android.extservices",
"com.android.permission",
"com.android.healthfitness",
+ "com.android.mediaprovider",
],
}
diff --git a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceBindings.kt b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceBindings.kt
index dbac17d4e8b8..44c93c77e33b 100644
--- a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceBindings.kt
+++ b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceBindings.kt
@@ -74,8 +74,14 @@ interface BooleanValuePreferenceBinding : PreferenceBinding {
override fun bind(preference: Preference, metadata: PreferenceMetadata) {
super.bind(preference, metadata)
(preference as TwoStatePreference).apply {
+ // MUST suppress persistent when initializing the checked state:
+ // 1. default value is written to datastore if not set (b/396260949)
+ // 2. avoid redundant read to the datastore
+ val suppressPersistent = isPersistent
+ if (suppressPersistent) isPersistent = false
// "false" is kind of placeholder, metadata datastore should provide the default value
isChecked = preferenceDataStore!!.getBoolean(key, false)
+ if (suppressPersistent) isPersistent = true
}
}
}
diff --git a/packages/SettingsLib/SettingsTheme/src/com/android/settingslib/widget/SettingsBasePreferenceFragment.kt b/packages/SettingsLib/SettingsTheme/src/com/android/settingslib/widget/SettingsBasePreferenceFragment.kt
index bfaeb42d5a31..8d12f01e24ed 100644
--- a/packages/SettingsLib/SettingsTheme/src/com/android/settingslib/widget/SettingsBasePreferenceFragment.kt
+++ b/packages/SettingsLib/SettingsTheme/src/com/android/settingslib/widget/SettingsBasePreferenceFragment.kt
@@ -17,7 +17,9 @@
package com.android.settingslib.widget
import android.os.Bundle
+import android.view.LayoutInflater;
import android.view.View
+import android.view.ViewGroup;
import androidx.annotation.CallSuper
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.PreferenceScreen
@@ -27,6 +29,15 @@ import androidx.recyclerview.widget.RecyclerView
abstract class SettingsBasePreferenceFragment : PreferenceFragmentCompat() {
@CallSuper
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ return super.onCreateView(inflater, container, savedInstanceState)
+ }
+
+ @CallSuper
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
if (SettingsThemeHelper.isExpressiveTheme(requireContext())) {
diff --git a/packages/SettingsLib/SettingsTransition/Android.bp b/packages/SettingsLib/SettingsTransition/Android.bp
index e04af6c1ab11..6b9cbfa8ece7 100644
--- a/packages/SettingsLib/SettingsTransition/Android.bp
+++ b/packages/SettingsLib/SettingsTransition/Android.bp
@@ -30,5 +30,6 @@ android_library {
"com.android.extservices",
"com.android.permission",
"com.android.healthfitness",
+ "com.android.mediaprovider",
],
}
diff --git a/packages/SettingsLib/TopIntroPreference/Android.bp b/packages/SettingsLib/TopIntroPreference/Android.bp
index 76e36dc5ff7d..bf26264a4f0e 100644
--- a/packages/SettingsLib/TopIntroPreference/Android.bp
+++ b/packages/SettingsLib/TopIntroPreference/Android.bp
@@ -32,5 +32,6 @@ android_library {
"com.android.cellbroadcast",
"com.android.devicelock",
"com.android.healthfitness",
+ "com.android.mediaprovider",
],
}
diff --git a/packages/SettingsLib/res/drawable/ic_mobile_0_4_bar.xml b/packages/SettingsLib/res/drawable/ic_mobile_0_4_bar.xml
index d9a417f1ea99..54c878810d10 100644
--- a/packages/SettingsLib/res/drawable/ic_mobile_0_4_bar.xml
+++ b/packages/SettingsLib/res/drawable/ic_mobile_0_4_bar.xml
@@ -1,5 +1,5 @@
<!--
- Copyright (C) 2023 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,24 +14,24 @@
limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="14dp"
- android:height="14dp"
- android:viewportWidth="14.0"
- android:viewportHeight="14.0">
+ android:width="17dp"
+ android:height="12dp"
+ android:viewportWidth="16.0"
+ android:viewportHeight="12.0">
<path
- android:pathData="M8.25,3L9.25,3A0.5,0.5 0,0 1,9.75 3.5L9.75,13.5A0.5,0.5 0,0 1,9.25 14L8.25,14A0.5,0.5 0,0 1,7.75 13.5L7.75,3.5A0.5,0.5 0,0 1,8.25 3z"
- android:fillAlpha="0.24"
+ android:pathData="M1.249,7C1.939,7 2.499,7.56 2.499,8.25L2.499,10.75C2.499,11.44 1.939,12 1.249,12C0.559,12 -0.001,11.44 -0.001,10.75L-0.001,8.25C-0.001,7.56 0.559,7 1.249,7Z"
+ android:fillAlpha="0.45"
android:fillColor="#000"/>
<path
- android:pathData="M11.75,0L12.75,0A0.5,0.5 0,0 1,13.25 0.5L13.25,13.5A0.5,0.5 0,0 1,12.75 14L11.75,14A0.5,0.5 0,0 1,11.25 13.5L11.25,0.5A0.5,0.5 0,0 1,11.75 0z"
- android:fillAlpha="0.24"
+ android:pathData="M5.749,4.5C6.439,4.5 6.999,5.06 6.999,5.75L6.999,10.75C6.999,11.44 6.439,12 5.749,12C5.059,12 4.499,11.44 4.499,10.75L4.499,5.75C4.499,5.06 5.059,4.5 5.749,4.5Z"
+ android:fillAlpha="0.45"
android:fillColor="#000"/>
<path
- android:pathData="M1.25,10L2.25,10A0.5,0.5 0,0 1,2.75 10.5L2.75,13.5A0.5,0.5 0,0 1,2.25 14L1.25,14A0.5,0.5 0,0 1,0.75 13.5L0.75,10.5A0.5,0.5 0,0 1,1.25 10z"
- android:fillAlpha="0.24"
+ android:pathData="M10.249,2C10.939,2 11.499,2.56 11.499,3.25L11.499,10.75C11.499,11.44 10.939,12 10.249,12C9.559,12 8.999,11.44 8.999,10.75L8.999,3.25C8.999,2.56 9.559,2 10.249,2Z"
+ android:fillAlpha="0.45"
android:fillColor="#000"/>
<path
- android:pathData="M4.75,6.5L5.75,6.5A0.5,0.5 0,0 1,6.25 7L6.25,13.5A0.5,0.5 0,0 1,5.75 14L4.75,14A0.5,0.5 0,0 1,4.25 13.5L4.25,7A0.5,0.5 0,0 1,4.75 6.5z"
- android:fillAlpha="0.24"
+ android:pathData="M14.749,0C15.439,0 15.999,0.56 15.999,1.25L15.999,10.75C15.999,11.44 15.439,12 14.749,12C14.059,12 13.499,11.44 13.499,10.75L13.499,1.25C13.499,0.56 14.059,0 14.749,0Z"
+ android:fillAlpha="0.45"
android:fillColor="#000"/>
</vector>
diff --git a/packages/SettingsLib/res/drawable/ic_mobile_0_4_bar_error.xml b/packages/SettingsLib/res/drawable/ic_mobile_0_4_bar_error.xml
index facc285a45ca..6015be8c894f 100644
--- a/packages/SettingsLib/res/drawable/ic_mobile_0_4_bar_error.xml
+++ b/packages/SettingsLib/res/drawable/ic_mobile_0_4_bar_error.xml
@@ -1,28 +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.
+-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="15dp"
- android:height="14dp"
- android:viewportWidth="15.0"
- android:viewportHeight="14.0">
+ android:width="17dp"
+ android:height="12dp"
+ android:viewportWidth="16.0"
+ android:viewportHeight="12.0">
+ <group>
+ <!-- clip-out the circle which will contain the exclamation point (below this group) -->
+ <clip-path
+ android:pathData="
+ M0,0
+ V13.5,0
+ H13.5,20
+ V0,20
+ H0,0
+ M14.999,13.5C17.761,13.5 19.999,11.261 19.999,8.5C19.999,5.739 17.761,3.5 14.999,3.5C12.238,3.5 9.999,5.739 9.999,8.5C9.999,11.261 12.238,13.5 14.999,13.5Z" />
+ <path
+ android:pathData="M1.249,7C1.939,7 2.499,7.56 2.499,8.25L2.499,10.75C2.499,11.44 1.939,12 1.249,12C0.559,12 -0.001,11.44 -0.001,10.75L-0.001,8.25C-0.001,7.56 0.559,7 1.249,7Z"
+ android:fillAlpha="0.45"
+ android:fillColor="#000"/>
+ <path
+ android:pathData="M5.749,4.5C6.439,4.5 6.999,5.06 6.999,5.75L6.999,10.75C6.999,11.44 6.439,12 5.749,12C5.059,12 4.499,11.44 4.499,10.75L4.499,5.75C4.499,5.06 5.059,4.5 5.749,4.5Z"
+ android:fillAlpha="0.45"
+ android:fillColor="#000"/>
+ <path
+ android:pathData="M10.249,2C10.939,2 11.499,2.56 11.499,3.25L11.499,10.75C11.499,11.44 10.939,12 10.249,12C9.559,12 8.999,11.44 8.999,10.75L8.999,3.25C8.999,2.56 9.559,2 10.249,2Z"
+ android:fillAlpha="0.45"
+ android:fillColor="#000"/>
+ <path
+ android:pathData="M14.749,0C15.439,0 15.999,0.56 15.999,1.25L15.999,10.75C15.999,11.44 15.439,12 14.749,12C14.059,12 13.499,11.44 13.499,10.75L13.499,1.25C13.499,0.56 14.059,0 14.749,0Z"
+ android:fillAlpha="0.45"
+ android:fillColor="#000"/>
+ </group>
<path
- android:pathData="M7,3.5C7,3.224 7.224,3 7.5,3H8.5C8.776,3 9,3.224 9,3.5V13.5C9,13.776 8.776,14 8.5,14H7.5C7.224,14 7,13.776 7,13.5V3.5Z"
- android:fillAlpha="0.3"
- android:fillColor="#000"/>
- <path
- android:pathData="M0,10.5C0,10.224 0.224,10 0.5,10H1.5C1.776,10 2,10.224 2,10.5V13.5C2,13.776 1.776,14 1.5,14H0.5C0.224,14 0,13.776 0,13.5V10.5Z"
- android:fillAlpha="0.3"
- android:fillColor="#000"/>
- <path
- android:pathData="M3.5,7C3.5,6.724 3.724,6.5 4,6.5H5C5.276,6.5 5.5,6.724 5.5,7V13.5C5.5,13.776 5.276,14 5,14H4C3.724,14 3.5,13.776 3.5,13.5V7Z"
- android:fillAlpha="0.3"
- android:fillColor="#000"/>
- <path
- android:pathData="M11,0C10.724,0 10.5,0.224 10.5,0.5V3H12.5V0.5C12.5,0.224 12.276,0 12,0H11Z"
- android:fillAlpha="0.3"
- android:fillColor="#000"/>
- <path
- android:pathData="M12.25,13C12.25,12.448 12.698,12 13.25,12C13.802,12 14.25,12.448 14.25,13C14.25,13.552 13.802,14 13.25,14C12.698,14 12.25,13.552 12.25,13Z"
- android:fillColor="#000"/>
- <path
- android:pathData="M12.25,5C12.25,4.724 12.474,4.5 12.75,4.5H13.75C14.026,4.5 14.25,4.724 14.25,5V10C14.25,10.276 14.026,10.5 13.75,10.5H12.75C12.474,10.5 12.25,10.276 12.25,10V5Z"
+ android:pathData="M14.999,5C14.589,5 14.249,5.34 14.249,5.75L14.249,8.75C14.249,9.16 14.589,9.5 14.999,9.5C15.409,9.5 15.749,9.16 15.749,8.75L15.749,5.75C15.749,5.34 15.409,5 14.999,5ZM14.999,12C15.409,12 15.749,11.66 15.749,11.25C15.749,10.84 15.409,10.5 14.999,10.5C14.589,10.5 14.249,10.84 14.249,11.25C14.249,11.66 14.589,12 14.999,12Z"
android:fillColor="#000"/>
</vector>
diff --git a/packages/SettingsLib/res/drawable/ic_mobile_0_5_bar.xml b/packages/SettingsLib/res/drawable/ic_mobile_0_5_bar.xml
index 2c05a938c2cf..7c85b9e44383 100644
--- a/packages/SettingsLib/res/drawable/ic_mobile_0_5_bar.xml
+++ b/packages/SettingsLib/res/drawable/ic_mobile_0_5_bar.xml
@@ -14,28 +14,28 @@
limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="16dp"
- android:height="14dp"
- android:viewportWidth="16.0"
- android:viewportHeight="14.0">
+ android:width="21dp"
+ android:height="12dp"
+ android:viewportWidth="20.5"
+ android:viewportHeight="12.0">
<path
- android:pathData="M7.5,5L8.5,5A0.5,0.5 0,0 1,9 5.5L9,13.5A0.5,0.5 0,0 1,8.5 14L7.5,14A0.5,0.5 0,0 1,7 13.5L7,5.5A0.5,0.5 0,0 1,7.5 5z"
- android:fillAlpha="0.24"
+ android:pathData="M1.249,7C1.939,7 2.499,7.56 2.499,8.25L2.499,10.75C2.499,11.44 1.939,12 1.249,12C0.559,12 -0.001,11.44 -0.001,10.75L-0.001,8.25C-0.001,7.56 0.559,7 1.249,7Z"
+ android:fillAlpha="0.45"
android:fillColor="#000"/>
<path
- android:pathData="M11,2L12,2A0.5,0.5 0,0 1,12.5 2.5L12.5,13.5A0.5,0.5 0,0 1,12 14L11,14A0.5,0.5 0,0 1,10.5 13.5L10.5,2.5A0.5,0.5 0,0 1,11 2z"
- android:fillAlpha="0.24"
+ android:pathData="M5.749,5C6.439,5 6.999,5.56 6.999,6.25L6.999,10.75C6.999,11.44 6.439,12 5.749,12C5.059,12 4.499,11.44 4.499,10.75L4.499,6.25C4.499,5.56 5.059,5 5.749,5Z"
+ android:fillAlpha="0.45"
android:fillColor="#000"/>
<path
- android:pathData="M0.5,11L1.5,11A0.5,0.5 0,0 1,2 11.5L2,13.5A0.5,0.5 0,0 1,1.5 14L0.5,14A0.5,0.5 0,0 1,0 13.5L0,11.5A0.5,0.5 0,0 1,0.5 11z"
- android:fillAlpha="0.24"
+ android:pathData="M10.249,3C10.939,3 11.499,3.56 11.499,4.25L11.499,10.75C11.499,11.44 10.939,12 10.249,12C9.559,12 8.999,11.44 8.999,10.75L8.999,4.25C8.999,3.56 9.559,3 10.249,3Z"
+ android:fillAlpha="0.45"
android:fillColor="#000"/>
<path
- android:pathData="M14.5,0L15.5,0A0.5,0.5 0,0 1,16 0.5L16,13.5A0.5,0.5 0,0 1,15.5 14L14.5,14A0.5,0.5 0,0 1,14 13.5L14,0.5A0.5,0.5 0,0 1,14.5 0z"
- android:fillAlpha="0.24"
+ android:pathData="M14.749,1.5C15.439,1.5 15.999,2.06 15.999,2.75L15.999,10.75C15.999,11.44 15.439,12 14.749,12C14.059,12 13.499,11.44 13.499,10.75L13.499,2.75C13.499,2.06 14.059,1.5 14.749,1.5Z"
+ android:fillAlpha="0.45"
android:fillColor="#000"/>
<path
- android:pathData="M4,8L5,8A0.5,0.5 0,0 1,5.5 8.5L5.5,13.5A0.5,0.5 0,0 1,5 14L4,14A0.5,0.5 0,0 1,3.5 13.5L3.5,8.5A0.5,0.5 0,0 1,4 8z"
- android:fillAlpha="0.24"
+ android:pathData="M19.249,0C19.939,0 20.499,0.56 20.499,1.25L20.499,10.75C20.499,11.44 19.939,12 19.249,12C18.559,12 17.999,11.44 17.999,10.75L17.999,1.25C17.999,0.56 18.559,0 19.249,0Z"
+ android:fillAlpha="0.45"
android:fillColor="#000"/>
</vector>
diff --git a/packages/SettingsLib/res/drawable/ic_mobile_0_5_bar_error.xml b/packages/SettingsLib/res/drawable/ic_mobile_0_5_bar_error.xml
index 328e45ec7e19..f75ec57f2a5e 100644
--- a/packages/SettingsLib/res/drawable/ic_mobile_0_5_bar_error.xml
+++ b/packages/SettingsLib/res/drawable/ic_mobile_0_5_bar_error.xml
@@ -1,32 +1,54 @@
+<!--
+ Copyright (C) 2025 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="18dp"
- android:height="14dp"
- android:viewportWidth="18.0"
- android:viewportHeight="14.0">
+ android:width="21dp"
+ android:height="12dp"
+ android:viewportHeight="12.0"
+ android:viewportWidth="20.5">
+ <!-- clip-out the circle which will contain the exclamation point (below this group) -->
+ <group>
+ <clip-path android:pathData="
+ M0,0
+ H20.5
+ V12.0
+ H0
+ Z
+ M19.499,13.5C22.261,13.5 24.499,11.261 24.499,8.5C24.499,5.739 22.261,3.5 19.499,3.5C16.738,3.5 14.499,5.739 14.499,8.5C14.499,11.261 16.738,13.5 19.499,13.5Z" />
+ <path
+ android:fillAlpha="0.45"
+ android:fillColor="#000"
+ android:pathData="M1.249,7C1.939,7 2.499,7.56 2.499,8.25L2.499,10.75C2.499,11.44 1.939,12 1.249,12C0.559,12 -0.001,11.44 -0.001,10.75L-0.001,8.25C-0.001,7.56 0.559,7 1.249,7Z" />
+ <path
+ android:fillAlpha="0.45"
+ android:fillColor="#000"
+ android:pathData="M5.749,5C6.439,5 6.999,5.56 6.999,6.25L6.999,10.75C6.999,11.44 6.439,12 5.749,12C5.059,12 4.499,11.44 4.499,10.75L4.499,6.25C4.499,5.56 5.059,5 5.749,5Z" />
+ <path
+ android:fillAlpha="0.45"
+ android:fillColor="#000"
+ android:pathData="M10.249,3C10.939,3 11.499,3.56 11.499,4.25L11.499,10.75C11.499,11.44 10.939,12 10.249,12C9.559,12 8.999,11.44 8.999,10.75L8.999,4.25C8.999,3.56 9.559,3 10.249,3Z" />
+ <path
+ android:fillAlpha="0.45"
+ android:fillColor="#000"
+ android:pathData="M14.749,1.5C15.439,1.5 15.999,2.06 15.999,2.75L15.999,10.75C15.999,11.44 15.439,12 14.749,12C14.059,12 13.499,11.44 13.499,10.75L13.499,2.75C13.499,2.06 14.059,1.5 14.749,1.5Z" />
+ <path
+ android:fillAlpha="0.45"
+ android:fillColor="#000"
+ android:pathData="M19.249,0C19.939,0 20.499,0.56 20.499,1.25L20.499,10.75C20.499,11.44 19.939,12 19.249,12C18.559,12 17.999,11.44 17.999,10.75L17.999,1.25C17.999,0.56 18.559,0 19.249,0Z" />
+ </group>
<path
- android:pathData="M14,0.5C14,0.224 14.224,0 14.5,0H15.5C15.776,0 16,0.224 16,0.5V3H14V0.5Z"
- android:fillAlpha="0.3"
- android:fillColor="#000"/>
- <path
- android:pathData="M10.5,2.5C10.5,2.224 10.724,2 11,2H12C12.276,2 12.5,2.224 12.5,2.5V13.5C12.5,13.776 12.276,14 12,14H11C10.724,14 10.5,13.776 10.5,13.5V2.5Z"
- android:fillAlpha="0.3"
- android:fillColor="#000"/>
- <path
- android:pathData="M7,5.5C7,5.224 7.224,5 7.5,5H8.5C8.776,5 9,5.224 9,5.5V13.5C9,13.776 8.776,14 8.5,14H7.5C7.224,14 7,13.776 7,13.5V5.5Z"
- android:fillAlpha="0.3"
- android:fillColor="#000"/>
- <path
- android:pathData="M0.5,11C0.224,11 0,11.224 0,11.5V13.5C0,13.776 0.224,14 0.5,14H1.5C1.776,14 2,13.776 2,13.5V11.5C2,11.224 1.776,11 1.5,11H0.5Z"
- android:fillAlpha="0.3"
- android:fillColor="#000"/>
- <path
- android:pathData="M4,8C3.724,8 3.5,8.224 3.5,8.5V13.5C3.5,13.776 3.724,14 4,14H5C5.276,14 5.5,13.776 5.5,13.5V8.5C5.5,8.224 5.276,8 5,8H4Z"
- android:fillAlpha="0.3"
- android:fillColor="#000"/>
- <path
- android:pathData="M16,13C16,12.448 16.448,12 17,12C17.552,12 18,12.448 18,13C18,13.552 17.552,14 17,14C16.448,14 16,13.552 16,13Z"
- android:fillColor="#000"/>
- <path
- android:pathData="M16,5C16,4.724 16.224,4.5 16.5,4.5H17.5C17.776,4.5 18,4.724 18,5V10C18,10.276 17.776,10.5 17.5,10.5H16.5C16.224,10.5 16,10.276 16,10V5Z"
- android:fillColor="#000"/>
+ android:fillColor="#000"
+ android:pathData="M19.499,5C19.089,5 18.749,5.34 18.749,5.75L18.749,8.75C18.749,9.16 19.089,9.5 19.499,9.5C19.909,9.5 20.249,9.16 20.249,8.75L20.249,5.75C20.249,5.34 19.909,5 19.499,5ZM19.499,12C19.909,12 20.249,11.66 20.249,11.25C20.249,10.84 19.909,10.5 19.499,10.5C19.089,10.5 18.749,10.84 18.749,11.25C18.749,11.66 19.089,12 19.499,12Z" />
</vector>
diff --git a/packages/SettingsLib/res/drawable/ic_mobile_1_4_bar.xml b/packages/SettingsLib/res/drawable/ic_mobile_1_4_bar.xml
index b9054ba7a4e3..df89aef3b63d 100644
--- a/packages/SettingsLib/res/drawable/ic_mobile_1_4_bar.xml
+++ b/packages/SettingsLib/res/drawable/ic_mobile_1_4_bar.xml
@@ -14,23 +14,23 @@
limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="14dp"
- android:height="14dp"
- android:viewportWidth="14.0"
- android:viewportHeight="14.0">
+ android:width="17dp"
+ android:height="12dp"
+ android:viewportWidth="16.0"
+ android:viewportHeight="12.0">
<path
- android:pathData="M8.25,3L9.25,3A0.5,0.5 0,0 1,9.75 3.5L9.75,13.5A0.5,0.5 0,0 1,9.25 14L8.25,14A0.5,0.5 0,0 1,7.75 13.5L7.75,3.5A0.5,0.5 0,0 1,8.25 3z"
- android:fillAlpha="0.24"
+ android:pathData="M1.249,7C1.939,7 2.499,7.56 2.499,8.25L2.499,10.75C2.499,11.44 1.939,12 1.249,12C0.559,12 -0.001,11.44 -0.001,10.75L-0.001,8.25C-0.001,7.56 0.559,7 1.249,7Z"
android:fillColor="#000"/>
<path
- android:pathData="M11.75,0L12.75,0A0.5,0.5 0,0 1,13.25 0.5L13.25,13.5A0.5,0.5 0,0 1,12.75 14L11.75,14A0.5,0.5 0,0 1,11.25 13.5L11.25,0.5A0.5,0.5 0,0 1,11.75 0z"
- android:fillAlpha="0.24"
+ android:pathData="M5.749,4.5C6.439,4.5 6.999,5.06 6.999,5.75L6.999,10.75C6.999,11.44 6.439,12 5.749,12C5.059,12 4.499,11.44 4.499,10.75L4.499,5.75C4.499,5.06 5.059,4.5 5.749,4.5Z"
+ android:fillAlpha="0.45"
android:fillColor="#000"/>
<path
- android:pathData="M1.25,10L2.25,10A0.5,0.5 0,0 1,2.75 10.5L2.75,13.5A0.5,0.5 0,0 1,2.25 14L1.25,14A0.5,0.5 0,0 1,0.75 13.5L0.75,10.5A0.5,0.5 0,0 1,1.25 10z"
+ android:pathData="M10.249,2C10.939,2 11.499,2.56 11.499,3.25L11.499,10.75C11.499,11.44 10.939,12 10.249,12C9.559,12 8.999,11.44 8.999,10.75L8.999,3.25C8.999,2.56 9.559,2 10.249,2Z"
+ android:fillAlpha="0.45"
android:fillColor="#000"/>
<path
- android:pathData="M4.75,6.5L5.75,6.5A0.5,0.5 0,0 1,6.25 7L6.25,13.5A0.5,0.5 0,0 1,5.75 14L4.75,14A0.5,0.5 0,0 1,4.25 13.5L4.25,7A0.5,0.5 0,0 1,4.75 6.5z"
- android:fillAlpha="0.24"
+ android:pathData="M14.749,0C15.439,0 15.999,0.56 15.999,1.25L15.999,10.75C15.999,11.44 15.439,12 14.749,12C14.059,12 13.499,11.44 13.499,10.75L13.499,1.25C13.499,0.56 14.059,0 14.749,0Z"
+ android:fillAlpha="0.45"
android:fillColor="#000"/>
</vector>
diff --git a/packages/SettingsLib/res/drawable/ic_mobile_1_4_bar_error.xml b/packages/SettingsLib/res/drawable/ic_mobile_1_4_bar_error.xml
index 03a93491491c..fb73b6b253e1 100644
--- a/packages/SettingsLib/res/drawable/ic_mobile_1_4_bar_error.xml
+++ b/packages/SettingsLib/res/drawable/ic_mobile_1_4_bar_error.xml
@@ -1,27 +1,35 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="15dp"
- android:height="14dp"
- android:viewportWidth="15.0"
- android:viewportHeight="14.0">
+ android:width="17dp"
+ android:height="12dp"
+ android:viewportWidth="16.0"
+ android:viewportHeight="12.0">
+ <group>
+ <!-- clip-out the circle which will contain the exclamation point (below this group) -->
+ <clip-path
+ android:pathData="
+ M0,0
+ V13.5,0
+ H13.5,20
+ V0,20
+ H0,0
+ M14.999,13.5C17.761,13.5 19.999,11.261 19.999,8.5C19.999,5.739 17.761,3.5 14.999,3.5C12.238,3.5 9.999,5.739 9.999,8.5C9.999,11.261 12.238,13.5 14.999,13.5Z" />
+ <path
+ android:pathData="M1.249,7C1.939,7 2.499,7.56 2.499,8.25L2.499,10.75C2.499,11.44 1.939,12 1.249,12C0.559,12 -0.001,11.44 -0.001,10.75L-0.001,8.25C-0.001,7.56 0.559,7 1.249,7Z"
+ android:fillColor="#000"/>
+ <path
+ android:pathData="M5.749,4.5C6.439,4.5 6.999,5.06 6.999,5.75L6.999,10.75C6.999,11.44 6.439,12 5.749,12C5.059,12 4.499,11.44 4.499,10.75L4.499,5.75C4.499,5.06 5.059,4.5 5.749,4.5Z"
+ android:fillAlpha="0.45"
+ android:fillColor="#000"/>
+ <path
+ android:pathData="M10.249,2C10.939,2 11.499,2.56 11.499,3.25L11.499,10.75C11.499,11.44 10.939,12 10.249,12C9.559,12 8.999,11.44 8.999,10.75L8.999,3.25C8.999,2.56 9.559,2 10.249,2Z"
+ android:fillAlpha="0.45"
+ android:fillColor="#000"/>
+ <path
+ android:pathData="M14.749,0C15.439,0 15.999,0.56 15.999,1.25L15.999,10.75C15.999,11.44 15.439,12 14.749,12C14.059,12 13.499,11.44 13.499,10.75L13.499,1.25C13.499,0.56 14.059,0 14.749,0Z"
+ android:fillAlpha="0.45"
+ android:fillColor="#000"/>
+ </group>
<path
- android:pathData="M7,3.5C7,3.224 7.224,3 7.5,3H8.5C8.776,3 9,3.224 9,3.5V13.5C9,13.776 8.776,14 8.5,14H7.5C7.224,14 7,13.776 7,13.5V3.5Z"
- android:fillAlpha="0.3"
- android:fillColor="#000"/>
- <path
- android:pathData="M0,10.5C0,10.224 0.224,10 0.5,10H1.5C1.776,10 2,10.224 2,10.5V13.5C2,13.776 1.776,14 1.5,14H0.5C0.224,14 0,13.776 0,13.5V10.5Z"
- android:fillColor="#000"/>
- <path
- android:pathData="M3.5,7C3.5,6.724 3.724,6.5 4,6.5H5C5.276,6.5 5.5,6.724 5.5,7V13.5C5.5,13.776 5.276,14 5,14H4C3.724,14 3.5,13.776 3.5,13.5V7Z"
- android:fillAlpha="0.3"
- android:fillColor="#000"/>
- <path
- android:pathData="M11,0C10.724,0 10.5,0.224 10.5,0.5V3H12.5V0.5C12.5,0.224 12.276,0 12,0H11Z"
- android:fillAlpha="0.3"
- android:fillColor="#000"/>
- <path
- android:pathData="M12.25,13C12.25,12.448 12.698,12 13.25,12C13.802,12 14.25,12.448 14.25,13C14.25,13.552 13.802,14 13.25,14C12.698,14 12.25,13.552 12.25,13Z"
- android:fillColor="#000"/>
- <path
- android:pathData="M12.25,5C12.25,4.724 12.474,4.5 12.75,4.5H13.75C14.026,4.5 14.25,4.724 14.25,5V10C14.25,10.276 14.026,10.5 13.75,10.5H12.75C12.474,10.5 12.25,10.276 12.25,10V5Z"
+ android:pathData="M14.999,5C14.589,5 14.249,5.34 14.249,5.75L14.249,8.75C14.249,9.16 14.589,9.5 14.999,9.5C15.409,9.5 15.749,9.16 15.749,8.75L15.749,5.75C15.749,5.34 15.409,5 14.999,5ZM14.999,12C15.409,12 15.749,11.66 15.749,11.25C15.749,10.84 15.409,10.5 14.999,10.5C14.589,10.5 14.249,10.84 14.249,11.25C14.249,11.66 14.589,12 14.999,12Z"
android:fillColor="#000"/>
</vector>
diff --git a/packages/SettingsLib/res/drawable/ic_mobile_1_5_bar.xml b/packages/SettingsLib/res/drawable/ic_mobile_1_5_bar.xml
index 774e91794df3..5b7d8daa74f2 100644
--- a/packages/SettingsLib/res/drawable/ic_mobile_1_5_bar.xml
+++ b/packages/SettingsLib/res/drawable/ic_mobile_1_5_bar.xml
@@ -14,27 +14,27 @@
limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="16dp"
- android:height="14dp"
- android:viewportWidth="16.0"
- android:viewportHeight="14.0">
+ android:width="21dp"
+ android:height="12dp"
+ android:viewportWidth="20.5"
+ android:viewportHeight="12.0">
<path
- android:pathData="M7.5,5L8.5,5A0.5,0.5 0,0 1,9 5.5L9,13.5A0.5,0.5 0,0 1,8.5 14L7.5,14A0.5,0.5 0,0 1,7 13.5L7,5.5A0.5,0.5 0,0 1,7.5 5z"
- android:fillAlpha="0.24"
+ android:pathData="M1.249,7C1.939,7 2.499,7.56 2.499,8.25L2.499,10.75C2.499,11.44 1.939,12 1.249,12C0.559,12 -0.001,11.44 -0.001,10.75L-0.001,8.25C-0.001,7.56 0.559,7 1.249,7Z"
android:fillColor="#000"/>
<path
- android:pathData="M11,2L12,2A0.5,0.5 0,0 1,12.5 2.5L12.5,13.5A0.5,0.5 0,0 1,12 14L11,14A0.5,0.5 0,0 1,10.5 13.5L10.5,2.5A0.5,0.5 0,0 1,11 2z"
- android:fillAlpha="0.24"
+ android:pathData="M5.749,5C6.439,5 6.999,5.56 6.999,6.25L6.999,10.75C6.999,11.44 6.439,12 5.749,12C5.059,12 4.499,11.44 4.499,10.75L4.499,6.25C4.499,5.56 5.059,5 5.749,5Z"
+ android:fillAlpha="0.45"
android:fillColor="#000"/>
<path
- android:pathData="M0.5,11L1.5,11A0.5,0.5 0,0 1,2 11.5L2,13.5A0.5,0.5 0,0 1,1.5 14L0.5,14A0.5,0.5 0,0 1,0 13.5L0,11.5A0.5,0.5 0,0 1,0.5 11z"
+ android:pathData="M10.249,3C10.939,3 11.499,3.56 11.499,4.25L11.499,10.75C11.499,11.44 10.939,12 10.249,12C9.559,12 8.999,11.44 8.999,10.75L8.999,4.25C8.999,3.56 9.559,3 10.249,3Z"
+ android:fillAlpha="0.45"
android:fillColor="#000"/>
<path
- android:pathData="M14.5,0L15.5,0A0.5,0.5 0,0 1,16 0.5L16,13.5A0.5,0.5 0,0 1,15.5 14L14.5,14A0.5,0.5 0,0 1,14 13.5L14,0.5A0.5,0.5 0,0 1,14.5 0z"
- android:fillAlpha="0.24"
+ android:pathData="M14.749,1.5C15.439,1.5 15.999,2.06 15.999,2.75L15.999,10.75C15.999,11.44 15.439,12 14.749,12C14.059,12 13.499,11.44 13.499,10.75L13.499,2.75C13.499,2.06 14.059,1.5 14.749,1.5Z"
+ android:fillAlpha="0.45"
android:fillColor="#000"/>
<path
- android:pathData="M4,8L5,8A0.5,0.5 0,0 1,5.5 8.5L5.5,13.5A0.5,0.5 0,0 1,5 14L4,14A0.5,0.5 0,0 1,3.5 13.5L3.5,8.5A0.5,0.5 0,0 1,4 8z"
- android:fillAlpha="0.24"
+ android:pathData="M19.249,0C19.939,0 20.499,0.56 20.499,1.25L20.499,10.75C20.499,11.44 19.939,12 19.249,12C18.559,12 17.999,11.44 17.999,10.75L17.999,1.25C17.999,0.56 18.559,0 19.249,0Z"
+ android:fillAlpha="0.45"
android:fillColor="#000"/>
</vector>
diff --git a/packages/SettingsLib/res/drawable/ic_mobile_1_5_bar_error.xml b/packages/SettingsLib/res/drawable/ic_mobile_1_5_bar_error.xml
index 343ec1b2e50f..27e233d244c7 100644
--- a/packages/SettingsLib/res/drawable/ic_mobile_1_5_bar_error.xml
+++ b/packages/SettingsLib/res/drawable/ic_mobile_1_5_bar_error.xml
@@ -1,31 +1,53 @@
+<!--
+ Copyright (C) 2025 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="18dp"
- android:height="14dp"
- android:viewportWidth="18.0"
- android:viewportHeight="14.0">
+ android:width="21dp"
+ android:height="12dp"
+ android:viewportHeight="12.0"
+ android:viewportWidth="20.5">
+ <!-- clip-out the circle which will contain the exclamation point (below this group) -->
+ <group>
+ <clip-path android:pathData="
+ M0,0
+ H20.5
+ V12.0
+ H0
+ Z
+ M19.499,13.5C22.261,13.5 24.499,11.261 24.499,8.5C24.499,5.739 22.261,3.5 19.499,3.5C16.738,3.5 14.499,5.739 14.499,8.5C14.499,11.261 16.738,13.5 19.499,13.5Z" />
+ <path
+ android:fillColor="#000"
+ android:pathData="M1.249,7C1.939,7 2.499,7.56 2.499,8.25L2.499,10.75C2.499,11.44 1.939,12 1.249,12C0.559,12 -0.001,11.44 -0.001,10.75L-0.001,8.25C-0.001,7.56 0.559,7 1.249,7Z" />
+ <path
+ android:fillAlpha="0.45"
+ android:fillColor="#000"
+ android:pathData="M5.749,5C6.439,5 6.999,5.56 6.999,6.25L6.999,10.75C6.999,11.44 6.439,12 5.749,12C5.059,12 4.499,11.44 4.499,10.75L4.499,6.25C4.499,5.56 5.059,5 5.749,5Z" />
+ <path
+ android:fillAlpha="0.45"
+ android:fillColor="#000"
+ android:pathData="M10.249,3C10.939,3 11.499,3.56 11.499,4.25L11.499,10.75C11.499,11.44 10.939,12 10.249,12C9.559,12 8.999,11.44 8.999,10.75L8.999,4.25C8.999,3.56 9.559,3 10.249,3Z" />
+ <path
+ android:fillAlpha="0.45"
+ android:fillColor="#000"
+ android:pathData="M14.749,1.5C15.439,1.5 15.999,2.06 15.999,2.75L15.999,10.75C15.999,11.44 15.439,12 14.749,12C14.059,12 13.499,11.44 13.499,10.75L13.499,2.75C13.499,2.06 14.059,1.5 14.749,1.5Z" />
+ <path
+ android:fillAlpha="0.45"
+ android:fillColor="#000"
+ android:pathData="M19.249,0C19.939,0 20.499,0.56 20.499,1.25L20.499,10.75C20.499,11.44 19.939,12 19.249,12C18.559,12 17.999,11.44 17.999,10.75L17.999,1.25C17.999,0.56 18.559,0 19.249,0Z" />
+ </group>
<path
- android:pathData="M7,5.5C7,5.224 7.224,5 7.5,5H8.5C8.776,5 9,5.224 9,5.5V13.5C9,13.776 8.776,14 8.5,14H7.5C7.224,14 7,13.776 7,13.5V5.5Z"
- android:fillAlpha="0.3"
- android:fillColor="#000"/>
- <path
- android:pathData="M10.5,2.5C10.5,2.224 10.724,2 11,2H12C12.276,2 12.5,2.224 12.5,2.5V13.5C12.5,13.776 12.276,14 12,14H11C10.724,14 10.5,13.776 10.5,13.5V2.5Z"
- android:fillAlpha="0.3"
- android:fillColor="#000"/>
- <path
- android:pathData="M0,11.5C0,11.224 0.224,11 0.5,11H1.5C1.776,11 2,11.224 2,11.5V13.5C2,13.776 1.776,14 1.5,14H0.5C0.224,14 0,13.776 0,13.5V11.5Z"
- android:fillColor="#000"/>
- <path
- android:pathData="M3.5,8.5C3.5,8.224 3.724,8 4,8H5C5.276,8 5.5,8.224 5.5,8.5V13.5C5.5,13.776 5.276,14 5,14H4C3.724,14 3.5,13.776 3.5,13.5V8.5Z"
- android:fillAlpha="0.3"
- android:fillColor="#000"/>
- <path
- android:pathData="M14.5,0C14.224,0 14,0.224 14,0.5V3H16V0.5C16,0.224 15.776,0 15.5,0H14.5Z"
- android:fillAlpha="0.3"
- android:fillColor="#000"/>
- <path
- android:pathData="M16,13C16,12.448 16.448,12 17,12C17.552,12 18,12.448 18,13C18,13.552 17.552,14 17,14C16.448,14 16,13.552 16,13Z"
- android:fillColor="#000"/>
- <path
- android:pathData="M16,5C16,4.724 16.224,4.5 16.5,4.5H17.5C17.776,4.5 18,4.724 18,5V10C18,10.276 17.776,10.5 17.5,10.5H16.5C16.224,10.5 16,10.276 16,10V5Z"
- android:fillColor="#000"/>
+ android:fillColor="#000"
+ android:pathData="M19.499,5C19.089,5 18.749,5.34 18.749,5.75L18.749,8.75C18.749,9.16 19.089,9.5 19.499,9.5C19.909,9.5 20.249,9.16 20.249,8.75L20.249,5.75C20.249,5.34 19.909,5 19.499,5ZM19.499,12C19.909,12 20.249,11.66 20.249,11.25C20.249,10.84 19.909,10.5 19.499,10.5C19.089,10.5 18.749,10.84 18.749,11.25C18.749,11.66 19.089,12 19.499,12Z" />
</vector>
diff --git a/packages/SettingsLib/res/drawable/ic_mobile_2_4_bar.xml b/packages/SettingsLib/res/drawable/ic_mobile_2_4_bar.xml
index b699203dd652..e7ebf6f6883a 100644
--- a/packages/SettingsLib/res/drawable/ic_mobile_2_4_bar.xml
+++ b/packages/SettingsLib/res/drawable/ic_mobile_2_4_bar.xml
@@ -1,5 +1,5 @@
<!--
- Copyright (C) 2023 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,22 +14,22 @@
limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="14dp"
- android:height="14dp"
- android:viewportWidth="14.0"
- android:viewportHeight="14.0">
+ android:width="17dp"
+ android:height="12dp"
+ android:viewportWidth="16.0"
+ android:viewportHeight="12.0">
<path
- android:pathData="M8.25,3L9.25,3A0.5,0.5 0,0 1,9.75 3.5L9.75,13.5A0.5,0.5 0,0 1,9.25 14L8.25,14A0.5,0.5 0,0 1,7.75 13.5L7.75,3.5A0.5,0.5 0,0 1,8.25 3z"
- android:fillAlpha="0.24"
+ android:pathData="M1.249,7C1.939,7 2.499,7.56 2.499,8.25L2.499,10.75C2.499,11.44 1.939,12 1.249,12C0.559,12 -0.001,11.44 -0.001,10.75L-0.001,8.25C-0.001,7.56 0.559,7 1.249,7Z"
android:fillColor="#000"/>
<path
- android:pathData="M11.75,0L12.75,0A0.5,0.5 0,0 1,13.25 0.5L13.25,13.5A0.5,0.5 0,0 1,12.75 14L11.75,14A0.5,0.5 0,0 1,11.25 13.5L11.25,0.5A0.5,0.5 0,0 1,11.75 0z"
- android:fillAlpha="0.24"
+ android:pathData="M5.749,4.5C6.439,4.5 6.999,5.06 6.999,5.75L6.999,10.75C6.999,11.44 6.439,12 5.749,12C5.059,12 4.499,11.44 4.499,10.75L4.499,5.75C4.499,5.06 5.059,4.5 5.749,4.5Z"
android:fillColor="#000"/>
<path
- android:pathData="M1.25,10L2.25,10A0.5,0.5 0,0 1,2.75 10.5L2.75,13.5A0.5,0.5 0,0 1,2.25 14L1.25,14A0.5,0.5 0,0 1,0.75 13.5L0.75,10.5A0.5,0.5 0,0 1,1.25 10z"
+ android:pathData="M10.249,2C10.939,2 11.499,2.56 11.499,3.25L11.499,10.75C11.499,11.44 10.939,12 10.249,12C9.559,12 8.999,11.44 8.999,10.75L8.999,3.25C8.999,2.56 9.559,2 10.249,2Z"
+ android:fillAlpha="0.45"
android:fillColor="#000"/>
<path
- android:pathData="M4.75,6.5L5.75,6.5A0.5,0.5 0,0 1,6.25 7L6.25,13.5A0.5,0.5 0,0 1,5.75 14L4.75,14A0.5,0.5 0,0 1,4.25 13.5L4.25,7A0.5,0.5 0,0 1,4.75 6.5z"
+ android:pathData="M14.749,0C15.439,0 15.999,0.56 15.999,1.25L15.999,10.75C15.999,11.44 15.439,12 14.749,12C14.059,12 13.499,11.44 13.499,10.75L13.499,1.25C13.499,0.56 14.059,0 14.749,0Z"
+ android:fillAlpha="0.45"
android:fillColor="#000"/>
</vector>
diff --git a/packages/SettingsLib/res/drawable/ic_mobile_2_4_bar_error.xml b/packages/SettingsLib/res/drawable/ic_mobile_2_4_bar_error.xml
index ba8649b23b38..49ae9e4ef17f 100644
--- a/packages/SettingsLib/res/drawable/ic_mobile_2_4_bar_error.xml
+++ b/packages/SettingsLib/res/drawable/ic_mobile_2_4_bar_error.xml
@@ -1,26 +1,49 @@
+<!--
+ Copyright (C) 2025 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="15dp"
- android:height="14dp"
- android:viewportWidth="15.0"
- android:viewportHeight="14.0">
+ android:width="17dp"
+ android:height="12dp"
+ android:viewportWidth="16.0"
+ android:viewportHeight="12.0">
+ <group>
+ <!-- clip-out the circle which will contain the exclamation point (below this group) -->
+ <clip-path
+ android:pathData="
+ M0,0
+ V13.5,0
+ H13.5,20
+ V0,20
+ H0,0
+ M14.999,13.5C17.761,13.5 19.999,11.261 19.999,8.5C19.999,5.739 17.761,3.5 14.999,3.5C12.238,3.5 9.999,5.739 9.999,8.5C9.999,11.261 12.238,13.5 14.999,13.5Z" />
+ <path
+ android:pathData="M1.249,7C1.939,7 2.499,7.56 2.499,8.25L2.499,10.75C2.499,11.44 1.939,12 1.249,12C0.559,12 -0.001,11.44 -0.001,10.75L-0.001,8.25C-0.001,7.56 0.559,7 1.249,7Z"
+ android:fillColor="#000"/>
+ <path
+ android:pathData="M5.749,4.5C6.439,4.5 6.999,5.06 6.999,5.75L6.999,10.75C6.999,11.44 6.439,12 5.749,12C5.059,12 4.499,11.44 4.499,10.75L4.499,5.75C4.499,5.06 5.059,4.5 5.749,4.5Z"
+ android:fillColor="#000"/>
+ <path
+ android:pathData="M10.249,2C10.939,2 11.499,2.56 11.499,3.25L11.499,10.75C11.499,11.44 10.939,12 10.249,12C9.559,12 8.999,11.44 8.999,10.75L8.999,3.25C8.999,2.56 9.559,2 10.249,2Z"
+ android:fillAlpha="0.45"
+ android:fillColor="#000"/>
+ <path
+ android:pathData="M14.749,0C15.439,0 15.999,0.56 15.999,1.25L15.999,10.75C15.999,11.44 15.439,12 14.749,12C14.059,12 13.499,11.44 13.499,10.75L13.499,1.25C13.499,0.56 14.059,0 14.749,0Z"
+ android:fillAlpha="0.45"
+ android:fillColor="#000"/>
+ </group>
<path
- android:pathData="M7,3.5C7,3.224 7.224,3 7.5,3H8.5C8.776,3 9,3.224 9,3.5V13.5C9,13.776 8.776,14 8.5,14H7.5C7.224,14 7,13.776 7,13.5V3.5Z"
- android:fillAlpha="0.3"
- android:fillColor="#000"/>
- <path
- android:pathData="M0,10.5C0,10.224 0.224,10 0.5,10H1.5C1.776,10 2,10.224 2,10.5V13.5C2,13.776 1.776,14 1.5,14H0.5C0.224,14 0,13.776 0,13.5V10.5Z"
- android:fillColor="#000"/>
- <path
- android:pathData="M3.5,7C3.5,6.724 3.724,6.5 4,6.5H5C5.276,6.5 5.5,6.724 5.5,7V13.5C5.5,13.776 5.276,14 5,14H4C3.724,14 3.5,13.776 3.5,13.5V7Z"
- android:fillColor="#000"/>
- <path
- android:pathData="M11,0C10.724,0 10.5,0.224 10.5,0.5V3H12.5V0.5C12.5,0.224 12.276,0 12,0H11Z"
- android:fillAlpha="0.3"
- android:fillColor="#000"/>
- <path
- android:pathData="M12.25,13C12.25,12.448 12.698,12 13.25,12C13.802,12 14.25,12.448 14.25,13C14.25,13.552 13.802,14 13.25,14C12.698,14 12.25,13.552 12.25,13Z"
- android:fillColor="#000"/>
- <path
- android:pathData="M12.25,5C12.25,4.724 12.474,4.5 12.75,4.5H13.75C14.026,4.5 14.25,4.724 14.25,5V10C14.25,10.276 14.026,10.5 13.75,10.5H12.75C12.474,10.5 12.25,10.276 12.25,10V5Z"
+ android:pathData="M14.999,5C14.589,5 14.249,5.34 14.249,5.75L14.249,8.75C14.249,9.16 14.589,9.5 14.999,9.5C15.409,9.5 15.749,9.16 15.749,8.75L15.749,5.75C15.749,5.34 15.409,5 14.999,5ZM14.999,12C15.409,12 15.749,11.66 15.749,11.25C15.749,10.84 15.409,10.5 14.999,10.5C14.589,10.5 14.249,10.84 14.249,11.25C14.249,11.66 14.589,12 14.999,12Z"
android:fillColor="#000"/>
</vector>
diff --git a/packages/SettingsLib/res/drawable/ic_mobile_2_5_bar.xml b/packages/SettingsLib/res/drawable/ic_mobile_2_5_bar.xml
index 43fa734c0c8d..19387bc6c75c 100644
--- a/packages/SettingsLib/res/drawable/ic_mobile_2_5_bar.xml
+++ b/packages/SettingsLib/res/drawable/ic_mobile_2_5_bar.xml
@@ -1,5 +1,5 @@
<!--
- Copyright (C) 2023 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,26 +14,26 @@
limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="16dp"
- android:height="14dp"
- android:viewportWidth="16.0"
- android:viewportHeight="14.0">
+ android:width="21dp"
+ android:height="12dp"
+ android:viewportWidth="20.5"
+ android:viewportHeight="12.0">
<path
- android:pathData="M7.5,5L8.5,5A0.5,0.5 0,0 1,9 5.5L9,13.5A0.5,0.5 0,0 1,8.5 14L7.5,14A0.5,0.5 0,0 1,7 13.5L7,5.5A0.5,0.5 0,0 1,7.5 5z"
- android:fillAlpha="0.24"
+ android:pathData="M1.249,7C1.939,7 2.499,7.56 2.499,8.25L2.499,10.75C2.499,11.44 1.939,12 1.249,12C0.559,12 -0.001,11.44 -0.001,10.75L-0.001,8.25C-0.001,7.56 0.559,7 1.249,7Z"
android:fillColor="#000"/>
<path
- android:pathData="M11,2L12,2A0.5,0.5 0,0 1,12.5 2.5L12.5,13.5A0.5,0.5 0,0 1,12 14L11,14A0.5,0.5 0,0 1,10.5 13.5L10.5,2.5A0.5,0.5 0,0 1,11 2z"
- android:fillAlpha="0.24"
+ android:pathData="M5.749,5C6.439,5 6.999,5.56 6.999,6.25L6.999,10.75C6.999,11.44 6.439,12 5.749,12C5.059,12 4.499,11.44 4.499,10.75L4.499,6.25C4.499,5.56 5.059,5 5.749,5Z"
android:fillColor="#000"/>
<path
- android:pathData="M0.5,11L1.5,11A0.5,0.5 0,0 1,2 11.5L2,13.5A0.5,0.5 0,0 1,1.5 14L0.5,14A0.5,0.5 0,0 1,0 13.5L0,11.5A0.5,0.5 0,0 1,0.5 11z"
+ android:pathData="M10.249,3C10.939,3 11.499,3.56 11.499,4.25L11.499,10.75C11.499,11.44 10.939,12 10.249,12C9.559,12 8.999,11.44 8.999,10.75L8.999,4.25C8.999,3.56 9.559,3 10.249,3Z"
+ android:fillAlpha="0.45"
android:fillColor="#000"/>
<path
- android:pathData="M14.5,0L15.5,0A0.5,0.5 0,0 1,16 0.5L16,13.5A0.5,0.5 0,0 1,15.5 14L14.5,14A0.5,0.5 0,0 1,14 13.5L14,0.5A0.5,0.5 0,0 1,14.5 0z"
- android:fillAlpha="0.24"
+ android:pathData="M14.749,1.5C15.439,1.5 15.999,2.06 15.999,2.75L15.999,10.75C15.999,11.44 15.439,12 14.749,12C14.059,12 13.499,11.44 13.499,10.75L13.499,2.75C13.499,2.06 14.059,1.5 14.749,1.5Z"
+ android:fillAlpha="0.45"
android:fillColor="#000"/>
<path
- android:pathData="M4,8L5,8A0.5,0.5 0,0 1,5.5 8.5L5.5,13.5A0.5,0.5 0,0 1,5 14L4,14A0.5,0.5 0,0 1,3.5 13.5L3.5,8.5A0.5,0.5 0,0 1,4 8z"
+ android:pathData="M19.249,0C19.939,0 20.499,0.56 20.499,1.25L20.499,10.75C20.499,11.44 19.939,12 19.249,12C18.559,12 17.999,11.44 17.999,10.75L17.999,1.25C17.999,0.56 18.559,0 19.249,0Z"
+ android:fillAlpha="0.45"
android:fillColor="#000"/>
</vector>
diff --git a/packages/SettingsLib/res/drawable/ic_mobile_2_5_bar_error.xml b/packages/SettingsLib/res/drawable/ic_mobile_2_5_bar_error.xml
index 6309e1772d4a..322ede67de5b 100644
--- a/packages/SettingsLib/res/drawable/ic_mobile_2_5_bar_error.xml
+++ b/packages/SettingsLib/res/drawable/ic_mobile_2_5_bar_error.xml
@@ -1,30 +1,52 @@
+<!--
+ Copyright (C) 2025 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="18dp"
- android:height="14dp"
- android:viewportWidth="18.0"
- android:viewportHeight="14.0">
+ android:width="21dp"
+ android:height="12dp"
+ android:viewportHeight="12.0"
+ android:viewportWidth="20.5">
+ <!-- clip-out the circle which will contain the exclamation point (below this group) -->
+ <group>
+ <clip-path android:pathData="
+ M0,0
+ H20.5
+ V12.0
+ H0
+ Z
+ M19.499,13.5C22.261,13.5 24.499,11.261 24.499,8.5C24.499,5.739 22.261,3.5 19.499,3.5C16.738,3.5 14.499,5.739 14.499,8.5C14.499,11.261 16.738,13.5 19.499,13.5Z" />
+ <path
+ android:fillColor="#000"
+ android:pathData="M1.249,7C1.939,7 2.499,7.56 2.499,8.25L2.499,10.75C2.499,11.44 1.939,12 1.249,12C0.559,12 -0.001,11.44 -0.001,10.75L-0.001,8.25C-0.001,7.56 0.559,7 1.249,7Z" />
+ <path
+ android:fillColor="#000"
+ android:pathData="M5.749,5C6.439,5 6.999,5.56 6.999,6.25L6.999,10.75C6.999,11.44 6.439,12 5.749,12C5.059,12 4.499,11.44 4.499,10.75L4.499,6.25C4.499,5.56 5.059,5 5.749,5Z" />
+ <path
+ android:fillAlpha="0.45"
+ android:fillColor="#000"
+ android:pathData="M10.249,3C10.939,3 11.499,3.56 11.499,4.25L11.499,10.75C11.499,11.44 10.939,12 10.249,12C9.559,12 8.999,11.44 8.999,10.75L8.999,4.25C8.999,3.56 9.559,3 10.249,3Z" />
+ <path
+ android:fillAlpha="0.45"
+ android:fillColor="#000"
+ android:pathData="M14.749,1.5C15.439,1.5 15.999,2.06 15.999,2.75L15.999,10.75C15.999,11.44 15.439,12 14.749,12C14.059,12 13.499,11.44 13.499,10.75L13.499,2.75C13.499,2.06 14.059,1.5 14.749,1.5Z" />
+ <path
+ android:fillAlpha="0.45"
+ android:fillColor="#000"
+ android:pathData="M19.249,0C19.939,0 20.499,0.56 20.499,1.25L20.499,10.75C20.499,11.44 19.939,12 19.249,12C18.559,12 17.999,11.44 17.999,10.75L17.999,1.25C17.999,0.56 18.559,0 19.249,0Z" />
+ </group>
<path
- android:pathData="M7,5.5C7,5.224 7.224,5 7.5,5H8.5C8.776,5 9,5.224 9,5.5V13.5C9,13.776 8.776,14 8.5,14H7.5C7.224,14 7,13.776 7,13.5V5.5Z"
- android:fillAlpha="0.3"
- android:fillColor="#000"/>
- <path
- android:pathData="M10.5,2.5C10.5,2.224 10.724,2 11,2H12C12.276,2 12.5,2.224 12.5,2.5V13.5C12.5,13.776 12.276,14 12,14H11C10.724,14 10.5,13.776 10.5,13.5V2.5Z"
- android:fillAlpha="0.3"
- android:fillColor="#000"/>
- <path
- android:pathData="M0,11.5C0,11.224 0.224,11 0.5,11H1.5C1.776,11 2,11.224 2,11.5V13.5C2,13.776 1.776,14 1.5,14H0.5C0.224,14 0,13.776 0,13.5V11.5Z"
- android:fillColor="#000"/>
- <path
- android:pathData="M3.5,8.5C3.5,8.224 3.724,8 4,8H5C5.276,8 5.5,8.224 5.5,8.5V13.5C5.5,13.776 5.276,14 5,14H4C3.724,14 3.5,13.776 3.5,13.5V8.5Z"
- android:fillColor="#000"/>
- <path
- android:pathData="M14.5,0C14.224,0 14,0.224 14,0.5V3H16V0.5C16,0.224 15.776,0 15.5,0H14.5Z"
- android:fillAlpha="0.3"
- android:fillColor="#000"/>
- <path
- android:pathData="M16,13C16,12.448 16.448,12 17,12C17.552,12 18,12.448 18,13C18,13.552 17.552,14 17,14C16.448,14 16,13.552 16,13Z"
- android:fillColor="#000"/>
- <path
- android:pathData="M16,5C16,4.724 16.224,4.5 16.5,4.5H17.5C17.776,4.5 18,4.724 18,5V10C18,10.276 17.776,10.5 17.5,10.5H16.5C16.224,10.5 16,10.276 16,10V5Z"
- android:fillColor="#000"/>
+ android:fillColor="#000"
+ android:pathData="M19.499,5C19.089,5 18.749,5.34 18.749,5.75L18.749,8.75C18.749,9.16 19.089,9.5 19.499,9.5C19.909,9.5 20.249,9.16 20.249,8.75L20.249,5.75C20.249,5.34 19.909,5 19.499,5ZM19.499,12C19.909,12 20.249,11.66 20.249,11.25C20.249,10.84 19.909,10.5 19.499,10.5C19.089,10.5 18.749,10.84 18.749,11.25C18.749,11.66 19.089,12 19.499,12Z" />
</vector>
diff --git a/packages/SettingsLib/res/drawable/ic_mobile_3_4_bar.xml b/packages/SettingsLib/res/drawable/ic_mobile_3_4_bar.xml
index 6a218b310b3a..b84b6583e8aa 100644
--- a/packages/SettingsLib/res/drawable/ic_mobile_3_4_bar.xml
+++ b/packages/SettingsLib/res/drawable/ic_mobile_3_4_bar.xml
@@ -1,5 +1,5 @@
<!--
- Copyright (C) 2023 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,21 +14,21 @@
limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="14dp"
- android:height="14dp"
- android:viewportWidth="14.0"
- android:viewportHeight="14.0">
+ android:width="17dp"
+ android:height="12dp"
+ android:viewportWidth="16.0"
+ android:viewportHeight="12.0">
<path
- android:pathData="M8.25,3L9.25,3A0.5,0.5 0,0 1,9.75 3.5L9.75,13.5A0.5,0.5 0,0 1,9.25 14L8.25,14A0.5,0.5 0,0 1,7.75 13.5L7.75,3.5A0.5,0.5 0,0 1,8.25 3z"
+ android:pathData="M1.249,7C1.939,7 2.499,7.56 2.499,8.25L2.499,10.75C2.499,11.44 1.939,12 1.249,12C0.559,12 -0.001,11.44 -0.001,10.75L-0.001,8.25C-0.001,7.56 0.559,7 1.249,7Z"
android:fillColor="#000"/>
<path
- android:pathData="M11.75,0L12.75,0A0.5,0.5 0,0 1,13.25 0.5L13.25,13.5A0.5,0.5 0,0 1,12.75 14L11.75,14A0.5,0.5 0,0 1,11.25 13.5L11.25,0.5A0.5,0.5 0,0 1,11.75 0z"
- android:fillAlpha="0.24"
+ android:pathData="M5.749,4.5C6.439,4.5 6.999,5.06 6.999,5.75L6.999,10.75C6.999,11.44 6.439,12 5.749,12C5.059,12 4.499,11.44 4.499,10.75L4.499,5.75C4.499,5.06 5.059,4.5 5.749,4.5Z"
android:fillColor="#000"/>
<path
- android:pathData="M1.25,10L2.25,10A0.5,0.5 0,0 1,2.75 10.5L2.75,13.5A0.5,0.5 0,0 1,2.25 14L1.25,14A0.5,0.5 0,0 1,0.75 13.5L0.75,10.5A0.5,0.5 0,0 1,1.25 10z"
+ android:pathData="M10.249,2C10.939,2 11.499,2.56 11.499,3.25L11.499,10.75C11.499,11.44 10.939,12 10.249,12C9.559,12 8.999,11.44 8.999,10.75L8.999,3.25C8.999,2.56 9.559,2 10.249,2Z"
android:fillColor="#000"/>
<path
- android:pathData="M4.75,6.5L5.75,6.5A0.5,0.5 0,0 1,6.25 7L6.25,13.5A0.5,0.5 0,0 1,5.75 14L4.75,14A0.5,0.5 0,0 1,4.25 13.5L4.25,7A0.5,0.5 0,0 1,4.75 6.5z"
+ android:pathData="M14.749,0C15.439,0 15.999,0.56 15.999,1.25L15.999,10.75C15.999,11.44 15.439,12 14.749,12C14.059,12 13.499,11.44 13.499,10.75L13.499,1.25C13.499,0.56 14.059,0 14.749,0Z"
+ android:fillAlpha="0.45"
android:fillColor="#000"/>
</vector>
diff --git a/packages/SettingsLib/res/drawable/ic_mobile_3_4_bar_error.xml b/packages/SettingsLib/res/drawable/ic_mobile_3_4_bar_error.xml
index 27433c79e8bb..7c4c1c6b1126 100644
--- a/packages/SettingsLib/res/drawable/ic_mobile_3_4_bar_error.xml
+++ b/packages/SettingsLib/res/drawable/ic_mobile_3_4_bar_error.xml
@@ -1,25 +1,48 @@
+<!--
+ Copyright (C) 2025 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="15dp"
- android:height="14dp"
- android:viewportWidth="15.0"
- android:viewportHeight="14.0">
+ android:width="17dp"
+ android:height="12dp"
+ android:viewportWidth="16.0"
+ android:viewportHeight="12.0">
+ <group>
+ <!-- clip-out the circle which will contain the exclamation point (below this group) -->
+ <clip-path
+ android:pathData="
+ M0,0
+ V13.5,0
+ H13.5,20
+ V0,20
+ H0,0
+ M14.999,13.5C17.761,13.5 19.999,11.261 19.999,8.5C19.999,5.739 17.761,3.5 14.999,3.5C12.238,3.5 9.999,5.739 9.999,8.5C9.999,11.261 12.238,13.5 14.999,13.5Z" />
+ <path
+ android:pathData="M1.249,7C1.939,7 2.499,7.56 2.499,8.25L2.499,10.75C2.499,11.44 1.939,12 1.249,12C0.559,12 -0.001,11.44 -0.001,10.75L-0.001,8.25C-0.001,7.56 0.559,7 1.249,7Z"
+ android:fillColor="#000"/>
+ <path
+ android:pathData="M5.749,4.5C6.439,4.5 6.999,5.06 6.999,5.75L6.999,10.75C6.999,11.44 6.439,12 5.749,12C5.059,12 4.499,11.44 4.499,10.75L4.499,5.75C4.499,5.06 5.059,4.5 5.749,4.5Z"
+ android:fillColor="#000"/>
+ <path
+ android:pathData="M10.249,2C10.939,2 11.499,2.56 11.499,3.25L11.499,10.75C11.499,11.44 10.939,12 10.249,12C9.559,12 8.999,11.44 8.999,10.75L8.999,3.25C8.999,2.56 9.559,2 10.249,2Z"
+ android:fillColor="#000"/>
+ <path
+ android:pathData="M14.749,0C15.439,0 15.999,0.56 15.999,1.25L15.999,10.75C15.999,11.44 15.439,12 14.749,12C14.059,12 13.499,11.44 13.499,10.75L13.499,1.25C13.499,0.56 14.059,0 14.749,0Z"
+ android:fillAlpha="0.45"
+ android:fillColor="#000"/>
+ </group>
<path
- android:pathData="M7,3.5C7,3.224 7.224,3 7.5,3H8.5C8.776,3 9,3.224 9,3.5V13.5C9,13.776 8.776,14 8.5,14H7.5C7.224,14 7,13.776 7,13.5V3.5Z"
- android:fillColor="#000"/>
- <path
- android:pathData="M0,10.5C0,10.224 0.224,10 0.5,10H1.5C1.776,10 2,10.224 2,10.5V13.5C2,13.776 1.776,14 1.5,14H0.5C0.224,14 0,13.776 0,13.5V10.5Z"
- android:fillColor="#000"/>
- <path
- android:pathData="M3.5,7C3.5,6.724 3.724,6.5 4,6.5H5C5.276,6.5 5.5,6.724 5.5,7V13.5C5.5,13.776 5.276,14 5,14H4C3.724,14 3.5,13.776 3.5,13.5V7Z"
- android:fillColor="#000"/>
- <path
- android:pathData="M11,0C10.724,0 10.5,0.224 10.5,0.5V3H12.5V0.5C12.5,0.224 12.276,0 12,0H11Z"
- android:fillAlpha="0.3"
- android:fillColor="#000"/>
- <path
- android:pathData="M12.25,13C12.25,12.448 12.698,12 13.25,12C13.802,12 14.25,12.448 14.25,13C14.25,13.552 13.802,14 13.25,14C12.698,14 12.25,13.552 12.25,13Z"
- android:fillColor="#000"/>
- <path
- android:pathData="M12.25,5C12.25,4.724 12.474,4.5 12.75,4.5H13.75C14.026,4.5 14.25,4.724 14.25,5V10C14.25,10.276 14.026,10.5 13.75,10.5H12.75C12.474,10.5 12.25,10.276 12.25,10V5Z"
+ android:pathData="M14.999,5C14.589,5 14.249,5.34 14.249,5.75L14.249,8.75C14.249,9.16 14.589,9.5 14.999,9.5C15.409,9.5 15.749,9.16 15.749,8.75L15.749,5.75C15.749,5.34 15.409,5 14.999,5ZM14.999,12C15.409,12 15.749,11.66 15.749,11.25C15.749,10.84 15.409,10.5 14.999,10.5C14.589,10.5 14.249,10.84 14.249,11.25C14.249,11.66 14.589,12 14.999,12Z"
android:fillColor="#000"/>
</vector>
diff --git a/packages/SettingsLib/res/drawable/ic_mobile_3_5_bar.xml b/packages/SettingsLib/res/drawable/ic_mobile_3_5_bar.xml
index 158ae016ffb5..973032f3c237 100644
--- a/packages/SettingsLib/res/drawable/ic_mobile_3_5_bar.xml
+++ b/packages/SettingsLib/res/drawable/ic_mobile_3_5_bar.xml
@@ -1,5 +1,5 @@
<!--
- Copyright (C) 2023 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,25 +14,25 @@
limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="16dp"
- android:height="14dp"
- android:viewportWidth="16.0"
- android:viewportHeight="14.0">
+ android:width="21dp"
+ android:height="12dp"
+ android:viewportWidth="20.5"
+ android:viewportHeight="12.0">
<path
- android:pathData="M7.5,5L8.5,5A0.5,0.5 0,0 1,9 5.5L9,13.5A0.5,0.5 0,0 1,8.5 14L7.5,14A0.5,0.5 0,0 1,7 13.5L7,5.5A0.5,0.5 0,0 1,7.5 5z"
+ android:pathData="M1.249,7C1.939,7 2.499,7.56 2.499,8.25L2.499,10.75C2.499,11.44 1.939,12 1.249,12C0.559,12 -0.001,11.44 -0.001,10.75L-0.001,8.25C-0.001,7.56 0.559,7 1.249,7Z"
android:fillColor="#000"/>
<path
- android:pathData="M11,2L12,2A0.5,0.5 0,0 1,12.5 2.5L12.5,13.5A0.5,0.5 0,0 1,12 14L11,14A0.5,0.5 0,0 1,10.5 13.5L10.5,2.5A0.5,0.5 0,0 1,11 2z"
- android:fillAlpha="0.24"
+ android:pathData="M5.749,5C6.439,5 6.999,5.56 6.999,6.25L6.999,10.75C6.999,11.44 6.439,12 5.749,12C5.059,12 4.499,11.44 4.499,10.75L4.499,6.25C4.499,5.56 5.059,5 5.749,5Z"
android:fillColor="#000"/>
<path
- android:pathData="M0.5,11L1.5,11A0.5,0.5 0,0 1,2 11.5L2,13.5A0.5,0.5 0,0 1,1.5 14L0.5,14A0.5,0.5 0,0 1,0 13.5L0,11.5A0.5,0.5 0,0 1,0.5 11z"
+ android:pathData="M10.249,3C10.939,3 11.499,3.56 11.499,4.25L11.499,10.75C11.499,11.44 10.939,12 10.249,12C9.559,12 8.999,11.44 8.999,10.75L8.999,4.25C8.999,3.56 9.559,3 10.249,3Z"
android:fillColor="#000"/>
<path
- android:pathData="M14.5,0L15.5,0A0.5,0.5 0,0 1,16 0.5L16,13.5A0.5,0.5 0,0 1,15.5 14L14.5,14A0.5,0.5 0,0 1,14 13.5L14,0.5A0.5,0.5 0,0 1,14.5 0z"
- android:fillAlpha="0.24"
+ android:pathData="M14.749,1.5C15.439,1.5 15.999,2.06 15.999,2.75L15.999,10.75C15.999,11.44 15.439,12 14.749,12C14.059,12 13.499,11.44 13.499,10.75L13.499,2.75C13.499,2.06 14.059,1.5 14.749,1.5Z"
+ android:fillAlpha="0.45"
android:fillColor="#000"/>
<path
- android:pathData="M4,8L5,8A0.5,0.5 0,0 1,5.5 8.5L5.5,13.5A0.5,0.5 0,0 1,5 14L4,14A0.5,0.5 0,0 1,3.5 13.5L3.5,8.5A0.5,0.5 0,0 1,4 8z"
+ android:pathData="M19.249,0C19.939,0 20.499,0.56 20.499,1.25L20.499,10.75C20.499,11.44 19.939,12 19.249,12C18.559,12 17.999,11.44 17.999,10.75L17.999,1.25C17.999,0.56 18.559,0 19.249,0Z"
+ android:fillAlpha="0.45"
android:fillColor="#000"/>
</vector>
diff --git a/packages/SettingsLib/res/drawable/ic_mobile_3_5_bar_error.xml b/packages/SettingsLib/res/drawable/ic_mobile_3_5_bar_error.xml
index e0517cfdfeee..25c9520ad011 100644
--- a/packages/SettingsLib/res/drawable/ic_mobile_3_5_bar_error.xml
+++ b/packages/SettingsLib/res/drawable/ic_mobile_3_5_bar_error.xml
@@ -1,29 +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.
+-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="18dp"
- android:height="14dp"
- android:viewportWidth="18.0"
- android:viewportHeight="14.0">
+ android:width="21dp"
+ android:height="12dp"
+ android:viewportHeight="12.0"
+ android:viewportWidth="20.5">
+ <!-- clip-out the circle which will contain the exclamation point (below this group) -->
+ <group>
+ <clip-path android:pathData="
+ M0,0
+ H20.5
+ V12.0
+ H0
+ Z
+ M19.499,13.5C22.261,13.5 24.499,11.261 24.499,8.5C24.499,5.739 22.261,3.5 19.499,3.5C16.738,3.5 14.499,5.739 14.499,8.5C14.499,11.261 16.738,13.5 19.499,13.5Z" />
+ <path
+ android:fillColor="#000"
+ android:pathData="M1.249,7C1.939,7 2.499,7.56 2.499,8.25L2.499,10.75C2.499,11.44 1.939,12 1.249,12C0.559,12 -0.001,11.44 -0.001,10.75L-0.001,8.25C-0.001,7.56 0.559,7 1.249,7Z" />
+ <path
+ android:fillColor="#000"
+ android:pathData="M5.749,5C6.439,5 6.999,5.56 6.999,6.25L6.999,10.75C6.999,11.44 6.439,12 5.749,12C5.059,12 4.499,11.44 4.499,10.75L4.499,6.25C4.499,5.56 5.059,5 5.749,5Z" />
+ <path
+ android:fillColor="#000"
+ android:pathData="M10.249,3C10.939,3 11.499,3.56 11.499,4.25L11.499,10.75C11.499,11.44 10.939,12 10.249,12C9.559,12 8.999,11.44 8.999,10.75L8.999,4.25C8.999,3.56 9.559,3 10.249,3Z" />
+ <path
+ android:fillAlpha="0.45"
+ android:fillColor="#000"
+ android:pathData="M14.749,1.5C15.439,1.5 15.999,2.06 15.999,2.75L15.999,10.75C15.999,11.44 15.439,12 14.749,12C14.059,12 13.499,11.44 13.499,10.75L13.499,2.75C13.499,2.06 14.059,1.5 14.749,1.5Z" />
+ <path
+ android:fillAlpha="0.45"
+ android:fillColor="#000"
+ android:pathData="M19.249,0C19.939,0 20.499,0.56 20.499,1.25L20.499,10.75C20.499,11.44 19.939,12 19.249,12C18.559,12 17.999,11.44 17.999,10.75L17.999,1.25C17.999,0.56 18.559,0 19.249,0Z" />
+ </group>
<path
- android:pathData="M7,5.5C7,5.224 7.224,5 7.5,5H8.5C8.776,5 9,5.224 9,5.5V13.5C9,13.776 8.776,14 8.5,14H7.5C7.224,14 7,13.776 7,13.5V5.5Z"
- android:fillColor="#000"/>
- <path
- android:pathData="M10.5,2.5C10.5,2.224 10.724,2 11,2H12C12.276,2 12.5,2.224 12.5,2.5V13.5C12.5,13.776 12.276,14 12,14H11C10.724,14 10.5,13.776 10.5,13.5V2.5Z"
- android:fillAlpha="0.3"
- android:fillColor="#000"/>
- <path
- android:pathData="M0,11.5C0,11.224 0.224,11 0.5,11H1.5C1.776,11 2,11.224 2,11.5V13.5C2,13.776 1.776,14 1.5,14H0.5C0.224,14 0,13.776 0,13.5V11.5Z"
- android:fillColor="#000"/>
- <path
- android:pathData="M3.5,8.5C3.5,8.224 3.724,8 4,8H5C5.276,8 5.5,8.224 5.5,8.5V13.5C5.5,13.776 5.276,14 5,14H4C3.724,14 3.5,13.776 3.5,13.5V8.5Z"
- android:fillColor="#000"/>
- <path
- android:pathData="M14.5,0C14.224,0 14,0.224 14,0.5V3H16V0.5C16,0.224 15.776,0 15.5,0H14.5Z"
- android:fillAlpha="0.3"
- android:fillColor="#000"/>
- <path
- android:pathData="M16,13C16,12.448 16.448,12 17,12C17.552,12 18,12.448 18,13C18,13.552 17.552,14 17,14C16.448,14 16,13.552 16,13Z"
- android:fillColor="#000"/>
- <path
- android:pathData="M16,5C16,4.724 16.224,4.5 16.5,4.5H17.5C17.776,4.5 18,4.724 18,5V10C18,10.276 17.776,10.5 17.5,10.5H16.5C16.224,10.5 16,10.276 16,10V5Z"
- android:fillColor="#000"/>
+ android:fillColor="#000"
+ android:pathData="M19.499,5C19.089,5 18.749,5.34 18.749,5.75L18.749,8.75C18.749,9.16 19.089,9.5 19.499,9.5C19.909,9.5 20.249,9.16 20.249,8.75L20.249,5.75C20.249,5.34 19.909,5 19.499,5ZM19.499,12C19.909,12 20.249,11.66 20.249,11.25C20.249,10.84 19.909,10.5 19.499,10.5C19.089,10.5 18.749,10.84 18.749,11.25C18.749,11.66 19.089,12 19.499,12Z" />
</vector>
diff --git a/packages/SettingsLib/res/drawable/ic_mobile_4_4_bar.xml b/packages/SettingsLib/res/drawable/ic_mobile_4_4_bar.xml
index 1ebd3965f36f..fc807fa90b44 100644
--- a/packages/SettingsLib/res/drawable/ic_mobile_4_4_bar.xml
+++ b/packages/SettingsLib/res/drawable/ic_mobile_4_4_bar.xml
@@ -1,5 +1,5 @@
<!--
- Copyright (C) 2023 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,20 +14,20 @@
limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="14dp"
- android:height="14dp"
- android:viewportWidth="14.0"
- android:viewportHeight="14.0">
+ android:width="17dp"
+ android:height="12dp"
+ android:viewportWidth="16.0"
+ android:viewportHeight="12.0">
<path
- android:pathData="M8.25,3L9.25,3A0.5,0.5 0,0 1,9.75 3.5L9.75,13.5A0.5,0.5 0,0 1,9.25 14L8.25,14A0.5,0.5 0,0 1,7.75 13.5L7.75,3.5A0.5,0.5 0,0 1,8.25 3z"
+ android:pathData="M1.249,7C1.939,7 2.499,7.56 2.499,8.25L2.499,10.75C2.499,11.44 1.939,12 1.249,12C0.559,12 -0.001,11.44 -0.001,10.75L-0.001,8.25C-0.001,7.56 0.559,7 1.249,7Z"
android:fillColor="#000"/>
<path
- android:pathData="M11.75,0L12.75,0A0.5,0.5 0,0 1,13.25 0.5L13.25,13.5A0.5,0.5 0,0 1,12.75 14L11.75,14A0.5,0.5 0,0 1,11.25 13.5L11.25,0.5A0.5,0.5 0,0 1,11.75 0z"
+ android:pathData="M5.749,4.5C6.439,4.5 6.999,5.06 6.999,5.75L6.999,10.75C6.999,11.44 6.439,12 5.749,12C5.059,12 4.499,11.44 4.499,10.75L4.499,5.75C4.499,5.06 5.059,4.5 5.749,4.5Z"
android:fillColor="#000"/>
<path
- android:pathData="M1.25,10L2.25,10A0.5,0.5 0,0 1,2.75 10.5L2.75,13.5A0.5,0.5 0,0 1,2.25 14L1.25,14A0.5,0.5 0,0 1,0.75 13.5L0.75,10.5A0.5,0.5 0,0 1,1.25 10z"
+ android:pathData="M10.249,2C10.939,2 11.499,2.56 11.499,3.25L11.499,10.75C11.499,11.44 10.939,12 10.249,12C9.559,12 8.999,11.44 8.999,10.75L8.999,3.25C8.999,2.56 9.559,2 10.249,2Z"
android:fillColor="#000"/>
<path
- android:pathData="M4.75,6.5L5.75,6.5A0.5,0.5 0,0 1,6.25 7L6.25,13.5A0.5,0.5 0,0 1,5.75 14L4.75,14A0.5,0.5 0,0 1,4.25 13.5L4.25,7A0.5,0.5 0,0 1,4.75 6.5z"
+ android:pathData="M14.749,0C15.439,0 15.999,0.56 15.999,1.25L15.999,10.75C15.999,11.44 15.439,12 14.749,12C14.059,12 13.499,11.44 13.499,10.75L13.499,1.25C13.499,0.56 14.059,0 14.749,0Z"
android:fillColor="#000"/>
</vector>
diff --git a/packages/SettingsLib/res/drawable/ic_mobile_4_4_bar_error.xml b/packages/SettingsLib/res/drawable/ic_mobile_4_4_bar_error.xml
index 4473c29d0866..d23680d17b9c 100644
--- a/packages/SettingsLib/res/drawable/ic_mobile_4_4_bar_error.xml
+++ b/packages/SettingsLib/res/drawable/ic_mobile_4_4_bar_error.xml
@@ -1,24 +1,47 @@
+<!--
+ Copyright (C) 2025 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="15dp"
- android:height="14dp"
- android:viewportWidth="15.0"
- android:viewportHeight="14.0">
+ android:width="17dp"
+ android:height="12dp"
+ android:viewportWidth="16.0"
+ android:viewportHeight="12.0">
+ <group>
+ <!-- clip-out the circle which will contain the exclamation point (below this group) -->
+ <clip-path
+ android:pathData="
+ M0,0
+ V13.5,0
+ H13.5,20
+ V0,20
+ H0,0
+ M14.999,13.5C17.761,13.5 19.999,11.261 19.999,8.5C19.999,5.739 17.761,3.5 14.999,3.5C12.238,3.5 9.999,5.739 9.999,8.5C9.999,11.261 12.238,13.5 14.999,13.5Z" />
+ <path
+ android:pathData="M1.249,7C1.939,7 2.499,7.56 2.499,8.25L2.499,10.75C2.499,11.44 1.939,12 1.249,12C0.559,12 -0.001,11.44 -0.001,10.75L-0.001,8.25C-0.001,7.56 0.559,7 1.249,7Z"
+ android:fillColor="#000"/>
+ <path
+ android:pathData="M5.749,4.5C6.439,4.5 6.999,5.06 6.999,5.75L6.999,10.75C6.999,11.44 6.439,12 5.749,12C5.059,12 4.499,11.44 4.499,10.75L4.499,5.75C4.499,5.06 5.059,4.5 5.749,4.5Z"
+ android:fillColor="#000"/>
+ <path
+ android:pathData="M10.249,2C10.939,2 11.499,2.56 11.499,3.25L11.499,10.75C11.499,11.44 10.939,12 10.249,12C9.559,12 8.999,11.44 8.999,10.75L8.999,3.25C8.999,2.56 9.559,2 10.249,2Z"
+ android:fillColor="#000"/>
+ <path
+ android:pathData="M14.749,0C15.439,0 15.999,0.56 15.999,1.25L15.999,10.75C15.999,11.44 15.439,12 14.749,12C14.059,12 13.499,11.44 13.499,10.75L13.499,1.25C13.499,0.56 14.059,0 14.749,0Z"
+ android:fillColor="#000"/>
+ </group>
<path
- android:pathData="M7,3.5C7,3.224 7.224,3 7.5,3H8.5C8.776,3 9,3.224 9,3.5V13.5C9,13.776 8.776,14 8.5,14H7.5C7.224,14 7,13.776 7,13.5V3.5Z"
- android:fillColor="#000"/>
- <path
- android:pathData="M0,10.5C0,10.224 0.224,10 0.5,10H1.5C1.776,10 2,10.224 2,10.5V13.5C2,13.776 1.776,14 1.5,14H0.5C0.224,14 0,13.776 0,13.5V10.5Z"
- android:fillColor="#000"/>
- <path
- android:pathData="M3.5,7C3.5,6.724 3.724,6.5 4,6.5H5C5.276,6.5 5.5,6.724 5.5,7V13.5C5.5,13.776 5.276,14 5,14H4C3.724,14 3.5,13.776 3.5,13.5V7Z"
- android:fillColor="#000"/>
- <path
- android:pathData="M11,0C10.724,0 10.5,0.224 10.5,0.5V3H12.5V0.5C12.5,0.224 12.276,0 12,0H11Z"
- android:fillColor="#000"/>
- <path
- android:pathData="M12.25,13C12.25,12.448 12.698,12 13.25,12C13.802,12 14.25,12.448 14.25,13C14.25,13.552 13.802,14 13.25,14C12.698,14 12.25,13.552 12.25,13Z"
- android:fillColor="#000"/>
- <path
- android:pathData="M12.25,5C12.25,4.724 12.474,4.5 12.75,4.5H13.75C14.026,4.5 14.25,4.724 14.25,5V10C14.25,10.276 14.026,10.5 13.75,10.5H12.75C12.474,10.5 12.25,10.276 12.25,10V5Z"
+ android:pathData="M14.999,5C14.589,5 14.249,5.34 14.249,5.75L14.249,8.75C14.249,9.16 14.589,9.5 14.999,9.5C15.409,9.5 15.749,9.16 15.749,8.75L15.749,5.75C15.749,5.34 15.409,5 14.999,5ZM14.999,12C15.409,12 15.749,11.66 15.749,11.25C15.749,10.84 15.409,10.5 14.999,10.5C14.589,10.5 14.249,10.84 14.249,11.25C14.249,11.66 14.589,12 14.999,12Z"
android:fillColor="#000"/>
</vector>
diff --git a/packages/SettingsLib/res/drawable/ic_mobile_4_5_bar.xml b/packages/SettingsLib/res/drawable/ic_mobile_4_5_bar.xml
index 1ed6ac86b21a..b1336d70fc39 100644
--- a/packages/SettingsLib/res/drawable/ic_mobile_4_5_bar.xml
+++ b/packages/SettingsLib/res/drawable/ic_mobile_4_5_bar.xml
@@ -1,5 +1,5 @@
<!--
- Copyright (C) 2023 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,24 +14,24 @@
limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="16dp"
- android:height="14dp"
- android:viewportWidth="16.0"
- android:viewportHeight="14.0">
+ android:width="21dp"
+ android:height="12dp"
+ android:viewportWidth="20.5"
+ android:viewportHeight="12.0">
<path
- android:pathData="M7.5,5L8.5,5A0.5,0.5 0,0 1,9 5.5L9,13.5A0.5,0.5 0,0 1,8.5 14L7.5,14A0.5,0.5 0,0 1,7 13.5L7,5.5A0.5,0.5 0,0 1,7.5 5z"
+ android:pathData="M1.249,7C1.939,7 2.499,7.56 2.499,8.25L2.499,10.75C2.499,11.44 1.939,12 1.249,12C0.559,12 -0.001,11.44 -0.001,10.75L-0.001,8.25C-0.001,7.56 0.559,7 1.249,7Z"
android:fillColor="#000"/>
<path
- android:pathData="M11,2L12,2A0.5,0.5 0,0 1,12.5 2.5L12.5,13.5A0.5,0.5 0,0 1,12 14L11,14A0.5,0.5 0,0 1,10.5 13.5L10.5,2.5A0.5,0.5 0,0 1,11 2z"
+ android:pathData="M5.749,5C6.439,5 6.999,5.56 6.999,6.25L6.999,10.75C6.999,11.44 6.439,12 5.749,12C5.059,12 4.499,11.44 4.499,10.75L4.499,6.25C4.499,5.56 5.059,5 5.749,5Z"
android:fillColor="#000"/>
<path
- android:pathData="M0.5,11L1.5,11A0.5,0.5 0,0 1,2 11.5L2,13.5A0.5,0.5 0,0 1,1.5 14L0.5,14A0.5,0.5 0,0 1,0 13.5L0,11.5A0.5,0.5 0,0 1,0.5 11z"
+ android:pathData="M10.249,3C10.939,3 11.499,3.56 11.499,4.25L11.499,10.75C11.499,11.44 10.939,12 10.249,12C9.559,12 8.999,11.44 8.999,10.75L8.999,4.25C8.999,3.56 9.559,3 10.249,3Z"
android:fillColor="#000"/>
<path
- android:pathData="M14.5,0L15.5,0A0.5,0.5 0,0 1,16 0.5L16,13.5A0.5,0.5 0,0 1,15.5 14L14.5,14A0.5,0.5 0,0 1,14 13.5L14,0.5A0.5,0.5 0,0 1,14.5 0z"
- android:fillAlpha="0.24"
+ android:pathData="M14.749,1.5C15.439,1.5 15.999,2.06 15.999,2.75L15.999,10.75C15.999,11.44 15.439,12 14.749,12C14.059,12 13.499,11.44 13.499,10.75L13.499,2.75C13.499,2.06 14.059,1.5 14.749,1.5Z"
android:fillColor="#000"/>
<path
- android:pathData="M4,8L5,8A0.5,0.5 0,0 1,5.5 8.5L5.5,13.5A0.5,0.5 0,0 1,5 14L4,14A0.5,0.5 0,0 1,3.5 13.5L3.5,8.5A0.5,0.5 0,0 1,4 8z"
+ android:pathData="M19.249,0C19.939,0 20.499,0.56 20.499,1.25L20.499,10.75C20.499,11.44 19.939,12 19.249,12C18.559,12 17.999,11.44 17.999,10.75L17.999,1.25C17.999,0.56 18.559,0 19.249,0Z"
+ android:fillAlpha="0.45"
android:fillColor="#000"/>
</vector>
diff --git a/packages/SettingsLib/res/drawable/ic_mobile_4_5_bar_error.xml b/packages/SettingsLib/res/drawable/ic_mobile_4_5_bar_error.xml
index 703e3acd5f75..bf62535b9574 100644
--- a/packages/SettingsLib/res/drawable/ic_mobile_4_5_bar_error.xml
+++ b/packages/SettingsLib/res/drawable/ic_mobile_4_5_bar_error.xml
@@ -1,28 +1,50 @@
+<!--
+ Copyright (C) 2025 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="18dp"
- android:height="14dp"
- android:viewportWidth="18.0"
- android:viewportHeight="14.0">
+ android:width="21dp"
+ android:height="12dp"
+ android:viewportHeight="12.0"
+ android:viewportWidth="20.5">
+ <!-- clip-out the circle which will contain the exclamation point (below this group) -->
+ <group>
+ <clip-path android:pathData="
+ M0,0
+ H20.5
+ V12.0
+ H0
+ Z
+ M19.499,13.5C22.261,13.5 24.499,11.261 24.499,8.5C24.499,5.739 22.261,3.5 19.499,3.5C16.738,3.5 14.499,5.739 14.499,8.5C14.499,11.261 16.738,13.5 19.499,13.5Z" />
+ <path
+ android:fillColor="#000"
+ android:pathData="M1.249,7C1.939,7 2.499,7.56 2.499,8.25L2.499,10.75C2.499,11.44 1.939,12 1.249,12C0.559,12 -0.001,11.44 -0.001,10.75L-0.001,8.25C-0.001,7.56 0.559,7 1.249,7Z" />
+ <path
+ android:fillColor="#000"
+ android:pathData="M5.749,5C6.439,5 6.999,5.56 6.999,6.25L6.999,10.75C6.999,11.44 6.439,12 5.749,12C5.059,12 4.499,11.44 4.499,10.75L4.499,6.25C4.499,5.56 5.059,5 5.749,5Z" />
+ <path
+ android:fillColor="#000"
+ android:pathData="M10.249,3C10.939,3 11.499,3.56 11.499,4.25L11.499,10.75C11.499,11.44 10.939,12 10.249,12C9.559,12 8.999,11.44 8.999,10.75L8.999,4.25C8.999,3.56 9.559,3 10.249,3Z" />
+ <path
+ android:fillColor="#000"
+ android:pathData="M14.749,1.5C15.439,1.5 15.999,2.06 15.999,2.75L15.999,10.75C15.999,11.44 15.439,12 14.749,12C14.059,12 13.499,11.44 13.499,10.75L13.499,2.75C13.499,2.06 14.059,1.5 14.749,1.5Z" />
+ <path
+ android:fillAlpha="0.45"
+ android:fillColor="#000"
+ android:pathData="M19.249,0C19.939,0 20.499,0.56 20.499,1.25L20.499,10.75C20.499,11.44 19.939,12 19.249,12C18.559,12 17.999,11.44 17.999,10.75L17.999,1.25C17.999,0.56 18.559,0 19.249,0Z" />
+ </group>
<path
- android:pathData="M7,5.5C7,5.224 7.224,5 7.5,5H8.5C8.776,5 9,5.224 9,5.5V13.5C9,13.776 8.776,14 8.5,14H7.5C7.224,14 7,13.776 7,13.5V5.5Z"
- android:fillColor="#000"/>
- <path
- android:pathData="M10.5,2.5C10.5,2.224 10.724,2 11,2H12C12.276,2 12.5,2.224 12.5,2.5V13.5C12.5,13.776 12.276,14 12,14H11C10.724,14 10.5,13.776 10.5,13.5V2.5Z"
- android:fillColor="#000"/>
- <path
- android:pathData="M0,11.5C0,11.224 0.224,11 0.5,11H1.5C1.776,11 2,11.224 2,11.5V13.5C2,13.776 1.776,14 1.5,14H0.5C0.224,14 0,13.776 0,13.5V11.5Z"
- android:fillColor="#000"/>
- <path
- android:pathData="M3.5,8.5C3.5,8.224 3.724,8 4,8H5C5.276,8 5.5,8.224 5.5,8.5V13.5C5.5,13.776 5.276,14 5,14H4C3.724,14 3.5,13.776 3.5,13.5V8.5Z"
- android:fillColor="#000"/>
- <path
- android:pathData="M14.5,0C14.224,0 14,0.224 14,0.5V3H16V0.5C16,0.224 15.776,0 15.5,0H14.5Z"
- android:fillAlpha="0.3"
- android:fillColor="#000"/>
- <path
- android:pathData="M16,13C16,12.448 16.448,12 17,12C17.552,12 18,12.448 18,13C18,13.552 17.552,14 17,14C16.448,14 16,13.552 16,13Z"
- android:fillColor="#000"/>
- <path
- android:pathData="M16,5C16,4.724 16.224,4.5 16.5,4.5H17.5C17.776,4.5 18,4.724 18,5V10C18,10.276 17.776,10.5 17.5,10.5H16.5C16.224,10.5 16,10.276 16,10V5Z"
- android:fillColor="#000"/>
+ android:fillColor="#000"
+ android:pathData="M19.499,5C19.089,5 18.749,5.34 18.749,5.75L18.749,8.75C18.749,9.16 19.089,9.5 19.499,9.5C19.909,9.5 20.249,9.16 20.249,8.75L20.249,5.75C20.249,5.34 19.909,5 19.499,5ZM19.499,12C19.909,12 20.249,11.66 20.249,11.25C20.249,10.84 19.909,10.5 19.499,10.5C19.089,10.5 18.749,10.84 18.749,11.25C18.749,11.66 19.089,12 19.499,12Z" />
</vector>
diff --git a/packages/SettingsLib/res/drawable/ic_mobile_5_5_bar.xml b/packages/SettingsLib/res/drawable/ic_mobile_5_5_bar.xml
index 420ffb601e8f..fa9bedc021d8 100644
--- a/packages/SettingsLib/res/drawable/ic_mobile_5_5_bar.xml
+++ b/packages/SettingsLib/res/drawable/ic_mobile_5_5_bar.xml
@@ -1,5 +1,5 @@
<!--
- Copyright (C) 2023 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,23 +14,23 @@
limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="16dp"
- android:height="14dp"
- android:viewportWidth="16.0"
- android:viewportHeight="14.0">
+ android:width="21dp"
+ android:height="12dp"
+ android:viewportWidth="20.5"
+ android:viewportHeight="12.0">
<path
- android:pathData="M7.5,5L8.5,5A0.5,0.5 0,0 1,9 5.5L9,13.5A0.5,0.5 0,0 1,8.5 14L7.5,14A0.5,0.5 0,0 1,7 13.5L7,5.5A0.5,0.5 0,0 1,7.5 5z"
+ android:pathData="M1.249,7C1.939,7 2.499,7.56 2.499,8.25L2.499,10.75C2.499,11.44 1.939,12 1.249,12C0.559,12 -0.001,11.44 -0.001,10.75L-0.001,8.25C-0.001,7.56 0.559,7 1.249,7Z"
android:fillColor="#000"/>
<path
- android:pathData="M11,2L12,2A0.5,0.5 0,0 1,12.5 2.5L12.5,13.5A0.5,0.5 0,0 1,12 14L11,14A0.5,0.5 0,0 1,10.5 13.5L10.5,2.5A0.5,0.5 0,0 1,11 2z"
+ android:pathData="M5.749,5C6.439,5 6.999,5.56 6.999,6.25L6.999,10.75C6.999,11.44 6.439,12 5.749,12C5.059,12 4.499,11.44 4.499,10.75L4.499,6.25C4.499,5.56 5.059,5 5.749,5Z"
android:fillColor="#000"/>
<path
- android:pathData="M0.5,11L1.5,11A0.5,0.5 0,0 1,2 11.5L2,13.5A0.5,0.5 0,0 1,1.5 14L0.5,14A0.5,0.5 0,0 1,0 13.5L0,11.5A0.5,0.5 0,0 1,0.5 11z"
+ android:pathData="M10.249,3C10.939,3 11.499,3.56 11.499,4.25L11.499,10.75C11.499,11.44 10.939,12 10.249,12C9.559,12 8.999,11.44 8.999,10.75L8.999,4.25C8.999,3.56 9.559,3 10.249,3Z"
android:fillColor="#000"/>
<path
- android:pathData="M14.5,0L15.5,0A0.5,0.5 0,0 1,16 0.5L16,13.5A0.5,0.5 0,0 1,15.5 14L14.5,14A0.5,0.5 0,0 1,14 13.5L14,0.5A0.5,0.5 0,0 1,14.5 0z"
+ android:pathData="M14.749,1.5C15.439,1.5 15.999,2.06 15.999,2.75L15.999,10.75C15.999,11.44 15.439,12 14.749,12C14.059,12 13.499,11.44 13.499,10.75L13.499,2.75C13.499,2.06 14.059,1.5 14.749,1.5Z"
android:fillColor="#000"/>
<path
- android:pathData="M4,8L5,8A0.5,0.5 0,0 1,5.5 8.5L5.5,13.5A0.5,0.5 0,0 1,5 14L4,14A0.5,0.5 0,0 1,3.5 13.5L3.5,8.5A0.5,0.5 0,0 1,4 8z"
+ android:pathData="M19.249,0C19.939,0 20.499,0.56 20.499,1.25L20.499,10.75C20.499,11.44 19.939,12 19.249,12C18.559,12 17.999,11.44 17.999,10.75L17.999,1.25C17.999,0.56 18.559,0 19.249,0Z"
android:fillColor="#000"/>
</vector>
diff --git a/packages/SettingsLib/res/drawable/ic_mobile_5_5_bar_error.xml b/packages/SettingsLib/res/drawable/ic_mobile_5_5_bar_error.xml
index e63ca77e9db1..1728bc78b22b 100644
--- a/packages/SettingsLib/res/drawable/ic_mobile_5_5_bar_error.xml
+++ b/packages/SettingsLib/res/drawable/ic_mobile_5_5_bar_error.xml
@@ -1,27 +1,49 @@
+<!--
+ Copyright (C) 2025 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="18dp"
- android:height="14dp"
- android:viewportWidth="18.0"
- android:viewportHeight="14.0">
+ android:width="21dp"
+ android:height="12dp"
+ android:viewportHeight="12.0"
+ android:viewportWidth="20.5">
+ <!-- clip-out the circle which will contain the exclamation point (below this group) -->
+ <group>
+ <clip-path android:pathData="
+ M0,0
+ H20.5
+ V12.0
+ H0
+ Z
+ M19.499,13.5C22.261,13.5 24.499,11.261 24.499,8.5C24.499,5.739 22.261,3.5 19.499,3.5C16.738,3.5 14.499,5.739 14.499,8.5C14.499,11.261 16.738,13.5 19.499,13.5Z" />
+ <path
+ android:fillColor="#000"
+ android:pathData="M1.249,7C1.939,7 2.499,7.56 2.499,8.25L2.499,10.75C2.499,11.44 1.939,12 1.249,12C0.559,12 -0.001,11.44 -0.001,10.75L-0.001,8.25C-0.001,7.56 0.559,7 1.249,7Z" />
+ <path
+ android:fillColor="#000"
+ android:pathData="M5.749,5C6.439,5 6.999,5.56 6.999,6.25L6.999,10.75C6.999,11.44 6.439,12 5.749,12C5.059,12 4.499,11.44 4.499,10.75L4.499,6.25C4.499,5.56 5.059,5 5.749,5Z" />
+ <path
+ android:fillColor="#000"
+ android:pathData="M10.249,3C10.939,3 11.499,3.56 11.499,4.25L11.499,10.75C11.499,11.44 10.939,12 10.249,12C9.559,12 8.999,11.44 8.999,10.75L8.999,4.25C8.999,3.56 9.559,3 10.249,3Z" />
+ <path
+ android:fillColor="#000"
+ android:pathData="M14.749,1.5C15.439,1.5 15.999,2.06 15.999,2.75L15.999,10.75C15.999,11.44 15.439,12 14.749,12C14.059,12 13.499,11.44 13.499,10.75L13.499,2.75C13.499,2.06 14.059,1.5 14.749,1.5Z" />
+ <path
+ android:fillColor="#000"
+ android:pathData="M19.249,0C19.939,0 20.499,0.56 20.499,1.25L20.499,10.75C20.499,11.44 19.939,12 19.249,12C18.559,12 17.999,11.44 17.999,10.75L17.999,1.25C17.999,0.56 18.559,0 19.249,0Z" />
+ </group>
<path
- android:pathData="M7,5.5C7,5.224 7.224,5 7.5,5H8.5C8.776,5 9,5.224 9,5.5V13.5C9,13.776 8.776,14 8.5,14H7.5C7.224,14 7,13.776 7,13.5V5.5Z"
- android:fillColor="#000"/>
- <path
- android:pathData="M10.5,2.5C10.5,2.224 10.724,2 11,2H12C12.276,2 12.5,2.224 12.5,2.5V13.5C12.5,13.776 12.276,14 12,14H11C10.724,14 10.5,13.776 10.5,13.5V2.5Z"
- android:fillColor="#000"/>
- <path
- android:pathData="M0,11.5C0,11.224 0.224,11 0.5,11H1.5C1.776,11 2,11.224 2,11.5V13.5C2,13.776 1.776,14 1.5,14H0.5C0.224,14 0,13.776 0,13.5V11.5Z"
- android:fillColor="#000"/>
- <path
- android:pathData="M3.5,8.5C3.5,8.224 3.724,8 4,8H5C5.276,8 5.5,8.224 5.5,8.5V13.5C5.5,13.776 5.276,14 5,14H4C3.724,14 3.5,13.776 3.5,13.5V8.5Z"
- android:fillColor="#000"/>
- <path
- android:pathData="M14.5,0C14.224,0 14,0.224 14,0.5V3H16V0.5C16,0.224 15.776,0 15.5,0H14.5Z"
- android:fillColor="#000"/>
- <path
- android:pathData="M16,13C16,12.448 16.448,12 17,12C17.552,12 18,12.448 18,13C18,13.552 17.552,14 17,14C16.448,14 16,13.552 16,13Z"
- android:fillColor="#000"/>
- <path
- android:pathData="M16,5C16,4.724 16.224,4.5 16.5,4.5H17.5C17.776,4.5 18,4.724 18,5V10C18,10.276 17.776,10.5 17.5,10.5H16.5C16.224,10.5 16,10.276 16,10V5Z"
- android:fillColor="#000"/>
+ android:fillColor="#000"
+ android:pathData="M19.499,5C19.089,5 18.749,5.34 18.749,5.75L18.749,8.75C18.749,9.16 19.089,9.5 19.499,9.5C19.909,9.5 20.249,9.16 20.249,8.75L20.249,5.75C20.249,5.34 19.909,5 19.499,5ZM19.499,12C19.909,12 20.249,11.66 20.249,11.25C20.249,10.84 19.909,10.5 19.499,10.5C19.089,10.5 18.749,10.84 18.749,11.25C18.749,11.66 19.089,12 19.499,12Z" />
</vector>
diff --git a/packages/SettingsLib/res/values/strings.xml b/packages/SettingsLib/res/values/strings.xml
index 4b0400fb3441..91ec83690722 100644
--- a/packages/SettingsLib/res/values/strings.xml
+++ b/packages/SettingsLib/res/values/strings.xml
@@ -889,6 +889,9 @@
<!-- Preference category for monitoring debugging development settings. [CHAR LIMIT=25] -->
<string name="debug_monitoring_category">Monitoring</string>
+ <!-- Preference category to alter window management settings, [CHAR LIMIT=50] -->
+ <string name="window_management_category">Window Management</string>
+
<!-- UI debug setting: always enable strict mode? [CHAR LIMIT=25] -->
<string name="strict_mode">Strict mode enabled</string>
<!-- UI debug setting: show strict mode summary [CHAR LIMIT=50] -->
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
index 7374f80fd9db..bb96041739eb 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
@@ -16,8 +16,7 @@
package com.android.settingslib.bluetooth;
-import static com.android.settingslib.flags.Flags.enableSetPreferredTransportForLeAudioDevice;
-import static com.android.settingslib.flags.Flags.ignoreA2dpDisconnectionForAndroidAuto;
+import static com.android.settingslib.media.flags.Flags.enableTvMediaOutputDialog;
import android.annotation.CallbackExecutor;
import android.annotation.StringRes;
@@ -53,7 +52,7 @@ import androidx.annotation.VisibleForTesting;
import com.android.internal.util.ArrayUtils;
import com.android.settingslib.R;
import com.android.settingslib.Utils;
-import com.android.settingslib.media.flags.Flags;
+import com.android.settingslib.flags.Flags;
import com.android.settingslib.utils.ThreadUtils;
import com.android.settingslib.widget.AdaptiveOutlineDrawable;
@@ -264,7 +263,7 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice>
mHandler.removeMessages(profile.getProfileId());
if (profile.getConnectionPolicy(mDevice) >
BluetoothProfile.CONNECTION_POLICY_FORBIDDEN) {
- if (ignoreA2dpDisconnectionForAndroidAuto()
+ if (Flags.ignoreA2dpDisconnectionForAndroidAuto()
&& profile instanceof A2dpProfile && isAndroidAuto()) {
Log.w(TAG,
"onProfileStateChanged(): Skip setting A2DP "
@@ -306,7 +305,7 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice>
mLocalNapRoleConnected = true;
}
}
- if (enableSetPreferredTransportForLeAudioDevice()
+ if (Flags.enableSetPreferredTransportForLeAudioDevice()
&& profile instanceof HidProfile) {
updatePreferredTransport();
}
@@ -322,7 +321,7 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice>
mLocalNapRoleConnected = false;
}
- if (enableSetPreferredTransportForLeAudioDevice()
+ if (Flags.enableSetPreferredTransportForLeAudioDevice()
&& profile instanceof LeAudioProfile) {
updatePreferredTransport();
}
@@ -1345,6 +1344,8 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice>
if (mBluetoothManager == null) {
mBluetoothManager = LocalBluetoothManager.getInstance(mContext, null);
}
+ boolean isTempBond = Flags.enableTemporaryBondDevicesUi()
+ && BluetoothUtils.isTemporaryBondDevice(getDevice());
if (BluetoothUtils.hasConnectedBroadcastSource(this, mBluetoothManager)) {
// Gets summary for the buds which are in the audio sharing.
int groupId = BluetoothUtils.getGroupId(this);
@@ -1363,14 +1364,23 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice>
shortSummary);
} else {
// The buds are not primary buds
- return getSummaryWithBatteryInfo(
- R.string.bluetooth_active_media_only_battery_level_untethered,
- R.string.bluetooth_active_media_only_battery_level,
- R.string.bluetooth_active_media_only_no_battery_level,
- leftBattery,
- rightBattery,
- batteryLevelPercentageString,
- shortSummary);
+ return isTempBond
+ ? getSummaryWithBatteryInfo(
+ R.string.bluetooth_guest_media_only_battery_level_untethered,
+ R.string.bluetooth_guest_media_only_battery_level,
+ R.string.bluetooth_guest_media_only_no_battery_level,
+ leftBattery,
+ rightBattery,
+ batteryLevelPercentageString,
+ shortSummary)
+ : getSummaryWithBatteryInfo(
+ R.string.bluetooth_active_media_only_battery_level_untethered,
+ R.string.bluetooth_active_media_only_battery_level,
+ R.string.bluetooth_active_media_only_no_battery_level,
+ leftBattery,
+ rightBattery,
+ batteryLevelPercentageString,
+ shortSummary);
}
} else {
// Gets summary for the buds which are not in the audio sharing.
@@ -1381,16 +1391,28 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice>
&& profile.isEnabled(getDevice()))) {
// The buds support le audio.
if (isConnected()) {
- return getSummaryWithBatteryInfo(
- R.string.bluetooth_battery_level_untethered_lea_support,
- R.string.bluetooth_battery_level_lea_support,
- R.string.bluetooth_no_battery_level_lea_support,
- leftBattery,
- rightBattery,
- batteryLevelPercentageString,
- shortSummary);
+ return isTempBond
+ ? getSummaryWithBatteryInfo(
+ R.string.bluetooth_guest_battery_level_untethered_lea_support,
+ R.string.bluetooth_guest_battery_level_lea_support,
+ R.string.bluetooth_guest_no_battery_level_lea_support,
+ leftBattery,
+ rightBattery,
+ batteryLevelPercentageString,
+ shortSummary)
+ : getSummaryWithBatteryInfo(
+ R.string.bluetooth_battery_level_untethered_lea_support,
+ R.string.bluetooth_battery_level_lea_support,
+ R.string.bluetooth_no_battery_level_lea_support,
+ leftBattery,
+ rightBattery,
+ batteryLevelPercentageString,
+ shortSummary);
} else {
- return mContext.getString(R.string.bluetooth_saved_device_lea_support);
+ return isTempBond
+ ? mContext.getString(
+ R.string.bluetooth_guest_saved_device_lea_support)
+ : mContext.getString(R.string.bluetooth_saved_device_lea_support);
}
}
}
@@ -1509,11 +1531,19 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice>
leftBattery = getLeftBatteryLevel();
rightBattery = getRightBatteryLevel();
+ boolean isTempBond = Flags.enableTemporaryBondDevicesUi()
+ && BluetoothUtils.isTemporaryBondDevice(getDevice());
// Set default string with battery level in device connected situation.
if (isTwsBatteryAvailable(leftBattery, rightBattery)) {
- stringRes = R.string.bluetooth_battery_level_untethered;
+ stringRes =
+ isTempBond
+ ? R.string.bluetooth_guest_battery_level_untethered
+ : R.string.bluetooth_battery_level_untethered;
} else if (batteryLevelPercentageString != null && !shortSummary) {
- stringRes = R.string.bluetooth_battery_level;
+ stringRes =
+ isTempBond
+ ? R.string.bluetooth_guest_battery_level
+ : R.string.bluetooth_battery_level;
}
// Set active string in following device connected situation, also show battery
@@ -1529,11 +1559,20 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice>
|| (mIsActiveDeviceA2dp && !isOnCall)
|| mIsActiveDeviceLeAudio) {
if (isTwsBatteryAvailable(leftBattery, rightBattery) && !shortSummary) {
- stringRes = R.string.bluetooth_active_battery_level_untethered;
+ stringRes =
+ isTempBond
+ ? R.string.bluetooth_guest_battery_level_untethered
+ : R.string.bluetooth_active_battery_level_untethered;
} else if (batteryLevelPercentageString != null && !shortSummary) {
- stringRes = R.string.bluetooth_active_battery_level;
+ stringRes =
+ isTempBond
+ ? R.string.bluetooth_guest_battery_level
+ : R.string.bluetooth_active_battery_level;
} else {
- stringRes = R.string.bluetooth_active_no_battery_level;
+ stringRes =
+ isTempBond
+ ? R.string.bluetooth_guest_no_battery_level
+ : R.string.bluetooth_active_no_battery_level;
}
}
@@ -1559,7 +1598,7 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice>
|| stringRes == R.string.bluetooth_active_battery_level_untethered_left
|| stringRes == R.string.bluetooth_active_battery_level_untethered_right
|| stringRes == R.string.bluetooth_battery_level_untethered;
- if (isTvSummary && summaryIncludesBatteryLevel && Flags.enableTvMediaOutputDialog()) {
+ if (isTvSummary && summaryIncludesBatteryLevel && enableTvMediaOutputDialog()) {
return getTvBatterySummary(
getMinBatteryLevelWithMemberDevices(),
leftBattery,
@@ -1949,6 +1988,17 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice>
}
/**
+ * @return {@code true} if {@code cachedBluetoothDevice} has member which is LeAudio device
+ */
+ public boolean hasConnectedLeAudioMemberDevice() {
+ LeAudioProfile leAudio = mProfileManager.getLeAudioProfile();
+ return leAudio != null && getMemberDevice().stream().anyMatch(
+ cachedDevice -> cachedDevice != null && cachedDevice.getDevice() != null
+ && leAudio.getConnectionStatus(cachedDevice.getDevice())
+ == BluetoothProfile.STATE_CONNECTED);
+ }
+
+ /**
* @return {@code true} if {@code cachedBluetoothDevice} supports broadcast assistant profile
*/
public boolean isConnectedLeAudioBroadcastAssistantDevice() {
diff --git a/packages/SettingsLib/src/com/android/settingslib/graph/SignalDrawable.java b/packages/SettingsLib/src/com/android/settingslib/graph/SignalDrawable.java
index 13a06017abbc..671dfa230f0d 100644
--- a/packages/SettingsLib/src/com/android/settingslib/graph/SignalDrawable.java
+++ b/packages/SettingsLib/src/com/android/settingslib/graph/SignalDrawable.java
@@ -128,12 +128,20 @@ public class SignalDrawable extends DrawableWrapper {
@Override
public int getIntrinsicWidth() {
- return mIntrinsicSize;
+ if (newStatusBarIcons()) {
+ return super.getIntrinsicWidth();
+ } else {
+ return mIntrinsicSize;
+ }
}
@Override
public int getIntrinsicHeight() {
- return mIntrinsicSize;
+ if (newStatusBarIcons()) {
+ return super.getIntrinsicHeight();
+ } else {
+ return mIntrinsicSize;
+ }
}
private void updateAnimation() {
diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/ZenModeRepository.kt b/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/ZenModeRepository.kt
index f446bb8e32d1..c4e724554c04 100644
--- a/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/ZenModeRepository.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/ZenModeRepository.kt
@@ -93,10 +93,9 @@ class ZenModeRepositoryImpl(
IntentFilter().apply {
addAction(NotificationManager.ACTION_INTERRUPTION_FILTER_CHANGED)
addAction(NotificationManager.ACTION_NOTIFICATION_POLICY_CHANGED)
- if (android.app.Flags.modesApi())
- addAction(
- NotificationManager.ACTION_CONSOLIDATED_NOTIFICATION_POLICY_CHANGED
- )
+ addAction(
+ NotificationManager.ACTION_CONSOLIDATED_NOTIFICATION_POLICY_CHANGED
+ )
},
/* broadcastPermission = */ null,
/* scheduler = */ backgroundHandler,
@@ -109,16 +108,13 @@ class ZenModeRepositoryImpl(
}
override val consolidatedNotificationPolicy: StateFlow<NotificationManager.Policy?> by lazy {
- if (android.app.Flags.modesApi())
- flowFromBroadcast(NotificationManager.ACTION_CONSOLIDATED_NOTIFICATION_POLICY_CHANGED) {
- // If available, get the value from extras to avoid a potential binder call.
- it?.extras?.getParcelable(EXTRA_NOTIFICATION_POLICY)
- ?: notificationManager.consolidatedNotificationPolicy
- }
- else
- flowFromBroadcast(NotificationManager.ACTION_NOTIFICATION_POLICY_CHANGED) {
- notificationManager.consolidatedNotificationPolicy
- }
+ flowFromBroadcast(NotificationManager.ACTION_CONSOLIDATED_NOTIFICATION_POLICY_CHANGED) {
+ // If available, get the value from extras to avoid a potential binder call.
+ it?.extras?.getParcelable(
+ EXTRA_NOTIFICATION_POLICY,
+ NotificationManager.Policy::class.java
+ ) ?: notificationManager.consolidatedNotificationPolicy
+ }
}
override val globalZenMode: StateFlow<Int?> by lazy {
diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/modes/EnableDndDialogFactory.java b/packages/SettingsLib/src/com/android/settingslib/notification/modes/EnableDndDialogFactory.java
index f0e7fb851d5f..52d62b6226b8 100644
--- a/packages/SettingsLib/src/com/android/settingslib/notification/modes/EnableDndDialogFactory.java
+++ b/packages/SettingsLib/src/com/android/settingslib/notification/modes/EnableDndDialogFactory.java
@@ -19,7 +19,6 @@ package com.android.settingslib.notification.modes;
import android.app.ActivityManager;
import android.app.AlarmManager;
import android.app.AlertDialog;
-import android.app.Flags;
import android.app.NotificationManager;
import android.content.Context;
import android.content.DialogInterface;
@@ -42,8 +41,6 @@ import android.widget.RadioGroup;
import android.widget.ScrollView;
import android.widget.TextView;
-import androidx.annotation.Nullable;
-
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.policy.PhoneWindow;
import com.android.settingslib.R;
@@ -80,7 +77,6 @@ public class EnableDndDialogFactory {
private static final int SECONDS_MS = 1000;
private static final int MINUTES_MS = 60 * SECONDS_MS;
- @Nullable
private final EnableDndDialogMetricsLogger mMetricsLogger;
@VisibleForTesting
@@ -152,16 +148,10 @@ public class EnableDndDialogFactory {
Slog.d(TAG, "Invalid manual condition: " + tag.condition);
}
// always triggers priority-only dnd with chosen condition
- if (Flags.modesApi()) {
- mNotificationManager.setZenMode(
- Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS,
- getRealConditionId(tag.condition), TAG,
- /* fromUser= */ true);
- } else {
- mNotificationManager.setZenMode(
- Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS,
- getRealConditionId(tag.condition), TAG);
- }
+ mNotificationManager.setZenMode(
+ Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS,
+ getRealConditionId(tag.condition), TAG,
+ /* fromUser= */ true);
}
});
diff --git a/packages/SettingsLib/src/com/android/settingslib/utils/CustomDialogHelper.java b/packages/SettingsLib/src/com/android/settingslib/utils/CustomDialogHelper.java
index 6e64c597f5cc..34e08af18f93 100644
--- a/packages/SettingsLib/src/com/android/settingslib/utils/CustomDialogHelper.java
+++ b/packages/SettingsLib/src/com/android/settingslib/utils/CustomDialogHelper.java
@@ -34,6 +34,8 @@ import com.android.settingslib.R;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
+import androidx.annotation.NonNull;
+
/**
* This class is used to create custom dialog with icon, title, message and custom view that are
* horizontally centered.
@@ -191,20 +193,52 @@ public class CustomDialogHelper {
}
/**
+ * Sets title of the dialog by string.
+ */
+ @NonNull public CustomDialogHelper setTitle(@NonNull CharSequence title) {
+ mDialogTitle.setText(title);
+ return this;
+ }
+
+ /**
+ * Sets title padding of the dialog.
+ */
+ @NonNull public CustomDialogHelper setTitlePadding(int left, int top, int right, int bottom) {
+ mDialogTitle.setPadding(left, top, right, bottom);
+ return this;
+ }
+
+ /**
* Sets message of the dialog.
*/
- public CustomDialogHelper setMessage(@StringRes int resid) {
+ @NonNull public CustomDialogHelper setMessage(@StringRes int resid) {
mDialogMessage.setText(resid);
return this;
}
/**
+ * Sets message of the dialog by string.
+ */
+ @NonNull public CustomDialogHelper setMessage(@NonNull CharSequence message) {
+ mDialogMessage.setText(message);
+ return this;
+ }
+
+ /**
* Sets message padding of the dialog.
*/
- public CustomDialogHelper setMessagePadding(int dp) {
+ @NonNull public CustomDialogHelper setMessagePadding(int dp) {
mDialogMessage.setPadding(dp, dp, dp, dp);
return this;
}
+ /**
+ * Sets message padding of the dialog.
+ */
+ @NonNull
+ public CustomDialogHelper setMessagePadding(int left, int top, int right, int bottom) {
+ mDialogMessage.setPadding(left, top, right, bottom);
+ return this;
+ }
/**
* Sets icon of the dialog.
@@ -215,6 +249,15 @@ public class CustomDialogHelper {
}
/**
+ * Sets icon padding of the dialog.
+ */
+ @NonNull
+ public CustomDialogHelper setIconPadding(int left, int top, int right, int bottom) {
+ mDialogIcon.setPadding(left, top, right, bottom);
+ return this;
+ }
+
+ /**
* Removes all views that were previously added to the custom layout part.
*/
public CustomDialogHelper clearCustomLayout() {
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceTest.java
index d933a1ced8bc..f6e26a7200ef 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceTest.java
@@ -17,6 +17,7 @@ package com.android.settingslib.bluetooth;
import static com.android.settingslib.flags.Flags.FLAG_ENABLE_LE_AUDIO_SHARING;
import static com.android.settingslib.flags.Flags.FLAG_ENABLE_SET_PREFERRED_TRANSPORT_FOR_LE_AUDIO_DEVICE;
+import static com.android.settingslib.flags.Flags.FLAG_ENABLE_TEMPORARY_BOND_DEVICES_UI;
import static com.google.common.truth.Truth.assertThat;
@@ -78,11 +79,14 @@ public class CachedBluetoothDeviceTest {
private static final String TWS_BATTERY_RIGHT = "25";
private static final String TWS_LOW_BATTERY_THRESHOLD_LOW = "10";
private static final String TWS_LOW_BATTERY_THRESHOLD_HIGH = "25";
+ private static final String TEMP_BOND_METADATA =
+ "<TEMP_BOND_TYPE>le_audio_sharing</TEMP_BOND_TYPE>";
private static final short RSSI_1 = 10;
private static final short RSSI_2 = 11;
private static final boolean JUSTDISCOVERED_1 = true;
private static final boolean JUSTDISCOVERED_2 = false;
private static final int LOW_BATTERY_COLOR = android.R.color.holo_red_dark;
+ private static final int METADATA_FAST_PAIR_CUSTOMIZED_FIELDS = 25;
@Mock
private LocalBluetoothProfileManager mProfileManager;
@Mock
@@ -128,6 +132,7 @@ public class CachedBluetoothDeviceTest {
mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_TV_MEDIA_OUTPUT_DIALOG);
mSetFlagsRule.enableFlags(FLAG_ENABLE_SET_PREFERRED_TRANSPORT_FOR_LE_AUDIO_DEVICE);
mSetFlagsRule.enableFlags(FLAG_ENABLE_LE_AUDIO_SHARING);
+ mSetFlagsRule.enableFlags(FLAG_ENABLE_TEMPORARY_BOND_DEVICES_UI);
mContext = RuntimeEnvironment.application;
mAudioManager = mContext.getSystemService(AudioManager.class);
mShadowBluetoothAdapter = Shadow.extract(BluetoothAdapter.getDefaultAdapter());
@@ -2075,6 +2080,87 @@ public class CachedBluetoothDeviceTest {
}
@Test
+ public void getConnectionSummary_GuestDeviceBroadcastPrimary_activeDevice_returnActive() {
+ when(mBroadcast.isEnabled(any())).thenReturn(true);
+ when(mCachedDevice.getDevice()).thenReturn(mDevice);
+ Settings.Secure.putInt(
+ mContext.getContentResolver(),
+ BluetoothUtils.getPrimaryGroupIdUriForBroadcast(),
+ BluetoothCsipSetCoordinator.GROUP_ID_INVALID);
+ when(mDevice.getMetadata(METADATA_FAST_PAIR_CUSTOMIZED_FIELDS))
+ .thenReturn(TEMP_BOND_METADATA.getBytes());
+
+ List<Long> bisSyncState = new ArrayList<>();
+ bisSyncState.add(1L);
+ when(mLeBroadcastReceiveState.getBisSyncState()).thenReturn(bisSyncState);
+ List<BluetoothLeBroadcastReceiveState> sourceList = new ArrayList<>();
+ sourceList.add(mLeBroadcastReceiveState);
+ when(mAssistant.getAllSources(any())).thenReturn(sourceList);
+
+ when(mCachedDevice.getGroupId()).thenReturn(1);
+ when(mCachedDevice.isActiveDevice(BluetoothProfile.LE_AUDIO)).thenReturn(true);
+
+ assertThat(mCachedDevice.getConnectionSummary(false))
+ .isEqualTo(mContext.getString(R.string.bluetooth_active_no_battery_level));
+ }
+
+ @Test
+ public void getConnectionSummary_GuestDeviceBroadcastSecondary_activeDevice_returnGuestMedia() {
+ when(mBroadcast.isEnabled(any())).thenReturn(true);
+ when(mCachedDevice.getDevice()).thenReturn(mDevice);
+ Settings.Secure.putInt(
+ mContext.getContentResolver(),
+ BluetoothUtils.getPrimaryGroupIdUriForBroadcast(),
+ 1);
+ when(mDevice.getMetadata(METADATA_FAST_PAIR_CUSTOMIZED_FIELDS))
+ .thenReturn(TEMP_BOND_METADATA.getBytes());
+
+ List<Long> bisSyncState = new ArrayList<>();
+ bisSyncState.add(1L);
+ when(mLeBroadcastReceiveState.getBisSyncState()).thenReturn(bisSyncState);
+ List<BluetoothLeBroadcastReceiveState> sourceList = new ArrayList<>();
+ sourceList.add(mLeBroadcastReceiveState);
+ when(mAssistant.getAllSources(any())).thenReturn(sourceList);
+
+ when(mCachedDevice.getGroupId()).thenReturn(BluetoothCsipSetCoordinator.GROUP_ID_INVALID);
+
+ assertThat(mCachedDevice.getConnectionSummary(false))
+ .isEqualTo(
+ mContext.getString(R.string.bluetooth_guest_media_only_no_battery_level));
+ }
+
+ @Test
+ public void getConnectionSummary_GuestDeviceSupportsBroadcastConnected_returnGuestSupportLe() {
+ when(mBroadcast.isEnabled(any())).thenReturn(true);
+ when(mCachedDevice.getDevice()).thenReturn(mDevice);
+ when(mLeAudioProfile.isEnabled(mDevice)).thenReturn(true);
+ when(mDevice.getMetadata(METADATA_FAST_PAIR_CUSTOMIZED_FIELDS))
+ .thenReturn(TEMP_BOND_METADATA.getBytes());
+
+ when(mCachedDevice.getProfiles()).thenReturn(ImmutableList.of(mLeAudioProfile));
+ when(mCachedDevice.isConnected()).thenReturn(true);
+
+ assertThat(mCachedDevice.getConnectionSummary(false))
+ .isEqualTo(
+ mContext.getString(R.string.bluetooth_guest_no_battery_level_lea_support));
+ }
+
+ @Test
+ public void getConnectionSummary_GuestDeviceSupportsBroadcastNotConnected_returnSavedGuest() {
+ when(mBroadcast.isEnabled(any())).thenReturn(true);
+ when(mCachedDevice.getDevice()).thenReturn(mDevice);
+ when(mLeAudioProfile.isEnabled(mDevice)).thenReturn(true);
+ when(mDevice.getMetadata(METADATA_FAST_PAIR_CUSTOMIZED_FIELDS))
+ .thenReturn(TEMP_BOND_METADATA.getBytes());
+
+ when(mCachedDevice.getProfiles()).thenReturn(ImmutableList.of(mLeAudioProfile));
+ when(mCachedDevice.isConnected()).thenReturn(false);
+
+ assertThat(mCachedDevice.getConnectionSummary(false))
+ .isEqualTo(mContext.getString(R.string.bluetooth_guest_saved_device_lea_support));
+ }
+
+ @Test
public void isHearingDevice_supportHearingRelatedProfiles_returnTrue() {
when(mCachedDevice.getProfiles()).thenReturn(
ImmutableList.of(mHapClientProfile, mHearingAidProfile));
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/data/repository/ZenModeRepositoryTest.kt b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/data/repository/ZenModeRepositoryTest.kt
index 388af61c6273..b364368df473 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/data/repository/ZenModeRepositoryTest.kt
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/data/repository/ZenModeRepositoryTest.kt
@@ -91,7 +91,6 @@ class ZenModeRepositoryTest {
)
}
- @EnableFlags(android.app.Flags.FLAG_MODES_API)
@Test
fun consolidatedPolicyChanges_repositoryEmits_flagsOn() {
testScope.runTest {
@@ -110,7 +109,6 @@ class ZenModeRepositoryTest {
}
}
- @EnableFlags(android.app.Flags.FLAG_MODES_API)
@Test
fun consolidatedPolicyChanges_repositoryEmitsFromExtras() {
testScope.runTest {
diff --git a/packages/SystemUI/OWNERS b/packages/SystemUI/OWNERS
index 236654deefb5..f5c0233d56b1 100644
--- a/packages/SystemUI/OWNERS
+++ b/packages/SystemUI/OWNERS
@@ -8,7 +8,6 @@ achalke@google.com
acul@google.com
adamcohen@google.com
aioana@google.com
-alexchau@google.com
alexflo@google.com
andonian@google.com
amiko@google.com
@@ -91,10 +90,8 @@ rahulbanerjee@google.com
rgl@google.com
roosa@google.com
saff@google.com
-samcackett@google.com
santie@google.com
shanh@google.com
-silvajordan@google.com
snoeberger@google.com
spdonghao@google.com
steell@google.com
@@ -106,7 +103,6 @@ thiruram@google.com
tracyzhou@google.com
tsuji@google.com
twickham@google.com
-uwaisashraf@google.com
vadimt@google.com
valiiftime@google.com
vanjan@google.com
@@ -121,3 +117,11 @@ yuandizhou@google.com
yurilin@google.com
yuzhechen@google.com
zakcohen@google.com
+
+# Overview eng team
+alexchau@google.com
+samcackett@google.com
+silvajordan@google.com
+uwaisashraf@google.com
+vinayjoglekar@google.com
+willosborn@google.com
diff --git a/packages/SystemUI/aconfig/Android.bp b/packages/SystemUI/aconfig/Android.bp
index 088ec136f24e..f5bff859269f 100644
--- a/packages/SystemUI/aconfig/Android.bp
+++ b/packages/SystemUI/aconfig/Android.bp
@@ -28,6 +28,7 @@ package {
"//frameworks/libs/systemui/tracinglib:__subpackages__",
"//frameworks/base/services/accessibility:__subpackages__",
"//frameworks/base/services/tests:__subpackages__",
+ "//packages/apps/Settings:__subpackages__",
"//platform_testing:__subpackages__",
"//vendor:__subpackages__",
"//cts:__subpackages__",
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index 2c3a5eacf940..29b578ae6e48 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -67,6 +67,13 @@ flag {
}
flag {
+ name: "notifications_redesign_guts"
+ namespace: "systemui"
+ description: "Notifications Redesign: Update the look of the notification guts (that appear on long press). This includes using the new cache for app icons."
+ bug: "394822197"
+}
+
+flag {
name: "notification_row_content_binder_refactor"
namespace: "systemui"
description: "Convert the NotificationContentInflater to Kotlin and restructure it to support modern views"
@@ -967,6 +974,26 @@ flag {
}
flag {
+ name: "use_notif_inflation_thread_for_footer"
+ namespace: "systemui"
+ description: "use the @NotifInflation thread for FooterView and EmptyShadeView inflation"
+ bug: "375320642"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+flag {
+ name: "use_notif_inflation_thread_for_row"
+ namespace: "systemui"
+ description: "use the @NotifInflation thread for ExpandableNotificationRow inflation"
+ bug: "375320642"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+flag {
name: "notify_power_manager_user_activity_background"
namespace: "systemui"
description: "Decide whether to notify the user activity to power manager in the background thread."
@@ -1970,6 +1997,16 @@ flag {
}
flag {
+ name: "hardware_color_styles"
+ namespace: "systemui"
+ description: "Enables loading initial colors based ion hardware color"
+ bug: "347286986"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+flag {
name: "shade_launch_accessibility"
namespace: "systemui"
description: "Intercept accessibility focus events for the Shade during launch animations to avoid stray TalkBack events."
@@ -1980,6 +2017,16 @@ flag {
}
flag {
+ name: "expand_collapse_privacy_dialog"
+ namespace: "systemui"
+ description: "Add expand and collapse actions to accessibility, to allow announcement in TalkBack when state changes."
+ bug: "380161221"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+flag {
name: "show_locked_by_your_watch_keyguard_indicator"
namespace: "systemui"
description: "Show a Locked by your watch indicator on the keyguard when the device is locked by the watch."
@@ -1999,3 +2046,23 @@ flag {
description: "Enables the clock fidget animation"
bug: "364664389"
}
+
+flag {
+ name: "notifications_launch_radius"
+ namespace: "systemui"
+ description: "Fixes a discrepancy in corner radius between expanding notification and opening window during launch animations."
+ bug: "396054791"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+flag {
+ name: "skip_hide_sensitive_notif_animation"
+ namespace: "systemui"
+ description: "Skip hide sensitive notification animation when the showing layout is not changed."
+ bug: "390624334"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewTransitionAnimatorController.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewTransitionAnimatorController.kt
index 65cd3c79cd16..444389fb26ea 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewTransitionAnimatorController.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewTransitionAnimatorController.kt
@@ -36,6 +36,7 @@ import android.view.ViewGroupOverlay
import android.widget.FrameLayout
import com.android.internal.jank.Cuj.CujType
import com.android.internal.jank.InteractionJankMonitor
+import com.android.systemui.Flags
import java.util.LinkedList
import kotlin.math.min
import kotlin.math.roundToInt
@@ -58,7 +59,7 @@ open class GhostedViewTransitionAnimatorController
@JvmOverloads
constructor(
/** The view that will be ghosted and from which the background will be extracted. */
- private val ghostedView: View,
+ transitioningView: View,
/** The [CujType] associated to this launch animation. */
private val launchCujType: Int? = null,
@@ -75,11 +76,24 @@ constructor(
private val isEphemeral: Boolean = false,
private var interactionJankMonitor: InteractionJankMonitor =
InteractionJankMonitor.getInstance(),
+
+ /** [ViewTransitionRegistry] to store the mapping of transitioning view and its token */
+ private val transitionRegistry: IViewTransitionRegistry? =
+ if (Flags.decoupleViewControllerInAnimlib()) {
+ ViewTransitionRegistry.instance
+ } else {
+ null
+ }
) : ActivityTransitionAnimator.Controller {
override val isLaunching: Boolean = true
/** The container to which we will add the ghost view and expanding background. */
- override var transitionContainer = ghostedView.rootView as ViewGroup
+ override var transitionContainer: ViewGroup
+ get() = ghostedView.rootView as ViewGroup
+ set(_) {
+ // empty, should never be set to avoid memory leak
+ }
+
private val transitionContainerOverlay: ViewGroupOverlay
get() = transitionContainer.overlay
@@ -138,9 +152,33 @@ constructor(
}
}
+ /** [ViewTransitionToken] to be used for storing transitioning view in [transitionRegistry] */
+ private val transitionToken =
+ if (Flags.decoupleViewControllerInAnimlib()) {
+ ViewTransitionToken(transitioningView::class.java)
+ } else {
+ null
+ }
+
+ /** The view that will be ghosted and from which the background will be extracted */
+ private val ghostedView: View
+ get() =
+ if (Flags.decoupleViewControllerInAnimlib()) {
+ transitionRegistry?.getView(transitionToken!!)
+ } else {
+ _ghostedView
+ }!!
+
+ private val _ghostedView =
+ if (Flags.decoupleViewControllerInAnimlib()) {
+ null
+ } else {
+ transitioningView
+ }
+
init {
// Make sure the View we launch from implements LaunchableView to avoid visibility issues.
- if (ghostedView !is LaunchableView) {
+ if (transitioningView !is LaunchableView) {
throw IllegalArgumentException(
"A GhostedViewLaunchAnimatorController was created from a View that does not " +
"implement LaunchableView. This can lead to subtle bugs where the visibility " +
@@ -148,6 +186,10 @@ constructor(
)
}
+ if (Flags.decoupleViewControllerInAnimlib()) {
+ transitionRegistry?.register(transitionToken!!, transitioningView)
+ }
+
/** Find the first view with a background in [view] and its children. */
fun findBackground(view: View): Drawable? {
if (view.background != null) {
@@ -184,6 +226,7 @@ constructor(
if (TransitionAnimator.returnAnimationsEnabled()) {
ghostedView.removeOnAttachStateChangeListener(detachListener)
}
+ transitionToken?.let { token -> transitionRegistry?.unregister(token) }
}
/**
@@ -237,7 +280,7 @@ constructor(
val insets = backgroundInsets
val boundCorrections: Rect =
if (ghostedView is LaunchableView) {
- ghostedView.getPaddingForLaunchAnimation()
+ (ghostedView as LaunchableView).getPaddingForLaunchAnimation()
} else {
Rect()
}
@@ -387,8 +430,8 @@ constructor(
if (ghostedView is LaunchableView) {
// Restore the ghosted view visibility.
- ghostedView.setShouldBlockVisibilityChanges(false)
- ghostedView.onActivityLaunchAnimationEnd()
+ (ghostedView as LaunchableView).setShouldBlockVisibilityChanges(false)
+ (ghostedView as LaunchableView).onActivityLaunchAnimationEnd()
} else {
// Make the ghosted view visible. We ensure that the view is considered VISIBLE by
// accessibility by first making it INVISIBLE then VISIBLE (see b/204944038#comment17
@@ -398,7 +441,7 @@ constructor(
ghostedView.invalidate()
}
- if (isEphemeral) {
+ if (isEphemeral || Flags.decoupleViewControllerInAnimlib()) {
onDispose()
}
}
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/IViewTransitionRegistry.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/IViewTransitionRegistry.kt
new file mode 100644
index 000000000000..af3ca87bf788
--- /dev/null
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/IViewTransitionRegistry.kt
@@ -0,0 +1,56 @@
+/*
+ * 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.animation
+
+import android.view.View
+
+/** Represents a Registry for holding a transitioning view mapped to a token */
+interface IViewTransitionRegistry {
+
+ /**
+ * Registers the transitioning [view] mapped to a [token]
+ *
+ * @param token The token corresponding to the transitioning view
+ * @param view The view undergoing transition
+ */
+ fun register(token: ViewTransitionToken, view: View)
+
+ /**
+ * Unregisters the transitioned view from its corresponding [token]
+ *
+ * @param token The token corresponding to the transitioning view
+ */
+ fun unregister(token: ViewTransitionToken)
+
+ /**
+ * Extracts a transitioning view from registry using its corresponding [token]
+ *
+ * @param token The token corresponding to the transitioning view
+ */
+ fun getView(token: ViewTransitionToken): View?
+
+ /**
+ * Return token mapped to the [view], if it is present in the registry
+ *
+ * @param view the transitioning view whose token we are requesting
+ * @return token associated with the [view] if present, else null
+ */
+ fun getViewToken(view: View): ViewTransitionToken?
+
+ /** Event call to run on registry update (on both [register] and [unregister]) */
+ fun onRegistryUpdate()
+}
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/TextAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/TextAnimator.kt
index b9f9bc7e2daa..5b073e49192a 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/TextAnimator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/TextAnimator.kt
@@ -27,9 +27,8 @@ import android.graphics.fonts.FontVariationAxis
import android.text.Layout
import android.util.Log
import android.util.LruCache
-
-private const val DEFAULT_ANIMATION_DURATION: Long = 300
-private const val TYPEFACE_CACHE_MAX_ENTRIES = 5
+import androidx.annotation.VisibleForTesting
+import com.android.app.animation.Interpolators
typealias GlyphCallback = (TextAnimator.PositionedGlyph, Float) -> Unit
@@ -76,6 +75,10 @@ class TypefaceVariantCacheImpl(var baseTypeface: Typeface, override val animatio
cache.put(fvar, it)
}
}
+
+ companion object {
+ private const val TYPEFACE_CACHE_MAX_ENTRIES = 5
+ }
}
/**
@@ -108,25 +111,12 @@ class TextAnimator(
private val typefaceCache: TypefaceVariantCache,
private val invalidateCallback: () -> Unit = {},
) {
- // Following two members are for mutable for testing purposes.
- public var textInterpolator = TextInterpolator(layout, typefaceCache)
- public var animator =
- ValueAnimator.ofFloat(1f).apply {
- duration = DEFAULT_ANIMATION_DURATION
- addUpdateListener {
- textInterpolator.progress = it.animatedValue as Float
- textInterpolator.linearProgress =
- it.currentPlayTime.toFloat() / it.duration.toFloat()
- invalidateCallback()
- }
- addListener(
- object : AnimatorListenerAdapter() {
- override fun onAnimationEnd(animation: Animator) = textInterpolator.rebase()
+ @VisibleForTesting var textInterpolator = TextInterpolator(layout, typefaceCache)
+ @VisibleForTesting var createAnimator: () -> ValueAnimator = { ValueAnimator.ofFloat(1f) }
- override fun onAnimationCancel(animation: Animator) = textInterpolator.rebase()
- }
- )
- }
+ var animator: ValueAnimator? = null
+
+ val fontVariationUtils = FontVariationUtils()
sealed class PositionedGlyph {
/** Mutable X coordinate of the glyph position relative from drawing offset. */
@@ -165,8 +155,6 @@ class TextAnimator(
protected set
}
- private val fontVariationUtils = FontVariationUtils()
-
fun updateLayout(layout: Layout, textSize: Float = -1f) {
textInterpolator.layout = layout
@@ -178,9 +166,8 @@ class TextAnimator(
}
}
- fun isRunning(): Boolean {
- return animator.isRunning
- }
+ val isRunning: Boolean
+ get() = animator?.isRunning ?: false
/**
* GlyphFilter applied just before drawing to canvas for tweaking positions and text size.
@@ -237,110 +224,110 @@ class TextAnimator(
fun draw(c: Canvas) = textInterpolator.draw(c)
- /**
- * Set text style with animation.
- *
- * ```
- * By passing -1 to weight, the view preserve the current weight.
- * By passing -1 to textSize, the view preserve the current text size.
- * By passing -1 to duration, the default text animation, 1000ms, is used.
- * By passing false to animate, the text will be updated without animation.
- * ```
- *
- * @param fvar an optional text fontVariationSettings.
- * @param textSize an optional font size.
- * @param colors an optional colors array that must be the same size as numLines passed to the
- * TextInterpolator
- * @param strokeWidth an optional paint stroke width
- * @param animate an optional boolean indicating true for showing style transition as animation,
- * false for immediate style transition. True by default.
- * @param duration an optional animation duration in milliseconds. This is ignored if animate is
- * false.
- * @param interpolator an optional time interpolator. If null is passed, last set interpolator
- * will be used. This is ignored if animate is false.
- */
- fun setTextStyle(
- fvar: String? = "",
- textSize: Float = -1f,
- color: Int? = null,
- strokeWidth: Float = -1f,
- animate: Boolean = true,
- duration: Long = -1L,
- interpolator: TimeInterpolator? = null,
- delay: Long = 0,
- onAnimationEnd: Runnable? = null,
+ /** Style spec to use when rendering the font */
+ data class Style(
+ val fVar: String? = null,
+ val textSize: Float? = null,
+ val color: Int? = null,
+ val strokeWidth: Float? = null,
) {
- setTextStyleInternal(
- fvar,
- textSize,
- color,
- strokeWidth,
- animate,
- duration,
- interpolator,
- delay,
- onAnimationEnd,
- updateLayoutOnFailure = true,
- )
+ fun withUpdatedFVar(
+ fontVariationUtils: FontVariationUtils,
+ weight: Int = -1,
+ width: Int = -1,
+ opticalSize: Int = -1,
+ roundness: Int = -1,
+ ): Style {
+ return this.copy(
+ fVar =
+ fontVariationUtils.updateFontVariation(
+ weight = weight,
+ width = width,
+ opticalSize = opticalSize,
+ roundness = roundness,
+ )
+ )
+ }
}
- private fun setTextStyleInternal(
- fvar: String?,
- textSize: Float,
- color: Int?,
- strokeWidth: Float,
- animate: Boolean,
- duration: Long,
- interpolator: TimeInterpolator?,
- delay: Long,
- onAnimationEnd: Runnable?,
- updateLayoutOnFailure: Boolean,
+ /** Animation Spec for use when style changes should be animated */
+ data class Animation(
+ val animate: Boolean = true,
+ val startDelay: Long = 0,
+ val duration: Long = DEFAULT_ANIMATION_DURATION,
+ val interpolator: TimeInterpolator = Interpolators.LINEAR,
+ val onAnimationEnd: Runnable? = null,
) {
- try {
- if (animate) {
- animator.cancel()
- textInterpolator.rebase()
+ fun configureAnimator(animator: Animator) {
+ animator.startDelay = startDelay
+ animator.duration = duration
+ animator.interpolator = interpolator
+ if (onAnimationEnd != null) {
+ animator.addListener(
+ object : AnimatorListenerAdapter() {
+ override fun onAnimationEnd(animation: Animator) {
+ onAnimationEnd.run()
+ }
+ }
+ )
}
+ }
- if (textSize >= 0) {
- textInterpolator.targetPaint.textSize = textSize
- }
- if (!fvar.isNullOrBlank()) {
- textInterpolator.targetPaint.typeface = typefaceCache.getTypefaceForVariant(fvar)
- }
- if (color != null) {
- textInterpolator.targetPaint.color = color
- }
- if (strokeWidth >= 0F) {
- textInterpolator.targetPaint.strokeWidth = strokeWidth
+ companion object {
+ val DISABLED = Animation(animate = false)
+ }
+ }
+
+ /** Sets the text style, optionally with animation */
+ fun setTextStyle(style: Style, animation: Animation = Animation.DISABLED) {
+ animator?.cancel()
+ setTextStyleInternal(style, rebase = animation.animate)
+
+ if (animation.animate) {
+ animator = buildAnimator(animation).apply { start() }
+ } else {
+ textInterpolator.progress = 1f
+ textInterpolator.rebase()
+ invalidateCallback()
+ }
+ }
+
+ /** Builds a ValueAnimator from the specified animation parameters */
+ private fun buildAnimator(animation: Animation): ValueAnimator {
+ return createAnimator().apply {
+ duration = DEFAULT_ANIMATION_DURATION
+ animation.configureAnimator(this)
+
+ addUpdateListener {
+ textInterpolator.progress = it.animatedValue as Float
+ textInterpolator.linearProgress = it.currentPlayTime / it.duration.toFloat()
+ invalidateCallback()
}
- textInterpolator.onTargetPaintModified()
- if (animate) {
- animator.startDelay = delay
- animator.duration = if (duration == -1L) DEFAULT_ANIMATION_DURATION else duration
- interpolator?.let { animator.interpolator = it }
- if (onAnimationEnd != null) {
- animator.addListener(
- object : AnimatorListenerAdapter() {
- override fun onAnimationEnd(animation: Animator) {
- onAnimationEnd.run()
- animator.removeListener(this)
- }
-
- override fun onAnimationCancel(animation: Animator) {
- animator.removeListener(this)
- }
- }
- )
+ addListener(
+ object : AnimatorListenerAdapter() {
+ override fun onAnimationEnd(animator: Animator) = textInterpolator.rebase()
+
+ override fun onAnimationCancel(animator: Animator) = textInterpolator.rebase()
}
- animator.start()
- } else {
- // No animation is requested, thus set base and target state to the same state.
- textInterpolator.progress = 1f
- textInterpolator.rebase()
- invalidateCallback()
+ )
+ }
+ }
+
+ private fun setTextStyleInternal(
+ style: Style,
+ rebase: Boolean,
+ updateLayoutOnFailure: Boolean = true,
+ ) {
+ try {
+ if (rebase) textInterpolator.rebase()
+ style.color?.let { textInterpolator.targetPaint.color = it }
+ style.textSize?.let { textInterpolator.targetPaint.textSize = it }
+ style.strokeWidth?.let { textInterpolator.targetPaint.strokeWidth = it }
+ style.fVar?.let {
+ textInterpolator.targetPaint.typeface = typefaceCache.getTypefaceForVariant(it)
}
+ textInterpolator.onTargetPaintModified()
} catch (ex: IllegalArgumentException) {
if (updateLayoutOnFailure) {
Log.e(
@@ -351,81 +338,15 @@ class TextAnimator(
)
updateLayout(textInterpolator.layout)
- setTextStyleInternal(
- fvar,
- textSize,
- color,
- strokeWidth,
- animate,
- duration,
- interpolator,
- delay,
- onAnimationEnd,
- updateLayoutOnFailure = false,
- )
+ setTextStyleInternal(style, rebase, updateLayoutOnFailure = false)
} else {
throw ex
}
}
}
- /**
- * Set text style with animation. Similar as
- *
- * ```
- * fun setTextStyle(
- * fvar: String? = "",
- * textSize: Float = -1f,
- * color: Int? = null,
- * strokeWidth: Float = -1f,
- * animate: Boolean = true,
- * duration: Long = -1L,
- * interpolator: TimeInterpolator? = null,
- * delay: Long = 0,
- * onAnimationEnd: Runnable? = null
- * )
- * ```
- *
- * @param weight an optional style value for `wght` in fontVariationSettings.
- * @param width an optional style value for `wdth` in fontVariationSettings.
- * @param opticalSize an optional style value for `opsz` in fontVariationSettings.
- * @param roundness an optional style value for `ROND` in fontVariationSettings.
- */
- fun setTextStyle(
- weight: Int = -1,
- width: Int = -1,
- opticalSize: Int = -1,
- roundness: Int = -1,
- textSize: Float = -1f,
- color: Int? = null,
- strokeWidth: Float = -1f,
- animate: Boolean = true,
- duration: Long = -1L,
- interpolator: TimeInterpolator? = null,
- delay: Long = 0,
- onAnimationEnd: Runnable? = null,
- ) {
- setTextStyleInternal(
- fvar =
- fontVariationUtils.updateFontVariation(
- weight = weight,
- width = width,
- opticalSize = opticalSize,
- roundness = roundness,
- ),
- textSize = textSize,
- color = color,
- strokeWidth = strokeWidth,
- animate = animate,
- duration = duration,
- interpolator = interpolator,
- delay = delay,
- onAnimationEnd = onAnimationEnd,
- updateLayoutOnFailure = true,
- )
- }
-
companion object {
private val TAG = TextAnimator::class.simpleName!!
+ const val DEFAULT_ANIMATION_DURATION = 300L
}
}
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/ViewTransitionRegistry.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/ViewTransitionRegistry.kt
index 58c2a1c98ec4..86c7f76c6bee 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/ViewTransitionRegistry.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/ViewTransitionRegistry.kt
@@ -24,14 +24,14 @@ import java.lang.ref.WeakReference
* A registry to temporarily store the view being transitioned into a Dialog (using
* [DialogTransitionAnimator]) or an Activity (using [ActivityTransitionAnimator])
*/
-class ViewTransitionRegistry {
+class ViewTransitionRegistry : IViewTransitionRegistry {
/**
* A map of a unique token to a WeakReference of the View being transitioned. WeakReference
* ensures that Views are garbage collected whenever they become eligible and avoid any
* memory leaks
*/
- private val registry by lazy { mutableMapOf<ViewTransitionToken, WeakReference<View>>() }
+ private val registry by lazy { mutableMapOf<ViewTransitionToken, WeakReference<View>>() }
/**
* A [View.OnAttachStateChangeListener] to be attached to all views stored in the registry to
@@ -45,8 +45,7 @@ class ViewTransitionRegistry {
}
override fun onViewDetachedFromWindow(view: View) {
- (view.getTag(R.id.tag_view_transition_token)
- as? ViewTransitionToken)?.let { token -> unregister(token) }
+ getViewToken(view)?.let { token -> unregister(token) }
}
}
}
@@ -57,12 +56,12 @@ class ViewTransitionRegistry {
* @param token unique token associated with the transitioning view
* @param view view undergoing transitions
*/
- fun register(token: ViewTransitionToken, view: View) {
+ override fun register(token: ViewTransitionToken, view: View) {
// token embedded as a view tag enables to use a single listener for all views
view.setTag(R.id.tag_view_transition_token, token)
view.addOnAttachStateChangeListener(listener)
registry[token] = WeakReference(view)
- emitCountForTrace()
+ onRegistryUpdate()
}
/**
@@ -70,30 +69,51 @@ class ViewTransitionRegistry {
*
* @param token unique token associated with the transitioning view
*/
- fun unregister(token: ViewTransitionToken) {
+ override fun unregister(token: ViewTransitionToken) {
registry.remove(token)?.let {
it.get()?.let { view ->
view.removeOnAttachStateChangeListener(listener)
view.setTag(R.id.tag_view_transition_token, null)
}
it.clear()
+ onRegistryUpdate()
}
- emitCountForTrace()
}
/**
* Access a view from registry using unique "token" associated with it
* WARNING - this returns a StrongReference to the View stored in the registry
*/
- fun getView(token: ViewTransitionToken): View? {
+ override fun getView(token: ViewTransitionToken): View? {
return registry[token]?.get()
}
/**
+ * Return token mapped to the [view], if it is present in the registry
+ *
+ * @param view the transitioning view whose token we are requesting
+ * @return token associated with the [view] if present, else null
+ */
+ override fun getViewToken(view: View): ViewTransitionToken? {
+ return (view.getTag(R.id.tag_view_transition_token) as? ViewTransitionToken)?.let { token ->
+ getView(token)?.let { token }
+ }
+ }
+
+ /** Event call to run on registry update (on both [register] and [unregister]) */
+ override fun onRegistryUpdate() {
+ emitCountForTrace()
+ }
+
+ /**
* Utility function to emit number of non-null views in the registry whenever the registry is
* updated (via [register] or [unregister])
*/
private fun emitCountForTrace() {
Trace.setCounter("transition_registry_view_count", registry.count().toLong())
}
+
+ companion object {
+ val instance by lazy(LazyThreadSafetyMode.SYNCHRONIZED) { ViewTransitionRegistry() }
+ }
}
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/ViewTransitionToken.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/ViewTransitionToken.kt
index c211a8ed1de2..e011df01504f 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/ViewTransitionToken.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/ViewTransitionToken.kt
@@ -16,17 +16,19 @@
package com.android.systemui.animation
+import java.util.UUID
+
/**
* A token uniquely mapped to a View in [ViewTransitionRegistry]. This token is guaranteed to be
* unique as timestamp is appended to the token string
*
- * @constructor creates an instance of [ViewTransitionToken] with token as "timestamp" or
- * "ClassName_timestamp"
+ * @constructor creates an instance of [ViewTransitionToken] with token as "UUID" or
+ * "ClassName_UUID"
*
* @property token String value of a unique token
*/
@JvmInline
value class ViewTransitionToken private constructor(val token: String) {
- constructor() : this(token = System.currentTimeMillis().toString())
- constructor(clazz: Class<*>) : this(token = clazz.simpleName + "_${System.currentTimeMillis()}")
+ constructor() : this(token = UUID.randomUUID().toString())
+ constructor(clazz: Class<*>) : this(token = clazz.simpleName + "_${UUID.randomUUID()}")
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenContent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenContent.kt
index ba85f9570d09..5806458da9b5 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenContent.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenContent.kt
@@ -20,11 +20,8 @@ import android.view.View
import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalView
-import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.android.compose.animation.scene.ContentScope
import com.android.internal.jank.Cuj
import com.android.internal.jank.Cuj.CujType
@@ -70,8 +67,7 @@ class LockscreenContent(
rememberViewModel("LockscreenContent-scrimViewModel") {
notificationScrimViewModelFactory.create()
}
- val isContentVisible: Boolean by viewModel.isContentVisible.collectAsStateWithLifecycle()
- if (!isContentVisible) {
+ if (!viewModel.isContentVisible) {
// If the content isn't supposed to be visible, show a large empty box as it's needed
// for scene transition animations (can't just skip rendering everything or shared
// elements won't have correct final/initial bounds from animating in and out of the
@@ -80,15 +76,13 @@ class LockscreenContent(
return
}
- val coroutineScope = rememberCoroutineScope()
- val blueprintId by viewModel.blueprintId(coroutineScope).collectAsStateWithLifecycle()
DisposableEffect(view) {
clockInteractor.clockEventController.registerListeners(view)
onDispose { clockInteractor.clockEventController.unregisterListeners() }
}
- val blueprint = blueprintByBlueprintId[blueprintId] ?: return
+ val blueprint = blueprintByBlueprintId[viewModel.blueprintId] ?: return
with(blueprint) {
Content(viewModel, modifier.sysuiResTag("keyguard_root_view"))
NotificationLockscreenScrim(notificationLockscreenScrimViewModel)
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprint.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprint.kt
index d2fff06ad746..590a74ee2a0d 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprint.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprint.kt
@@ -23,7 +23,6 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.graphicsLayer
@@ -32,7 +31,6 @@ import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntRect
import androidx.compose.ui.unit.dp
-import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.android.compose.animation.scene.ContentScope
import com.android.compose.modifiers.padding
import com.android.systemui.compose.modifiers.sysuiResTag
@@ -45,6 +43,8 @@ import com.android.systemui.keyguard.ui.composable.section.SettingsMenuSection
import com.android.systemui.keyguard.ui.composable.section.StatusBarSection
import com.android.systemui.keyguard.ui.composable.section.TopAreaSection
import com.android.systemui.keyguard.ui.viewmodel.LockscreenContentViewModel
+import com.android.systemui.keyguard.ui.viewmodel.LockscreenContentViewModel.NotificationsPlacement.BelowClock
+import com.android.systemui.keyguard.ui.viewmodel.LockscreenContentViewModel.NotificationsPlacement.BesideClock
import com.android.systemui.res.R
import java.util.Optional
import javax.inject.Inject
@@ -71,11 +71,8 @@ constructor(
@Composable
override fun ContentScope.Content(viewModel: LockscreenContentViewModel, modifier: Modifier) {
val isUdfpsVisible = viewModel.isUdfpsVisible
- val isShadeLayoutWide by viewModel.isShadeLayoutWide.collectAsStateWithLifecycle()
- val unfoldTranslations by viewModel.unfoldTranslations.collectAsStateWithLifecycle()
- val areNotificationsVisible by
- viewModel.areNotificationsVisible().collectAsStateWithLifecycle(initialValue = false)
- val isBypassEnabled by viewModel.isBypassEnabled.collectAsStateWithLifecycle()
+ val isBypassEnabled = viewModel.isBypassEnabled
+ val notificationsPlacement = viewModel.notificationsPlacement
if (isBypassEnabled) {
with(notificationSection) { HeadsUpNotifications() }
@@ -92,7 +89,9 @@ constructor(
modifier =
Modifier.fillMaxWidth()
.padding(
- horizontal = { unfoldTranslations.start.roundToInt() }
+ horizontal = {
+ viewModel.unfoldTranslations.start.roundToInt()
+ }
)
)
}
@@ -101,28 +100,28 @@ constructor(
with(topAreaSection) {
DefaultClockLayout(
smartSpacePaddingTop = viewModel::getSmartSpacePaddingTop,
- isShadeLayoutWide = isShadeLayoutWide,
modifier =
Modifier.fillMaxWidth().graphicsLayer {
- translationX = unfoldTranslations.start
+ translationX = viewModel.unfoldTranslations.start
},
)
}
- if (isShadeLayoutWide && !isBypassEnabled) {
+ if (notificationsPlacement is BesideClock && !isBypassEnabled) {
with(notificationSection) {
Box(modifier = Modifier.fillMaxHeight()) {
AodPromotedNotificationArea(
modifier =
Modifier.fillMaxWidth(0.5f)
- .align(alignment = Alignment.TopEnd)
+ .align(notificationsPlacement.alignment)
)
Notifications(
- areNotificationsVisible = areNotificationsVisible,
+ areNotificationsVisible =
+ viewModel.areNotificationsVisible,
burnInParams = null,
modifier =
Modifier.fillMaxWidth(0.5f)
.fillMaxHeight()
- .align(alignment = Alignment.TopEnd)
+ .align(notificationsPlacement.alignment)
.padding(top = 12.dp),
)
}
@@ -138,7 +137,7 @@ constructor(
dimensionResource(R.dimen.below_clock_padding_start_icons)
with(notificationSection) {
- if (!isShadeLayoutWide && !isBypassEnabled) {
+ if (notificationsPlacement is BelowClock && !isBypassEnabled) {
Box(modifier = Modifier.weight(weight = 1f)) {
Column(Modifier.align(alignment = Alignment.TopStart)) {
AodPromotedNotificationArea(
@@ -150,13 +149,13 @@ constructor(
)
}
Notifications(
- areNotificationsVisible = areNotificationsVisible,
+ areNotificationsVisible = viewModel.areNotificationsVisible,
burnInParams = null,
)
}
} else {
Column {
- if (!isShadeLayoutWide) {
+ if (viewModel.notificationsPlacement is BelowClock) {
AodPromotedNotificationArea(
modifier =
Modifier.padding(top = aodPromotedNotifTopPadding)
@@ -204,13 +203,17 @@ constructor(
isStart = true,
applyPadding = true,
modifier =
- Modifier.graphicsLayer { translationX = unfoldTranslations.start },
+ Modifier.graphicsLayer {
+ translationX = viewModel.unfoldTranslations.start
+ },
)
Shortcut(
isStart = false,
applyPadding = true,
modifier =
- Modifier.graphicsLayer { translationX = unfoldTranslations.end },
+ Modifier.graphicsLayer {
+ translationX = viewModel.unfoldTranslations.end
+ },
)
}
with(settingsMenuSection) { SettingsMenu(onSettingsMenuPlaced) }
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/MediaCarouselSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/MediaCarouselSection.kt
index d8b3f742b447..0876631cf5c1 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/MediaCarouselSection.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/MediaCarouselSection.kt
@@ -41,16 +41,13 @@ constructor(
) {
@Composable
- fun ContentScope.KeyguardMediaCarousel(
- isShadeLayoutWide: Boolean,
- modifier: Modifier = Modifier,
- ) {
+ fun ContentScope.KeyguardMediaCarousel(modifier: Modifier = Modifier) {
val viewModel =
rememberViewModel(traceName = "KeyguardMediaCarousel") {
keyguardMediaViewModelFactory.create()
}
val horizontalPadding =
- if (isShadeLayoutWide) {
+ if (viewModel.isShadeLayoutWide) {
dimensionResource(id = R.dimen.notification_side_paddings)
} else {
dimensionResource(id = R.dimen.notification_side_paddings) +
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/TopAreaSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/TopAreaSection.kt
index 6293fc26f96a..013424006668 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/TopAreaSection.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/TopAreaSection.kt
@@ -60,7 +60,6 @@ constructor(
@Composable
fun ContentScope.DefaultClockLayout(
smartSpacePaddingTop: (Resources) -> Int,
- isShadeLayoutWide: Boolean,
modifier: Modifier = Modifier,
) {
val currentClockLayout by clockViewModel.currentClockLayout.collectAsStateWithLifecycle()
@@ -128,7 +127,7 @@ constructor(
)
}
}
- with(mediaCarouselSection) { KeyguardMediaCarousel(isShadeLayoutWide) }
+ with(mediaCarouselSection) { KeyguardMediaCarousel() }
}
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeOverlay.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeOverlay.kt
index 64f3cb13662a..297995becfb2 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeOverlay.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeOverlay.kt
@@ -23,7 +23,7 @@ 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.layout.layoutId
+import androidx.compose.ui.platform.LocalResources
import androidx.compose.ui.res.dimensionResource
import com.android.compose.animation.scene.ContentScope
import com.android.compose.animation.scene.ElementKey
@@ -34,6 +34,13 @@ import com.android.systemui.keyguard.ui.composable.blueprint.rememberBurnIn
import com.android.systemui.keyguard.ui.composable.section.DefaultClockSection
import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel
import com.android.systemui.lifecycle.rememberViewModel
+import com.android.systemui.media.controls.ui.composable.MediaCarousel
+import com.android.systemui.media.controls.ui.composable.isLandscape
+import com.android.systemui.media.controls.ui.controller.MediaCarouselController
+import com.android.systemui.media.controls.ui.view.MediaHost
+import com.android.systemui.media.controls.ui.view.MediaHostState.Companion.COLLAPSED
+import com.android.systemui.media.controls.ui.view.MediaHostState.Companion.EXPANDED
+import com.android.systemui.media.dagger.MediaModule.QUICK_QS_PANEL
import com.android.systemui.notifications.ui.viewmodel.NotificationsShadeOverlayActionsViewModel
import com.android.systemui.notifications.ui.viewmodel.NotificationsShadeOverlayContentViewModel
import com.android.systemui.res.R
@@ -42,10 +49,11 @@ import com.android.systemui.scene.shared.model.Overlays
import com.android.systemui.scene.ui.composable.Overlay
import com.android.systemui.shade.ui.composable.OverlayShade
import com.android.systemui.shade.ui.composable.OverlayShadeHeader
-import com.android.systemui.shade.ui.composable.SingleShadeMeasurePolicy
import com.android.systemui.statusbar.notification.stack.ui.view.NotificationScrollView
+import com.android.systemui.util.Utils
import dagger.Lazy
import javax.inject.Inject
+import javax.inject.Named
import kotlinx.coroutines.flow.Flow
@SysUISingleton
@@ -58,6 +66,8 @@ constructor(
private val stackScrollView: Lazy<NotificationScrollView>,
private val clockSection: DefaultClockSection,
private val keyguardClockViewModel: KeyguardClockViewModel,
+ private val mediaCarouselController: MediaCarouselController,
+ @Named(QUICK_QS_PANEL) private val mediaHost: Lazy<MediaHost>,
) : Overlay {
override val key = Overlays.NotificationsShade
@@ -84,6 +94,11 @@ constructor(
viewModel.notificationsPlaceholderViewModelFactory.create()
}
+ val usingCollapsedLandscapeMedia =
+ Utils.useCollapsedMediaInLandscape(LocalResources.current)
+ mediaHost.get().expansion =
+ if (usingCollapsedLandscapeMedia && isLandscape()) COLLAPSED else EXPANDED
+
OverlayShade(
panelElement = NotificationsShade.Elements.Panel,
alignmentOnWideScreens = Alignment.TopStart,
@@ -96,9 +111,7 @@ constructor(
}
OverlayShadeHeader(
viewModel = headerViewModel,
- modifier =
- Modifier.element(NotificationsShade.Elements.StatusBar)
- .layoutId(SingleShadeMeasurePolicy.LayoutId.ShadeHeader),
+ modifier = Modifier.element(NotificationsShade.Elements.StatusBar),
)
},
) {
@@ -116,6 +129,19 @@ constructor(
}
}
+ MediaCarousel(
+ isVisible = viewModel.showMedia,
+ mediaHost = mediaHost.get(),
+ carouselController = mediaCarouselController,
+ usingCollapsedLandscapeMedia = usingCollapsedLandscapeMedia,
+ modifier =
+ Modifier.padding(
+ top = notificationStackPadding,
+ start = notificationStackPadding,
+ end = notificationStackPadding,
+ ),
+ )
+
NotificationScrollingStack(
shadeSession = shadeSession,
stackScrollView = stackScrollView.get(),
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneTransitionLayoutDataSource.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneTransitionLayoutDataSource.kt
index 8aa5bc7b7c6f..60eaa28e3822 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneTransitionLayoutDataSource.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneTransitionLayoutDataSource.kt
@@ -21,6 +21,7 @@ import com.android.compose.animation.scene.MutableSceneTransitionLayoutState
import com.android.compose.animation.scene.OverlayKey
import com.android.compose.animation.scene.SceneKey
import com.android.compose.animation.scene.TransitionKey
+import com.android.compose.animation.scene.content.state.TransitionState
import com.android.compose.animation.scene.observableTransitionState
import com.android.systemui.scene.shared.model.SceneDataSource
import kotlinx.coroutines.CoroutineScope
@@ -103,4 +104,8 @@ class SceneTransitionLayoutDataSource(
override fun instantlyHideOverlay(overlay: OverlayKey) {
state.snapTo(overlays = state.currentOverlays - overlay)
}
+
+ override fun freezeAndAnimateToCurrentState() {
+ (state.transitionState as? TransitionState.Transition)?.freezeAndAnimateToCurrentState()
+ }
}
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt
index b76656d78cc4..4bf0ceb51784 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt
@@ -366,7 +366,7 @@ constructor(
fun animateCharge(isDozing: () -> Boolean) {
// Skip charge animation if dozing animation is already playing.
- if (textAnimator == null || textAnimator!!.isRunning()) {
+ if (textAnimator == null || textAnimator!!.isRunning) {
return
}
@@ -444,29 +444,28 @@ constructor(
delay: Long,
onAnimationEnd: Runnable?,
) {
- textAnimator?.let {
- it.setTextStyle(
- weight = weight,
- color = color,
+ val style = TextAnimator.Style(color = color)
+ val animation =
+ TextAnimator.Animation(
animate = animate && isAnimationEnabled,
duration = duration,
- interpolator = interpolator,
- delay = delay,
+ interpolator = interpolator ?: Interpolators.LINEAR,
+ startDelay = delay,
onAnimationEnd = onAnimationEnd,
)
+ textAnimator?.let {
+ it.setTextStyle(
+ style.withUpdatedFVar(it.fontVariationUtils, weight = weight),
+ animation,
+ )
it.glyphFilter = glyphFilter
}
?: run {
// when the text animator is set, update its start values
onTextAnimatorInitialized = { textAnimator ->
textAnimator.setTextStyle(
- weight = weight,
- color = color,
- animate = false,
- duration = duration,
- interpolator = interpolator,
- delay = delay,
- onAnimationEnd = onAnimationEnd,
+ style.withUpdatedFVar(textAnimator.fontVariationUtils, weight = weight),
+ animation.copy(animate = false),
)
textAnimator.glyphFilter = glyphFilter
}
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleDigitalHandLayerController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleDigitalHandLayerController.kt
index a5adfa2a1ac6..0b7ea1a335ef 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleDigitalHandLayerController.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleDigitalHandLayerController.kt
@@ -21,6 +21,7 @@ import android.view.ViewGroup
import android.view.animation.Interpolator
import android.widget.RelativeLayout
import androidx.annotation.VisibleForTesting
+import com.android.systemui.animation.TextAnimator
import com.android.systemui.customization.R
import com.android.systemui.log.core.Logger
import com.android.systemui.plugins.clocks.AlarmData
@@ -65,7 +66,7 @@ data class DigitalAlignment(
data class FontTextStyle(
val lineHeight: Float? = null,
val fontSizeScale: Float? = null,
- val transitionDuration: Long = -1L,
+ val transitionDuration: Long = TextAnimator.DEFAULT_ANIMATION_DURATION,
val transitionInterpolator: Interpolator? = null,
)
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt
index 92fa6b5be1ed..8317aa39ef2b 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt
@@ -33,7 +33,9 @@ import android.util.MathUtils
import android.util.TypedValue
import android.view.View.MeasureSpec.EXACTLY
import android.view.animation.Interpolator
+import android.view.animation.PathInterpolator
import android.widget.TextView
+import com.android.app.animation.Interpolators
import com.android.internal.annotations.VisibleForTesting
import com.android.systemui.animation.GSFAxes
import com.android.systemui.animation.TextAnimator
@@ -84,17 +86,20 @@ open class SimpleDigitalClockTextView(
else -> listOf(FLEX_AOD_SMALL_WEIGHT_AXIS, FLEX_AOD_WIDTH_AXIS)
}
- private var lsFontVariation =
- if (!isLegacyFlex) listOf(LS_WEIGHT_AXIS, WIDTH_AXIS, ROUND_AXIS, SLANT_AXIS).toFVar()
- else listOf(FLEX_LS_WEIGHT_AXIS, FLEX_LS_WIDTH_AXIS, FLEX_ROUND_AXIS, SLANT_AXIS).toFVar()
+ private var lsFontVariation: String
+ private var aodFontVariation: String
+ private var fidgetFontVariation: String
- private var aodFontVariation = run {
+ init {
val roundAxis = if (!isLegacyFlex) ROUND_AXIS else FLEX_ROUND_AXIS
- (fixedAodAxes + listOf(roundAxis, SLANT_AXIS)).toFVar()
- }
+ val lsFontAxes =
+ if (!isLegacyFlex) listOf(LS_WEIGHT_AXIS, WIDTH_AXIS, ROUND_AXIS, SLANT_AXIS)
+ else listOf(FLEX_LS_WEIGHT_AXIS, FLEX_LS_WIDTH_AXIS, FLEX_ROUND_AXIS, SLANT_AXIS)
- // TODO(b/374306512): Fidget endpoint to spec
- private var fidgetFontVariation = aodFontVariation
+ lsFontVariation = lsFontAxes.toFVar()
+ aodFontVariation = (fixedAodAxes + listOf(roundAxis, SLANT_AXIS)).toFVar()
+ fidgetFontVariation = buildFidgetVariation(lsFontAxes).toFVar()
+ }
private val parser = DimensionParser(clockCtx.context)
var maxSingleDigitHeight = -1
@@ -121,7 +126,7 @@ open class SimpleDigitalClockTextView(
protected val logger = ClockLogger(this, clockCtx.messageBuffer, this::class.simpleName!!)
get() = field ?: ClockLogger.INIT_LOGGER
- private var aodDozingInterpolator: Interpolator? = null
+ private var aodDozingInterpolator: Interpolator = Interpolators.LINEAR
@VisibleForTesting lateinit var textAnimator: TextAnimator
@@ -149,7 +154,7 @@ open class SimpleDigitalClockTextView(
lockscreenColor = color
lockScreenPaint.color = lockscreenColor
if (dozeFraction < 1f) {
- textAnimator.setTextStyle(color = lockscreenColor, animate = false)
+ textAnimator.setTextStyle(TextAnimator.Style(color = lockscreenColor))
}
invalidate()
}
@@ -157,10 +162,8 @@ open class SimpleDigitalClockTextView(
fun updateAxes(lsAxes: List<ClockFontAxisSetting>) {
lsFontVariation = lsAxes.toFVar()
aodFontVariation = lsAxes.replace(fixedAodAxes).toFVar()
- logger.i({ "updateAxes(LS = $str1, AOD = $str2)" }) {
- str1 = lsFontVariation
- str2 = aodFontVariation
- }
+ fidgetFontVariation = buildFidgetVariation(lsAxes).toFVar()
+ logger.updateAxes(lsFontVariation, aodFontVariation)
lockScreenPaint.typeface = typefaceCache.getTypefaceForVariant(lsFontVariation)
typeface = lockScreenPaint.typeface
@@ -168,13 +171,28 @@ open class SimpleDigitalClockTextView(
lockScreenPaint.getTextBounds(text, 0, text.length, textBounds)
targetTextBounds.set(textBounds)
- textAnimator.setTextStyle(fvar = lsFontVariation, animate = false)
+ textAnimator.setTextStyle(TextAnimator.Style(fVar = lsFontVariation))
measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED)
recomputeMaxSingleDigitSizes()
requestLayout()
invalidate()
}
+ fun buildFidgetVariation(axes: List<ClockFontAxisSetting>): List<ClockFontAxisSetting> {
+ val result = mutableListOf<ClockFontAxisSetting>()
+ for (axis in axes) {
+ result.add(
+ FIDGET_DISTS.get(axis.key)?.let { (dist, midpoint) ->
+ ClockFontAxisSetting(
+ axis.key,
+ axis.value + dist * if (axis.value > midpoint) -1 else 1,
+ )
+ } ?: axis
+ )
+ }
+ return result
+ }
+
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
logger.onMeasure()
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
@@ -245,40 +263,54 @@ open class SimpleDigitalClockTextView(
fun animateDoze(isDozing: Boolean, isAnimated: Boolean) {
if (!this::textAnimator.isInitialized) return
+ logger.animateDoze()
textAnimator.setTextStyle(
- animate = isAnimated && isAnimationEnabled,
- color = if (isDozing) AOD_COLOR else lockscreenColor,
- textSize = if (isDozing) aodFontSizePx else lockScreenPaint.textSize,
- fvar = if (isDozing) aodFontVariation else lsFontVariation,
- duration = aodStyle.transitionDuration,
- interpolator = aodDozingInterpolator,
+ TextAnimator.Style(
+ fVar = if (isDozing) aodFontVariation else lsFontVariation,
+ color = if (isDozing) AOD_COLOR else lockscreenColor,
+ textSize = if (isDozing) aodFontSizePx else lockScreenPaint.textSize,
+ ),
+ TextAnimator.Animation(
+ animate = isAnimated && isAnimationEnabled,
+ duration = aodStyle.transitionDuration,
+ interpolator = aodDozingInterpolator,
+ ),
)
updateTextBoundsForTextAnimator()
}
fun animateCharge() {
- if (!this::textAnimator.isInitialized || textAnimator.isRunning()) {
+ if (!this::textAnimator.isInitialized || textAnimator.isRunning) {
// Skip charge animation if dozing animation is already playing.
return
}
- logger.d("animateCharge()")
+ logger.animateCharge()
+
+ val lsStyle = TextAnimator.Style(fVar = lsFontVariation)
+ val aodStyle = TextAnimator.Style(fVar = aodFontVariation)
+
textAnimator.setTextStyle(
- fvar = if (dozeFraction == 0F) aodFontVariation else lsFontVariation,
- animate = isAnimationEnabled,
- onAnimationEnd =
- Runnable {
+ if (dozeFraction == 0f) aodStyle else lsStyle,
+ TextAnimator.Animation(
+ animate = isAnimationEnabled,
+ duration = CHARGE_ANIMATION_DURATION,
+ onAnimationEnd = {
textAnimator.setTextStyle(
- fvar = if (dozeFraction == 0F) lsFontVariation else aodFontVariation,
- animate = isAnimationEnabled,
+ if (dozeFraction == 0f) lsStyle else aodStyle,
+ TextAnimator.Animation(
+ animate = isAnimationEnabled,
+ duration = CHARGE_ANIMATION_DURATION,
+ ),
)
updateTextBoundsForTextAnimator()
},
+ ),
)
updateTextBoundsForTextAnimator()
}
fun animateFidget(x: Float, y: Float) {
- if (!this::textAnimator.isInitialized || textAnimator.isRunning()) {
+ if (!this::textAnimator.isInitialized || textAnimator.isRunning) {
// Skip fidget animation if other animation is already playing.
return
}
@@ -286,19 +318,25 @@ open class SimpleDigitalClockTextView(
logger.animateFidget(x, y)
clockCtx.vibrator?.vibrate(FIDGET_HAPTICS)
- // TODO(b/374306512): Duplicated charge animation as placeholder. Implement final version
- // when we have a complete spec. May require additional code to animate individual digits.
+ // TODO(b/374306512): Delay each glyph's animation based on x/y position
textAnimator.setTextStyle(
- fvar = fidgetFontVariation,
- animate = isAnimationEnabled,
- onAnimationEnd =
- Runnable {
+ TextAnimator.Style(fVar = fidgetFontVariation),
+ TextAnimator.Animation(
+ animate = isAnimationEnabled,
+ duration = FIDGET_ANIMATION_DURATION,
+ interpolator = FIDGET_INTERPOLATOR,
+ onAnimationEnd = {
textAnimator.setTextStyle(
- fvar = if (dozeFraction == 0F) lsFontVariation else aodFontVariation,
- animate = isAnimationEnabled,
+ TextAnimator.Style(fVar = lsFontVariation),
+ TextAnimator.Animation(
+ animate = isAnimationEnabled,
+ duration = FIDGET_ANIMATION_DURATION,
+ interpolator = FIDGET_INTERPOLATOR,
+ ),
)
updateTextBoundsForTextAnimator()
},
+ ),
)
updateTextBoundsForTextAnimator()
}
@@ -329,42 +367,20 @@ open class SimpleDigitalClockTextView(
}
private fun getInterpolatedTextBounds(): Rect {
- val interpolatedTextBounds = Rect()
- if (textAnimator.animator.animatedFraction != 1.0f && textAnimator.animator.isRunning) {
- interpolatedTextBounds.left =
- MathUtils.lerp(
- prevTextBounds.left,
- targetTextBounds.left,
- textAnimator.animator.animatedValue as Float,
- )
- .toInt()
-
- interpolatedTextBounds.right =
- MathUtils.lerp(
- prevTextBounds.right,
- targetTextBounds.right,
- textAnimator.animator.animatedValue as Float,
- )
- .toInt()
-
- interpolatedTextBounds.top =
- MathUtils.lerp(
- prevTextBounds.top,
- targetTextBounds.top,
- textAnimator.animator.animatedValue as Float,
- )
- .toInt()
-
- interpolatedTextBounds.bottom =
- MathUtils.lerp(
- prevTextBounds.bottom,
- targetTextBounds.bottom,
- textAnimator.animator.animatedValue as Float,
- )
- .toInt()
- } else {
- interpolatedTextBounds.set(targetTextBounds)
+ val progress = textAnimator.animator?.let { it.animatedValue as Float } ?: 1f
+ if (!textAnimator.isRunning || progress >= 1f) {
+ return Rect(targetTextBounds)
}
+
+ val interpolatedTextBounds = Rect()
+ interpolatedTextBounds.left =
+ MathUtils.lerp(prevTextBounds.left, targetTextBounds.left, progress).toInt()
+ interpolatedTextBounds.right =
+ MathUtils.lerp(prevTextBounds.right, targetTextBounds.right, progress).toInt()
+ interpolatedTextBounds.top =
+ MathUtils.lerp(prevTextBounds.top, targetTextBounds.top, progress).toInt()
+ interpolatedTextBounds.bottom =
+ MathUtils.lerp(prevTextBounds.bottom, targetTextBounds.bottom, progress).toInt()
return interpolatedTextBounds
}
@@ -471,7 +487,7 @@ open class SimpleDigitalClockTextView(
textStyle.lineHeight?.let { lineHeight = it.toInt() }
this.aodStyle = aodStyle ?: textStyle.copy()
- this.aodStyle.transitionInterpolator?.let { aodDozingInterpolator = it }
+ aodDozingInterpolator = this.aodStyle.transitionInterpolator ?: Interpolators.LINEAR
lockScreenPaint.strokeWidth = textBorderWidth
measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED)
setInterpolatorPaint()
@@ -500,7 +516,7 @@ open class SimpleDigitalClockTextView(
recomputeMaxSingleDigitSizes()
if (this::textAnimator.isInitialized) {
- textAnimator.setTextStyle(textSize = lockScreenPaint.textSize, animate = false)
+ textAnimator.setTextStyle(TextAnimator.Style(textSize = lockScreenPaint.textSize))
}
}
@@ -525,10 +541,11 @@ open class SimpleDigitalClockTextView(
textAnimator.textInterpolator.targetPaint.set(lockScreenPaint)
textAnimator.textInterpolator.onTargetPaintModified()
textAnimator.setTextStyle(
- fvar = lsFontVariation,
- textSize = lockScreenPaint.textSize,
- color = lockscreenColor,
- animate = false,
+ TextAnimator.Style(
+ fVar = lsFontVariation,
+ textSize = lockScreenPaint.textSize,
+ color = lockscreenColor,
+ )
)
}
}
@@ -572,6 +589,17 @@ open class SimpleDigitalClockTextView(
.addPrimitive(VibrationEffect.Composition.PRIMITIVE_QUICK_RISE, 1.0f, 43)
.compose()
+ val CHARGE_ANIMATION_DURATION = 500L
+ val FIDGET_ANIMATION_DURATION = 250L
+ val FIDGET_INTERPOLATOR = PathInterpolator(0.26873f, 0f, 0.45042f, 1f)
+ val FIDGET_DISTS =
+ mapOf(
+ GSFAxes.WEIGHT to Pair(200f, 500f),
+ GSFAxes.WIDTH to Pair(30f, 75f),
+ GSFAxes.ROUND to Pair(0f, 50f),
+ GSFAxes.SLANT to Pair(0f, -5f),
+ )
+
val AOD_COLOR = Color.WHITE
val LS_WEIGHT_AXIS = ClockFontAxisSetting(GSFAxes.WEIGHT, 400f)
val AOD_WEIGHT_AXIS = ClockFontAxisSetting(GSFAxes.WEIGHT, 200f)
diff --git a/packages/SystemUI/docs/qs-tiles.md b/packages/SystemUI/docs/qs-tiles.md
index ee388ec8e5c5..87d9b7c3b853 100644
--- a/packages/SystemUI/docs/qs-tiles.md
+++ b/packages/SystemUI/docs/qs-tiles.md
@@ -8,6 +8,14 @@ This document is a more or less comprehensive summary of the state and infrastru
Settings tiles. It provides descriptions about the lifecycle of a tile, how to create new tiles and
how SystemUI manages and displays tiles, among other topics.
+A lot of the tile backend architecture is in the process of being replaced by a new architecture in
+order to align with the
+[recommended architecture](https://developer.android.com/topic/architecture#recommended-app-arch).
+
+While we are in the process of migrating, this document will try to provide a comprehensive
+overview of the current architecture as well as the new one. The sections documenting the new
+architecture are marked with the tag [NEW-ARCH].
+
## What are Quick Settings Tiles?
Quick Settings (from now on, QS) is the expanded panel that contains shortcuts for the user to
@@ -72,6 +80,27 @@ The interfaces in `QSTile` as well as other interfaces described in this documen
implement plugins to add additional tiles or different behavior. For more information,
see [plugins.md](plugins.md)
+
+#### [NEW-ARCH] Tile backend
+Instead of `QSTileImpl` the tile backend is made of a view model called `QSTileViewModelImpl`,
+which in turn is composed of 3 interfaces:
+
+* [`QSTileDataInteractor`](/packages/SystemUI/src/com/android/systemui/qs/tiles/base/interactor/QSTileDataInteractor.kt)
+is responsible for providing the data for the tile. It is responsible for fetching the state of
+the tile from the source of truth and providing that information to the tile. Typically the data
+interactor will read system state from a repository or a controller and provide a flow of
+domain-specific data model.
+
+* [`QSTileUserActionInteractor`](/packages/SystemUI/src/com/android/systemui/qs/tiles/base/interactor/QSTileUserActionInteractor.kt) is responsible for handling the user actions on the tile.
+This interactor decides what should happen when the user clicks, long clicks on the tile.
+
+* [`QSTileDataToStateMapper`](/packages/SystemUI/src/com/android/systemui/qs/tiles/base/mapper/QSTileMapper.kt)
+is responsible for mapping the data received from the data interactor to a state that the view
+model can use to update the UI.
+
+At the time being, the `QSTileViewModel`s are adapted to `QSTile`. This conversion is done by
+`QSTileViewModelAdapter`.
+
#### Tile State
Each tile has an associated `State` object that is used to communicate information to the
@@ -94,6 +123,11 @@ Additionally. `BooleanState` has a `value` boolean field that usually would be s
to `state == Tile#STATE_ACTIVE`. This is used by accessibility services along
with `expandedAccessibilityClassName`.
+#### [NEW-ARCH] Tile State
+In the new architecture, the mapper generates
+[`QSTileState`](packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileState.kt),
+which again is converted to the old state by `QSTileViewModelAdapter`.
+
#### SystemUI tiles
Each tile defined in SystemUI extends `QSTileImpl`. This abstract class implements some common
@@ -103,6 +137,9 @@ to handle different events (refresh, click, etc.).
For more information on how to implement a tile in SystemUI,
see [Implementing a SystemUI tile](#implementing-a-systemui-tile).
+As mentioned before, when the [NEW-ARCH] migration is complete, we will remove the `QSTileImpl`
+and `QSTileViewModelAdapter` and directly use`QSTileViewModelImpl`.
+
### Tile views
Each Tile has a couple of associated views for displaying it in QS and QQS. These views are updated
@@ -154,37 +191,24 @@ corresponding `QSTile` with its `QSTileView`, doing the following:
#### Life of a tile click
This is a brief run-down of what happens when a user clicks on a tile. Internal changes on the
-device (for example, changes from Settings) will trigger this process starting in step 3. Throughout
-this section, we assume that we are dealing with a `QSTileImpl`.
-
-1. User clicks on tile. The following calls happen in sequence:
- 1. `QSTileViewImpl#onClickListener`.
- 2. `QSTile#click`.
- 3. `QSTileImpl#handleClick`. This last call sets the new state for the device by using the
- associated controller.
-2. State in the device changes. This is normally outside of SystemUI's control.
-3. Controller receives a callback (or `Intent`) indicating the change in the device. The following
- calls happen:
- 1. `QSTileImpl#refreshState`, maybe passing an object with necessary information regarding the
- new state.
- 2. `QSTileImpl#handleRefreshState`
-4. `QSTileImpl#handleUpdateState` is called to update the state with the new information. This
- information can be obtained both from the `Object` passed to `refreshState` as well as from the
- controller.
-5. If the state has changed (in at least one element), `QSTileImpl#handleStateChanged` is called.
- This will trigger a call to all the associated `QSTile.Callback#onStateChanged`, passing the
- new `State`.
-6. `QSTileView#onStateChanged` is called and this calls `QSTileView#handleStateChanged`. This method
- maps the state into the view:
- * The tile colors change to match the new state.
- * `QSIconView.setIcon` is called to apply the correct state to the icon and the correct icon to
- the view.
- * The tile labels change to match the new state.
+device (for example, changes from Settings) will trigger this process starting in step 5.
+
+Step | Legacy Tiles | [NEW-ARCH] Tiles
+-------|-------|---------
+1 | User clicks on tile. | Same as legacy tiles.
+2 | `QSTileViewImpl#onClickListener` | Same as legacy tiles.
+3 | `QSTile#click` | Same as legacy tiles.
+4| `QSTileImpl#handleClick` | `QSTileUserActionInteractor#handleInput`
+5| State in the device changes. This is normally outside of SystemUI's control. Controller receives a callback (or `Intent`) indicating the change in the device. | Same as legacy tiles.
+6 | `QSTile#refreshState`and `QSTileImpl#handleRefreshState` | `QSTileDataInteractor#tileData()`
+7| `QSTileImpl#handleUpdateState` is called to update the state with the new information. This information can be obtained both from the `Object` passed to `refreshState` as well as from the controller. | The data that was generated by the data interactor is read by the `QSTileViewModelImpl.state` flow which calls `QSTileMapper#map` on the data to generate a new `QSTileState`.
+8| If the state has changed (in at least one element `QSTileImpl#handleStateChanged` is called. This will trigger a call to all the associated `QSTile.Callback#onStateChanged`, passing the new `State`. | The newly mapped QSTileState is read by the `QSTileViewModelAdapter` which then maps it to a legacy `State`. Similarly to the legacy tiles, the new state is compared to the old one and if there is a difference, `QSTile.Callback#onStateChanged` is called for all the associated callbacks.
+9 | `QSTileView#onStateChanged` is called and this calls `QSTileView#handleStateChanged`. This method maps the state updating tile color and label, and calling `QSIconView.setIcon` | Same as legacy tiles.
## Third party tiles (TileService)
-A third party tile is any Quick Settings tile that is provided by an app (that's not SystemUI). This
-is implemented by developers
+A third party tile is any Quick Settings tile that is provided by an app (that's not SystemUI).
+This is implemented by developers
subclassing [`TileService`](/core/java/android/service/quicksettings/TileService.java) and
interacting with its API.
@@ -220,9 +244,9 @@ from SystemUI:
* **`onTileAdded`**: called when the tile is added to QS.
* **`onTileRemoved`**: called when the tile is removed from QS.
* **`onStartListening`**: called when QS is opened and the tile is showing. This marks the start of
- the window when calling `getQSTile` is safe and will provide the correct object.
-* **`onStopListening`**: called when QS is closed or the tile is no longer visible by the user. This
- marks the end of the window described in `onStartListening`.
+the window when calling `getQSTile` is safe and will provide the correct object.
+* **`onStopListening`**: called when QS is closed or the tile is no longer visible by the user.
+This marks the end of the window described in `onStartListening`.
* **`onClick`**: called when the user clicks on the tile.
Additionally, the following final methods are provided:
@@ -379,13 +403,14 @@ correct list of tiles.
### QSFactory
+`CurrentTilesInteractorImpl` uses the `QSFactory` interface to create the tiles.
+
This interface provides a way of creating tiles and views from a spec. It can be used in plugins to
provide different definitions for tiles.
-In SystemUI there is only one implementation of this factory and that is the default
-factory (`QSFactoryImpl`) in `CurrentTilesInteractorImpl`.
+In SystemUI there are two implementation of this factory. The first one is `QSFactoryImpl` in used for legacy tiles. The second one is `NewQSFactory` used for [NEW-ARCH] tiles.
-#### QSFactoryImpl
+#### QSFactoryImpl (legacy tiles)
This class implements the following method as specified in the `QSFactory` interface:
@@ -402,6 +427,12 @@ This class implements the following method as specified in the `QSFactory` inter
As part of filtering not valid tiles, custom tiles that don't have a corresponding valid service
component are never instantiated.
+#### NewQSFactory ([NEW-ARCH] tiles)
+
+This class also implements the `createTile` method as specified in the `QSFactory` interface.
+However, it first uses the spec to get a `QSTileViewModel`. The view model is then adapted into a
+`QSTile` using the `QSTileViewModelAdapter`.
+
### Lifecycle of a Tile
We describe first the parts of the lifecycle that are common to SystemUI tiles and third party
@@ -415,7 +446,7 @@ tiles.
2. This updates the flow that `CurrentTilesInteractor` is collecting from, triggering the process
described above.
3. `CurrentTilesInteractor` calls the available `QSFactory` classes in order to find one that will
- be able to create a tile with that spec. Assuming that `QSFactoryImpl` managed to create the
+ be able to create a tile with that spec. Assuming that some factory managed to create the
tile, which is some implementation of `QSTile` (either a SystemUI subclass
of `QSTileImpl` or a `CustomTile`) it will be added to the current list.
If the tile is available, it's stored in a map and things proceed forward.
@@ -452,7 +483,7 @@ in [Third party tiles (TileService)](#third-party-tiles-tileservice).
This section describes necessary and recommended steps when implementing a Quick Settings tile. Some
of them are optional and depend on the requirements of the tile.
-### Implementing a SystemUI tile
+### Implementing a legacy SystemUI tile
1. Create a class (preferably
in [`SystemUI/src/com/android/systemui/qs/tiles`](/packages/SystemUI/src/com/android/systemui/qs/tiles))
@@ -579,6 +610,70 @@ type variable of type `State`.
Provides a default label for this Tile. Used by the QS Panel customizer to show a name next to
each available tile.
+### Implementing a [NEW-ARCH] SystemUI tile
+In the new system the tiles are created in the path
+[`packages/SystemUI/src/com/android/systemui/qs/tiles/impl/<spec>`](packages/SystemUI/src/com/android/systemui/qs/tiles/impl/<spec>)
+where the `<spec>` should be replaced by the spec of the tile e.g. rotation for `RotationLockTile`.
+
+To create a new tile, the developer needs to implement the following data class and interfaces:
+
+[`DataModel`] is a class that describes the system state of the feature that the tile is trying to
+represent. Let's refer to the type of this class as DATA_TYPE. For example a simplified version of
+the data model for a flashlight tile could be a class with a boolean field that represents
+whether the flashlight is on or not.
+
+This file should be placed in the relative path `domain/model/` down from the tile's package.
+
+[`QSTileDataInteractor`] There are two abstract methods that need to be implemented:
+* `fun tileData(user: UserHandle, triggers: Flow<DataUpdateTrigger>): Flow<DATA_TYPE>`: This method
+returns a flow of data that will be used to create the state of the tile. This is where the system
+state is listened to and converted to a flow of data model. Avoid loading data or settings up
+listeners on the main thread. The userHandle is the user for which the tile is created.
+The triggers flow is a flow of events that can be used to trigger a refresh of the data.
+The most common triggers the force update and initial request.
+
+* `fun availability(user: UserHandle): Flow<Boolean>`: This method returns a flow of booleans that
+indicates if the tile should be shown or not. This is where the availability of the system feature
+(e.g. wifi) is checked. The userHandle is the user for which the tile is created.
+
+This file should be placed in the relative path `domain/interactor/` down from the tile's package.
+
+[`QSTileUserActionInteractor`]
+* `fun handleInput(input: QSTileInput)` is the method that needs to be implemented. This is the
+method that will be called when the user interacts with the tile. The input parameter contains
+the type of interaction (click, long click, toggle) and the DATA_TYPE of the latest data when the
+input was received.
+
+This file should be placed in the relative path `/domain/interactor` down from the tile's package.
+
+[`QSTileDataToStateMapper`]
+* `fun map(data: DATA_TYPE): QSTileState` is the method that needs to be implemented. This method
+is responsible for mapping the data received from the data interactor to a state that the view
+model can use to update the UI. This is where for example the icon should be loaded, and the
+label and content description set. The map function will run on UIBackground thread, a single
+thread which has higher priority than the background thread and lower than UI thread. Loading a
+resource on UI thread can cause jank by blocking the UI thread. On the other end of the spectrum,
+loading resources using a background dispatcher may cause jank due to background thread contention
+since it is possible for the background dispatcher to use more than one background thread
+at the same time. In contrast, the UIBackground dispatcher uses a single thread that is
+shared by all tiles. Therefore the system will use UIBackground dispatcher to execute
+the map function.
+
+The most important resource to load in the map function is the icon. We prefer `Icon.Loaded` with a
+resource id over `Icon.Resource`, because then (a) we can guarantee that the drawable loading will
+happen on the UIBackground thread and (b) we can cache the drawables using the resource id.
+
+This file should be placed in the relative path `/ui/mapper` down from the tile's package.
+
+#### Testing a [NEW-ARCH] SystemUI tile
+
+When writing tests, the mapper is usually a good place to start, since that is where the
+business logic decisions are being made that can inform the shape of data interactor.
+
+We suggest taking advantage of the existing class `QSTileStateSubject`. So rather than
+asserting an individual field's value, a test will assert the whole state. That can be
+achieved by `QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)`.
+
### Implementing a third party tile
For information about this, use the Android Developer documentation
diff --git a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardMessageAreaControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardMessageAreaControllerTest.java
index bd811814eb24..4140a956182c 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardMessageAreaControllerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardMessageAreaControllerTest.java
@@ -18,9 +18,7 @@ package com.android.keyguard;
import static com.google.common.truth.Truth.assertThat;
-import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
-import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
@@ -28,8 +26,6 @@ import static org.mockito.Mockito.when;
import android.hardware.biometrics.BiometricSourceType;
import android.testing.TestableLooper;
-import android.text.Editable;
-import android.text.TextWatcher;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
@@ -99,19 +95,6 @@ public class KeyguardMessageAreaControllerTest extends SysuiTestCase {
}
@Test
- public void textChanged_AnnounceForAccessibility() {
- ArgumentCaptor<TextWatcher> textWatcherArgumentCaptor = ArgumentCaptor.forClass(
- TextWatcher.class);
- mMessageAreaController.onViewAttached();
- verify(mKeyguardMessageArea).addTextChangedListener(textWatcherArgumentCaptor.capture());
-
- textWatcherArgumentCaptor.getValue().afterTextChanged(
- Editable.Factory.getInstance().newEditable("abc"));
- verify(mKeyguardMessageArea).removeCallbacks(any(Runnable.class));
- verify(mKeyguardMessageArea).postDelayed(any(Runnable.class), anyLong());
- }
-
- @Test
public void testSetBouncerVisible() {
mMessageAreaController.setIsVisible(true);
verify(mKeyguardMessageArea).setIsVisible(true);
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/activity/data/repository/ActivityManagerRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/activity/data/repository/ActivityManagerRepositoryTest.kt
index d6ba98d65d15..441f807a8ec8 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/activity/data/repository/ActivityManagerRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/activity/data/repository/ActivityManagerRepositoryTest.kt
@@ -31,6 +31,7 @@ import com.android.systemui.kosmos.useUnconfinedTestDispatcher
import com.android.systemui.log.core.Logger
import com.android.systemui.log.logcatLogBuffer
import com.android.systemui.testKosmos
+import com.android.systemui.util.time.fakeSystemClock
import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith
@@ -136,6 +137,118 @@ class ActivityManagerRepositoryTest : SysuiTestCase() {
assertThat(latest).isFalse()
}
+ @Test
+ fun createAppVisibilityFlow_fetchesInitialValue_trueWithLastVisibleTime() =
+ kosmos.runTest {
+ whenever(activityManager.getUidImportance(THIS_UID)).thenReturn(IMPORTANCE_FOREGROUND)
+ fakeSystemClock.setCurrentTimeMillis(5000)
+
+ val latest by
+ collectLastValue(underTest.createAppVisibilityFlow(THIS_UID, logger, LOG_TAG))
+
+ assertThat(latest!!.isAppCurrentlyVisible).isTrue()
+ assertThat(latest!!.lastAppVisibleTime).isEqualTo(5000)
+ }
+
+ @Test
+ fun createAppVisibilityFlow_fetchesInitialValue_falseWithoutLastVisibleTime() =
+ kosmos.runTest {
+ whenever(activityManager.getUidImportance(THIS_UID)).thenReturn(IMPORTANCE_GONE)
+ fakeSystemClock.setCurrentTimeMillis(5000)
+
+ val latest by
+ collectLastValue(underTest.createAppVisibilityFlow(THIS_UID, logger, LOG_TAG))
+
+ assertThat(latest!!.isAppCurrentlyVisible).isFalse()
+ assertThat(latest!!.lastAppVisibleTime).isNull()
+ }
+
+ @Test
+ fun createAppVisibilityFlow_getsImportanceUpdates_updatesLastVisibleTimeOnlyWhenVisible() =
+ kosmos.runTest {
+ whenever(activityManager.getUidImportance(THIS_UID)).thenReturn(IMPORTANCE_GONE)
+ fakeSystemClock.setCurrentTimeMillis(5000)
+ val latest by
+ collectLastValue(underTest.createAppVisibilityFlow(THIS_UID, logger, LOG_TAG))
+
+ assertThat(latest!!.isAppCurrentlyVisible).isFalse()
+ assertThat(latest!!.lastAppVisibleTime).isNull()
+
+ val listenerCaptor = argumentCaptor<ActivityManager.OnUidImportanceListener>()
+ verify(activityManager).addOnUidImportanceListener(listenerCaptor.capture(), any())
+ val listener = listenerCaptor.firstValue
+
+ // WHEN the app becomes visible
+ fakeSystemClock.setCurrentTimeMillis(7000)
+ listener.onUidImportance(THIS_UID, IMPORTANCE_FOREGROUND)
+
+ // THEN the status and lastAppVisibleTime are updated
+ assertThat(latest!!.isAppCurrentlyVisible).isTrue()
+ assertThat(latest!!.lastAppVisibleTime).isEqualTo(7000)
+
+ // WHEN the app is no longer visible
+ listener.onUidImportance(THIS_UID, IMPORTANCE_TOP_SLEEPING)
+
+ // THEN the lastAppVisibleTime is preserved
+ assertThat(latest!!.isAppCurrentlyVisible).isFalse()
+ assertThat(latest!!.lastAppVisibleTime).isEqualTo(7000)
+
+ // WHEN the app is visible again
+ fakeSystemClock.setCurrentTimeMillis(9000)
+ listener.onUidImportance(THIS_UID, IMPORTANCE_FOREGROUND)
+
+ // THEN the lastAppVisibleTime is updated
+ assertThat(latest!!.isAppCurrentlyVisible).isTrue()
+ assertThat(latest!!.lastAppVisibleTime).isEqualTo(9000)
+ }
+
+ @Test
+ fun createAppVisibilityFlow_ignoresUpdatesForOtherUids() =
+ kosmos.runTest {
+ val latest by
+ collectLastValue(underTest.createAppVisibilityFlow(THIS_UID, logger, LOG_TAG))
+
+ val listenerCaptor = argumentCaptor<ActivityManager.OnUidImportanceListener>()
+ verify(activityManager).addOnUidImportanceListener(listenerCaptor.capture(), any())
+ val listener = listenerCaptor.firstValue
+
+ listener.onUidImportance(THIS_UID, IMPORTANCE_GONE)
+ assertThat(latest!!.isAppCurrentlyVisible).isFalse()
+
+ // WHEN another UID becomes foreground
+ listener.onUidImportance(THIS_UID + 2, IMPORTANCE_FOREGROUND)
+
+ // THEN this UID still stays not visible
+ assertThat(latest!!.isAppCurrentlyVisible).isFalse()
+ }
+
+ @Test
+ fun createAppVisibilityFlow_securityExceptionOnUidRegistration_ok() =
+ kosmos.runTest {
+ whenever(activityManager.getUidImportance(THIS_UID)).thenReturn(IMPORTANCE_GONE)
+ whenever(activityManager.addOnUidImportanceListener(any(), any()))
+ .thenThrow(SecurityException())
+
+ val latest by
+ collectLastValue(underTest.createAppVisibilityFlow(THIS_UID, logger, LOG_TAG))
+
+ // Verify no crash, and we get a value emitted
+ assertThat(latest!!.isAppCurrentlyVisible).isFalse()
+ }
+
+ /** Regression test for b/216248574. */
+ @Test
+ fun createAppVisibilityFlow_getUidImportanceThrowsException_ok() =
+ kosmos.runTest {
+ whenever(activityManager.getUidImportance(any())).thenThrow(SecurityException())
+
+ val latest by
+ collectLastValue(underTest.createAppVisibilityFlow(THIS_UID, logger, LOG_TAG))
+
+ // Verify no crash, and we get a value emitted
+ assertThat(latest!!.isAppCurrentlyVisible).isFalse()
+ }
+
companion object {
private const val THIS_UID = 558
private const val LOG_TAG = "LogTag"
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/animation/GhostedViewTransitionAnimatorControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/animation/GhostedViewTransitionAnimatorControllerTest.kt
index e492c63d095c..052d520ac92f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/animation/GhostedViewTransitionAnimatorControllerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/animation/GhostedViewTransitionAnimatorControllerTest.kt
@@ -17,16 +17,20 @@
package com.android.systemui.animation
import android.os.HandlerThread
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
import android.testing.TestableLooper
import android.view.View
import android.widget.FrameLayout
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.internal.jank.InteractionJankMonitor
+import com.android.systemui.Flags
import com.android.systemui.SysuiTestCase
import com.android.systemui.animation.view.LaunchableFrameLayout
import com.google.common.truth.Truth.assertThat
import org.junit.Assert.assertThrows
+import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -40,6 +44,14 @@ class GhostedViewTransitionAnimatorControllerTest : SysuiTestCase() {
}
private val interactionJankMonitor = FakeInteractionJankMonitor()
+ private lateinit var transitionRegistry: FakeViewTransitionRegistry
+ private lateinit var transitioningView: View
+
+ @Before
+ fun setup() {
+ transitioningView = LaunchableFrameLayout(mContext)
+ transitionRegistry = FakeViewTransitionRegistry()
+ }
@Test
fun animatingOrphanViewDoesNotCrash() {
@@ -67,7 +79,7 @@ class GhostedViewTransitionAnimatorControllerTest : SysuiTestCase() {
parent.addView((launchView))
val launchController =
GhostedViewTransitionAnimatorController(
- launchView,
+ launchView,
launchCujType = LAUNCH_CUJ,
returnCujType = RETURN_CUJ,
interactionJankMonitor = interactionJankMonitor
@@ -96,6 +108,26 @@ class GhostedViewTransitionAnimatorControllerTest : SysuiTestCase() {
assertThat(interactionJankMonitor.finished).containsExactly(LAUNCH_CUJ, RETURN_CUJ)
}
+ @EnableFlags(Flags.FLAG_DECOUPLE_VIEW_CONTROLLER_IN_ANIMLIB)
+ @Test
+ fun testViewsAreRegisteredInTransitionRegistry() {
+ GhostedViewTransitionAnimatorController(
+ transitioningView = transitioningView,
+ transitionRegistry = transitionRegistry
+ )
+ assertThat(transitionRegistry.registry).isNotEmpty()
+ }
+
+ @DisableFlags(Flags.FLAG_DECOUPLE_VIEW_CONTROLLER_IN_ANIMLIB)
+ @Test
+ fun testNotUseRegistryIfDecouplingFlagDisabled() {
+ GhostedViewTransitionAnimatorController(
+ transitioningView = transitioningView,
+ transitionRegistry = transitionRegistry
+ )
+ assertThat(transitionRegistry.registry).isEmpty()
+ }
+
/**
* A fake implementation of [InteractionJankMonitor] which stores ongoing and finished CUJs and
* allows inspection.
@@ -117,4 +149,30 @@ class GhostedViewTransitionAnimatorControllerTest : SysuiTestCase() {
return true
}
}
+
+ private class FakeViewTransitionRegistry : IViewTransitionRegistry {
+
+ val registry = mutableMapOf<ViewTransitionToken, View>()
+
+ override fun register(token: ViewTransitionToken, view: View) {
+ registry[token] = view
+ view.setTag(R.id.tag_view_transition_token, token)
+ }
+
+ override fun unregister(token: ViewTransitionToken) {
+ registry.remove(token)?.setTag(R.id.tag_view_transition_token, null)
+ }
+
+ override fun getView(token: ViewTransitionToken): View? {
+ return registry[token]
+ }
+
+ override fun getViewToken(view: View): ViewTransitionToken? {
+ return view.getTag(R.id.tag_view_transition_token) as? ViewTransitionToken
+ }
+
+ override fun onRegistryUpdate() {
+ //empty
+ }
+ }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/common/ui/view/SeekBarWithIconButtonsViewTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/common/ui/view/SeekBarWithIconButtonsViewTest.java
index 01baadda7c87..c40c1a3b0e93 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/common/ui/view/SeekBarWithIconButtonsViewTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/common/ui/view/SeekBarWithIconButtonsViewTest.java
@@ -130,12 +130,17 @@ public class SeekBarWithIconButtonsViewTest extends SysuiTestCase {
@Test
public void setProgress_onProgressChangedAndOnUserInteractionFinalized() {
reset(mOnSeekBarChangeListener);
- mIconDiscreteSliderLinearLayout.setProgress(1);
+
+ // Trigger the progress changed listener with fromUser but without clicking.
+ // This is similar to what would happen if an accessibility service changed the
+ // progress.
+ mIconDiscreteSliderLinearLayout.getSeekBarChangeListener().onProgressChanged(
+ mIconDiscreteSliderLinearLayout.getSeekbar(), 1, /*fromUser=*/ true);
// If users are changing seekbar progress without touching the seekbar or clicking the
// buttons, trigger onUserInteractionFinalized.
verify(mOnSeekBarChangeListener).onProgressChanged(
- eq(mSeekbar), /* progress= */ eq(1), /* fromUser= */ eq(false));
+ eq(mSeekbar), /* progress= */ eq(1), /* fromUser= */ eq(true));
verify(mOnSeekBarChangeListener, never()).onStartTrackingTouch(/* seekBar= */ any());
verify(mOnSeekBarChangeListener, never()).onStopTrackingTouch(/* seekBar= */ any());
verify(mOnSeekBarChangeListener).onUserInteractionFinalized(
@@ -144,6 +149,22 @@ public class SeekBarWithIconButtonsViewTest extends SysuiTestCase {
}
@Test
+ public void setProgress_onProgressChangedWithoutUserInteractionFinalized() {
+ reset(mOnSeekBarChangeListener);
+ mIconDiscreteSliderLinearLayout.setProgress(1);
+
+ // If seekbar progress changes due to a non-user event, without touching the seekbar or
+ // clicking the buttons, do not trigger onUserInteractionFinalized.
+ verify(mOnSeekBarChangeListener).onProgressChanged(
+ eq(mSeekbar), /* progress= */ eq(1), /* fromUser= */ eq(false));
+ verify(mOnSeekBarChangeListener, never()).onStartTrackingTouch(/* seekBar= */ any());
+ verify(mOnSeekBarChangeListener, never()).onStopTrackingTouch(/* seekBar= */ any());
+ verify(mOnSeekBarChangeListener, never()).onUserInteractionFinalized(
+ /* seekBar= */ any(),
+ eq(OnSeekBarWithIconButtonsChangeListener.ControlUnitType.SLIDER));
+ }
+
+ @Test
public void setProgressToSeekBarByTouch_onUserInteractionFinalizedAfterTouchEnds() {
reset(mOnSeekBarChangeListener);
final SeekBarWithIconButtonsView.SeekBarChangeListener seekBarChangeListener =
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/viewmodel/CommunalTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/viewmodel/CommunalTransitionViewModelTest.kt
index 1a3606e413cc..da25bcac6c95 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/viewmodel/CommunalTransitionViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/viewmodel/CommunalTransitionViewModelTest.kt
@@ -35,6 +35,7 @@ import com.android.systemui.scene.domain.interactor.sceneInteractor
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
@@ -44,6 +45,7 @@ import org.junit.runner.RunWith
import platform.test.runner.parameterized.ParameterizedAndroidJunit4
import platform.test.runner.parameterized.Parameters
+@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(ParameterizedAndroidJunit4::class)
class CommunalTransitionViewModelTest(flags: FlagsParameterization) : SysuiTestCase() {
@@ -65,7 +67,7 @@ class CommunalTransitionViewModelTest(flags: FlagsParameterization) : SysuiTestC
private val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository
private val communalSceneRepository = kosmos.fakeCommunalSceneRepository
- private val sceneInteractor = kosmos.sceneInteractor
+ private val sceneInteractor by lazy { kosmos.sceneInteractor }
private val underTest: CommunalTransitionViewModel by lazy {
kosmos.communalTransitionViewModel
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorTest.kt
index 329627af8ec2..e36d2455d316 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorTest.kt
@@ -61,6 +61,7 @@ import com.android.systemui.user.data.model.SelectionStatus
import com.android.systemui.user.data.repository.fakeUserRepository
import com.android.systemui.util.mockito.eq
import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.TestScope
@@ -73,6 +74,7 @@ import org.mockito.ArgumentMatchers.anyInt
import org.mockito.Mockito.never
import org.mockito.Mockito.verify
+@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(AndroidJUnit4::class)
class DeviceEntryFaceAuthInteractorTest : SysuiTestCase() {
@@ -80,21 +82,26 @@ class DeviceEntryFaceAuthInteractorTest : SysuiTestCase() {
private val testScope: TestScope = kosmos.testScope
private lateinit var underTest: SystemUIDeviceEntryFaceAuthInteractor
+
private val bouncerRepository = kosmos.fakeKeyguardBouncerRepository
private val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository
- private val keyguardTransitionInteractor = kosmos.keyguardTransitionInteractor
private val faceAuthRepository = kosmos.fakeDeviceEntryFaceAuthRepository
private val fakeUserRepository = kosmos.fakeUserRepository
private val facePropertyRepository = kosmos.facePropertyRepository
- private val fakeDeviceEntryFingerprintAuthInteractor =
- kosmos.deviceEntryFingerprintAuthInteractor
- private val powerInteractor = kosmos.powerInteractor
private val fakeBiometricSettingsRepository = kosmos.fakeBiometricSettingsRepository
- private val keyguardUpdateMonitor = kosmos.keyguardUpdateMonitor
+ private val keyguardUpdateMonitor by lazy { kosmos.keyguardUpdateMonitor }
private val faceWakeUpTriggersConfig = kosmos.fakeFaceWakeUpTriggersConfig
private val trustManager = kosmos.trustManager
- private val deviceEntryFaceAuthStatusInteractor = kosmos.deviceEntryFaceAuthStatusInteractor
+
+ private val keyguardTransitionInteractor by lazy { kosmos.keyguardTransitionInteractor }
+ private val fakeDeviceEntryFingerprintAuthInteractor by lazy {
+ kosmos.deviceEntryFingerprintAuthInteractor
+ }
+ private val powerInteractor by lazy { kosmos.powerInteractor }
+ private val deviceEntryFaceAuthStatusInteractor by lazy {
+ kosmos.deviceEntryFaceAuthStatusInteractor
+ }
@Before
fun setup() {
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 183e4d6f624b..98486a22854a 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
@@ -541,7 +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", "Use full screen"),
simpleShortcutCategory(
MultiTasking,
"Split screen",
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromAlternateBouncerTransitionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromAlternateBouncerTransitionInteractorTest.kt
index 9b80ca303cd3..63229dbb47a4 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromAlternateBouncerTransitionInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromAlternateBouncerTransitionInteractorTest.kt
@@ -229,6 +229,39 @@ class FromAlternateBouncerTransitionInteractorTest(flags: FlagsParameterization)
}
@Test
+ @EnableFlags(FLAG_GLANCEABLE_HUB_V2)
+ fun transitionToDreaming() =
+ kosmos.runTest {
+ fakePowerRepository.updateWakefulness(
+ WakefulnessState.AWAKE,
+ WakeSleepReason.POWER_BUTTON,
+ WakeSleepReason.POWER_BUTTON,
+ false,
+ )
+ fakeKeyguardRepository.setKeyguardOccluded(false)
+ fakeKeyguardBouncerRepository.setAlternateVisible(true)
+ runCurrent()
+
+ transitionRepository.sendTransitionSteps(
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.ALTERNATE_BOUNCER,
+ testScope,
+ )
+ reset(transitionRepository)
+
+ fakeKeyguardRepository.setKeyguardOccluded(true)
+ fakeKeyguardRepository.setDreaming(true)
+ fakeKeyguardBouncerRepository.setAlternateVisible(false)
+ testScope.advanceTimeBy(200) // advance past delay
+
+ assertThat(transitionRepository)
+ .startedTransition(
+ from = KeyguardState.ALTERNATE_BOUNCER,
+ to = KeyguardState.DREAMING,
+ )
+ }
+
+ @Test
@DisableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR)
fun transitionToGone_whenOpeningGlanceableHubEditMode() =
testScope.runTest {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractorTest.kt
index 282bebcd629a..a08c0dea6fa6 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractorTest.kt
@@ -30,6 +30,7 @@ import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepos
import com.android.systemui.keyguard.data.repository.keyguardClockRepository
import com.android.systemui.keyguard.data.repository.keyguardRepository
import com.android.systemui.keyguard.shared.model.ClockSize
+import com.android.systemui.keyguard.shared.model.ClockSizeSetting
import com.android.systemui.keyguard.shared.model.DozeStateModel
import com.android.systemui.keyguard.shared.model.DozeTransitionModel
import com.android.systemui.keyguard.shared.model.KeyguardState
@@ -68,6 +69,7 @@ class KeyguardClockInteractorTest : SysuiTestCase() {
fun clockSize_sceneContainerFlagOff_basedOnRepository() =
testScope.runTest {
val value by collectLastValue(underTest.clockSize)
+ kosmos.fakeKeyguardClockRepository.setSelectedClockSize(ClockSizeSetting.DYNAMIC)
kosmos.keyguardClockRepository.setClockSize(ClockSize.LARGE)
assertThat(value).isEqualTo(ClockSize.LARGE)
@@ -76,6 +78,17 @@ class KeyguardClockInteractorTest : SysuiTestCase() {
}
@Test
+ @DisableSceneContainer
+ fun clockSize_sceneContainerFlagOff_smallClockSettingSelected_SMALL() =
+ testScope.runTest {
+ val value by collectLastValue(underTest.clockSize)
+ kosmos.fakeKeyguardClockRepository.setSelectedClockSize(ClockSizeSetting.SMALL)
+ kosmos.keyguardClockRepository.setClockSize(ClockSize.LARGE)
+
+ assertThat(value).isEqualTo(ClockSize.SMALL)
+ }
+
+ @Test
@EnableSceneContainer
fun clockSize_forceSmallClock_SMALL() =
testScope.runTest {
@@ -91,61 +104,80 @@ class KeyguardClockInteractorTest : SysuiTestCase() {
@Test
@EnableSceneContainer
- fun clockSize_SceneContainerFlagOn_shadeModeSingle_hasNotifs_SMALL() =
+ fun clockSize_sceneContainerFlagOn_shadeModeSingle_hasNotifs_SMALL() =
testScope.runTest {
val value by collectLastValue(underTest.clockSize)
kosmos.shadeRepository.setShadeLayoutWide(false)
kosmos.activeNotificationListRepository.setActiveNotifs(1)
+
assertThat(value).isEqualTo(ClockSize.SMALL)
}
@Test
@EnableSceneContainer
- fun clockSize_SceneContainerFlagOn_shadeModeSingle_hasMedia_SMALL() =
+ fun clockSize_sceneContainerFlagOn_shadeModeSingle_hasMedia_SMALL() =
testScope.runTest {
val value by collectLastValue(underTest.clockSize)
kosmos.shadeRepository.setShadeLayoutWide(false)
val userMedia = MediaData().copy(active = true)
kosmos.mediaFilterRepository.addSelectedUserMediaEntry(userMedia)
+
assertThat(value).isEqualTo(ClockSize.SMALL)
}
@Test
@EnableSceneContainer
- fun clockSize_SceneContainerFlagOn_shadeModeSplit_isMediaVisible_SMALL() =
+ fun clockSize_sceneContainerFlagOn_shadeModeSplit_isMediaVisible_SMALL() =
testScope.runTest {
val value by collectLastValue(underTest.clockSize)
val userMedia = MediaData().copy(active = true)
kosmos.shadeRepository.setShadeLayoutWide(true)
kosmos.mediaFilterRepository.addSelectedUserMediaEntry(userMedia)
kosmos.keyguardRepository.setIsDozing(false)
+
assertThat(value).isEqualTo(ClockSize.SMALL)
}
@Test
@EnableSceneContainer
- fun clockSize_SceneContainerFlagOn_shadeModeSplit_noMedia_LARGE() =
+ fun clockSize_sceneContainerFlagOn_shadeModeSplit_noMedia_LARGE() =
testScope.runTest {
val value by collectLastValue(underTest.clockSize)
kosmos.shadeRepository.setShadeLayoutWide(true)
kosmos.keyguardRepository.setIsDozing(false)
+
assertThat(value).isEqualTo(ClockSize.LARGE)
}
@Test
@EnableSceneContainer
- fun clockSize_SceneContainerFlagOn_shadeModeSplit_isDozing_LARGE() =
+ fun clockSize_sceneContainerFlagOn_shadeModeSplit_isDozing_LARGE() =
testScope.runTest {
val value by collectLastValue(underTest.clockSize)
val userMedia = MediaData().copy(active = true)
kosmos.shadeRepository.setShadeLayoutWide(true)
kosmos.mediaFilterRepository.addSelectedUserMediaEntry(userMedia)
kosmos.keyguardRepository.setIsDozing(true)
+
assertThat(value).isEqualTo(ClockSize.LARGE)
}
@Test
@EnableSceneContainer
+ fun clockSize_sceneContainerFlagOn_shadeModeSplit_smallClockSettingSelectd_SMALL() =
+ testScope.runTest {
+ val value by collectLastValue(underTest.clockSize)
+ val userMedia = MediaData().copy(active = true)
+ kosmos.fakeKeyguardClockRepository.setSelectedClockSize(ClockSizeSetting.SMALL)
+ kosmos.shadeRepository.setShadeLayoutWide(true)
+ kosmos.mediaFilterRepository.addSelectedUserMediaEntry(userMedia)
+ kosmos.keyguardRepository.setIsDozing(true)
+
+ assertThat(value).isEqualTo(ClockSize.SMALL)
+ }
+
+ @Test
+ @EnableSceneContainer
fun clockShouldBeCentered_sceneContainerFlagOn_notSplitMode_true() =
testScope.runTest {
val value by collectLastValue(underTest.clockShouldBeCentered)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt
index 29e95cd911f8..0b42898d82ae 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt
@@ -23,6 +23,7 @@ import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectValues
import com.android.systemui.flags.DisableSceneContainer
import com.android.systemui.flags.EnableSceneContainer
+import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
import com.android.systemui.keyguard.shared.model.Edge
import com.android.systemui.keyguard.shared.model.KeyguardState
@@ -46,20 +47,31 @@ import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
import junit.framework.Assert.assertEquals
+import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertThrows
+import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
+@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(AndroidJUnit4::class)
class KeyguardTransitionInteractorTest : SysuiTestCase() {
- val kosmos = testKosmos()
- val underTest = kosmos.keyguardTransitionInteractor
- val repository = kosmos.fakeKeyguardTransitionRepository
- val testScope = kosmos.testScope
+
+ private val kosmos = testKosmos()
+ private val testScope = kosmos.testScope
+
+ private lateinit var repository: FakeKeyguardTransitionRepository
+ private lateinit var underTest: KeyguardTransitionInteractor
+
+ @Before
+ fun setup() {
+ repository = kosmos.fakeKeyguardTransitionRepository
+ underTest = kosmos.keyguardTransitionInteractor
+ }
@Test
fun transitionCollectorsReceivesOnlyAppropriateEvents() =
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/scenetransition/LockscreenSceneTransitionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/scenetransition/LockscreenSceneTransitionInteractorTest.kt
index 26fe379f00bf..3cff0fc96af4 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/scenetransition/LockscreenSceneTransitionInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/scenetransition/LockscreenSceneTransitionInteractorTest.kt
@@ -1358,6 +1358,45 @@ class LockscreenSceneTransitionInteractorTest : SysuiTestCase() {
)
}
+ /**
+ * When a transition away from the lockscreen is interrupted by an `Idle(Lockscreen)`, a
+ * `sceneState` that was set during the transition is consumed and passed to KTF.
+ */
+ @Test
+ fun transition_from_ls_scene_sceneStateSet_then_interrupted_by_idle_on_ls() =
+ testScope.runTest {
+ val currentStep by collectLastValue(kosmos.realKeyguardTransitionRepository.transitions)
+ sceneTransitions.value =
+ ObservableTransitionState.Transition(
+ Scenes.Lockscreen,
+ Scenes.Gone,
+ flowOf(Scenes.Lockscreen),
+ progress,
+ false,
+ flowOf(false),
+ )
+ progress.value = 0.4f
+ assertTransition(
+ step = currentStep!!,
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.UNDEFINED,
+ state = TransitionState.RUNNING,
+ progress = 0.4f,
+ )
+
+ val sceneState = KeyguardState.AOD
+ underTest.onSceneAboutToChange(toScene = Scenes.Lockscreen, sceneState = sceneState)
+ sceneTransitions.value = ObservableTransitionState.Idle(Scenes.Lockscreen)
+
+ assertTransition(
+ step = currentStep!!,
+ from = KeyguardState.UNDEFINED,
+ to = KeyguardState.AOD,
+ state = TransitionState.FINISHED,
+ progress = 1f,
+ )
+ }
+
private fun assertTransition(
step: TransitionStep,
from: KeyguardState? = null,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/view/layout/blueprints/DefaultKeyguardBlueprintTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/view/layout/blueprints/DefaultKeyguardBlueprintTest.kt
index 3a016ff7152a..63770803ff48 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/view/layout/blueprints/DefaultKeyguardBlueprintTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/view/layout/blueprints/DefaultKeyguardBlueprintTest.kt
@@ -40,7 +40,6 @@ import com.android.systemui.keyguard.ui.view.layout.sections.DefaultStatusBarSec
import com.android.systemui.keyguard.ui.view.layout.sections.DefaultUdfpsAccessibilityOverlaySection
import com.android.systemui.keyguard.ui.view.layout.sections.KeyguardSliceViewSection
import com.android.systemui.keyguard.ui.view.layout.sections.SmartspaceSection
-import com.android.systemui.keyguard.ui.view.layout.sections.SplitShadeGuidelines
import com.android.systemui.util.mockito.whenever
import java.util.Optional
import org.junit.Before
@@ -66,7 +65,6 @@ class DefaultKeyguardBlueprintTest : SysuiTestCase() {
@Mock private lateinit var defaultSettingsPopupMenuSection: DefaultSettingsPopupMenuSection
@Mock private lateinit var defaultStatusBarViewSection: DefaultStatusBarSection
@Mock private lateinit var defaultNSSLSection: DefaultNotificationStackScrollLayoutSection
- @Mock private lateinit var splitShadeGuidelines: SplitShadeGuidelines
@Mock private lateinit var aodPromotedNotificationSection: AodPromotedNotificationSection
@Mock private lateinit var aodNotificationIconsSection: AodNotificationIconsSection
@Mock private lateinit var aodBurnInSection: AodBurnInSection
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/DozingToDreamingTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/DozingToDreamingTransitionViewModelTest.kt
deleted file mode 100644
index 052dfd52887f..000000000000
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/DozingToDreamingTransitionViewModelTest.kt
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- * 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.keyguard.ui.viewmodel
-
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.SmallTest
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
-import com.android.systemui.keyguard.shared.model.KeyguardState
-import com.android.systemui.kosmos.collectValues
-import com.android.systemui.kosmos.runTest
-import com.android.systemui.kosmos.testScope
-import com.android.systemui.testKosmos
-import com.google.common.truth.Truth.assertThat
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@SmallTest
-@RunWith(AndroidJUnit4::class)
-class DozingToDreamingTransitionViewModelTest : SysuiTestCase() {
- val kosmos = testKosmos()
-
- val underTest by lazy { kosmos.dozingToDreamingTransitionViewModel }
-
- @Test
- fun notificationShadeAlpha() =
- kosmos.runTest {
- val values by collectValues(underTest.notificationAlpha)
- assertThat(values).isEmpty()
-
- fakeKeyguardTransitionRepository.sendTransitionSteps(
- from = KeyguardState.DOZING,
- to = KeyguardState.DREAMING,
- testScope,
- )
-
- assertThat(values).isNotEmpty()
- values.forEach { assertThat(it).isEqualTo(0) }
- }
-}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModelTest.kt
index 8a599a1bd948..20d015f4d77c 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModelTest.kt
@@ -27,7 +27,6 @@ import com.android.systemui.keyguard.data.repository.fakeKeyguardClockRepository
import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
import com.android.systemui.keyguard.data.repository.keyguardClockRepository
import com.android.systemui.keyguard.shared.model.ClockSize
-import com.android.systemui.keyguard.shared.model.ClockSizeSetting
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel.ClockLayout
import com.android.systemui.kosmos.testScope
@@ -55,17 +54,18 @@ import platform.test.runner.parameterized.Parameters
@SmallTest
@RunWith(ParameterizedAndroidJunit4::class)
class KeyguardClockViewModelTest(flags: FlagsParameterization) : SysuiTestCase() {
- val kosmos = testKosmos()
- val testScope = kosmos.testScope
- val underTest by lazy { kosmos.keyguardClockViewModel }
- val res = context.resources
- @Mock lateinit var clockController: ClockController
- @Mock lateinit var largeClock: ClockFaceController
- @Mock lateinit var smallClock: ClockFaceController
+ private val kosmos = testKosmos()
+ private val testScope = kosmos.testScope
+ private val underTest by lazy { kosmos.keyguardClockViewModel }
+ private val res = context.resources
- var config = ClockConfig("TEST", "Test", "")
- var faceConfig = ClockFaceConfig()
+ @Mock private lateinit var clockController: ClockController
+ @Mock private lateinit var largeClock: ClockFaceController
+ @Mock private lateinit var smallClock: ClockFaceController
+
+ private var config = ClockConfig("TEST", "Test", "")
+ private var faceConfig = ClockFaceConfig()
init {
mSetFlagsRule.setFlagsParameterization(flags)
@@ -196,35 +196,6 @@ class KeyguardClockViewModelTest(flags: FlagsParameterization) : SysuiTestCase()
}
@Test
- fun testClockSize_alwaysSmallClockSize() =
- testScope.runTest {
- val value by collectLastValue(underTest.clockSize)
-
- with(kosmos) {
- fakeKeyguardClockRepository.setSelectedClockSize(ClockSizeSetting.SMALL)
- keyguardClockRepository.setClockSize(ClockSize.LARGE)
- }
-
- assertThat(value).isEqualTo(ClockSize.SMALL)
- }
-
- @Test
- @DisableSceneContainer
- fun testClockSize_dynamicClockSize() =
- testScope.runTest {
- with(kosmos) {
- val value by collectLastValue(underTest.clockSize)
- fakeKeyguardClockRepository.setSelectedClockSize(ClockSizeSetting.DYNAMIC)
-
- keyguardClockRepository.setClockSize(ClockSize.SMALL)
- assertThat(value).isEqualTo(ClockSize.SMALL)
-
- keyguardClockRepository.setClockSize(ClockSize.LARGE)
- assertThat(value).isEqualTo(ClockSize.LARGE)
- }
- }
-
- @Test
fun isLargeClockVisible_whenLargeClockSize_isTrue() =
testScope.runTest {
val value by collectLastValue(underTest.isLargeClockVisible)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardMediaViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardMediaViewModelTest.kt
index 38829da69c28..583fd1e03002 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardMediaViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardMediaViewModelTest.kt
@@ -26,6 +26,7 @@ import com.android.systemui.kosmos.useUnconfinedTestDispatcher
import com.android.systemui.lifecycle.activateIn
import com.android.systemui.media.controls.data.repository.mediaFilterRepository
import com.android.systemui.media.controls.shared.model.MediaData
+import com.android.systemui.shade.data.repository.shadeRepository
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
import org.junit.Before
@@ -81,4 +82,20 @@ class KeyguardMediaViewModelTest : SysuiTestCase() {
assertThat(underTest.isMediaVisible).isFalse()
}
+
+ @Test
+ fun isShadeLayoutWide_withConfigTrue_true() =
+ kosmos.runTest {
+ shadeRepository.setShadeLayoutWide(true)
+
+ assertThat(underTest.isShadeLayoutWide).isTrue()
+ }
+
+ @Test
+ fun isShadeLayoutWide_withConfigFalse_false() =
+ kosmos.runTest {
+ shadeRepository.setShadeLayoutWide(false)
+
+ assertThat(underTest.isShadeLayoutWide).isFalse()
+ }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelTest.kt
index af025273458f..0b34a01a0fe0 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelTest.kt
@@ -17,19 +17,24 @@
package com.android.systemui.keyguard.ui.viewmodel
import android.platform.test.flag.junit.FlagsParameterization
+import androidx.compose.ui.Alignment
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.biometrics.authController
import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository
import com.android.systemui.flags.DisableSceneContainer
+import com.android.systemui.flags.EnableSceneContainer
import com.android.systemui.flags.andSceneContainer
import com.android.systemui.keyguard.data.repository.fakeKeyguardClockRepository
import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
import com.android.systemui.keyguard.data.repository.keyguardOcclusionRepository
+import com.android.systemui.keyguard.domain.interactor.keyguardClockInteractor
import com.android.systemui.keyguard.shared.model.ClockSize
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.transition.fakeKeyguardTransitionAnimationCallback
import com.android.systemui.keyguard.shared.transition.keyguardTransitionAnimationCallbackDelegator
+import com.android.systemui.keyguard.ui.viewmodel.LockscreenContentViewModel.NotificationsPlacement.BelowClock
+import com.android.systemui.keyguard.ui.viewmodel.LockscreenContentViewModel.NotificationsPlacement.BesideClock
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.collectLastValue
import com.android.systemui.kosmos.runCurrent
@@ -41,6 +46,10 @@ import com.android.systemui.scene.domain.interactor.sceneInteractor
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.shade.data.repository.shadeRepository
import com.android.systemui.shade.domain.interactor.enableDualShade
+import com.android.systemui.shade.domain.interactor.enableSingleShade
+import com.android.systemui.shade.domain.interactor.enableSplitShade
+import com.android.systemui.shade.domain.interactor.shadeModeInteractor
+import com.android.systemui.shade.shared.model.ShadeMode
import com.android.systemui.testKosmos
import com.android.systemui.unfold.fakeUnfoldTransitionProgressProvider
import com.android.systemui.util.mockito.whenever
@@ -99,136 +108,143 @@ class LockscreenContentViewModelTest(flags: FlagsParameterization) : SysuiTestCa
}
@Test
- @DisableSceneContainer
- fun clockSize_withLargeClock_true() =
+ fun notificationsPlacement_splitShade_topEnd() =
kosmos.runTest {
- val clockSize by collectLastValue(underTest.clockSize)
- fakeKeyguardClockRepository.setClockSize(ClockSize.LARGE)
- assertThat(clockSize).isEqualTo(ClockSize.LARGE)
+ setupState(shadeMode = ShadeMode.Split, clockSize = ClockSize.SMALL)
+
+ assertThat(underTest.notificationsPlacement)
+ .isEqualTo(BesideClock(alignment = Alignment.TopEnd))
}
@Test
- @DisableSceneContainer
- fun clockSize_withSmallClock_false() =
+ fun notificationsPlacement_singleShade_below() =
kosmos.runTest {
- val clockSize by collectLastValue(underTest.clockSize)
- fakeKeyguardClockRepository.setClockSize(ClockSize.SMALL)
- assertThat(clockSize).isEqualTo(ClockSize.SMALL)
+ setupState(shadeMode = ShadeMode.Single, clockSize = ClockSize.SMALL)
+
+ assertThat(underTest.notificationsPlacement).isEqualTo(BelowClock)
}
@Test
- fun areNotificationsVisible_splitShadeTrue_true() =
+ @EnableSceneContainer
+ fun notificationsPlacement_dualShadeSmallClock_below() =
kosmos.runTest {
- val areNotificationsVisible by collectLastValue(underTest.areNotificationsVisible())
- shadeRepository.setShadeLayoutWide(true)
- fakeKeyguardClockRepository.setClockSize(ClockSize.LARGE)
+ setupState(
+ shadeMode = ShadeMode.Dual,
+ clockSize = ClockSize.SMALL,
+ shadeLayoutWide = true,
+ )
- assertThat(areNotificationsVisible).isTrue()
+ assertThat(underTest.notificationsPlacement).isEqualTo(BelowClock)
}
@Test
- fun areNotificationsVisible_dualShadeWideOnLockscreen_true() =
+ @EnableSceneContainer
+ fun notificationsPlacement_dualShadeLargeClock_topStart() =
kosmos.runTest {
- val areNotificationsVisible by collectLastValue(underTest.areNotificationsVisible())
- kosmos.enableDualShade()
- shadeRepository.setShadeLayoutWide(true)
- fakeKeyguardClockRepository.setClockSize(ClockSize.LARGE)
+ setupState(
+ shadeMode = ShadeMode.Dual,
+ clockSize = ClockSize.LARGE,
+ shadeLayoutWide = true,
+ )
- assertThat(areNotificationsVisible).isTrue()
+ assertThat(underTest.notificationsPlacement)
+ .isEqualTo(BesideClock(alignment = Alignment.TopStart))
}
@Test
- @DisableSceneContainer
- fun areNotificationsVisible_withSmallClock_true() =
+ fun areNotificationsVisible_splitShadeTrue_true() =
kosmos.runTest {
- val areNotificationsVisible by collectLastValue(underTest.areNotificationsVisible())
- fakeKeyguardClockRepository.setClockSize(ClockSize.SMALL)
- assertThat(areNotificationsVisible).isTrue()
+ setupState(shadeMode = ShadeMode.Split, clockSize = ClockSize.LARGE)
+
+ assertThat(underTest.areNotificationsVisible).isTrue()
}
@Test
- @DisableSceneContainer
- fun areNotificationsVisible_withLargeClock_false() =
+ @EnableSceneContainer
+ fun areNotificationsVisible_dualShadeWideOnLockscreen_true() =
kosmos.runTest {
- val areNotificationsVisible by collectLastValue(underTest.areNotificationsVisible())
- fakeKeyguardClockRepository.setClockSize(ClockSize.LARGE)
- assertThat(areNotificationsVisible).isFalse()
+ setupState(
+ shadeMode = ShadeMode.Dual,
+ clockSize = ClockSize.LARGE,
+ shadeLayoutWide = true,
+ )
+
+ assertThat(underTest.areNotificationsVisible).isTrue()
}
@Test
- fun isShadeLayoutWide_withConfigTrue_true() =
+ @DisableSceneContainer
+ fun areNotificationsVisible_withSmallClock_true() =
kosmos.runTest {
- val isShadeLayoutWide by collectLastValue(underTest.isShadeLayoutWide)
- shadeRepository.setShadeLayoutWide(true)
+ setupState(shadeMode = ShadeMode.Single, clockSize = ClockSize.SMALL)
- assertThat(isShadeLayoutWide).isTrue()
+ assertThat(underTest.areNotificationsVisible).isTrue()
}
@Test
- fun isShadeLayoutWide_withConfigFalse_false() =
+ @DisableSceneContainer
+ fun areNotificationsVisible_withLargeClock_false() =
kosmos.runTest {
- val isShadeLayoutWide by collectLastValue(underTest.isShadeLayoutWide)
- shadeRepository.setShadeLayoutWide(false)
+ setupState(shadeMode = ShadeMode.Single, clockSize = ClockSize.LARGE)
- assertThat(isShadeLayoutWide).isFalse()
+ assertThat(underTest.areNotificationsVisible).isFalse()
}
@Test
fun unfoldTranslations() =
kosmos.runTest {
val maxTranslation = prepareConfiguration()
- val translations by collectLastValue(underTest.unfoldTranslations)
val unfoldProvider = fakeUnfoldTransitionProgressProvider
unfoldProvider.onTransitionStarted()
- assertThat(translations?.start).isEqualTo(0f)
- assertThat(translations?.end).isEqualTo(-0f)
+ runCurrent()
+ assertThat(underTest.unfoldTranslations.start).isZero()
+ assertThat(underTest.unfoldTranslations.end).isZero()
repeat(10) { repetition ->
val transitionProgress = 0.1f * (repetition + 1)
unfoldProvider.onTransitionProgress(transitionProgress)
- assertThat(translations?.start).isEqualTo((1 - transitionProgress) * maxTranslation)
- assertThat(translations?.end).isEqualTo(-(1 - transitionProgress) * maxTranslation)
+ runCurrent()
+ assertThat(underTest.unfoldTranslations.start)
+ .isEqualTo((1 - transitionProgress) * maxTranslation)
+ assertThat(underTest.unfoldTranslations.end)
+ .isEqualTo(-(1 - transitionProgress) * maxTranslation)
}
unfoldProvider.onTransitionFinishing()
- assertThat(translations?.start).isEqualTo(0f)
- assertThat(translations?.end).isEqualTo(-0f)
+ runCurrent()
+ assertThat(underTest.unfoldTranslations.start).isZero()
+ assertThat(underTest.unfoldTranslations.end).isZero()
unfoldProvider.onTransitionFinished()
- assertThat(translations?.start).isEqualTo(0f)
- assertThat(translations?.end).isEqualTo(-0f)
+ runCurrent()
+ assertThat(underTest.unfoldTranslations.start).isZero()
+ assertThat(underTest.unfoldTranslations.end).isZero()
}
@Test
fun isContentVisible_whenNotOccluded_visible() =
kosmos.runTest {
- val isContentVisible by collectLastValue(underTest.isContentVisible)
-
keyguardOcclusionRepository.setShowWhenLockedActivityInfo(false, null)
runCurrent()
- assertThat(isContentVisible).isTrue()
+ assertThat(underTest.isContentVisible).isTrue()
}
@Test
fun isContentVisible_whenOccluded_notVisible() =
kosmos.runTest {
- val isContentVisible by collectLastValue(underTest.isContentVisible)
-
keyguardOcclusionRepository.setShowWhenLockedActivityInfo(true, null)
fakeKeyguardTransitionRepository.transitionTo(
KeyguardState.LOCKSCREEN,
KeyguardState.OCCLUDED,
)
runCurrent()
- assertThat(isContentVisible).isFalse()
+ assertThat(underTest.isContentVisible).isFalse()
}
@Test
fun isContentVisible_whenOccluded_notVisible_evenIfShadeShown() =
kosmos.runTest {
- val isContentVisible by collectLastValue(underTest.isContentVisible)
-
keyguardOcclusionRepository.setShowWhenLockedActivityInfo(true, null)
fakeKeyguardTransitionRepository.transitionTo(
KeyguardState.LOCKSCREEN,
@@ -238,7 +254,7 @@ class LockscreenContentViewModelTest(flags: FlagsParameterization) : SysuiTestCa
sceneInteractor.snapToScene(Scenes.Shade, "")
runCurrent()
- assertThat(isContentVisible).isFalse()
+ assertThat(underTest.isContentVisible).isFalse()
}
@Test
@@ -260,17 +276,16 @@ class LockscreenContentViewModelTest(flags: FlagsParameterization) : SysuiTestCa
@Test
fun isContentVisible_whenOccluded_notVisibleInOccluded_visibleInAod() =
kosmos.runTest {
- val isContentVisible by collectLastValue(underTest.isContentVisible)
keyguardOcclusionRepository.setShowWhenLockedActivityInfo(true, null)
fakeKeyguardTransitionRepository.transitionTo(
- KeyguardState.LOCKSCREEN,
- KeyguardState.OCCLUDED,
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.OCCLUDED,
)
runCurrent()
sceneInteractor.snapToScene(Scenes.Shade, "")
runCurrent()
- assertThat(isContentVisible).isFalse()
+ assertThat(underTest.isContentVisible).isFalse()
fakeKeyguardTransitionRepository.transitionTo(KeyguardState.OCCLUDED, KeyguardState.AOD)
runCurrent()
@@ -278,9 +293,32 @@ class LockscreenContentViewModelTest(flags: FlagsParameterization) : SysuiTestCa
sceneInteractor.snapToScene(Scenes.Lockscreen, "")
runCurrent()
- assertThat(isContentVisible).isTrue()
+ assertThat(underTest.isContentVisible).isTrue()
}
+ private fun Kosmos.setupState(
+ shadeMode: ShadeMode,
+ clockSize: ClockSize,
+ shadeLayoutWide: Boolean? = null,
+ ) {
+ val isShadeLayoutWide by collectLastValue(kosmos.shadeRepository.isShadeLayoutWide)
+ val collectedClockSize by collectLastValue(kosmos.keyguardClockInteractor.clockSize)
+ val collectedShadeMode by collectLastValue(kosmos.shadeModeInteractor.shadeMode)
+ when (shadeMode) {
+ ShadeMode.Dual -> kosmos.enableDualShade(wideLayout = shadeLayoutWide)
+ ShadeMode.Single -> kosmos.enableSingleShade()
+ ShadeMode.Split -> kosmos.enableSplitShade()
+ }
+ fakeKeyguardClockRepository.setShouldForceSmallClock(clockSize == ClockSize.SMALL)
+ fakeKeyguardClockRepository.setClockSize(clockSize)
+ runCurrent()
+ if (shadeLayoutWide != null) {
+ assertThat(isShadeLayoutWide).isEqualTo(shadeLayoutWide)
+ }
+ assertThat(collectedShadeMode).isEqualTo(shadeMode)
+ assertThat(collectedClockSize).isEqualTo(clockSize)
+ }
+
private fun prepareConfiguration(): Int {
val configuration = context.resources.configuration
configuration.setLayoutDirection(Locale.US)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/dialog/MediaOutputAdapterLegacyTest.java
index f5a71113235a..a7a0c24e2163 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/dialog/MediaOutputAdapterLegacyTest.java
@@ -33,8 +33,6 @@ import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
-import android.app.WallpaperColors;
-import android.graphics.Bitmap;
import android.graphics.drawable.Icon;
import android.platform.test.annotations.DisableFlags;
import android.platform.test.annotations.EnableFlags;
@@ -68,7 +66,7 @@ import java.util.stream.Collectors;
@SmallTest
@RunWith(AndroidJUnit4.class)
@TestableLooper.RunWithLooper(setAsMainLooper = true)
-public class MediaOutputAdapterTest extends SysuiTestCase {
+public class MediaOutputAdapterLegacyTest extends SysuiTestCase {
private static final String TEST_DEVICE_NAME_1 = "test_device_name_1";
private static final String TEST_DEVICE_NAME_2 = "test_device_name_2";
@@ -92,8 +90,8 @@ public class MediaOutputAdapterTest extends SysuiTestCase {
@Captor
private ArgumentCaptor<SeekBar.OnSeekBarChangeListener> mOnSeekBarChangeListenerCaptor;
- private MediaOutputAdapter mMediaOutputAdapter;
- private MediaOutputAdapter.MediaDeviceViewHolder mViewHolder;
+ private MediaOutputAdapterLegacy mMediaOutputAdapter;
+ private MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy mViewHolder;
private List<MediaDevice> mMediaDevices = new ArrayList<>();
private List<MediaItem> mMediaItems = new ArrayList<>();
MediaOutputSeekbar mSpyMediaOutputSeekbar;
@@ -124,9 +122,9 @@ public class MediaOutputAdapterTest extends SysuiTestCase {
mMediaItems.add(MediaItem.createDeviceMediaItem(mMediaDevice1, true));
mMediaItems.add(MediaItem.createDeviceMediaItem(mMediaDevice2, false));
- mMediaOutputAdapter = new MediaOutputAdapter(mMediaSwitchingController);
+ mMediaOutputAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController);
mMediaOutputAdapter.updateItems();
- mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter
+ mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter
.onCreateViewHolder(new LinearLayout(mContext), 0);
mSpyMediaOutputSeekbar = spy(mViewHolder.mSeekBar);
}
@@ -150,9 +148,9 @@ public class MediaOutputAdapterTest extends SysuiTestCase {
@Test
public void onBindViewHolder_bindPairNew_verifyView() {
- mMediaOutputAdapter = new MediaOutputAdapter(mMediaSwitchingController);
+ mMediaOutputAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController);
mMediaOutputAdapter.updateItems();
- mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter
+ mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter
.onCreateViewHolder(new LinearLayout(mContext), 0);
mMediaItems.add(MediaItem.createPairNewDeviceMediaItem());
mMediaItems.add(MediaItem.createPairNewDeviceMediaItem());
@@ -175,9 +173,9 @@ public class MediaOutputAdapterTest extends SysuiTestCase {
.map((item) -> item.getMediaDevice().get())
.collect(Collectors.toList()));
when(mMediaSwitchingController.getSessionName()).thenReturn(TEST_SESSION_NAME);
- mMediaOutputAdapter = new MediaOutputAdapter(mMediaSwitchingController);
+ mMediaOutputAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController);
mMediaOutputAdapter.updateItems();
- mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter
+ mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter
.onCreateViewHolder(new LinearLayout(mContext), 0);
mMediaOutputAdapter.getItemCount();
mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);
@@ -197,9 +195,9 @@ public class MediaOutputAdapterTest extends SysuiTestCase {
.map((item) -> item.getMediaDevice().get())
.collect(Collectors.toList()));
when(mMediaSwitchingController.getSessionName()).thenReturn(null);
- mMediaOutputAdapter = new MediaOutputAdapter(mMediaSwitchingController);
+ mMediaOutputAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController);
mMediaOutputAdapter.updateItems();
- mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter
+ mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter
.onCreateViewHolder(new LinearLayout(mContext), 0);
mMediaOutputAdapter.getItemCount();
mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);
@@ -225,7 +223,7 @@ public class MediaOutputAdapterTest extends SysuiTestCase {
@Test
public void onBindViewHolder_bindNonRemoteConnectedDevice_verifyView() {
when(mMediaSwitchingController.isActiveRemoteDevice(mMediaDevice1)).thenReturn(false);
- mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter
+ mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter
.onCreateViewHolder(new LinearLayout(mContext), 0);
mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);
@@ -245,7 +243,7 @@ public class MediaOutputAdapterTest extends SysuiTestCase {
when(mMediaSwitchingController.getSelectableMediaDevice())
.thenReturn(ImmutableList.of(mMediaDevice2));
when(mMediaSwitchingController.isCurrentConnectedDeviceRemote()).thenReturn(true);
- mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter
+ mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter
.onCreateViewHolder(new LinearLayout(mContext), 0);
mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);
@@ -264,7 +262,7 @@ public class MediaOutputAdapterTest extends SysuiTestCase {
when(mMediaSwitchingController.getSelectableMediaDevice())
.thenReturn(ImmutableList.of(mMediaDevice2));
when(mMediaSwitchingController.isCurrentConnectedDeviceRemote()).thenReturn(true);
- mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter
+ mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter
.onCreateViewHolder(new LinearLayout(mContext), 0);
mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);
@@ -276,7 +274,7 @@ public class MediaOutputAdapterTest extends SysuiTestCase {
public void onBindViewHolder_bindSingleConnectedRemoteDevice_verifyView() {
when(mMediaSwitchingController.getSelectableMediaDevice()).thenReturn(ImmutableList.of());
when(mMediaSwitchingController.isCurrentConnectedDeviceRemote()).thenReturn(true);
- mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter
+ mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter
.onCreateViewHolder(new LinearLayout(mContext), 0);
mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);
@@ -294,7 +292,7 @@ public class MediaOutputAdapterTest extends SysuiTestCase {
when(mMediaDevice1.hasOngoingSession()).thenReturn(true);
when(mMediaSwitchingController.getSelectableMediaDevice()).thenReturn(ImmutableList.of());
when(mMediaSwitchingController.isCurrentConnectedDeviceRemote()).thenReturn(true);
- mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter
+ mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter
.onCreateViewHolder(new LinearLayout(mContext), 0);
mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);
@@ -315,7 +313,7 @@ public class MediaOutputAdapterTest extends SysuiTestCase {
when(mMediaDevice1.isHostForOngoingSession()).thenReturn(true);
when(mMediaSwitchingController.getSelectableMediaDevice()).thenReturn(ImmutableList.of());
when(mMediaSwitchingController.isCurrentConnectedDeviceRemote()).thenReturn(true);
- mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter
+ mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter
.onCreateViewHolder(new LinearLayout(mContext), 0);
mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);
@@ -348,7 +346,7 @@ public class MediaOutputAdapterTest extends SysuiTestCase {
when(mMediaDevice1.isMutingExpectedDevice()).thenReturn(true);
when(mMediaSwitchingController.isCurrentConnectedDeviceRemote()).thenReturn(false);
when(mMediaSwitchingController.isActiveRemoteDevice(mMediaDevice1)).thenReturn(false);
- mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter
+ mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter
.onCreateViewHolder(new LinearLayout(mContext), 0);
mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);
@@ -498,7 +496,7 @@ public class MediaOutputAdapterTest extends SysuiTestCase {
when(mMediaDevice1.getSubtextString()).thenReturn(TEST_CUSTOM_SUBTEXT);
when(mMediaDevice1.hasOngoingSession()).thenReturn(true);
when(mMediaDevice1.getSelectionBehavior()).thenReturn(SELECTION_BEHAVIOR_GO_TO_APP);
- mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter
+ mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter
.onCreateViewHolder(new LinearLayout(mContext), 0);
mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);
@@ -522,7 +520,7 @@ public class MediaOutputAdapterTest extends SysuiTestCase {
when(mMediaDevice2.getSubtext()).thenReturn(SUBTEXT_SUBSCRIPTION_REQUIRED);
when(mMediaDevice2.getSubtextString()).thenReturn(deviceStatus);
when(mMediaDevice2.getSelectionBehavior()).thenReturn(SELECTION_BEHAVIOR_GO_TO_APP);
- mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter
+ mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter
.onCreateViewHolder(new LinearLayout(mContext), 0);
mMediaOutputAdapter.onBindViewHolder(mViewHolder, 1);
@@ -545,7 +543,7 @@ public class MediaOutputAdapterTest extends SysuiTestCase {
when(mMediaDevice2.getSubtext()).thenReturn(SUBTEXT_AD_ROUTING_DISALLOWED);
when(mMediaDevice2.getSubtextString()).thenReturn(deviceStatus);
when(mMediaDevice2.getSelectionBehavior()).thenReturn(SELECTION_BEHAVIOR_NONE);
- mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter
+ mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter
.onCreateViewHolder(new LinearLayout(mContext), 0);
mMediaOutputAdapter.onBindViewHolder(mViewHolder, 1);
@@ -567,7 +565,7 @@ public class MediaOutputAdapterTest extends SysuiTestCase {
when(mMediaDevice1.getSubtextString()).thenReturn(TEST_CUSTOM_SUBTEXT);
when(mMediaDevice1.hasOngoingSession()).thenReturn(true);
when(mMediaDevice1.getSelectionBehavior()).thenReturn(SELECTION_BEHAVIOR_GO_TO_APP);
- mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter
+ mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter
.onCreateViewHolder(new LinearLayout(mContext), 0);
mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);
@@ -627,9 +625,9 @@ public class MediaOutputAdapterTest extends SysuiTestCase {
@Test
public void onItemClick_clickPairNew_verifyLaunchBluetoothPairing() {
- mMediaOutputAdapter = new MediaOutputAdapter(mMediaSwitchingController);
+ mMediaOutputAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController);
mMediaOutputAdapter.updateItems();
- mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter
+ mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter
.onCreateViewHolder(new LinearLayout(mContext), 0);
mMediaItems.add(MediaItem.createPairNewDeviceMediaItem());
mMediaOutputAdapter.updateItems();
@@ -645,9 +643,9 @@ public class MediaOutputAdapterTest extends SysuiTestCase {
assertThat(mMediaDevice2.getState()).isEqualTo(
LocalMediaManager.MediaDeviceState.STATE_DISCONNECTED);
when(mMediaDevice2.getSelectionBehavior()).thenReturn(SELECTION_BEHAVIOR_TRANSFER);
- mMediaOutputAdapter = new MediaOutputAdapter(mMediaSwitchingController);
+ mMediaOutputAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController);
mMediaOutputAdapter.updateItems();
- mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter
+ mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter
.onCreateViewHolder(new LinearLayout(mContext), 0);
mMediaOutputAdapter.getItemCount();
mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);
@@ -663,11 +661,12 @@ public class MediaOutputAdapterTest extends SysuiTestCase {
assertThat(mMediaDevice2.getState()).isEqualTo(
LocalMediaManager.MediaDeviceState.STATE_DISCONNECTED);
when(mMediaDevice2.getSelectionBehavior()).thenReturn(SELECTION_BEHAVIOR_TRANSFER);
- mMediaOutputAdapter = new MediaOutputAdapter(mMediaSwitchingController);
+ mMediaOutputAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController);
mMediaOutputAdapter.updateItems();
- mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter
+ mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter
.onCreateViewHolder(new LinearLayout(mContext), 0);
- MediaOutputAdapter.MediaDeviceViewHolder spyMediaDeviceViewHolder = spy(mViewHolder);
+ MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy spyMediaDeviceViewHolder = spy(
+ mViewHolder);
mMediaOutputAdapter.getItemCount();
mMediaOutputAdapter.onBindViewHolder(spyMediaDeviceViewHolder, 0);
@@ -684,11 +683,12 @@ public class MediaOutputAdapterTest extends SysuiTestCase {
when(mMediaDevice2.getState()).thenReturn(
LocalMediaManager.MediaDeviceState.STATE_DISCONNECTED);
when(mMediaDevice2.getSelectionBehavior()).thenReturn(SELECTION_BEHAVIOR_GO_TO_APP);
- mMediaOutputAdapter = new MediaOutputAdapter(mMediaSwitchingController);
+ mMediaOutputAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController);
mMediaOutputAdapter.updateItems();
- mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter
+ mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter
.onCreateViewHolder(new LinearLayout(mContext), 0);
- MediaOutputAdapter.MediaDeviceViewHolder spyMediaDeviceViewHolder = spy(mViewHolder);
+ MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy spyMediaDeviceViewHolder = spy(
+ mViewHolder);
mMediaOutputAdapter.onBindViewHolder(spyMediaDeviceViewHolder, 1);
spyMediaDeviceViewHolder.mContainerLayout.performClick();
@@ -715,7 +715,7 @@ public class MediaOutputAdapterTest extends SysuiTestCase {
List<MediaDevice> selectableDevices = new ArrayList<>();
selectableDevices.add(mMediaDevice2);
when(mMediaSwitchingController.getSelectableMediaDevice()).thenReturn(selectableDevices);
- mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter
+ mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter
.onCreateViewHolder(new LinearLayout(mContext), 0);
mMediaOutputAdapter.onBindViewHolder(mViewHolder, 1);
@@ -859,7 +859,7 @@ public class MediaOutputAdapterTest extends SysuiTestCase {
when(mMediaSwitchingController.getSelectedMediaDevice())
.thenReturn(ImmutableList.of(mMediaDevice1));
when(mMediaSwitchingController.isCurrentConnectedDeviceRemote()).thenReturn(true);
- mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter
+ mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter
.onCreateViewHolder(new LinearLayout(mContext), 0);
mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);
@@ -899,16 +899,6 @@ public class MediaOutputAdapterTest extends SysuiTestCase {
}
@Test
- public void updateColorScheme_triggerController() {
- WallpaperColors wallpaperColors = WallpaperColors.fromBitmap(
- Bitmap.createBitmap(10, 10, Bitmap.Config.ARGB_8888));
-
- mMediaOutputAdapter.updateColorScheme(wallpaperColors, true);
-
- verify(mMediaSwitchingController).setCurrentColorScheme(wallpaperColors, true);
- }
-
- @Test
public void updateItems_controllerItemsUpdated_notUpdatesInAdapterUntilUpdateItems() {
mMediaOutputAdapter.updateItems();
List<MediaItem> updatedList = new ArrayList<>();
@@ -990,7 +980,7 @@ public class MediaOutputAdapterTest extends SysuiTestCase {
public void multipleSelectedDevices_verifySessionView() {
initializeSession();
- mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter
+ mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter
.onCreateViewHolder(
new LinearLayout(mContext), MediaItem.MediaItemType.TYPE_DEVICE);
mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);
@@ -1011,7 +1001,7 @@ public class MediaOutputAdapterTest extends SysuiTestCase {
public void multipleSelectedDevices_verifyCollapsedView() {
initializeSession();
- mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter
+ mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter
.onCreateViewHolder(
new LinearLayout(mContext), MediaItem.MediaItemType.TYPE_DEVICE);
mMediaOutputAdapter.onBindViewHolder(mViewHolder, 1);
@@ -1024,13 +1014,13 @@ public class MediaOutputAdapterTest extends SysuiTestCase {
@Test
public void multipleSelectedDevices_expandIconClicked_verifyInitialView() {
initializeSession();
- mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter
+ mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter
.onCreateViewHolder(
new LinearLayout(mContext), MediaItem.MediaItemType.TYPE_DEVICE);
mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);
mViewHolder.mEndTouchArea.performClick();
- mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter
+ mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter
.onCreateViewHolder(
new LinearLayout(mContext), MediaItem.MediaItemType.TYPE_DEVICE);
mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);
@@ -1047,13 +1037,13 @@ public class MediaOutputAdapterTest extends SysuiTestCase {
@Test
public void multipleSelectedDevices_expandIconClicked_verifyCollapsedView() {
initializeSession();
- mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter
+ mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter
.onCreateViewHolder(
new LinearLayout(mContext), MediaItem.MediaItemType.TYPE_DEVICE);
mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);
mViewHolder.mEndTouchArea.performClick();
- mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter
+ mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter
.onCreateViewHolder(
new LinearLayout(mContext), MediaItem.MediaItemType.TYPE_DEVICE);
mMediaOutputAdapter.onBindViewHolder(mViewHolder, 1);
@@ -1075,7 +1065,7 @@ public class MediaOutputAdapterTest extends SysuiTestCase {
when(mMediaSwitchingController.getSelectedMediaDevice()).thenReturn(selectedDevices);
when(mMediaSwitchingController.getDeselectableMediaDevice()).thenReturn(new ArrayList<>());
- mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter
+ mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter
.onCreateViewHolder(
new LinearLayout(mContext), MediaItem.MediaItemType.TYPE_DEVICE);
mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModelTest.kt
index 0bba8bba2419..b23cd5e5547f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModelTest.kt
@@ -16,6 +16,7 @@
package com.android.systemui.notifications.ui.viewmodel
+import android.app.StatusBarManager.DISABLE2_QUICK_SETTINGS
import android.testing.TestableLooper
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
@@ -28,6 +29,8 @@ import com.android.systemui.flags.EnableSceneContainer
import com.android.systemui.kosmos.runCurrent
import com.android.systemui.kosmos.testScope
import com.android.systemui.lifecycle.activateIn
+import com.android.systemui.media.controls.data.repository.mediaFilterRepository
+import com.android.systemui.media.controls.shared.model.MediaData
import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAsleepForTest
import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest
import com.android.systemui.power.domain.interactor.powerInteractor
@@ -39,10 +42,13 @@ import com.android.systemui.shade.data.repository.shadeRepository
import com.android.systemui.shade.domain.interactor.enableDualShade
import com.android.systemui.shade.domain.interactor.shadeInteractor
import com.android.systemui.shade.ui.viewmodel.notificationsShadeOverlayContentViewModel
+import com.android.systemui.statusbar.disableflags.data.repository.fakeDisableFlagsRepository
import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository
import com.android.systemui.statusbar.notification.data.repository.setActiveNotifs
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.update
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
@@ -50,6 +56,7 @@ import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
+@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(AndroidJUnit4::class)
@TestableLooper.RunWithLooper
@@ -155,6 +162,36 @@ class NotificationsShadeOverlayContentViewModelTest : SysuiTestCase() {
assertThat(underTest.showClock).isFalse()
}
+ @Test
+ fun showMedia_activeMedia_true() =
+ testScope.runTest {
+ kosmos.mediaFilterRepository.addSelectedUserMediaEntry(MediaData(active = true))
+ runCurrent()
+
+ assertThat(underTest.showMedia).isTrue()
+ }
+
+ @Test
+ fun showMedia_noActiveMedia_false() =
+ testScope.runTest {
+ kosmos.mediaFilterRepository.addSelectedUserMediaEntry(MediaData(active = false))
+ runCurrent()
+
+ assertThat(underTest.showMedia).isFalse()
+ }
+
+ @Test
+ fun showMedia_qsDisabled_false() =
+ testScope.runTest {
+ kosmos.mediaFilterRepository.addSelectedUserMediaEntry(MediaData(active = true))
+ kosmos.fakeDisableFlagsRepository.disableFlags.update {
+ it.copy(disable2 = DISABLE2_QUICK_SETTINGS)
+ }
+ runCurrent()
+
+ assertThat(underTest.showMedia).isFalse()
+ }
+
private fun TestScope.lockDevice() {
val currentScene by collectLastValue(sceneInteractor.currentScene)
kosmos.powerInteractor.setAsleepForTest()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/GridLayoutTypeInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/GridLayoutTypeInteractorTest.kt
index c775bfd75f6e..9e400a6c0a4c 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/GridLayoutTypeInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/GridLayoutTypeInteractorTest.kt
@@ -19,6 +19,7 @@ package com.android.systemui.qs.panels.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.Kosmos
import com.android.systemui.kosmos.collectLastValue
import com.android.systemui.kosmos.runTest
@@ -34,6 +35,7 @@ import org.junit.runner.RunWith
@SmallTest
@RunWith(AndroidJUnit4::class)
+@EnableSceneContainer
class GridLayoutTypeInteractorTest : SysuiTestCase() {
val kosmos = testKosmos()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/QSColumnsInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/QSColumnsInteractorTest.kt
index 2e7aeb433e04..9fe783b98046 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/QSColumnsInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/QSColumnsInteractorTest.kt
@@ -22,6 +22,7 @@ import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.common.ui.data.repository.configurationRepository
import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.flags.EnableSceneContainer
import com.android.systemui.kosmos.testCase
import com.android.systemui.kosmos.testScope
import com.android.systemui.qs.panels.data.repository.QSColumnsRepository
@@ -76,6 +77,7 @@ class QSColumnsInteractorTest : SysuiTestCase() {
}
@Test
+ @EnableSceneContainer
fun withDualShade_returnsCorrectValue() =
with(kosmos) {
testScope.runTest {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/MediaInRowInLandscapeViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/MediaInRowInLandscapeViewModelTest.kt
index fdbf42c9afd8..d5e502e99de5 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/MediaInRowInLandscapeViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/MediaInRowInLandscapeViewModelTest.kt
@@ -21,6 +21,7 @@ import android.content.res.mainResources
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository
+import com.android.systemui.flags.EnableSceneContainer
import com.android.systemui.kosmos.testScope
import com.android.systemui.lifecycle.activateIn
import com.android.systemui.media.controls.ui.controller.MediaHierarchyManager.Companion.LOCATION_QQS
@@ -36,6 +37,7 @@ import com.android.systemui.shade.shared.model.ShadeMode
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
import kotlin.test.Test
+import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
@@ -43,6 +45,7 @@ import org.junit.runner.RunWith
import platform.test.runner.parameterized.ParameterizedAndroidJunit4
import platform.test.runner.parameterized.Parameters
+@OptIn(ExperimentalCoroutinesApi::class)
@RunWith(ParameterizedAndroidJunit4::class)
@SmallTest
class MediaInRowInLandscapeViewModelTest(private val testData: TestData) : SysuiTestCase() {
@@ -63,6 +66,7 @@ class MediaInRowInLandscapeViewModelTest(private val testData: TestData) : Sysui
}
@Test
+ @EnableSceneContainer
fun shouldMediaShowInRow() =
with(kosmos) {
testScope.runTest {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/QSColumnsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/QSColumnsViewModelTest.kt
index 241cdbfbef83..4912c319bf2e 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/QSColumnsViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/QSColumnsViewModelTest.kt
@@ -21,6 +21,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.common.ui.data.repository.configurationRepository
+import com.android.systemui.flags.EnableSceneContainer
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.runCurrent
import com.android.systemui.kosmos.testCase
@@ -88,6 +89,7 @@ class QSColumnsViewModelTest : SysuiTestCase() {
}
@Test
+ @EnableSceneContainer
fun mediaLocationNull_dualShade_alwaysDualShadeColumns() =
with(kosmos) {
testScope.runTest {
@@ -111,6 +113,7 @@ class QSColumnsViewModelTest : SysuiTestCase() {
}
@Test
+ @EnableSceneContainer
fun mediaLocationQS_dualShade_alwaysDualShadeColumns() =
with(kosmos) {
testScope.runTest {
@@ -133,6 +136,7 @@ class QSColumnsViewModelTest : SysuiTestCase() {
}
@Test
+ @EnableSceneContainer
fun mediaLocationQQS_dualShade_alwaysDualShadeColumns() =
with(kosmos) {
testScope.runTest {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt
index 80c7026b0cea..23a0f6224fb7 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
@@ -757,4 +757,46 @@ class SceneInteractorTest : SysuiTestCase() {
verify(processor, never()).onSceneAboutToChange(any(), any())
}
+
+ @Test
+ fun changeScene_sameScene_withFreeze() =
+ kosmos.runTest {
+ val currentScene by collectLastValue(underTest.currentScene)
+ assertThat(currentScene).isEqualTo(Scenes.Lockscreen)
+ val processor = mock<SceneInteractor.OnSceneAboutToChangeListener>()
+ underTest.registerSceneStateProcessor(processor)
+ verify(processor, never()).onSceneAboutToChange(any(), any())
+ assertThat(fakeSceneDataSource.freezeAndAnimateToCurrentStateCallCount).isEqualTo(0)
+
+ underTest.changeScene(
+ toScene = Scenes.Lockscreen,
+ loggingReason = "test",
+ sceneState = KeyguardState.AOD,
+ forceSettleToTargetScene = true,
+ )
+
+ verify(processor).onSceneAboutToChange(Scenes.Lockscreen, KeyguardState.AOD)
+ assertThat(fakeSceneDataSource.freezeAndAnimateToCurrentStateCallCount).isEqualTo(1)
+ }
+
+ @Test
+ fun changeScene_sameScene_withoutFreeze() =
+ kosmos.runTest {
+ val currentScene by collectLastValue(underTest.currentScene)
+ assertThat(currentScene).isEqualTo(Scenes.Lockscreen)
+ val processor = mock<SceneInteractor.OnSceneAboutToChangeListener>()
+ underTest.registerSceneStateProcessor(processor)
+ verify(processor, never()).onSceneAboutToChange(any(), any())
+ assertThat(fakeSceneDataSource.freezeAndAnimateToCurrentStateCallCount).isEqualTo(0)
+
+ underTest.changeScene(
+ toScene = Scenes.Lockscreen,
+ loggingReason = "test",
+ sceneState = KeyguardState.AOD,
+ forceSettleToTargetScene = false,
+ )
+
+ verify(processor, never()).onSceneAboutToChange(any(), any())
+ assertThat(fakeSceneDataSource.freezeAndAnimateToCurrentStateCallCount).isEqualTo(0)
+ }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractorImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractorImplTest.kt
index 668f568d7f46..d26e195d360a 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractorImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractorImplTest.kt
@@ -20,6 +20,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.flags.EnableSceneContainer
import com.android.systemui.kosmos.testScope
import com.android.systemui.shade.shared.model.ShadeMode
import com.android.systemui.testKosmos
@@ -31,6 +32,7 @@ import org.junit.runner.RunWith
@SmallTest
@RunWith(AndroidJUnit4::class)
+@EnableSceneContainer
class ShadeModeInteractorImplTest : SysuiTestCase() {
private val kosmos = testKosmos()
@@ -80,7 +82,7 @@ class ShadeModeInteractorImplTest : SysuiTestCase() {
}
@Test
- fun isDualShade_settingEnabled_returnsTrue() =
+ fun isDualShade_settingEnabledSceneContainerEnabled_returnsTrue() =
testScope.runTest {
// TODO(b/391578667): Add a test case for user switching once the bug is fixed.
val shadeMode by collectLastValue(underTest.shadeMode)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/startable/ShadeStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/startable/ShadeStartableTest.kt
index b8f66acf6413..dde867814159 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/startable/ShadeStartableTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/startable/ShadeStartableTest.kt
@@ -48,6 +48,7 @@ import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.TestScope
@@ -59,6 +60,7 @@ import org.mockito.kotlin.verify
import platform.test.runner.parameterized.ParameterizedAndroidJunit4
import platform.test.runner.parameterized.Parameters
+@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(ParameterizedAndroidJunit4::class)
class ShadeStartableTest(flags: FlagsParameterization) : SysuiTestCase() {
@@ -103,6 +105,7 @@ class ShadeStartableTest(flags: FlagsParameterization) : SysuiTestCase() {
}
@Test
+ @EnableSceneContainer
fun hydrateShadeMode_dualShadeEnabled() =
testScope.runTest {
overrideResource(R.bool.config_use_split_notification_shade, false)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shared/plugins/PluginInstanceTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/shared/plugins/PluginInstanceTest.java
index 93ba8e1317fa..064fd485dab4 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shared/plugins/PluginInstanceTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shared/plugins/PluginInstanceTest.java
@@ -28,6 +28,7 @@ import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.util.Log;
+import androidx.test.filters.FlakyTest;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
@@ -52,6 +53,7 @@ import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Supplier;
@SmallTest
+@FlakyTest(bugId = 395832204)
@RunWith(AndroidJUnit4.class)
public class PluginInstanceTest extends SysuiTestCase {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractorTest.kt
index cabe4afdea60..5d1950670777 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractorTest.kt
@@ -31,6 +31,7 @@ import com.android.systemui.statusbar.core.StatusBarConnectedDisplays
import com.android.systemui.statusbar.notification.data.model.activeNotificationModel
import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel
import com.android.systemui.testKosmos
+import com.android.systemui.util.time.fakeSystemClock
import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith
@@ -183,7 +184,7 @@ class SingleNotificationChipInteractorTest : SysuiTestCase() {
statusBarChipIcon = null,
promotedContent = PROMOTED_CONTENT,
),
- 32L,
+ creationTime = 32L,
)
val latest by collectLastValue(underTest.notificationChip)
@@ -246,7 +247,7 @@ class SingleNotificationChipInteractorTest : SysuiTestCase() {
statusBarChipIcon = mock(),
promotedContent = PROMOTED_CONTENT,
)
- val underTest = factory.create(startingNotif, 123L)
+ val underTest = factory.create(startingNotif, creationTime = 123L)
val latest by collectLastValue(underTest.notificationChip)
assertThat(latest).isNotNull()
@@ -306,9 +307,10 @@ class SingleNotificationChipInteractorTest : SysuiTestCase() {
}
@Test
- fun notificationChip_appIsVisibleOnCreation_emitsIsAppVisibleTrue() =
+ fun notificationChip_appIsVisibleOnCreation_emitsIsAppVisibleTrueWithTime() =
kosmos.runTest {
activityManagerRepository.fake.startingIsAppVisibleValue = true
+ fakeSystemClock.setCurrentTimeMillis(9000)
val underTest =
factory.create(
@@ -325,12 +327,14 @@ class SingleNotificationChipInteractorTest : SysuiTestCase() {
assertThat(latest).isNotNull()
assertThat(latest!!.isAppVisible).isTrue()
+ assertThat(latest!!.lastAppVisibleTime).isEqualTo(9000)
}
@Test
- fun notificationChip_appNotVisibleOnCreation_emitsIsAppVisibleFalse() =
+ fun notificationChip_appNotVisibleOnCreation_emitsIsAppVisibleFalseWithNoTime() =
kosmos.runTest {
activityManagerRepository.fake.startingIsAppVisibleValue = false
+ fakeSystemClock.setCurrentTimeMillis(9000)
val underTest =
factory.create(
@@ -347,11 +351,15 @@ class SingleNotificationChipInteractorTest : SysuiTestCase() {
assertThat(latest).isNotNull()
assertThat(latest!!.isAppVisible).isFalse()
+ assertThat(latest!!.lastAppVisibleTime).isNull()
}
@Test
fun notificationChip_updatesWhenAppIsVisible() =
kosmos.runTest {
+ activityManagerRepository.fake.startingIsAppVisibleValue = false
+ fakeSystemClock.setCurrentTimeMillis(9000)
+
val underTest =
factory.create(
activeNotificationModel(
@@ -365,32 +373,39 @@ class SingleNotificationChipInteractorTest : SysuiTestCase() {
val latest by collectLastValue(underTest.notificationChip)
- activityManagerRepository.fake.setIsAppVisible(UID, false)
+ activityManagerRepository.fake.setIsAppVisible(UID, isAppVisible = false)
assertThat(latest!!.isAppVisible).isFalse()
+ assertThat(latest!!.lastAppVisibleTime).isNull()
- activityManagerRepository.fake.setIsAppVisible(UID, true)
+ fakeSystemClock.setCurrentTimeMillis(11000)
+ activityManagerRepository.fake.setIsAppVisible(UID, isAppVisible = true)
assertThat(latest!!.isAppVisible).isTrue()
+ assertThat(latest!!.lastAppVisibleTime).isEqualTo(11000)
- activityManagerRepository.fake.setIsAppVisible(UID, false)
+ fakeSystemClock.setCurrentTimeMillis(13000)
+ activityManagerRepository.fake.setIsAppVisible(UID, isAppVisible = false)
assertThat(latest!!.isAppVisible).isFalse()
+ assertThat(latest!!.lastAppVisibleTime).isEqualTo(11000)
+
+ fakeSystemClock.setCurrentTimeMillis(15000)
+ activityManagerRepository.fake.setIsAppVisible(UID, isAppVisible = true)
+ assertThat(latest!!.isAppVisible).isTrue()
+ assertThat(latest!!.lastAppVisibleTime).isEqualTo(15000)
}
- // Note: This test is theoretically impossible because the notification key should contain the
- // UID, so if the UID changes then the key would also change and a new interactor would be
- // created. But, test it just in case.
@Test
- fun notificationChip_updatedUid_rechecksAppVisibility_oldObserverUnregistered() =
+ fun notificationChip_updatedUid_newUidIsIgnoredButOtherDataNotIgnored() =
kosmos.runTest {
activityManagerRepository.fake.startingIsAppVisibleValue = false
- val hiddenUid = 100
- val shownUid = 101
+ val originalUid = 100
+ val newUid = 101
val underTest =
factory.create(
activeNotificationModel(
key = "notif",
- uid = hiddenUid,
+ uid = originalUid,
statusBarChipIcon = mock(),
promotedContent = PROMOTED_CONTENT,
),
@@ -402,16 +417,34 @@ class SingleNotificationChipInteractorTest : SysuiTestCase() {
// WHEN the notif gets a new UID that starts as visible
activityManagerRepository.fake.startingIsAppVisibleValue = true
+ val newPromotedContentBuilder =
+ PromotedNotificationContentModel.Builder("notif").apply {
+ this.shortCriticalText = "Arrived"
+ }
+ val newPromotedContent = newPromotedContentBuilder.build()
underTest.setNotification(
activeNotificationModel(
key = "notif",
- uid = shownUid,
+ uid = newUid,
statusBarChipIcon = mock(),
- promotedContent = PROMOTED_CONTENT,
+ promotedContent = newPromotedContent,
)
)
- // THEN we re-fetch the app visibility state with the new UID
+ // THEN we do update other fields like promoted content
+ assertThat(latest!!.promotedContent).isEqualTo(newPromotedContent)
+
+ // THEN we don't fetch the app visibility state for the new UID
+ assertThat(latest!!.isAppVisible).isFalse()
+
+ // AND don't listen to updates for the new UID
+ activityManagerRepository.fake.setIsAppVisible(newUid, isAppVisible = false)
+ activityManagerRepository.fake.setIsAppVisible(newUid, isAppVisible = true)
+ assertThat(latest!!.isAppVisible).isFalse()
+
+ // AND we still use updates from the old UID
+ // TODO(b/364653005): This particular behavior isn't great, can we do better?
+ activityManagerRepository.fake.setIsAppVisible(originalUid, isAppVisible = true)
assertThat(latest!!.isAppVisible).isTrue()
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractorTest.kt
index d8e4cd927bec..7ed2bd38bcd2 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractorTest.kt
@@ -333,7 +333,7 @@ class StatusBarNotificationChipsInteractorTest : SysuiTestCase() {
@Test
@EnableFlags(StatusBarNotifChips.FLAG_NAME)
- fun shownNotificationChips_sortedBasedOnFirstAppearanceTime() =
+ fun shownNotificationChips_sortedByFirstAppearanceTime() =
kosmos.runTest {
val latest by collectLastValue(underTest.shownNotificationChips)
@@ -349,8 +349,7 @@ class StatusBarNotificationChipsInteractorTest : SysuiTestCase() {
promotedContent = PromotedNotificationContentModel.Builder("notif1").build(),
)
setNotifs(listOf(notif1))
- assertThat(latest).hasSize(1)
- assertThat(latest!![0].key).isEqualTo("notif1")
+ assertThat(latest!!.map { it.key }).containsExactly("notif1").inOrder()
// WHEN we add notif2 at t=2000
fakeSystemClock.advanceTime(1000)
@@ -362,26 +361,20 @@ class StatusBarNotificationChipsInteractorTest : SysuiTestCase() {
)
setNotifs(listOf(notif1, notif2))
- // THEN notif2 is ranked above notif1 because it appeared later
- assertThat(latest).hasSize(2)
- assertThat(latest!![0].key).isEqualTo("notif2")
- assertThat(latest!![1].key).isEqualTo("notif1")
+ // THEN notif2 is ranked above notif1 because notif2 appeared later
+ assertThat(latest!!.map { it.key }).containsExactly("notif2", "notif1").inOrder()
// WHEN notif1 and notif2 swap places
setNotifs(listOf(notif2, notif1))
// THEN notif2 is still ranked above notif1 to preserve chip ordering
- assertThat(latest).hasSize(2)
- assertThat(latest!![0].key).isEqualTo("notif2")
- assertThat(latest!![1].key).isEqualTo("notif1")
+ assertThat(latest!!.map { it.key }).containsExactly("notif2", "notif1").inOrder()
// WHEN notif1 and notif2 swap places again
setNotifs(listOf(notif1, notif2))
// THEN notif2 is still ranked above notif1 to preserve chip ordering
- assertThat(latest).hasSize(2)
- assertThat(latest!![0].key).isEqualTo("notif2")
- assertThat(latest!![1].key).isEqualTo("notif1")
+ assertThat(latest!!.map { it.key }).containsExactly("notif2", "notif1").inOrder()
// WHEN notif1 gets an update
val notif1NewPromotedContent =
@@ -400,9 +393,7 @@ class StatusBarNotificationChipsInteractorTest : SysuiTestCase() {
)
// THEN notif2 is still ranked above notif1 to preserve chip ordering
- assertThat(latest).hasSize(2)
- assertThat(latest!![0].key).isEqualTo("notif2")
- assertThat(latest!![1].key).isEqualTo("notif1")
+ assertThat(latest!!.map { it.key }).containsExactly("notif2", "notif1").inOrder()
// WHEN notif1 disappears and then reappears
fakeSystemClock.advanceTime(1000)
@@ -413,9 +404,238 @@ class StatusBarNotificationChipsInteractorTest : SysuiTestCase() {
setNotifs(listOf(notif2, notif1))
// THEN notif1 is now ranked first
- assertThat(latest).hasSize(2)
- assertThat(latest!![0].key).isEqualTo("notif1")
- assertThat(latest!![1].key).isEqualTo("notif2")
+ assertThat(latest!!.map { it.key }).containsExactly("notif1", "notif2").inOrder()
+ }
+
+ @Test
+ @EnableFlags(StatusBarNotifChips.FLAG_NAME)
+ fun shownNotificationChips_sortedByLastAppVisibleTime() =
+ kosmos.runTest {
+ val latest by collectLastValue(underTest.shownNotificationChips)
+
+ val notif1Info = NotifInfo("notif1", mock<StatusBarIconView>(), uid = 100)
+ val notif2Info = NotifInfo("notif2", mock<StatusBarIconView>(), uid = 200)
+
+ activityManagerRepository.fake.startingIsAppVisibleValue = false
+ fakeSystemClock.setCurrentTimeMillis(1000)
+ val notif1 =
+ activeNotificationModel(
+ key = notif1Info.key,
+ uid = notif1Info.uid,
+ statusBarChipIcon = notif1Info.icon,
+ promotedContent =
+ PromotedNotificationContentModel.Builder(notif1Info.key).build(),
+ )
+ val notif2 =
+ activeNotificationModel(
+ key = notif2Info.key,
+ uid = notif2Info.uid,
+ statusBarChipIcon = notif2Info.icon,
+ promotedContent =
+ PromotedNotificationContentModel.Builder(notif2Info.key).build(),
+ )
+ setNotifs(listOf(notif1, notif2))
+ assertThat(latest!!.map { it.key }).containsExactly("notif1", "notif2").inOrder()
+
+ // WHEN notif2's app becomes visible
+ fakeSystemClock.advanceTime(1000)
+ activityManagerRepository.fake.setIsAppVisible(notif2Info.uid, isAppVisible = true)
+
+ // THEN notif2 is no longer shown
+ assertThat(latest!!.map { it.key }).containsExactly("notif1").inOrder()
+
+ // WHEN notif2's app is no longer visible
+ fakeSystemClock.advanceTime(1000)
+ activityManagerRepository.fake.setIsAppVisible(notif2Info.uid, isAppVisible = false)
+
+ // THEN notif2 is ranked above notif1 because it was more recently visible
+ assertThat(latest!!.map { it.key }).containsExactly("notif2", "notif1").inOrder()
+
+ // WHEN the app associated with notif1 becomes visible then un-visible
+ fakeSystemClock.advanceTime(1000)
+ activityManagerRepository.fake.setIsAppVisible(notif1Info.uid, isAppVisible = true)
+ fakeSystemClock.advanceTime(1000)
+ activityManagerRepository.fake.setIsAppVisible(notif1Info.uid, isAppVisible = false)
+
+ // THEN notif1 is now ranked above notif2 because it was more recently visible
+ assertThat(latest!!.map { it.key }).containsExactly("notif1", "notif2").inOrder()
+ }
+
+ @Test
+ @EnableFlags(StatusBarNotifChips.FLAG_NAME)
+ fun shownNotificationChips_newNotificationTakesPriorityOverLastAppVisible() =
+ kosmos.runTest {
+ val latest by collectLastValue(underTest.shownNotificationChips)
+
+ val notif1Info = NotifInfo("notif1", mock<StatusBarIconView>(), uid = 100)
+ val notif2Info = NotifInfo("notif2", mock<StatusBarIconView>(), uid = 200)
+ val notif3Info = NotifInfo("notif3", mock<StatusBarIconView>(), uid = 300)
+
+ activityManagerRepository.fake.startingIsAppVisibleValue = false
+ fakeSystemClock.setCurrentTimeMillis(1000)
+ val notif1 =
+ activeNotificationModel(
+ key = notif1Info.key,
+ uid = notif1Info.uid,
+ statusBarChipIcon = notif1Info.icon,
+ promotedContent =
+ PromotedNotificationContentModel.Builder(notif1Info.key).build(),
+ )
+ val notif2 =
+ activeNotificationModel(
+ key = notif2Info.key,
+ uid = notif2Info.uid,
+ statusBarChipIcon = notif2Info.icon,
+ promotedContent =
+ PromotedNotificationContentModel.Builder(notif2Info.key).build(),
+ )
+ setNotifs(listOf(notif1, notif2))
+ assertThat(latest!!.map { it.key }).containsExactly("notif1", "notif2").inOrder()
+
+ // WHEN notif2's app becomes visible then not visible
+ fakeSystemClock.advanceTime(1000)
+ activityManagerRepository.fake.setIsAppVisible(notif2Info.uid, isAppVisible = true)
+ fakeSystemClock.advanceTime(1000)
+ activityManagerRepository.fake.setIsAppVisible(notif2Info.uid, isAppVisible = false)
+
+ // THEN notif2 is ranked above notif1 because it was more recently visible
+ assertThat(latest!!.map { it.key }).containsExactly("notif2", "notif1").inOrder()
+
+ // WHEN a new notif3 appears
+ fakeSystemClock.advanceTime(1000)
+ val notif3 =
+ activeNotificationModel(
+ key = notif3Info.key,
+ uid = notif3Info.uid,
+ statusBarChipIcon = notif3Info.icon,
+ promotedContent =
+ PromotedNotificationContentModel.Builder(notif3Info.key).build(),
+ )
+ setNotifs(listOf(notif1, notif2, notif3))
+
+ // THEN notif3 is ranked above everything else
+ // AND notif2 is still before notif1 because it was more recently visible
+ assertThat(latest!!.map { it.key })
+ .containsExactly("notif3", "notif2", "notif1")
+ .inOrder()
+ }
+
+ @Test
+ @EnableFlags(StatusBarNotifChips.FLAG_NAME)
+ fun shownNotificationChips_fullSort() =
+ kosmos.runTest {
+ val latest by collectLastValue(underTest.shownNotificationChips)
+
+ val notif1Info = NotifInfo("notif1", mock<StatusBarIconView>(), uid = 100)
+ val notif2Info = NotifInfo("notif2", mock<StatusBarIconView>(), uid = 200)
+ val notif3Info = NotifInfo("notif3", mock<StatusBarIconView>(), uid = 300)
+
+ // First, add notif1 at t=1000
+ activityManagerRepository.fake.startingIsAppVisibleValue = false
+ fakeSystemClock.setCurrentTimeMillis(1000)
+ val notif1 =
+ activeNotificationModel(
+ key = notif1Info.key,
+ uid = notif1Info.uid,
+ statusBarChipIcon = notif1Info.icon,
+ promotedContent =
+ PromotedNotificationContentModel.Builder(notif1Info.key).build(),
+ )
+ setNotifs(listOf(notif1))
+
+ // WHEN we add notif2 at t=2000
+ fakeSystemClock.advanceTime(1000)
+ val notif2 =
+ activeNotificationModel(
+ key = notif2Info.key,
+ uid = notif2Info.uid,
+ statusBarChipIcon = notif2Info.icon,
+ promotedContent =
+ PromotedNotificationContentModel.Builder(notif2Info.key).build(),
+ )
+ setNotifs(listOf(notif1, notif2))
+
+ // THEN notif2 is ranked above notif1 because notif2 appeared later
+ assertThat(latest!!.map { it.key }).containsExactly("notif2", "notif1").inOrder()
+
+ // WHEN notif2's app becomes visible then un-visible
+ fakeSystemClock.advanceTime(1000)
+ activityManagerRepository.fake.setIsAppVisible(notif2Info.uid, isAppVisible = true)
+ fakeSystemClock.advanceTime(1000)
+ activityManagerRepository.fake.setIsAppVisible(notif2Info.uid, isAppVisible = false)
+
+ // THEN notif2 is ranked above notif1 because it was more recently visible
+ assertThat(latest!!.map { it.key }).containsExactly("notif2", "notif1").inOrder()
+
+ // WHEN the app associated with notif1 becomes visible then un-visible
+ fakeSystemClock.advanceTime(1000)
+ activityManagerRepository.fake.setIsAppVisible(notif1Info.uid, isAppVisible = true)
+ fakeSystemClock.advanceTime(1000)
+ activityManagerRepository.fake.setIsAppVisible(notif1Info.uid, isAppVisible = false)
+
+ // THEN notif1 is ranked above notif2 because it was more recently visible
+ assertThat(latest!!.map { it.key }).containsExactly("notif1", "notif2").inOrder()
+
+ // WHEN notif2 gets an update
+ val notif2NewPromotedContent =
+ PromotedNotificationContentModel.Builder("notif2").apply {
+ this.shortCriticalText = "Arrived"
+ }
+ setNotifs(
+ listOf(
+ notif1,
+ activeNotificationModel(
+ key = notif2Info.key,
+ uid = notif2Info.uid,
+ statusBarChipIcon = notif2Info.icon,
+ promotedContent = notif2NewPromotedContent.build(),
+ ),
+ )
+ )
+
+ // THEN notif1 is still ranked above notif2 to preserve chip ordering
+ assertThat(latest!!.map { it.key }).containsExactly("notif1", "notif2").inOrder()
+
+ // WHEN a new notification appears
+ fakeSystemClock.advanceTime(1000)
+ val notif3 =
+ activeNotificationModel(
+ key = notif3Info.key,
+ uid = notif3Info.uid,
+ statusBarChipIcon = notif3Info.icon,
+ promotedContent =
+ PromotedNotificationContentModel.Builder(notif3Info.key).build(),
+ )
+ setNotifs(listOf(notif1, notif2, notif3))
+
+ // THEN it's ranked first because it's new
+ assertThat(latest!!.map { it.key })
+ .containsExactly("notif3", "notif1", "notif2")
+ .inOrder()
+
+ // WHEN notif2 becomes visible then un-visible again
+ fakeSystemClock.advanceTime(1000)
+ activityManagerRepository.fake.setIsAppVisible(notif2Info.uid, isAppVisible = true)
+ fakeSystemClock.advanceTime(1000)
+ activityManagerRepository.fake.setIsAppVisible(notif2Info.uid, isAppVisible = false)
+
+ // THEN it moves to the front
+ assertThat(latest!!.map { it.key })
+ .containsExactly("notif2", "notif3", "notif1")
+ .inOrder()
+
+ // WHEN notif1 disappears and then reappears
+ fakeSystemClock.advanceTime(1000)
+ setNotifs(listOf(notif2, notif3))
+ assertThat(latest!!.map { it.key }).containsExactly("notif2", "notif3").inOrder()
+
+ fakeSystemClock.advanceTime(1000)
+ setNotifs(listOf(notif2, notif1, notif3))
+
+ // THEN notif1 is now ranked first
+ assertThat(latest!!.map { it.key })
+ .containsExactly("notif1", "notif2", "notif3")
+ .inOrder()
}
@Test
@@ -495,4 +715,6 @@ class StatusBarNotificationChipsInteractorTest : SysuiTestCase() {
.apply { notifs.forEach { addIndividualNotif(it) } }
.build()
}
+
+ private data class NotifInfo(val key: String, val icon: StatusBarIconView, val uid: Int)
}
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 aaa9b58a45df..7cf817a06225 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
@@ -49,8 +49,11 @@ import com.android.systemui.statusbar.notification.shared.ActiveNotificationMode
import com.android.systemui.statusbar.notification.stack.data.repository.headsUpNotificationRepository
import com.android.systemui.statusbar.phone.ongoingcall.StatusBarChipsModernization
import com.android.systemui.testKosmos
+import com.android.systemui.util.time.fakeSystemClock
import com.google.common.truth.Truth.assertThat
import kotlin.test.Test
+import kotlin.time.Duration.Companion.minutes
+import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.flow.MutableStateFlow
import org.junit.Before
import org.junit.runner.RunWith
@@ -286,13 +289,15 @@ class NotifChipsViewModelTest : SysuiTestCase() {
fun chips_hasShortCriticalText_usesTextInsteadOfTime() =
kosmos.runTest {
val latest by collectLastValue(underTest.chips)
+ val currentTime = 30.minutes.inWholeMilliseconds
+ fakeSystemClock.setCurrentTimeMillis(currentTime)
val promotedContentBuilder =
PromotedNotificationContentModel.Builder("notif").apply {
this.shortCriticalText = "Arrived"
this.time =
PromotedNotificationContentModel.When(
- time = 6543L,
+ time = currentTime + 30.minutes.inWholeMilliseconds,
mode = PromotedNotificationContentModel.When.Mode.BasicTime,
)
}
@@ -340,13 +345,15 @@ class NotifChipsViewModelTest : SysuiTestCase() {
fun chips_basicTime_timeHiddenIfAutomaticallyPromoted() =
kosmos.runTest {
val latest by collectLastValue(underTest.chips)
+ val currentTime = 30.minutes.inWholeMilliseconds
+ fakeSystemClock.setCurrentTimeMillis(currentTime)
val promotedContentBuilder =
PromotedNotificationContentModel.Builder("notif").apply {
this.wasPromotedAutomatically = true
this.time =
PromotedNotificationContentModel.When(
- time = 6543L,
+ time = currentTime + 30.minutes.inWholeMilliseconds,
mode = PromotedNotificationContentModel.When.Mode.BasicTime,
)
}
@@ -370,13 +377,15 @@ class NotifChipsViewModelTest : SysuiTestCase() {
fun chips_basicTime_timeShownIfNotAutomaticallyPromoted() =
kosmos.runTest {
val latest by collectLastValue(underTest.chips)
+ val currentTime = 30.minutes.inWholeMilliseconds
+ fakeSystemClock.setCurrentTimeMillis(currentTime)
val promotedContentBuilder =
PromotedNotificationContentModel.Builder("notif").apply {
this.wasPromotedAutomatically = false
this.time =
PromotedNotificationContentModel.When(
- time = 6543L,
+ time = currentTime + 30.minutes.inWholeMilliseconds,
mode = PromotedNotificationContentModel.When.Mode.BasicTime,
)
}
@@ -397,18 +406,117 @@ class NotifChipsViewModelTest : SysuiTestCase() {
@Test
@DisableFlags(FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY)
- fun chips_basicTime_isShortTimeDelta() =
+ fun chips_basicTime_timeInFuture_isShortTimeDelta() =
kosmos.runTest {
val latest by collectLastValue(underTest.chips)
+ val currentTime = 3.minutes.inWholeMilliseconds
+ fakeSystemClock.setCurrentTimeMillis(currentTime)
val promotedContentBuilder =
PromotedNotificationContentModel.Builder("notif").apply {
this.time =
PromotedNotificationContentModel.When(
- time = 6543L,
+ time = currentTime + 13.minutes.inWholeMilliseconds,
mode = PromotedNotificationContentModel.When.Mode.BasicTime,
)
}
+
+ setNotifs(
+ listOf(
+ activeNotificationModel(
+ key = "notif",
+ statusBarChipIcon = createStatusBarIconViewOrNull(),
+ promotedContent = promotedContentBuilder.build(),
+ )
+ )
+ )
+
+ assertThat(latest).hasSize(1)
+ assertThat(latest!![0])
+ .isInstanceOf(OngoingActivityChipModel.Active.ShortTimeDelta::class.java)
+ }
+
+ @Test
+ @DisableFlags(FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY)
+ fun chips_basicTime_timeLessThanOneMinInFuture_isIconOnly() =
+ kosmos.runTest {
+ val latest by collectLastValue(underTest.chips)
+ val currentTime = 3.minutes.inWholeMilliseconds
+ fakeSystemClock.setCurrentTimeMillis(currentTime)
+
+ val promotedContentBuilder =
+ PromotedNotificationContentModel.Builder("notif").apply {
+ this.time =
+ PromotedNotificationContentModel.When(
+ time = currentTime + 500,
+ mode = PromotedNotificationContentModel.When.Mode.BasicTime,
+ )
+ }
+
+ setNotifs(
+ listOf(
+ activeNotificationModel(
+ key = "notif",
+ statusBarChipIcon = createStatusBarIconViewOrNull(),
+ promotedContent = promotedContentBuilder.build(),
+ )
+ )
+ )
+
+ assertThat(latest).hasSize(1)
+ assertThat(latest!![0])
+ .isInstanceOf(OngoingActivityChipModel.Active.IconOnly::class.java)
+ }
+
+ @Test
+ @DisableFlags(FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY)
+ fun chips_basicTime_timeIsNow_isIconOnly() =
+ kosmos.runTest {
+ val latest by collectLastValue(underTest.chips)
+ val currentTime = 62.seconds.inWholeMilliseconds
+ fakeSystemClock.setCurrentTimeMillis(currentTime)
+
+ val promotedContentBuilder =
+ PromotedNotificationContentModel.Builder("notif").apply {
+ this.time =
+ PromotedNotificationContentModel.When(
+ time = currentTime,
+ mode = PromotedNotificationContentModel.When.Mode.BasicTime,
+ )
+ }
+
+ setNotifs(
+ listOf(
+ activeNotificationModel(
+ key = "notif",
+ statusBarChipIcon = createStatusBarIconViewOrNull(),
+ promotedContent = promotedContentBuilder.build(),
+ )
+ )
+ )
+
+ assertThat(latest).hasSize(1)
+ assertThat(latest!![0])
+ .isInstanceOf(OngoingActivityChipModel.Active.IconOnly::class.java)
+ }
+
+ @Test
+ @DisableFlags(FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY)
+ fun chips_basicTime_timeInPast_isIconOnly() =
+ kosmos.runTest {
+ val latest by collectLastValue(underTest.chips)
+ val currentTime = 62.minutes.inWholeMilliseconds
+ fakeSystemClock.setCurrentTimeMillis(currentTime)
+
+ val promotedContentBuilder =
+ PromotedNotificationContentModel.Builder("notif").apply {
+ this.time =
+ PromotedNotificationContentModel.When(
+ time = currentTime - 2.minutes.inWholeMilliseconds,
+ mode = PromotedNotificationContentModel.When.Mode.BasicTime,
+ )
+ }
+
setNotifs(
listOf(
activeNotificationModel(
@@ -421,6 +529,45 @@ class NotifChipsViewModelTest : SysuiTestCase() {
assertThat(latest).hasSize(1)
assertThat(latest!![0])
+ .isInstanceOf(OngoingActivityChipModel.Active.IconOnly::class.java)
+ }
+
+ // Not necessarily the behavior we *want* to have, but it's the currently implemented behavior.
+ @Test
+ @DisableFlags(FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY)
+ fun chips_basicTime_timeIsInFuture_thenTimeAdvances_stillShortTimeDelta() =
+ kosmos.runTest {
+ val latest by collectLastValue(underTest.chips)
+ val currentTime = 30.minutes.inWholeMilliseconds
+ fakeSystemClock.setCurrentTimeMillis(currentTime)
+
+ val promotedContentBuilder =
+ PromotedNotificationContentModel.Builder("notif").apply {
+ this.time =
+ PromotedNotificationContentModel.When(
+ time = currentTime + 3.minutes.inWholeMilliseconds,
+ mode = PromotedNotificationContentModel.When.Mode.BasicTime,
+ )
+ }
+
+ setNotifs(
+ listOf(
+ activeNotificationModel(
+ key = "notif",
+ statusBarChipIcon = createStatusBarIconViewOrNull(),
+ promotedContent = promotedContentBuilder.build(),
+ )
+ )
+ )
+
+ assertThat(latest).hasSize(1)
+ assertThat(latest!![0])
+ .isInstanceOf(OngoingActivityChipModel.Active.ShortTimeDelta::class.java)
+
+ fakeSystemClock.advanceTime(5.minutes.inWholeMilliseconds)
+
+ assertThat(latest).hasSize(1)
+ assertThat(latest!![0])
.isInstanceOf(OngoingActivityChipModel.Active.ShortTimeDelta::class.java)
}
@@ -429,12 +576,14 @@ class NotifChipsViewModelTest : SysuiTestCase() {
fun chips_countUpTime_isTimer() =
kosmos.runTest {
val latest by collectLastValue(underTest.chips)
+ val currentTime = 30.minutes.inWholeMilliseconds
+ fakeSystemClock.setCurrentTimeMillis(currentTime)
val promotedContentBuilder =
PromotedNotificationContentModel.Builder("notif").apply {
this.time =
PromotedNotificationContentModel.When(
- time = 6543L,
+ time = currentTime + 10.minutes.inWholeMilliseconds,
mode = PromotedNotificationContentModel.When.Mode.CountUp,
)
}
@@ -457,12 +606,14 @@ class NotifChipsViewModelTest : SysuiTestCase() {
fun chips_countDownTime_isTimer() =
kosmos.runTest {
val latest by collectLastValue(underTest.chips)
+ val currentTime = 30.minutes.inWholeMilliseconds
+ fakeSystemClock.setCurrentTimeMillis(currentTime)
val promotedContentBuilder =
PromotedNotificationContentModel.Builder("notif").apply {
this.time =
PromotedNotificationContentModel.When(
- time = 6543L,
+ time = currentTime + 10.minutes.inWholeMilliseconds,
mode = PromotedNotificationContentModel.When.Mode.CountDown,
)
}
@@ -485,12 +636,14 @@ class NotifChipsViewModelTest : SysuiTestCase() {
fun chips_noHeadsUp_showsTime() =
kosmos.runTest {
val latest by collectLastValue(underTest.chips)
+ val currentTime = 30.minutes.inWholeMilliseconds
+ fakeSystemClock.setCurrentTimeMillis(currentTime)
val promotedContentBuilder =
PromotedNotificationContentModel.Builder("notif").apply {
this.time =
PromotedNotificationContentModel.When(
- time = 6543L,
+ time = currentTime + 10.minutes.inWholeMilliseconds,
mode = PromotedNotificationContentModel.When.Mode.BasicTime,
)
}
@@ -517,12 +670,14 @@ class NotifChipsViewModelTest : SysuiTestCase() {
fun chips_hasHeadsUpBySystem_showsTime() =
kosmos.runTest {
val latest by collectLastValue(underTest.chips)
+ val currentTime = 30.minutes.inWholeMilliseconds
+ fakeSystemClock.setCurrentTimeMillis(currentTime)
val promotedContentBuilder =
PromotedNotificationContentModel.Builder("notif").apply {
this.time =
PromotedNotificationContentModel.When(
- time = 6543L,
+ time = currentTime + 10.minutes.inWholeMilliseconds,
mode = PromotedNotificationContentModel.When.Mode.BasicTime,
)
}
@@ -556,12 +711,14 @@ class NotifChipsViewModelTest : SysuiTestCase() {
fun chips_hasHeadsUpByUser_forOtherNotif_showsTime() =
kosmos.runTest {
val latest by collectLastValue(underTest.chips)
+ val currentTime = 30.minutes.inWholeMilliseconds
+ fakeSystemClock.setCurrentTimeMillis(currentTime)
val promotedContentBuilder =
PromotedNotificationContentModel.Builder("notif").apply {
this.time =
PromotedNotificationContentModel.When(
- time = 6543L,
+ time = currentTime + 10.minutes.inWholeMilliseconds,
mode = PromotedNotificationContentModel.When.Mode.BasicTime,
)
}
@@ -569,7 +726,7 @@ class NotifChipsViewModelTest : SysuiTestCase() {
PromotedNotificationContentModel.Builder("other notif").apply {
this.time =
PromotedNotificationContentModel.When(
- time = 654321L,
+ time = currentTime + 10.minutes.inWholeMilliseconds,
mode = PromotedNotificationContentModel.When.Mode.BasicTime,
)
}
@@ -610,12 +767,14 @@ class NotifChipsViewModelTest : SysuiTestCase() {
fun chips_hasHeadsUpByUser_forThisNotif_onlyShowsIcon() =
kosmos.runTest {
val latest by collectLastValue(underTest.chips)
+ val currentTime = 30.minutes.inWholeMilliseconds
+ fakeSystemClock.setCurrentTimeMillis(currentTime)
val promotedContentBuilder =
PromotedNotificationContentModel.Builder("notif").apply {
this.time =
PromotedNotificationContentModel.When(
- time = 6543L,
+ time = currentTime + 10.minutes.inWholeMilliseconds,
mode = PromotedNotificationContentModel.When.Mode.BasicTime,
)
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/TimeRemainingStateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/TimeRemainingStateTest.kt
new file mode 100644
index 000000000000..5dc59e893715
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/TimeRemainingStateTest.kt
@@ -0,0 +1,237 @@
+/*
+ * 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.viewmodel
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.internal.R.string.duration_hours_medium
+import com.android.internal.R.string.duration_minutes_medium
+import com.android.internal.R.string.now_string_shortest
+import com.android.systemui.SysuiTestCase
+import com.google.common.truth.Truth.assertThat
+import kotlin.time.Duration.Companion.hours
+import kotlin.time.Duration.Companion.minutes
+import kotlin.time.Duration.Companion.seconds
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.cancelAndJoin
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.advanceTimeBy
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class TimeRemainingStateTest : SysuiTestCase() {
+
+ private var fakeTimeSource: MutableTimeSource = MutableTimeSource()
+ // We need a non-zero start time to advance to. This is needed to ensure `TimeRemainingState` is
+ // updated at least once.
+ private val startTime = 1.seconds.inWholeMilliseconds
+
+ @Test
+ fun timeRemainingState_pastTime() = runTest {
+ val state = TimeRemainingState(fakeTimeSource, startTime - 62.seconds.inWholeMilliseconds)
+ val job = launch { state.run() }
+
+ fakeTimeSource.time = startTime
+ advanceTimeBy(startTime)
+ assertThat(state.timeRemainingData).isNull()
+ job.cancelAndJoin()
+ }
+
+ @Test
+ fun timeRemainingState_lessThanOneMinute() = runTest {
+ val state = TimeRemainingState(fakeTimeSource, startTime + 59.seconds.inWholeMilliseconds)
+ val job = launch { state.run() }
+
+ fakeTimeSource.time = startTime
+ advanceTimeBy(startTime)
+ assertThat(state.timeRemainingData!!.first).isEqualTo(now_string_shortest)
+ job.cancelAndJoin()
+ }
+
+ @Test
+ fun timeRemainingState_lessThanOneMinuteInThePast() = runTest {
+ val state = TimeRemainingState(fakeTimeSource, startTime - 59.seconds.inWholeMilliseconds)
+ val job = launch { state.run() }
+
+ fakeTimeSource.time = startTime
+ advanceTimeBy(startTime)
+ assertThat(state.timeRemainingData!!.first).isEqualTo(now_string_shortest)
+ job.cancelAndJoin()
+ }
+
+ @Test
+ fun timeRemainingState_oneMinute() = runTest {
+ val state = TimeRemainingState(fakeTimeSource, startTime + 60.seconds.inWholeMilliseconds)
+ val job = launch { state.run() }
+
+ fakeTimeSource.time = startTime
+ advanceTimeBy(startTime)
+ assertThat(state.timeRemainingData!!.first).isEqualTo(duration_minutes_medium)
+ assertThat(state.timeRemainingData!!.second).isEqualTo(1)
+ job.cancelAndJoin()
+ }
+
+ @Test
+ fun timeRemainingState_lessThanOneHour() = runTest {
+ val state = TimeRemainingState(fakeTimeSource, startTime + 59.minutes.inWholeMilliseconds)
+ val job = launch { state.run() }
+
+ fakeTimeSource.time = startTime
+ advanceTimeBy(startTime)
+ assertThat(state.timeRemainingData!!.first).isEqualTo(duration_minutes_medium)
+ assertThat(state.timeRemainingData!!.second).isEqualTo(59)
+ job.cancelAndJoin()
+ }
+
+ @Test
+ fun timeRemainingState_oneHour() = runTest {
+ val state = TimeRemainingState(fakeTimeSource, startTime + 60.minutes.inWholeMilliseconds)
+ val job = launch { state.run() }
+
+ fakeTimeSource.time = startTime
+ advanceTimeBy(startTime)
+ assertThat(state.timeRemainingData!!.first).isEqualTo(duration_hours_medium)
+ assertThat(state.timeRemainingData!!.second).isEqualTo(1)
+ job.cancelAndJoin()
+ }
+
+ @Test
+ fun timeRemainingState_betweenOneAndTwoHours() = runTest {
+ val state = TimeRemainingState(fakeTimeSource, startTime + 119.minutes.inWholeMilliseconds)
+ val job = launch { state.run() }
+
+ fakeTimeSource.time = startTime
+ advanceTimeBy(startTime)
+
+ assertThat(state.timeRemainingData).isNotNull()
+ assertThat(state.timeRemainingData!!.first).isEqualTo(duration_hours_medium)
+ assertThat(state.timeRemainingData!!.second).isEqualTo(1)
+ job.cancelAndJoin()
+ }
+
+ @Test
+ fun timeRemainingState_betweenFiveAndSixHours() = runTest {
+ val state = TimeRemainingState(fakeTimeSource, startTime + 320.minutes.inWholeMilliseconds)
+ val job = launch { state.run() }
+
+ fakeTimeSource.time = startTime
+ advanceTimeBy(startTime)
+ assertThat(state.timeRemainingData!!.first).isEqualTo(duration_hours_medium)
+ assertThat(state.timeRemainingData!!.second).isEqualTo(5)
+ job.cancelAndJoin()
+ }
+
+ fun timeRemainingState_moreThan24Hours() = runTest {
+ val state =
+ TimeRemainingState(fakeTimeSource, startTime + (25 * 60.minutes.inWholeMilliseconds))
+ val job = launch { state.run() }
+
+ fakeTimeSource.time = startTime
+ advanceTimeBy(startTime)
+ assertThat(state.timeRemainingData).isNull()
+
+ job.cancelAndJoin()
+ }
+
+ @Test
+ fun timeRemainingState_updateFromMinuteToNow() = runTest {
+ fakeTimeSource.time = startTime
+ val state = TimeRemainingState(fakeTimeSource, startTime + 119.seconds.inWholeMilliseconds)
+ val job = launch { state.run() }
+
+ advanceTimeBy(startTime)
+ assertThat(state.timeRemainingData!!.first).isEqualTo(duration_minutes_medium)
+ assertThat(state.timeRemainingData!!.second).isEqualTo(1)
+
+ fakeTimeSource.time += 59.seconds.inWholeMilliseconds
+ advanceTimeBy(59.seconds.inWholeMilliseconds)
+ assertThat(state.timeRemainingData!!.first).isEqualTo(duration_minutes_medium)
+ assertThat(state.timeRemainingData!!.second).isEqualTo(1)
+
+ fakeTimeSource.time += 1.seconds.inWholeMilliseconds
+ advanceTimeBy(1.seconds.inWholeMilliseconds)
+ assertThat(state.timeRemainingData!!.first).isEqualTo(now_string_shortest)
+
+ job.cancelAndJoin()
+ }
+
+ fun timeRemainingState_updateFromNowToEmpty() = runTest {
+ fakeTimeSource.time = startTime
+ val state = TimeRemainingState(fakeTimeSource, startTime)
+ val job = launch { state.run() }
+
+ advanceTimeBy(startTime)
+ assertThat(state.timeRemainingData!!.first).isEqualTo(now_string_shortest)
+
+ fakeTimeSource.time += 62.seconds.inWholeMilliseconds
+ advanceTimeBy(62.seconds.inWholeMilliseconds)
+ assertThat(state.timeRemainingData).isNull()
+
+ job.cancelAndJoin()
+ }
+
+ @Test
+ fun timeRemainingState_updateFromHourToMinutes() = runTest {
+ fakeTimeSource.time = startTime
+ val state = TimeRemainingState(fakeTimeSource, startTime + 119.minutes.inWholeMilliseconds)
+ val job = launch { state.run() }
+
+ advanceTimeBy(startTime)
+ assertThat(state.timeRemainingData!!.first).isEqualTo(duration_hours_medium)
+ assertThat(state.timeRemainingData!!.second).isEqualTo(1)
+
+ fakeTimeSource.time += 59.minutes.inWholeMilliseconds
+ advanceTimeBy(59.minutes.inWholeMilliseconds)
+ assertThat(state.timeRemainingData!!.first).isEqualTo(duration_hours_medium)
+ assertThat(state.timeRemainingData!!.second).isEqualTo(1)
+
+ fakeTimeSource.time += 1.seconds.inWholeMilliseconds
+ advanceTimeBy(1.seconds.inWholeMilliseconds)
+ assertThat(state.timeRemainingData!!.first).isEqualTo(duration_minutes_medium)
+ assertThat(state.timeRemainingData!!.second).isEqualTo(59)
+
+ job.cancelAndJoin()
+ }
+
+ @Test
+ fun timeRemainingState_showAfterLessThan24Hours() = runTest {
+ fakeTimeSource.time = startTime
+ val state = TimeRemainingState(fakeTimeSource, startTime + 25.hours.inWholeMilliseconds)
+ val job = launch { state.run() }
+
+ advanceTimeBy(startTime)
+ assertThat(state.timeRemainingData).isNull()
+
+ fakeTimeSource.time += 1.hours.inWholeMilliseconds + 1.seconds.inWholeMilliseconds
+ advanceTimeBy(1.hours.inWholeMilliseconds + 1.seconds.inWholeMilliseconds)
+ assertThat(state.timeRemainingData!!.first).isEqualTo(duration_hours_medium)
+ assertThat(state.timeRemainingData!!.second).isEqualTo(23)
+
+ job.cancelAndJoin()
+ }
+
+ /** A fake implementation of [TimeSource] that allows the caller to set the current time */
+ private class MutableTimeSource(var time: Long = 0L) : TimeSource {
+ override fun getCurrentTime(): Long {
+ return time
+ }
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/featurepods/media/domain/interactor/MediaControlChipInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/featurepods/media/domain/interactor/MediaControlChipInteractorTest.kt
index 1a5f57dd43f8..6409a20d5156 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/featurepods/media/domain/interactor/MediaControlChipInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/featurepods/media/domain/interactor/MediaControlChipInteractorTest.kt
@@ -17,100 +17,119 @@
package com.android.systemui.statusbar.featurepods.media.domain.interactor
import android.graphics.drawable.Drawable
-import androidx.test.ext.junit.runners.AndroidJUnit4
+import android.platform.test.flag.junit.FlagsParameterization
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
+import com.android.systemui.flags.parameterizeSceneContainerFlag
+import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.collectLastValue
import com.android.systemui.kosmos.runTest
import com.android.systemui.kosmos.useUnconfinedTestDispatcher
import com.android.systemui.media.controls.data.repository.mediaFilterRepository
+import com.android.systemui.media.controls.domain.pipeline.MediaDataManager
import com.android.systemui.media.controls.shared.model.MediaAction
import com.android.systemui.media.controls.shared.model.MediaButton
import com.android.systemui.media.controls.shared.model.MediaData
import com.android.systemui.media.controls.shared.model.MediaDataLoadingModel
+import com.android.systemui.scene.shared.flag.SceneContainerFlag
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.ArgumentCaptor
+import org.mockito.Captor
+import org.mockito.MockitoAnnotations
import org.mockito.kotlin.mock
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4
+import platform.test.runner.parameterized.Parameters
+@RunWith(ParameterizedAndroidJunit4::class)
@SmallTest
-@RunWith(AndroidJUnit4::class)
-class MediaControlChipInteractorTest : SysuiTestCase() {
-
+class MediaControlChipInteractorTest(flags: FlagsParameterization) : SysuiTestCase() {
private val kosmos = testKosmos().useUnconfinedTestDispatcher()
- private val underTest = kosmos.mediaControlChipInteractor
+ private val mediaFilterRepository = kosmos.mediaFilterRepository
+ private val Kosmos.underTest by Kosmos.Fixture { kosmos.mediaControlChipInteractor }
+ @Captor lateinit var listener: ArgumentCaptor<MediaDataManager.Listener>
+
+ companion object {
+ @JvmStatic
+ @Parameters(name = "{0}")
+ fun getParams(): List<FlagsParameterization> {
+ return parameterizeSceneContainerFlag()
+ }
+ }
+
+ @Before
+ fun setUp() {
+ kosmos.underTest.initialize()
+ MockitoAnnotations.initMocks(this)
+ }
+
+ init {
+ mSetFlagsRule.setFlagsParameterization(flags)
+ }
@Test
- fun mediaControlModel_noActiveMedia_null() =
+ fun mediaControlChipModel_noActiveMedia_null() =
kosmos.runTest {
- val model by collectLastValue(underTest.mediaControlModel)
+ val model by collectLastValue(underTest.mediaControlChipModel)
assertThat(model).isNull()
}
@Test
- fun mediaControlModel_activeMedia_notNull() =
+ fun mediaControlChipModel_activeMedia_notNull() =
kosmos.runTest {
- val model by collectLastValue(underTest.mediaControlModel)
+ val model by collectLastValue(underTest.mediaControlChipModel)
val userMedia = MediaData(active = true)
- val instanceId = userMedia.instanceId
- mediaFilterRepository.addSelectedUserMediaEntry(userMedia)
- mediaFilterRepository.addMediaDataLoadingState(MediaDataLoadingModel.Loaded(instanceId))
+ updateMedia(userMedia)
assertThat(model).isNotNull()
}
@Test
- fun mediaControlModel_mediaRemoved_null() =
+ fun mediaControlChipModel_mediaRemoved_null() =
kosmos.runTest {
- val model by collectLastValue(underTest.mediaControlModel)
+ val model by collectLastValue(underTest.mediaControlChipModel)
val userMedia = MediaData(active = true)
- val instanceId = userMedia.instanceId
- mediaFilterRepository.addSelectedUserMediaEntry(userMedia)
- mediaFilterRepository.addMediaDataLoadingState(MediaDataLoadingModel.Loaded(instanceId))
+ updateMedia(userMedia)
assertThat(model).isNotNull()
- assertThat(mediaFilterRepository.removeSelectedUserMediaEntry(instanceId, userMedia))
- .isTrue()
- mediaFilterRepository.addMediaDataLoadingState(
- MediaDataLoadingModel.Removed(instanceId)
- )
+ removeMedia(userMedia)
assertThat(model).isNull()
}
@Test
- fun mediaControlModel_songNameChanged_emitsUpdatedModel() =
+ fun mediaControlChipModel_songNameChanged_emitsUpdatedModel() =
kosmos.runTest {
- val model by collectLastValue(underTest.mediaControlModel)
+ val model by collectLastValue(underTest.mediaControlChipModel)
val initialSongName = "Initial Song"
val newSongName = "New Song"
val userMedia = MediaData(active = true, song = initialSongName)
- val instanceId = userMedia.instanceId
- mediaFilterRepository.addSelectedUserMediaEntry(userMedia)
- mediaFilterRepository.addMediaDataLoadingState(MediaDataLoadingModel.Loaded(instanceId))
+ updateMedia(userMedia)
assertThat(model).isNotNull()
assertThat(model?.songName).isEqualTo(initialSongName)
val updatedUserMedia = userMedia.copy(song = newSongName)
- mediaFilterRepository.addSelectedUserMediaEntry(updatedUserMedia)
+ updateMedia(updatedUserMedia)
assertThat(model?.songName).isEqualTo(newSongName)
}
@Test
- fun mediaControlModel_playPauseActionChanges_emitsUpdatedModel() =
+ fun mediaControlChipModel_playPauseActionChanges_emitsUpdatedModel() =
kosmos.runTest {
- val model by collectLastValue(underTest.mediaControlModel)
+ val model by collectLastValue(underTest.mediaControlChipModel)
val mockDrawable = mock<Drawable>()
@@ -123,9 +142,7 @@ class MediaControlChipInteractorTest : SysuiTestCase() {
)
val mediaButton = MediaButton(playOrPause = initialAction)
val userMedia = MediaData(active = true, semanticActions = mediaButton)
- val instanceId = userMedia.instanceId
- mediaFilterRepository.addSelectedUserMediaEntry(userMedia)
- mediaFilterRepository.addMediaDataLoadingState(MediaDataLoadingModel.Loaded(instanceId))
+ updateMedia(userMedia)
assertThat(model).isNotNull()
assertThat(model?.playOrPause).isEqualTo(initialAction)
@@ -139,15 +156,15 @@ class MediaControlChipInteractorTest : SysuiTestCase() {
)
val updatedMediaButton = MediaButton(playOrPause = newAction)
val updatedUserMedia = userMedia.copy(semanticActions = updatedMediaButton)
- mediaFilterRepository.addSelectedUserMediaEntry(updatedUserMedia)
+ updateMedia(updatedUserMedia)
assertThat(model?.playOrPause).isEqualTo(newAction)
}
@Test
- fun mediaControlModel_playPauseActionRemoved_playPauseNull() =
+ fun mediaControlChipModel_playPauseActionRemoved_playPauseNull() =
kosmos.runTest {
- val model by collectLastValue(underTest.mediaControlModel)
+ val model by collectLastValue(underTest.mediaControlChipModel)
val mockDrawable = mock<Drawable>()
@@ -160,16 +177,36 @@ class MediaControlChipInteractorTest : SysuiTestCase() {
)
val mediaButton = MediaButton(playOrPause = initialAction)
val userMedia = MediaData(active = true, semanticActions = mediaButton)
- val instanceId = userMedia.instanceId
- mediaFilterRepository.addSelectedUserMediaEntry(userMedia)
- mediaFilterRepository.addMediaDataLoadingState(MediaDataLoadingModel.Loaded(instanceId))
+ updateMedia(userMedia)
assertThat(model).isNotNull()
assertThat(model?.playOrPause).isEqualTo(initialAction)
val updatedUserMedia = userMedia.copy(semanticActions = MediaButton())
- mediaFilterRepository.addSelectedUserMediaEntry(updatedUserMedia)
+ updateMedia(updatedUserMedia)
assertThat(model?.playOrPause).isNull()
}
+
+ private fun updateMedia(mediaData: MediaData) {
+ if (SceneContainerFlag.isEnabled) {
+ val instanceId = mediaData.instanceId
+ mediaFilterRepository.addSelectedUserMediaEntry(mediaData)
+ mediaFilterRepository.addMediaDataLoadingState(MediaDataLoadingModel.Loaded(instanceId))
+ } else {
+ kosmos.underTest.updateMediaControlChipModelLegacy(mediaData)
+ }
+ }
+
+ private fun removeMedia(mediaData: MediaData) {
+ if (SceneContainerFlag.isEnabled) {
+ val instanceId = mediaData.instanceId
+ mediaFilterRepository.removeSelectedUserMediaEntry(instanceId, mediaData)
+ mediaFilterRepository.addMediaDataLoadingState(
+ MediaDataLoadingModel.Removed(instanceId)
+ )
+ } else {
+ kosmos.underTest.updateMediaControlChipModelLegacy(null)
+ }
+ }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/featurepods/media/ui/viewmodel/MediaControlChipViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/featurepods/media/ui/viewmodel/MediaControlChipViewModelTest.kt
index 8650e4b8cfce..d36dbbe8d36f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/featurepods/media/ui/viewmodel/MediaControlChipViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/featurepods/media/ui/viewmodel/MediaControlChipViewModelTest.kt
@@ -16,26 +16,57 @@
package com.android.systemui.statusbar.featurepods.media.ui.viewmodel
-import androidx.test.ext.junit.runners.AndroidJUnit4
+import android.platform.test.flag.junit.FlagsParameterization
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
+import com.android.systemui.flags.parameterizeSceneContainerFlag
+import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.collectLastValue
import com.android.systemui.kosmos.runTest
import com.android.systemui.kosmos.useUnconfinedTestDispatcher
import com.android.systemui.media.controls.data.repository.mediaFilterRepository
+import com.android.systemui.media.controls.domain.pipeline.MediaDataManager
import com.android.systemui.media.controls.shared.model.MediaData
import com.android.systemui.media.controls.shared.model.MediaDataLoadingModel
+import com.android.systemui.scene.shared.flag.SceneContainerFlag
+import com.android.systemui.statusbar.featurepods.media.domain.interactor.mediaControlChipInteractor
import com.android.systemui.statusbar.featurepods.popups.shared.model.PopupChipModel
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
import kotlin.test.Test
+import org.junit.Before
import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.Captor
+import org.mockito.MockitoAnnotations
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4
+import platform.test.runner.parameterized.Parameters
@SmallTest
-@RunWith(AndroidJUnit4::class)
-class MediaControlChipViewModelTest : SysuiTestCase() {
+@RunWith(ParameterizedAndroidJunit4::class)
+class MediaControlChipViewModelTest(flags: FlagsParameterization) : SysuiTestCase() {
private val kosmos = testKosmos().useUnconfinedTestDispatcher()
- private val underTest = kosmos.mediaControlChipViewModel
+ private val mediaControlChipInteractor by lazy { kosmos.mediaControlChipInteractor }
+ private val Kosmos.underTest by Kosmos.Fixture { kosmos.mediaControlChipViewModel }
+ @Captor lateinit var listener: ArgumentCaptor<MediaDataManager.Listener>
+
+ companion object {
+ @JvmStatic
+ @Parameters(name = "{0}")
+ fun getParams(): List<FlagsParameterization> {
+ return parameterizeSceneContainerFlag()
+ }
+ }
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+ mediaControlChipInteractor.initialize()
+ }
+
+ init {
+ mSetFlagsRule.setFlagsParameterization(flags)
+ }
@Test
fun chip_noActiveMedia_IsHidden() =
@@ -51,10 +82,7 @@ class MediaControlChipViewModelTest : SysuiTestCase() {
val chip by collectLastValue(underTest.chip)
val userMedia = MediaData(active = true, song = "test")
- val instanceId = userMedia.instanceId
-
- mediaFilterRepository.addSelectedUserMediaEntry(userMedia)
- mediaFilterRepository.addMediaDataLoadingState(MediaDataLoadingModel.Loaded(instanceId))
+ updateMedia(userMedia)
assertThat(chip).isInstanceOf(PopupChipModel.Shown::class.java)
}
@@ -67,16 +95,25 @@ class MediaControlChipViewModelTest : SysuiTestCase() {
val initialSongName = "Initial Song"
val newSongName = "New Song"
val userMedia = MediaData(active = true, song = initialSongName)
- val instanceId = userMedia.instanceId
-
- mediaFilterRepository.addSelectedUserMediaEntry(userMedia)
- mediaFilterRepository.addMediaDataLoadingState(MediaDataLoadingModel.Loaded(instanceId))
-
+ updateMedia(userMedia)
+ assertThat(chip).isInstanceOf(PopupChipModel.Shown::class.java)
assertThat((chip as PopupChipModel.Shown).chipText).isEqualTo(initialSongName)
val updatedUserMedia = userMedia.copy(song = newSongName)
- mediaFilterRepository.addSelectedUserMediaEntry(updatedUserMedia)
+ updateMedia(updatedUserMedia)
assertThat((chip as PopupChipModel.Shown).chipText).isEqualTo(newSongName)
}
+
+ private fun updateMedia(mediaData: MediaData) {
+ if (SceneContainerFlag.isEnabled) {
+ val instanceId = mediaData.instanceId
+ kosmos.mediaFilterRepository.addSelectedUserMediaEntry(mediaData)
+ kosmos.mediaFilterRepository.addMediaDataLoadingState(
+ MediaDataLoadingModel.Loaded(instanceId)
+ )
+ } else {
+ mediaControlChipInteractor.updateMediaControlChipModelLegacy(mediaData)
+ }
+ }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/AssistantFeedbackControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/AssistantFeedbackControllerTest.java
index d66b010daefd..a58f7f72f08a 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/AssistantFeedbackControllerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/AssistantFeedbackControllerTest.java
@@ -51,8 +51,10 @@ import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder;
+import com.android.systemui.statusbar.notification.people.NotificationPersonExtractor;
import com.android.systemui.util.DeviceConfigProxyFake;
+import org.jetbrains.annotations.NotNull;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/DynamicChildBindControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/DynamicChildBindControllerTest.java
index 77fd06757595..8520508c7611 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/DynamicChildBindControllerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/DynamicChildBindControllerTest.java
@@ -132,7 +132,7 @@ public class DynamicChildBindControllerTest extends SysuiTestCase {
LayoutInflater inflater = LayoutInflater.from(mContext);
inflater.setFactory2(
new RowInflaterTask.RowAsyncLayoutInflater(entry, new FakeSystemClock(), mock(
- RowInflaterTaskLogger.class)));
+ RowInflaterTaskLogger.class), mContext.getUser()));
ExpandableNotificationRow row = (ExpandableNotificationRow)
inflater.inflate(R.layout.status_bar_notification_row, null);
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/BundleEntryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/BundleEntryTest.kt
new file mode 100644
index 000000000000..426af264da07
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/BundleEntryTest.kt
@@ -0,0 +1,75 @@
+/*
+ * 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.collection
+
+import android.platform.test.annotations.EnableFlags
+import android.platform.test.flag.junit.SetFlagsRule
+import android.testing.TestableLooper.RunWithLooper
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.notification.shared.NotificationBundleUi
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@RunWithLooper
+class BundleEntryTest : SysuiTestCase() {
+ private lateinit var underTest: BundleEntry
+
+ @get:Rule
+ val setFlagsRule = SetFlagsRule()
+
+ @Before
+ fun setUp() {
+ underTest = BundleEntry("key")
+ }
+
+ @Test
+ @EnableFlags(NotificationBundleUi.FLAG_NAME)
+ fun getParent_adapter() {
+ assertThat(underTest.entryAdapter.parent).isEqualTo(GroupEntry.ROOT_ENTRY)
+ }
+
+ @Test
+ @EnableFlags(NotificationBundleUi.FLAG_NAME)
+ fun isTopLevelEntry_adapter() {
+ assertThat(underTest.entryAdapter.isTopLevelEntry).isTrue()
+ }
+
+ @Test
+ @EnableFlags(NotificationBundleUi.FLAG_NAME)
+ fun getRow_adapter() {
+ assertThat(underTest.entryAdapter.row).isNull()
+ }
+
+ @Test
+ @EnableFlags(NotificationBundleUi.FLAG_NAME)
+ fun getGroupRoot_adapter() {
+ assertThat(underTest.entryAdapter.groupRoot).isEqualTo(underTest.entryAdapter)
+ }
+
+ @Test
+ @EnableFlags(NotificationBundleUi.FLAG_NAME)
+ fun getKey_adapter() {
+ assertThat(underTest.entryAdapter.key).isEqualTo("key")
+ }
+} \ No newline at end of file
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/HighPriorityProviderTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/HighPriorityProviderTest.java
index 8e95ac599ce1..76e2d619a4df 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/HighPriorityProviderTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/HighPriorityProviderTest.java
@@ -30,6 +30,9 @@ import static org.mockito.Mockito.when;
import android.app.Notification;
import android.app.NotificationChannel;
+import android.platform.test.annotations.DisableFlags;
+import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.SetFlagsRule;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
@@ -39,8 +42,10 @@ import com.android.systemui.statusbar.RankingBuilder;
import com.android.systemui.statusbar.notification.collection.provider.HighPriorityProvider;
import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager;
import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier;
+import com.android.systemui.statusbar.notification.shared.NotificationBundleUi;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
@@ -57,6 +62,9 @@ public class HighPriorityProviderTest extends SysuiTestCase {
@Mock private GroupMembershipManager mGroupMembershipManager;
private HighPriorityProvider mHighPriorityProvider;
+ @Rule
+ public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
@@ -210,6 +218,7 @@ public class HighPriorityProviderTest extends SysuiTestCase {
}
@Test
+ @DisableFlags(NotificationBundleUi.FLAG_NAME)
public void testIsHighPriority_checkChildrenToCalculatePriority_legacy() {
// GIVEN: a summary with low priority has a highPriorityChild and a lowPriorityChild
final NotificationEntry summary = createNotifEntry(false);
@@ -247,20 +256,18 @@ public class HighPriorityProviderTest extends SysuiTestCase {
}
@Test
- public void testIsHighPriority_checkChildrenToCalculatePriority() {
+ public void testIsHighPriority_checkChildrenViewsToCalculatePriority() {
// GIVEN:
// parent with summary = lowPrioritySummary
// NotificationEntry = lowPriorityChild
// NotificationEntry = highPriorityChild
+ List<NotificationEntry> children = List.of(createNotifEntry(false), createNotifEntry(true));
final NotificationEntry lowPrioritySummary = createNotifEntry(false);
final GroupEntry parentEntry = new GroupEntryBuilder()
.setSummary(lowPrioritySummary)
+ .setChildren(children)
.build();
- when(mGroupMembershipManager.getChildren(parentEntry)).thenReturn(
- new ArrayList<>(
- List.of(
- createNotifEntry(false),
- createNotifEntry(true))));
+ when(mGroupMembershipManager.getChildren(parentEntry)).thenReturn(children);
// THEN the GroupEntry parentEntry is high priority since it has a high priority child
assertTrue(mHighPriorityProvider.isHighPriority(parentEntry));
@@ -272,10 +279,11 @@ public class HighPriorityProviderTest extends SysuiTestCase {
// parent with summary = lowPrioritySummary
// NotificationEntry = lowPriorityChild
final NotificationEntry lowPrioritySummary = createNotifEntry(false);
+ final NotificationEntry lowPriorityChild = createNotifEntry(false);
final GroupEntry parentEntry = new GroupEntryBuilder()
.setSummary(lowPrioritySummary)
+ .setChildren(List.of(lowPriorityChild))
.build();
- final NotificationEntry lowPriorityChild = createNotifEntry(false);
when(mGroupMembershipManager.getChildren(parentEntry)).thenReturn(
new ArrayList<>(List.of(lowPriorityChild)));
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/ColorizedFgsCoordinatorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/ColorizedFgsCoordinatorTest.kt
index e93c74252251..7fa157fa7cb3 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/ColorizedFgsCoordinatorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/ColorizedFgsCoordinatorTest.kt
@@ -27,14 +27,29 @@ import android.testing.TestableLooper.RunWithLooper
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.kosmos.collectLastValue
+import com.android.systemui.kosmos.runTest
import com.android.systemui.kosmos.useUnconfinedTestDispatcher
+import com.android.systemui.statusbar.chips.notification.domain.interactor.statusBarNotificationChipsInteractor
+import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips
+import com.android.systemui.statusbar.core.StatusBarRootModernization
+import com.android.systemui.statusbar.notification.buildNotificationEntry
+import com.android.systemui.statusbar.notification.buildOngoingCallEntry
+import com.android.systemui.statusbar.notification.buildPromotedOngoingEntry
import com.android.systemui.statusbar.notification.collection.buildEntry
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifPromoter
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner
import com.android.systemui.statusbar.notification.collection.notifPipeline
+import com.android.systemui.statusbar.notification.domain.interactor.renderNotificationListInteractor
import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUi
+import com.android.systemui.statusbar.notification.promoted.domain.interactor.promotedNotificationsInteractor
+import com.android.systemui.statusbar.phone.ongoingcall.StatusBarChipsModernization
import com.android.systemui.testKosmos
import com.android.systemui.util.mockito.withArgCaptor
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
@@ -59,7 +74,13 @@ class ColorizedFgsCoordinatorTest : SysuiTestCase() {
fun setup() {
allowTestableLooperAsMainThread()
- colorizedFgsCoordinator = ColorizedFgsCoordinator()
+ kosmos.statusBarNotificationChipsInteractor.start()
+
+ colorizedFgsCoordinator =
+ ColorizedFgsCoordinator(
+ kosmos.applicationCoroutineScope,
+ kosmos.promotedNotificationsInteractor,
+ )
colorizedFgsCoordinator.attach(notifPipeline)
sectioner = colorizedFgsCoordinator.sectioner
}
@@ -178,6 +199,37 @@ class ColorizedFgsCoordinatorTest : SysuiTestCase() {
verify(notifPipeline, never()).addPromoter(any())
}
+ @Test
+ @EnableFlags(
+ PromotedNotificationUi.FLAG_NAME,
+ StatusBarNotifChips.FLAG_NAME,
+ StatusBarChipsModernization.FLAG_NAME,
+ StatusBarRootModernization.FLAG_NAME,
+ )
+ fun comparatorPutsCallBeforeOther() =
+ kosmos.runTest {
+ // GIVEN a call and a promoted ongoing notification
+ val callEntry = buildOngoingCallEntry(promoted = false)
+ val ronEntry = buildPromotedOngoingEntry()
+ val otherEntry = buildNotificationEntry(tag = "other")
+
+ kosmos.renderNotificationListInteractor.setRenderedList(
+ listOf(callEntry, ronEntry, otherEntry)
+ )
+
+ val orderedChipNotificationKeys by
+ collectLastValue(kosmos.promotedNotificationsInteractor.orderedChipNotificationKeys)
+
+ // THEN the order of the notification keys should be the call then the RON
+ assertThat(orderedChipNotificationKeys)
+ .containsExactly("0|test_pkg|0|call|0", "0|test_pkg|0|ron|0")
+
+ // VERIFY that the comparator puts the call before the ron
+ assertThat(sectioner.comparator!!.compare(callEntry, ronEntry)).isLessThan(0)
+ // VERIFY that the comparator puts the ron before the other
+ assertThat(sectioner.comparator!!.compare(ronEntry, otherEntry)).isLessThan(0)
+ }
+
private fun makeCallStyle(): Notification.CallStyle {
val pendingIntent =
PendingIntent.getBroadcast(mContext, 0, Intent("action"), PendingIntent.FLAG_IMMUTABLE)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerTest.kt
index db5921d8bd36..3dd0982ba2ff 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerTest.kt
@@ -17,23 +17,29 @@
package com.android.systemui.statusbar.notification.collection.render
import android.os.Build
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
+import android.platform.test.flag.junit.SetFlagsRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.dump.DumpManager
import com.android.systemui.log.assertLogsWtf
+import com.android.systemui.statusbar.notification.collection.GroupEntry
import com.android.systemui.statusbar.notification.collection.GroupEntryBuilder
import com.android.systemui.statusbar.notification.collection.ListEntry
import com.android.systemui.statusbar.notification.collection.NotifPipeline
import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeRenderListListener
import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManager.OnGroupExpansionChangeListener
+import com.android.systemui.statusbar.notification.shared.NotificationBundleUi
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.withArgCaptor
import com.google.common.truth.Truth.assertThat
import org.junit.Assume
import org.junit.Before
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.never
@@ -44,6 +50,9 @@ import org.mockito.Mockito.`when` as whenever
@SmallTest
@RunWith(AndroidJUnit4::class)
class GroupExpansionManagerTest : SysuiTestCase() {
+ @get:Rule
+ val setFlagsRule = SetFlagsRule()
+
private lateinit var underTest: GroupExpansionManagerImpl
private val dumpManager: DumpManager = mock()
@@ -52,8 +61,8 @@ class GroupExpansionManagerTest : SysuiTestCase() {
private val pipeline: NotifPipeline = mock()
private lateinit var beforeRenderListListener: OnBeforeRenderListListener
- private val summary1 = notificationEntry("foo", 1)
- private val summary2 = notificationEntry("bar", 1)
+ private val summary1 = notificationSummaryEntry("foo", 1)
+ private val summary2 = notificationSummaryEntry("bar", 1)
private val entries =
listOf<ListEntry>(
GroupEntryBuilder()
@@ -82,15 +91,25 @@ class GroupExpansionManagerTest : SysuiTestCase() {
private fun notificationEntry(pkg: String, id: Int) =
NotificationEntryBuilder().setPkg(pkg).setId(id).build().apply { row = mock() }
+ private fun notificationSummaryEntry(pkg: String, id: Int) =
+ NotificationEntryBuilder().setPkg(pkg).setId(id).setParent(GroupEntry.ROOT_ENTRY).build()
+ .apply { row = mock() }
+
@Before
fun setUp() {
whenever(groupMembershipManager.getGroupSummary(summary1)).thenReturn(summary1)
whenever(groupMembershipManager.getGroupSummary(summary2)).thenReturn(summary2)
+ whenever(groupMembershipManager.getGroupRoot(summary1.entryAdapter))
+ .thenReturn(summary1.entryAdapter)
+ whenever(groupMembershipManager.getGroupRoot(summary2.entryAdapter))
+ .thenReturn(summary2.entryAdapter)
+
underTest = GroupExpansionManagerImpl(dumpManager, groupMembershipManager)
}
@Test
+ @DisableFlags(NotificationBundleUi.FLAG_NAME)
fun notifyOnlyOnChange() {
var listenerCalledCount = 0
underTest.registerGroupExpansionChangeListener { _, _ -> listenerCalledCount++ }
@@ -108,6 +127,25 @@ class GroupExpansionManagerTest : SysuiTestCase() {
}
@Test
+ @EnableFlags(NotificationBundleUi.FLAG_NAME)
+ fun notifyOnlyOnChange_withEntryAdapter() {
+ var listenerCalledCount = 0
+ underTest.registerGroupExpansionChangeListener { _, _ -> listenerCalledCount++ }
+
+ underTest.setGroupExpanded(summary1.entryAdapter, false)
+ assertThat(listenerCalledCount).isEqualTo(0)
+ underTest.setGroupExpanded(summary1.entryAdapter, true)
+ assertThat(listenerCalledCount).isEqualTo(1)
+ underTest.setGroupExpanded(summary2.entryAdapter, true)
+ assertThat(listenerCalledCount).isEqualTo(2)
+ underTest.setGroupExpanded(summary1.entryAdapter, true)
+ assertThat(listenerCalledCount).isEqualTo(2)
+ underTest.setGroupExpanded(summary2.entryAdapter, false)
+ assertThat(listenerCalledCount).isEqualTo(3)
+ }
+
+ @Test
+ @DisableFlags(NotificationBundleUi.FLAG_NAME)
fun expandUnattachedEntry() {
// First, expand the entry when it is attached.
underTest.setGroupExpanded(summary1, true)
@@ -122,6 +160,22 @@ class GroupExpansionManagerTest : SysuiTestCase() {
}
@Test
+ @EnableFlags(NotificationBundleUi.FLAG_NAME)
+ fun expandUnattachedEntryAdapter() {
+ // First, expand the entry when it is attached.
+ underTest.setGroupExpanded(summary1.entryAdapter, true)
+ assertThat(underTest.isGroupExpanded(summary1.entryAdapter)).isTrue()
+
+ // Un-attach it, and un-expand it.
+ NotificationEntryBuilder.setNewParent(summary1, null)
+ underTest.setGroupExpanded(summary1.entryAdapter, false)
+
+ // Expanding again should throw.
+ assertLogsWtf { underTest.setGroupExpanded(summary1.entryAdapter, true) }
+ }
+
+ @Test
+ @DisableFlags(NotificationBundleUi.FLAG_NAME)
fun syncWithPipeline() {
underTest.attach(pipeline)
beforeRenderListListener = withArgCaptor {
@@ -143,4 +197,28 @@ class GroupExpansionManagerTest : SysuiTestCase() {
verify(listener).onGroupExpansionChange(summary1.row, false)
verifyNoMoreInteractions(listener)
}
+
+ @Test
+ @EnableFlags(NotificationBundleUi.FLAG_NAME)
+ fun syncWithPipeline_withEntryAdapter() {
+ underTest.attach(pipeline)
+ beforeRenderListListener = withArgCaptor {
+ verify(pipeline).addOnBeforeRenderListListener(capture())
+ }
+
+ val listener: OnGroupExpansionChangeListener = mock()
+ underTest.registerGroupExpansionChangeListener(listener)
+
+ beforeRenderListListener.onBeforeRenderList(entries)
+ verify(listener, never()).onGroupExpansionChange(any(), any())
+
+ // Expand one of the groups.
+ underTest.setGroupExpanded(summary1.entryAdapter, true)
+ verify(listener).onGroupExpansionChange(summary1.row, true)
+
+ // Empty the pipeline list and verify that the group is no longer expanded.
+ beforeRenderListListener.onBeforeRenderList(emptyList())
+ verify(listener).onGroupExpansionChange(summary1.row, false)
+ verifyNoMoreInteractions(listener)
+ }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManagerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManagerTest.kt
index 2cbcc5a8d925..dcbf44e6e301 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManagerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManagerTest.kt
@@ -16,34 +16,46 @@
package com.android.systemui.statusbar.notification.collection.render
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
+import android.platform.test.flag.junit.SetFlagsRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.statusbar.notification.collection.GroupEntry
import com.android.systemui.statusbar.notification.collection.GroupEntryBuilder
import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
+import com.android.systemui.statusbar.notification.shared.NotificationBundleUi
import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@SmallTest
@RunWith(AndroidJUnit4::class)
class GroupMembershipManagerTest : SysuiTestCase() {
+
+ @get:Rule
+ val setFlagsRule = SetFlagsRule()
+
private var underTest = GroupMembershipManagerImpl()
@Test
+ @DisableFlags(NotificationBundleUi.FLAG_NAME)
fun isChildInGroup_topLevel() {
val topLevelEntry = NotificationEntryBuilder().setParent(GroupEntry.ROOT_ENTRY).build()
assertThat(underTest.isChildInGroup(topLevelEntry)).isFalse()
}
@Test
+ @DisableFlags(NotificationBundleUi.FLAG_NAME)
fun isChildInGroup_noParent() {
val noParentEntry = NotificationEntryBuilder().setParent(null).build()
assertThat(underTest.isChildInGroup(noParentEntry)).isFalse()
}
@Test
+ @DisableFlags(NotificationBundleUi.FLAG_NAME)
fun isChildInGroup_summary() {
val groupKey = "group"
val summary =
@@ -57,12 +69,14 @@ class GroupMembershipManagerTest : SysuiTestCase() {
}
@Test
+ @DisableFlags(NotificationBundleUi.FLAG_NAME)
fun isGroupSummary_topLevelEntry() {
val entry = NotificationEntryBuilder().setParent(GroupEntry.ROOT_ENTRY).build()
assertThat(underTest.isGroupSummary(entry)).isFalse()
}
@Test
+ @DisableFlags(NotificationBundleUi.FLAG_NAME)
fun isGroupSummary_summary() {
val groupKey = "group"
val summary =
@@ -76,6 +90,7 @@ class GroupMembershipManagerTest : SysuiTestCase() {
}
@Test
+ @DisableFlags(NotificationBundleUi.FLAG_NAME)
fun isGroupSummary_child() {
val groupKey = "group"
val summary =
@@ -90,12 +105,14 @@ class GroupMembershipManagerTest : SysuiTestCase() {
}
@Test
+ @DisableFlags(NotificationBundleUi.FLAG_NAME)
fun getGroupSummary_topLevelEntry() {
val entry = NotificationEntryBuilder().setParent(GroupEntry.ROOT_ENTRY).build()
assertThat(underTest.getGroupSummary(entry)).isNull()
}
@Test
+ @DisableFlags(NotificationBundleUi.FLAG_NAME)
fun getGroupSummary_summary() {
val groupKey = "group"
val summary =
@@ -109,6 +126,7 @@ class GroupMembershipManagerTest : SysuiTestCase() {
}
@Test
+ @DisableFlags(NotificationBundleUi.FLAG_NAME)
fun getGroupSummary_child() {
val groupKey = "group"
val summary =
@@ -121,4 +139,104 @@ class GroupMembershipManagerTest : SysuiTestCase() {
assertThat(underTest.getGroupSummary(entry)).isEqualTo(summary)
}
+
+ @Test
+ @EnableFlags(NotificationBundleUi.FLAG_NAME)
+ fun isChildEntryAdapterInGroup_topLevel() {
+ val topLevelEntry = NotificationEntryBuilder().setParent(GroupEntry.ROOT_ENTRY).build()
+ assertThat(underTest.isChildInGroup(topLevelEntry.entryAdapter)).isFalse()
+ }
+
+ @Test
+ @EnableFlags(NotificationBundleUi.FLAG_NAME)
+ fun isChildEntryAdapterInGroup_noParent() {
+ val noParentEntry = NotificationEntryBuilder().setParent(null).build()
+ assertThat(underTest.isChildInGroup(noParentEntry.entryAdapter)).isFalse()
+ }
+
+ @Test
+ @EnableFlags(NotificationBundleUi.FLAG_NAME)
+ fun isChildEntryAdapterInGroup_summary() {
+ val groupKey = "group"
+ val summary =
+ NotificationEntryBuilder()
+ .setGroup(mContext, groupKey)
+ .setGroupSummary(mContext, true)
+ .build()
+ GroupEntryBuilder().setKey(groupKey).setSummary(summary).build()
+
+ assertThat(underTest.isChildInGroup(summary.entryAdapter)).isFalse()
+ }
+
+ @Test
+ @EnableFlags(NotificationBundleUi.FLAG_NAME)
+ fun isGroupRoot_topLevelEntry() {
+ val entry = NotificationEntryBuilder().setParent(GroupEntry.ROOT_ENTRY).build()
+ assertThat(underTest.isGroupRoot(entry.entryAdapter)).isFalse()
+ }
+
+ @Test
+ @EnableFlags(NotificationBundleUi.FLAG_NAME)
+ fun isGroupRoot_summary() {
+ val groupKey = "group"
+ val summary =
+ NotificationEntryBuilder()
+ .setGroup(mContext, groupKey)
+ .setGroupSummary(mContext, true)
+ .build()
+ GroupEntryBuilder().setKey(groupKey).setSummary(summary).build()
+
+ assertThat(underTest.isGroupRoot(summary.entryAdapter)).isTrue()
+ }
+
+ @Test
+ @EnableFlags(NotificationBundleUi.FLAG_NAME)
+ fun isGroupRoot_child() {
+ val groupKey = "group"
+ val summary =
+ NotificationEntryBuilder()
+ .setGroup(mContext, groupKey)
+ .setGroupSummary(mContext, true)
+ .build()
+ val entry = NotificationEntryBuilder().setGroup(mContext, groupKey).build()
+ GroupEntryBuilder().setKey(groupKey).setSummary(summary).addChild(entry).build()
+
+ assertThat(underTest.isGroupRoot(entry.entryAdapter)).isFalse()
+ }
+
+ @Test
+ @EnableFlags(NotificationBundleUi.FLAG_NAME)
+ fun getGroupRoot_topLevelEntry() {
+ val entry = NotificationEntryBuilder().setParent(GroupEntry.ROOT_ENTRY).build()
+ assertThat(underTest.getGroupRoot(entry.entryAdapter)).isNull()
+ }
+
+ @Test
+ @EnableFlags(NotificationBundleUi.FLAG_NAME)
+ fun getGroupRoot_summary() {
+ val groupKey = "group"
+ val summary =
+ NotificationEntryBuilder()
+ .setGroup(mContext, groupKey)
+ .setGroupSummary(mContext, true)
+ .build()
+ GroupEntryBuilder().setKey(groupKey).setSummary(summary).build()
+
+ assertThat(underTest.getGroupRoot(summary.entryAdapter)).isEqualTo(summary.entryAdapter)
+ }
+
+ @Test
+ @EnableFlags(NotificationBundleUi.FLAG_NAME)
+ fun getGroupRoot_child() {
+ val groupKey = "group"
+ val summary =
+ NotificationEntryBuilder()
+ .setGroup(mContext, groupKey)
+ .setGroupSummary(mContext, true)
+ .build()
+ val entry = NotificationEntryBuilder().setGroup(mContext, groupKey).build()
+ GroupEntryBuilder().setKey(groupKey).setSummary(summary).addChild(entry).build()
+
+ assertThat(underTest.getGroupRoot(entry.entryAdapter)).isEqualTo(summary.entryAdapter)
+ }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewmodel/EmptyShadeViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewmodel/EmptyShadeViewModelTest.kt
index 5ba972def76d..7cbc839c0ab5 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewmodel/EmptyShadeViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewmodel/EmptyShadeViewModelTest.kt
@@ -140,7 +140,7 @@ class EmptyShadeViewModelTest(flags: FlagsParameterization) : SysuiTestCase() {
@Test
@EnableFlags(ModesEmptyShadeFix.FLAG_NAME)
- @DisableFlags(Flags.FLAG_MODES_UI, Flags.FLAG_MODES_API)
+ @DisableFlags(Flags.FLAG_MODES_UI)
fun text_changesWhenNotifsHiddenInShade() =
testScope.runTest {
val text by collectLastValue(underTest.text)
@@ -163,7 +163,7 @@ class EmptyShadeViewModelTest(flags: FlagsParameterization) : SysuiTestCase() {
}
@Test
- @EnableFlags(ModesEmptyShadeFix.FLAG_NAME, Flags.FLAG_MODES_UI, Flags.FLAG_MODES_API)
+ @EnableFlags(ModesEmptyShadeFix.FLAG_NAME, Flags.FLAG_MODES_UI)
fun text_changesWhenLocaleChanges() =
testScope.runTest {
val text by collectLastValue(underTest.text)
@@ -186,7 +186,7 @@ class EmptyShadeViewModelTest(flags: FlagsParameterization) : SysuiTestCase() {
}
@Test
- @EnableFlags(ModesEmptyShadeFix.FLAG_NAME, Flags.FLAG_MODES_UI, Flags.FLAG_MODES_API)
+ @EnableFlags(ModesEmptyShadeFix.FLAG_NAME, Flags.FLAG_MODES_UI)
fun text_reflectsModesHidingNotifications() =
testScope.runTest {
val text by collectLastValue(underTest.text)
@@ -250,7 +250,7 @@ class EmptyShadeViewModelTest(flags: FlagsParameterization) : SysuiTestCase() {
}
@Test
- @EnableFlags(ModesEmptyShadeFix.FLAG_NAME, Flags.FLAG_MODES_UI, Flags.FLAG_MODES_API)
+ @EnableFlags(ModesEmptyShadeFix.FLAG_NAME, Flags.FLAG_MODES_UI)
fun onClick_whenHistoryDisabled_leadsToSettingsPage() =
testScope.runTest {
val onClick by collectLastValue(underTest.onClick)
@@ -264,7 +264,7 @@ class EmptyShadeViewModelTest(flags: FlagsParameterization) : SysuiTestCase() {
}
@Test
- @EnableFlags(ModesEmptyShadeFix.FLAG_NAME, Flags.FLAG_MODES_UI, Flags.FLAG_MODES_API)
+ @EnableFlags(ModesEmptyShadeFix.FLAG_NAME, Flags.FLAG_MODES_UI)
fun onClick_whenHistoryEnabled_leadsToHistoryPage() =
testScope.runTest {
val onClick by collectLastValue(underTest.onClick)
@@ -279,7 +279,7 @@ class EmptyShadeViewModelTest(flags: FlagsParameterization) : SysuiTestCase() {
}
@Test
- @EnableFlags(ModesEmptyShadeFix.FLAG_NAME, Flags.FLAG_MODES_UI, Flags.FLAG_MODES_API)
+ @EnableFlags(ModesEmptyShadeFix.FLAG_NAME, Flags.FLAG_MODES_UI)
fun onClick_whenOneModeHidingNotifications_leadsToModeSettings() =
testScope.runTest {
val onClick by collectLastValue(underTest.onClick)
@@ -305,7 +305,7 @@ class EmptyShadeViewModelTest(flags: FlagsParameterization) : SysuiTestCase() {
}
@Test
- @EnableFlags(ModesEmptyShadeFix.FLAG_NAME, Flags.FLAG_MODES_UI, Flags.FLAG_MODES_API)
+ @EnableFlags(ModesEmptyShadeFix.FLAG_NAME, Flags.FLAG_MODES_UI)
fun onClick_whenMultipleModesHidingNotifications_leadsToGeneralModesSettings() =
testScope.runTest {
val onClick by collectLastValue(underTest.onClick)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImplTest.kt
index 339f8fac3820..e22acd53e584 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImplTest.kt
@@ -106,11 +106,15 @@ class HeadsUpManagerImplTest(flags: FlagsParameterization) : SysuiTestCase() {
this.addOverride(R.integer.touch_acceptance_delay, TEST_TOUCH_ACCEPTANCE_TIME)
this.addOverride(
R.integer.heads_up_notification_minimum_time,
- TEST_MINIMUM_DISPLAY_TIME,
+ TEST_MINIMUM_DISPLAY_TIME_DEFAULT,
)
this.addOverride(
R.integer.heads_up_notification_minimum_time_with_throttling,
- TEST_MINIMUM_DISPLAY_TIME,
+ TEST_MINIMUM_DISPLAY_TIME_DEFAULT,
+ )
+ this.addOverride(
+ R.integer.heads_up_notification_minimum_time_for_user_initiated,
+ TEST_MINIMUM_DISPLAY_TIME_FOR_USER_INITIATED,
)
this.addOverride(R.integer.heads_up_notification_decay, TEST_AUTO_DISMISS_TIME)
this.addOverride(
@@ -414,7 +418,7 @@ class HeadsUpManagerImplTest(flags: FlagsParameterization) : SysuiTestCase() {
}
@Test
- fun testRemoveNotification_beforeMinimumDisplayTime() {
+ fun testRemoveNotification_beforeMinimumDisplayTime_notUserInitiatedHun() {
val entry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
useAccessibilityTimeout(false)
@@ -429,18 +433,22 @@ class HeadsUpManagerImplTest(flags: FlagsParameterization) : SysuiTestCase() {
assertThat(removedImmediately).isFalse()
assertThat(underTest.isHeadsUpEntry(entry.key)).isTrue()
- systemClock.advanceTime(((TEST_MINIMUM_DISPLAY_TIME + TEST_AUTO_DISMISS_TIME) / 2).toLong())
+ systemClock.advanceTime(
+ ((TEST_MINIMUM_DISPLAY_TIME_DEFAULT + TEST_AUTO_DISMISS_TIME) / 2).toLong()
+ )
assertThat(underTest.isHeadsUpEntry(entry.key)).isFalse()
}
@Test
- fun testRemoveNotification_afterMinimumDisplayTime() {
+ fun testRemoveNotification_afterMinimumDisplayTime_notUserInitiatedHun() {
val entry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
useAccessibilityTimeout(false)
underTest.showNotification(entry)
- systemClock.advanceTime(((TEST_MINIMUM_DISPLAY_TIME + TEST_AUTO_DISMISS_TIME) / 2).toLong())
+ systemClock.advanceTime(
+ ((TEST_MINIMUM_DISPLAY_TIME_DEFAULT + TEST_AUTO_DISMISS_TIME) / 2).toLong()
+ )
assertThat(underTest.isHeadsUpEntry(entry.key)).isTrue()
@@ -455,6 +463,57 @@ class HeadsUpManagerImplTest(flags: FlagsParameterization) : SysuiTestCase() {
}
@Test
+ @EnableFlags(StatusBarNotifChips.FLAG_NAME)
+ fun testRemoveNotification_beforeMinimumDisplayTime_forUserInitiatedHun() {
+ useAccessibilityTimeout(false)
+
+ val entry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
+ entry.row = testHelper.createRow()
+ underTest.showNotification(entry, isPinnedByUser = true)
+
+ val removedImmediately =
+ underTest.removeNotification(
+ entry.key,
+ /* releaseImmediately = */ false,
+ "beforeMinimumDisplayTime",
+ )
+ assertThat(removedImmediately).isFalse()
+ assertThat(underTest.isHeadsUpEntry(entry.key)).isTrue()
+
+ systemClock.advanceTime(
+ ((TEST_MINIMUM_DISPLAY_TIME_FOR_USER_INITIATED + TEST_AUTO_DISMISS_TIME) / 2).toLong()
+ )
+
+ assertThat(underTest.isHeadsUpEntry(entry.key)).isFalse()
+ }
+
+ @Test
+ @EnableFlags(StatusBarNotifChips.FLAG_NAME)
+ fun testRemoveNotification_afterMinimumDisplayTime_forUserInitiatedHun() {
+ useAccessibilityTimeout(false)
+
+ val entry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
+ entry.row = testHelper.createRow()
+ underTest.showNotification(entry, isPinnedByUser = true)
+
+ systemClock.advanceTime(
+ ((TEST_MINIMUM_DISPLAY_TIME_FOR_USER_INITIATED + TEST_AUTO_DISMISS_TIME) / 2).toLong()
+ )
+
+ assertThat(underTest.isHeadsUpEntry(entry.key)).isTrue()
+
+ val removedImmediately =
+ underTest.removeNotification(
+ entry.key,
+ /* releaseImmediately = */ false,
+ "afterMinimumDisplayTime",
+ )
+
+ assertThat(removedImmediately).isTrue()
+ assertThat(underTest.isHeadsUpEntry(entry.key)).isFalse()
+ }
+
+ @Test
fun testRemoveNotification_releaseImmediately() {
val entry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
@@ -1047,16 +1106,21 @@ class HeadsUpManagerImplTest(flags: FlagsParameterization) : SysuiTestCase() {
}
companion object {
- const val TEST_TOUCH_ACCEPTANCE_TIME = 200
- const val TEST_A11Y_AUTO_DISMISS_TIME = 1000
- const val TEST_EXTENSION_TIME = 500
+ private const val TEST_TOUCH_ACCEPTANCE_TIME = 200
+ private const val TEST_A11Y_AUTO_DISMISS_TIME = 1000
+ private const val TEST_EXTENSION_TIME = 500
- const val TEST_MINIMUM_DISPLAY_TIME = 400
- const val TEST_AUTO_DISMISS_TIME = 600
- const val TEST_STICKY_AUTO_DISMISS_TIME = 800
+ private const val TEST_MINIMUM_DISPLAY_TIME_DEFAULT = 400
+ private const val TEST_MINIMUM_DISPLAY_TIME_FOR_USER_INITIATED = 500
+ private const val TEST_AUTO_DISMISS_TIME = 600
+ private const val TEST_STICKY_AUTO_DISMISS_TIME = 800
init {
- assertThat(TEST_MINIMUM_DISPLAY_TIME).isLessThan(TEST_AUTO_DISMISS_TIME)
+ assertThat(TEST_MINIMUM_DISPLAY_TIME_DEFAULT)
+ .isLessThan(TEST_MINIMUM_DISPLAY_TIME_FOR_USER_INITIATED)
+ assertThat(TEST_MINIMUM_DISPLAY_TIME_DEFAULT).isLessThan(TEST_AUTO_DISMISS_TIME)
+ assertThat(TEST_MINIMUM_DISPLAY_TIME_FOR_USER_INITIATED)
+ .isLessThan(TEST_AUTO_DISMISS_TIME)
assertThat(TEST_AUTO_DISMISS_TIME).isLessThan(TEST_STICKY_AUTO_DISMISS_TIME)
assertThat(TEST_STICKY_AUTO_DISMISS_TIME).isLessThan(TEST_A11Y_AUTO_DISMISS_TIME)
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/icon/domain/interactor/NotificationIconsInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/icon/domain/interactor/NotificationIconsInteractorTest.kt
index ee4d0990d38f..ee698ae20adb 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/icon/domain/interactor/NotificationIconsInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/icon/domain/interactor/NotificationIconsInteractorTest.kt
@@ -32,8 +32,12 @@ import com.android.systemui.statusbar.notification.data.repository.activeNotific
import com.android.systemui.statusbar.notification.data.repository.notificationsKeyguardViewStateRepository
import com.android.systemui.statusbar.notification.domain.interactor.activeNotificationsInteractor
import com.android.systemui.statusbar.notification.domain.interactor.headsUpNotificationIconInteractor
+import com.android.systemui.statusbar.notification.promoted.domain.interactor.aodPromotedNotificationInteractor
+import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel
+import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel.Style.Base
import com.android.systemui.statusbar.notification.shared.byIsAmbient
import com.android.systemui.statusbar.notification.shared.byIsLastMessageFromReply
+import com.android.systemui.statusbar.notification.shared.byIsPromoted
import com.android.systemui.statusbar.notification.shared.byIsPulsing
import com.android.systemui.statusbar.notification.shared.byIsRowDismissed
import com.android.systemui.statusbar.notification.shared.byIsSilent
@@ -58,12 +62,14 @@ class NotificationIconsInteractorTest : SysuiTestCase() {
private val testScope = kosmos.testScope
private val activeNotificationListRepository = kosmos.activeNotificationListRepository
private val notificationsKeyguardInteractor = kosmos.notificationsKeyguardInteractor
+ private val aodPromotedNotificationInteractor = kosmos.aodPromotedNotificationInteractor
private val underTest =
NotificationIconsInteractor(
kosmos.activeNotificationsInteractor,
kosmos.bubblesOptional,
kosmos.headsUpNotificationIconInteractor,
+ kosmos.aodPromotedNotificationInteractor,
kosmos.notificationsKeyguardViewStateRepository,
)
@@ -141,6 +147,22 @@ class NotificationIconsInteractorTest : SysuiTestCase() {
notificationsKeyguardInteractor.setNotificationsFullyHidden(true)
assertThat(filteredSet).comparingElementsUsing(byIsPulsing).contains(true)
}
+
+ @Test
+ fun filteredEntrySet_showAodPromoted() {
+ testScope.runTest {
+ val filteredSet by collectLastValue(underTest.filteredNotifSet(showAodPromoted = true))
+ assertThat(filteredSet).comparingElementsUsing(byIsPromoted).contains(true)
+ }
+ }
+
+ @Test
+ fun filteredEntrySet_noAodPromoted() {
+ testScope.runTest {
+ val filteredSet by collectLastValue(underTest.filteredNotifSet(showAodPromoted = false))
+ assertThat(filteredSet).comparingElementsUsing(byIsPromoted).doesNotContain(true)
+ }
+ }
}
@SmallTest
@@ -326,4 +348,12 @@ private val testIcons =
activeNotificationModel(key = "notif5", isLastMessageFromReply = true),
activeNotificationModel(key = "notif6", isSuppressedFromStatusBar = true),
activeNotificationModel(key = "notif7", isPulsing = true),
+ activeNotificationModel(key = "notif8", promotedContent = promotedContent("notif8", Base)),
)
+
+private fun promotedContent(
+ key: String,
+ style: PromotedNotificationContentModel.Style,
+): PromotedNotificationContentModel {
+ return PromotedNotificationContentModel.Builder(key).apply { this.style = style }.build()
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/people/PeopleNotificationIdentifierTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/people/PeopleNotificationIdentifierTest.kt
new file mode 100644
index 000000000000..75f5de0118d4
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/people/PeopleNotificationIdentifierTest.kt
@@ -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.systemui.statusbar.notification.people
+
+import android.app.Notification
+import android.app.NotificationChannel
+import android.content.pm.ShortcutInfo
+import android.service.notification.NotificationListenerService.Ranking
+import android.service.notification.StatusBarNotification
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.RankingBuilder
+import com.android.systemui.statusbar.notification.collection.GroupEntry
+import com.android.systemui.statusbar.notification.collection.GroupEntryBuilder
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
+import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManagerImpl
+import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier.Companion.TYPE_FULL_PERSON
+import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier.Companion.TYPE_IMPORTANT_PERSON
+import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier.Companion.TYPE_NON_PERSON
+import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier.Companion.TYPE_PERSON
+import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito
+import org.mockito.Mockito.mock
+
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class PeopleNotificationIdentifierTest : SysuiTestCase() {
+
+ private lateinit var underTest: PeopleNotificationIdentifierImpl
+
+ private val summary1 = notificationEntry("foo", 1, summary = true)
+ private val summary2 = notificationEntry("bar", 1, summary = true)
+ private val entries =
+ listOf<GroupEntry>(
+ GroupEntryBuilder()
+ .setSummary(summary1)
+ .setChildren(
+ listOf(
+ notificationEntry("foo", 2),
+ notificationEntry("foo", 3),
+ notificationEntry("foo", 4)
+ )
+ )
+ .build(),
+ GroupEntryBuilder()
+ .setSummary(summary2)
+ .setChildren(
+ listOf(
+ notificationEntry("bar", 2),
+ notificationEntry("bar", 3),
+ notificationEntry("bar", 4)
+ )
+ )
+ .build()
+ )
+
+ private fun notificationEntry(
+ pkg: String,
+ id: Int,
+ summary: Boolean = false
+ ): NotificationEntry {
+ val sbn = mock(StatusBarNotification::class.java)
+ Mockito.`when`(sbn.key).thenReturn("key")
+ Mockito.`when`(sbn.notification).thenReturn(mock(Notification::class.java))
+ if (summary)
+ Mockito.`when`(sbn.notification.isGroupSummary).thenReturn(true)
+ return NotificationEntryBuilder().setPkg(pkg)
+ .setId(id)
+ .setSbn(sbn)
+ .build().apply {
+ row = mock(ExpandableNotificationRow::class.java)
+ }
+ }
+
+ private fun personRanking(entry: NotificationEntry, personType: Int): Ranking {
+ val channel = NotificationChannel("person", "person", 4)
+ channel.setConversationId("parent", "person")
+ channel.setImportantConversation(true)
+
+ val br = RankingBuilder(entry.ranking)
+
+ when (personType) {
+ TYPE_NON_PERSON -> br.setIsConversation(false)
+ TYPE_PERSON -> {
+ br.setIsConversation(true)
+ br.setShortcutInfo(null)
+ }
+
+ TYPE_IMPORTANT_PERSON -> {
+ br.setIsConversation(true)
+ br.setShortcutInfo(mock(ShortcutInfo::class.java))
+ br.setChannel(channel)
+ }
+
+ else -> {
+ br.setIsConversation(true)
+ br.setShortcutInfo(mock(ShortcutInfo::class.java))
+ }
+ }
+
+ return br.build()
+ }
+
+ @Before
+ fun setUp() {
+ val personExtractor = object : NotificationPersonExtractor {
+ public override fun isPersonNotification(sbn: StatusBarNotification): Boolean {
+ return true
+ }
+ }
+
+ underTest = PeopleNotificationIdentifierImpl(
+ personExtractor,
+ GroupMembershipManagerImpl()
+ )
+ }
+
+ private val Ranking.personTypeInfo
+ get() = when {
+ !isConversation -> TYPE_NON_PERSON
+ conversationShortcutInfo == null -> TYPE_PERSON
+ channel?.isImportantConversation == true -> TYPE_IMPORTANT_PERSON
+ else -> TYPE_FULL_PERSON
+ }
+
+ @Test
+ fun getPeopleNotificationType_entryIsImportant() {
+ summary1.setRanking(personRanking(summary1, TYPE_IMPORTANT_PERSON))
+
+ assertThat(underTest.getPeopleNotificationType(summary1)).isEqualTo(TYPE_IMPORTANT_PERSON)
+ }
+
+ @Test
+ fun getPeopleNotificationType_importantChild() {
+ entries.get(0).getChildren().get(0).setRanking(
+ personRanking(entries.get(0).getChildren().get(0), TYPE_IMPORTANT_PERSON)
+ )
+
+ assertThat(entries.get(0).summary?.let { underTest.getPeopleNotificationType(it) })
+ .isEqualTo(TYPE_IMPORTANT_PERSON)
+ }
+} \ No newline at end of file
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/PromotedNotificationsInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/PromotedNotificationsInteractorTest.kt
new file mode 100644
index 000000000000..aa6e76d08c17
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/PromotedNotificationsInteractorTest.kt
@@ -0,0 +1,156 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.promoted.domain.interactor
+
+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.Kosmos.Fixture
+import com.android.systemui.kosmos.collectLastValue
+import com.android.systemui.kosmos.runTest
+import com.android.systemui.kosmos.useUnconfinedTestDispatcher
+import com.android.systemui.statusbar.chips.notification.domain.interactor.statusBarNotificationChipsInteractor
+import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips
+import com.android.systemui.statusbar.core.StatusBarRootModernization
+import com.android.systemui.statusbar.notification.buildNotificationEntry
+import com.android.systemui.statusbar.notification.buildOngoingCallEntry
+import com.android.systemui.statusbar.notification.buildPromotedOngoingEntry
+import com.android.systemui.statusbar.notification.domain.interactor.renderNotificationListInteractor
+import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUi
+import com.android.systemui.statusbar.phone.ongoingcall.StatusBarChipsModernization
+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
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@EnableFlags(
+ PromotedNotificationUi.FLAG_NAME,
+ StatusBarNotifChips.FLAG_NAME,
+ StatusBarChipsModernization.FLAG_NAME,
+ StatusBarRootModernization.FLAG_NAME,
+)
+class PromotedNotificationsInteractorTest : SysuiTestCase() {
+ private val kosmos = testKosmos().useUnconfinedTestDispatcher()
+
+ private val Kosmos.underTest by Fixture { promotedNotificationsInteractor }
+
+ @Before
+ fun setUp() {
+ kosmos.statusBarNotificationChipsInteractor.start()
+ }
+
+ @Test
+ fun orderedChipNotificationKeys_containsNonPromotedCalls() =
+ kosmos.runTest {
+ // GIVEN a call and a promoted ongoing notification
+ val callEntry = buildOngoingCallEntry(promoted = false)
+ val ronEntry = buildPromotedOngoingEntry()
+ val otherEntry = buildNotificationEntry(tag = "other")
+
+ renderNotificationListInteractor.setRenderedList(
+ listOf(callEntry, ronEntry, otherEntry)
+ )
+
+ val orderedChipNotificationKeys by
+ collectLastValue(underTest.orderedChipNotificationKeys)
+
+ // THEN the order of the notification keys should be the call then the RON
+ assertThat(orderedChipNotificationKeys)
+ .containsExactly("0|test_pkg|0|call|0", "0|test_pkg|0|ron|0")
+ }
+
+ @Test
+ fun orderedChipNotificationKeys_containsPromotedCalls() =
+ kosmos.runTest {
+ // GIVEN a call and a promoted ongoing notification
+ val callEntry = buildOngoingCallEntry(promoted = true)
+ val ronEntry = buildPromotedOngoingEntry()
+ val otherEntry = buildNotificationEntry(tag = "other")
+
+ renderNotificationListInteractor.setRenderedList(
+ listOf(callEntry, ronEntry, otherEntry)
+ )
+
+ val orderedChipNotificationKeys by
+ collectLastValue(underTest.orderedChipNotificationKeys)
+
+ // THEN the order of the notification keys should be the call then the RON
+ assertThat(orderedChipNotificationKeys)
+ .containsExactly("0|test_pkg|0|call|0", "0|test_pkg|0|ron|0")
+ }
+
+ @Test
+ fun topPromotedNotificationContent_skipsNonPromotedCalls() =
+ kosmos.runTest {
+ // GIVEN a non-promoted call and a promoted ongoing notification
+ val callEntry = buildOngoingCallEntry(promoted = false)
+ val ronEntry = buildPromotedOngoingEntry()
+ val otherEntry = buildNotificationEntry(tag = "other")
+
+ renderNotificationListInteractor.setRenderedList(
+ listOf(callEntry, ronEntry, otherEntry)
+ )
+
+ val topPromotedNotificationContent by
+ collectLastValue(underTest.topPromotedNotificationContent)
+
+ // THEN the ron is first because the call has no content
+ assertThat(topPromotedNotificationContent?.identity?.key)
+ .isEqualTo("0|test_pkg|0|ron|0")
+ }
+
+ @Test
+ fun topPromotedNotificationContent_includesPromotedCalls() =
+ kosmos.runTest {
+ // GIVEN a promoted call and a promoted ongoing notification
+ val callEntry = buildOngoingCallEntry(promoted = true)
+ val ronEntry = buildPromotedOngoingEntry()
+ val otherEntry = buildNotificationEntry(tag = "other")
+
+ renderNotificationListInteractor.setRenderedList(
+ listOf(callEntry, ronEntry, otherEntry)
+ )
+
+ val topPromotedNotificationContent by
+ collectLastValue(underTest.topPromotedNotificationContent)
+
+ // THEN the call is the top notification
+ assertThat(topPromotedNotificationContent?.identity?.key)
+ .isEqualTo("0|test_pkg|0|call|0")
+ }
+
+ @Test
+ fun topPromotedNotificationContent_nullWithNoPromotedNotifications() =
+ kosmos.runTest {
+ // GIVEN a a non-promoted call and no promoted ongoing entry
+ val callEntry = buildOngoingCallEntry(promoted = false)
+ val otherEntry = buildNotificationEntry(tag = "other")
+
+ renderNotificationListInteractor.setRenderedList(listOf(callEntry, otherEntry))
+
+ val topPromotedNotificationContent by
+ collectLastValue(underTest.topPromotedNotificationContent)
+
+ // THEN there is no top promoted notification
+ assertThat(topPromotedNotificationContent).isNull()
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java
index 7d406b4b397c..9f35d631bd45 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java
@@ -67,6 +67,7 @@ import com.android.systemui.media.controls.util.MediaFeatureFlag;
import com.android.systemui.statusbar.NotificationRemoteInputManager;
import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips;
import com.android.systemui.statusbar.notification.ConversationNotificationProcessor;
+import com.android.systemui.statusbar.notification.collection.EntryAdapter;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.promoted.FakePromotedNotificationContentExtractor;
import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUi;
@@ -248,14 +249,13 @@ public class NotificationContentInflaterTest extends SysuiTestCase {
true /* isNewView */, (v, p, r) -> true,
new InflationCallback() {
@Override
- public void handleInflationException(NotificationEntry entry,
- Exception e) {
+ public void handleInflationException(Exception e) {
countDownLatch.countDown();
throw new RuntimeException("No Exception expected");
}
@Override
- public void onAsyncInflationFinished(NotificationEntry entry) {
+ public void onAsyncInflationFinished() {
countDownLatch.countDown();
}
}, mRow.getPrivateLayout(), null, null, new HashMap<>(),
@@ -539,8 +539,7 @@ public class NotificationContentInflaterTest extends SysuiTestCase {
inflater.setInflateSynchronously(true);
InflationCallback callback = new InflationCallback() {
@Override
- public void handleInflationException(NotificationEntry entry,
- Exception e) {
+ public void handleInflationException(Exception e) {
if (!expectingException) {
exceptionHolder.setException(e);
}
@@ -548,7 +547,7 @@ public class NotificationContentInflaterTest extends SysuiTestCase {
}
@Override
- public void onAsyncInflationFinished(NotificationEntry entry) {
+ public void onAsyncInflationFinished() {
if (expectingException) {
exceptionHolder.setException(new RuntimeException(
"Inflation finished even though there should be an error"));
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerTest.kt
index d43cc78e20dc..4c1f4f17e00c 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerTest.kt
@@ -71,6 +71,10 @@ import com.android.systemui.statusbar.notification.collection.provider.HighPrior
import com.android.systemui.statusbar.notification.domain.interactor.activeNotificationsInteractor
import com.android.systemui.statusbar.notification.headsup.HeadsUpManager
import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier
+import com.android.systemui.statusbar.notification.row.icon.AppIconProvider
+import com.android.systemui.statusbar.notification.row.icon.NotificationIconStyleProvider
+import com.android.systemui.statusbar.notification.row.icon.appIconProvider
+import com.android.systemui.statusbar.notification.row.icon.notificationIconStyleProvider
import com.android.systemui.statusbar.notification.stack.NotificationListContainer
import com.android.systemui.statusbar.policy.DeviceProvisionedController
import com.android.systemui.testKosmos
@@ -203,6 +207,8 @@ class NotificationGutsManagerTest(flags: FlagsParameterization) : SysuiTestCase(
accessibilityManager,
highPriorityProvider,
iNotificationManager,
+ kosmos.appIconProvider,
+ kosmos.notificationIconStyleProvider,
userManager,
peopleSpaceWidgetManager,
launcherApps,
@@ -512,6 +518,8 @@ class NotificationGutsManagerTest(flags: FlagsParameterization) : SysuiTestCase(
.bindNotification(
any<PackageManager>(),
any<INotificationManager>(),
+ any<AppIconProvider>(),
+ any<NotificationIconStyleProvider>(),
eq(onUserInteractionCallback),
eq(channelEditorDialogController),
eq(statusBarNotification.packageName),
@@ -550,6 +558,8 @@ class NotificationGutsManagerTest(flags: FlagsParameterization) : SysuiTestCase(
.bindNotification(
any<PackageManager>(),
any<INotificationManager>(),
+ any<AppIconProvider>(),
+ any<NotificationIconStyleProvider>(),
eq(onUserInteractionCallback),
eq(channelEditorDialogController),
eq(statusBarNotification.packageName),
@@ -586,6 +596,8 @@ class NotificationGutsManagerTest(flags: FlagsParameterization) : SysuiTestCase(
.bindNotification(
any<PackageManager>(),
any<INotificationManager>(),
+ any<AppIconProvider>(),
+ any<NotificationIconStyleProvider>(),
eq(onUserInteractionCallback),
eq(channelEditorDialogController),
eq(statusBarNotification.packageName),
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationInfoTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationInfoTest.kt
index 2945fa98caad..96ae07035ed2 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationInfoTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationInfoTest.kt
@@ -64,6 +64,10 @@ import com.android.systemui.statusbar.RankingBuilder
import com.android.systemui.statusbar.notification.AssistantFeedbackController
import com.android.systemui.statusbar.notification.collection.NotificationEntry
import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
+import com.android.systemui.statusbar.notification.row.icon.AppIconProvider
+import com.android.systemui.statusbar.notification.row.icon.NotificationIconStyleProvider
+import com.android.systemui.statusbar.notification.row.icon.appIconProvider
+import com.android.systemui.statusbar.notification.row.icon.notificationIconStyleProvider
import com.android.telecom.telecomManager
import com.google.common.truth.Truth.assertThat
import java.util.concurrent.CountDownLatch
@@ -862,6 +866,8 @@ class NotificationInfoTest : SysuiTestCase() {
private fun bindNotification(
pm: PackageManager = this.mockPackageManager,
iNotificationManager: INotificationManager = this.mockINotificationManager,
+ appIconProvider: AppIconProvider = kosmos.appIconProvider,
+ iconStyleProvider: NotificationIconStyleProvider = kosmos.notificationIconStyleProvider,
onUserInteractionCallback: OnUserInteractionCallback = this.onUserInteractionCallback,
channelEditorDialogController: ChannelEditorDialogController =
this.channelEditorDialogController,
@@ -882,6 +888,8 @@ class NotificationInfoTest : SysuiTestCase() {
underTest.bindNotification(
pm,
iNotificationManager,
+ appIconProvider,
+ iconStyleProvider,
onUserInteractionCallback,
channelEditorDialogController,
pkg,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImplTest.kt
index 82eca3735a71..ce3aee1d88d2 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImplTest.kt
@@ -41,6 +41,7 @@ import com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTIO
import com.android.systemui.statusbar.NotificationLockscreenUserManager.RedactionType
import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips
import com.android.systemui.statusbar.notification.ConversationNotificationProcessor
+import com.android.systemui.statusbar.notification.collection.EntryAdapter
import com.android.systemui.statusbar.notification.collection.NotificationEntry
import com.android.systemui.statusbar.notification.promoted.FakePromotedNotificationContentExtractor
import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUi
@@ -223,12 +224,12 @@ class NotificationRowContentBinderImplTest : SysuiTestCase() {
remoteViewClickHandler = { _, _, _ -> true },
callback =
object : InflationCallback {
- override fun handleInflationException(entry: NotificationEntry, e: Exception) {
+ override fun handleInflationException(e: Exception) {
countDownLatch.countDown()
throw RuntimeException("No Exception expected")
}
- override fun onAsyncInflationFinished(entry: NotificationEntry) {
+ override fun onAsyncInflationFinished() {
countDownLatch.countDown()
}
},
@@ -675,14 +676,14 @@ class NotificationRowContentBinderImplTest : SysuiTestCase() {
inflater.setInflateSynchronously(true)
val callback: InflationCallback =
object : InflationCallback {
- override fun handleInflationException(entry: NotificationEntry, e: Exception) {
+ override fun handleInflationException(e: Exception) {
if (!expectingException) {
exceptionHolder.exception = e
}
countDownLatch.countDown()
}
- override fun onAsyncInflationFinished(entry: NotificationEntry) {
+ override fun onAsyncInflationFinished() {
if (expectingException) {
exceptionHolder.exception =
RuntimeException(
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java
index c39b252cd795..f2131da8f0bb 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java
@@ -615,7 +615,7 @@ public class NotificationTestHelper {
LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(
Context.LAYOUT_INFLATER_SERVICE);
inflater.setFactory2(new RowInflaterTask.RowAsyncLayoutInflater(entry, mSystemClock,
- mRowInflaterTaskLogger));
+ mRowInflaterTaskLogger, UserHandle.of(entry.getSbn().getNormalizedUserId())));
mRow = (ExpandableNotificationRow) inflater.inflate(
R.layout.status_bar_notification_row,
null /* root */,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/PromotedNotificationInfoTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/PromotedNotificationInfoTest.java
index acdbd6237733..5638e0b434aa 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/PromotedNotificationInfoTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/PromotedNotificationInfoTest.java
@@ -48,6 +48,8 @@ import com.android.systemui.res.R;
import com.android.systemui.statusbar.notification.AssistantFeedbackController;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder;
+import com.android.systemui.statusbar.notification.row.icon.AppIconProvider;
+import com.android.systemui.statusbar.notification.row.icon.NotificationIconStyleProvider;
import org.junit.Before;
import org.junit.Rule;
@@ -57,8 +59,6 @@ import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
-import java.util.concurrent.CountDownLatch;
-
@SmallTest
@RunWith(AndroidJUnit4.class)
@TestableLooper.RunWithLooper
@@ -82,6 +82,10 @@ public class PromotedNotificationInfoTest extends SysuiTestCase {
@Mock
private INotificationManager mMockINotificationManager;
@Mock
+ private AppIconProvider mMockAppIconProvider;
+ @Mock
+ private NotificationIconStyleProvider mMockIconStyleProvider;
+ @Mock
private PackageManager mMockPackageManager;
@Mock
private OnUserInteractionCallback mOnUserInteractionCallback;
@@ -127,10 +131,11 @@ public class PromotedNotificationInfoTest extends SysuiTestCase {
public void testBindNotification_setsOnClickListenerForFeedback() throws Exception {
// Bind the notification to the Info object
- final CountDownLatch latch = new CountDownLatch(1);
mInfo.bindNotification(
mMockPackageManager,
mMockINotificationManager,
+ mMockAppIconProvider,
+ mMockIconStyleProvider,
mOnUserInteractionCallback,
mChannelEditorDialogController,
TEST_PACKAGE_NAME,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/shared/TestActiveNotificationModel.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/shared/TestActiveNotificationModel.kt
index 16c5c8a98253..531b30b9547a 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/shared/TestActiveNotificationModel.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/shared/TestActiveNotificationModel.kt
@@ -33,7 +33,12 @@ val byIsRowDismissed: Correspondence<ActiveNotificationModel, Boolean> =
val byIsLastMessageFromReply: Correspondence<ActiveNotificationModel, Boolean> =
Correspondence.transforming(
{ it?.isLastMessageFromReply },
- "has an isLastMessageFromReply value of"
+ "has an isLastMessageFromReply value of",
)
val byIsPulsing: Correspondence<ActiveNotificationModel, Boolean> =
Correspondence.transforming({ it?.isPulsing }, "has an isPulsing value of")
+val byIsPromoted: Correspondence<ActiveNotificationModel, Boolean> =
+ Correspondence.transforming(
+ { it?.promotedContent != null },
+ "has (or doesn't have) a promoted content model",
+ )
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModelTest.kt
index d570f18e35d8..6381b4e0fef7 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModelTest.kt
@@ -57,11 +57,12 @@ class NotificationShelfViewModelTest : SysuiTestCase() {
statusBarStateController = mock()
whenever(screenOffAnimationController.allowWakeUpIfDozing()).thenReturn(true)
}
- private val underTest = kosmos.notificationShelfViewModel
private val deviceEntryFaceAuthRepository = kosmos.fakeDeviceEntryFaceAuthRepository
private val keyguardRepository = kosmos.fakeKeyguardRepository
- private val keyguardTransitionController = kosmos.lockscreenShadeTransitionController
private val powerRepository = kosmos.fakePowerRepository
+ private val keyguardTransitionController by lazy { kosmos.lockscreenShadeTransitionController }
+
+ private val underTest by lazy { kosmos.notificationShelfViewModel }
@Test
fun canModifyColorOfNotifications_whenKeyguardNotShowing() =
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationShelfTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationShelfTest.kt
index 256da253588c..9c5d65ec12ec 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationShelfTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationShelfTest.kt
@@ -1,5 +1,6 @@
package com.android.systemui.statusbar.notification.stack
+import android.os.UserHandle
import android.platform.test.annotations.EnableFlags
import android.service.notification.StatusBarNotification
import android.testing.TestableLooper.RunWithLooper
@@ -21,6 +22,7 @@ import com.android.systemui.statusbar.StatusBarIconView
import com.android.systemui.statusbar.notification.collection.NotificationEntry
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
import com.android.systemui.statusbar.notification.row.ExpandableView
+import com.android.systemui.statusbar.notification.shared.NotificationBundleUi
import com.android.systemui.statusbar.notification.shared.NotificationMinimalism
import com.android.systemui.statusbar.notification.shelf.NotificationShelfIconContainer
import com.android.systemui.statusbar.notification.stack.StackScrollAlgorithm.StackScrollAlgorithmState
@@ -978,7 +980,10 @@ open class NotificationShelfTest : SysuiTestCase() {
) {
val sbnMock: StatusBarNotification = mock()
val mockEntry = mock<NotificationEntry>().apply { whenever(this.sbn).thenReturn(sbnMock) }
- val row = ExpandableNotificationRow(mContext, null, mockEntry)
+ val row = when (NotificationBundleUi.isEnabled) {
+ true -> ExpandableNotificationRow(mContext, null, UserHandle.CURRENT)
+ false -> ExpandableNotificationRow(mContext, null, mockEntry)
+ }
whenever(ambientState.lastVisibleBackgroundChild).thenReturn(row)
whenever(ambientState.isExpansionChanging).thenReturn(true)
whenever(ambientState.expansionFraction).thenReturn(expansionFraction)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallbackTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallbackTest.java
index 8ec17dadcfe7..345ddae42798 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallbackTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallbackTest.java
@@ -46,9 +46,11 @@ import com.android.systemui.statusbar.CommandQueue;
import com.android.systemui.statusbar.NotificationLockscreenUserManager;
import com.android.systemui.statusbar.SysuiStatusBarStateController;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
+import com.android.systemui.statusbar.notification.collection.NotificationEntry.NotifEntryAdapter;
import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManager;
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
import com.android.systemui.statusbar.notification.row.NotificationContentView;
+import com.android.systemui.statusbar.notification.shared.NotificationBundleUi;
import com.android.systemui.statusbar.policy.DeviceProvisionedController;
import com.android.systemui.statusbar.policy.KeyguardStateController;
import com.android.systemui.util.concurrency.FakeExecutor;
@@ -133,9 +135,11 @@ public class StatusBarRemoteInputCallbackTest extends SysuiTestCase {
final ExpandableNotificationRow enr = mock(ExpandableNotificationRow.class);
final NotificationContentView privateLayout = mock(NotificationContentView.class);
final NotificationEntry enrEntry = mock(NotificationEntry.class);
+ final NotifEntryAdapter enrEntryAdapter = mock(NotifEntryAdapter.class);
when(enr.getPrivateLayout()).thenReturn(privateLayout);
when(enr.getEntry()).thenReturn(enrEntry);
+ when(enr.getEntryAdapter()).thenReturn(enrEntryAdapter);
when(enr.isChildInGroup()).thenReturn(true);
when(enr.areChildrenExpanded()).thenReturn(false);
@@ -144,7 +148,11 @@ public class StatusBarRemoteInputCallbackTest extends SysuiTestCase {
enr, mock(View.class), false, onExpandedVisibleRunner);
// THEN
- verify(mGroupExpansionManager).toggleGroupExpansion(enrEntry);
+ if (NotificationBundleUi.isEnabled()) {
+ verify(mGroupExpansionManager).toggleGroupExpansion(enrEntryAdapter);
+ } else {
+ verify(mGroupExpansionManager).toggleGroupExpansion(enrEntry);
+ }
verify(enr).setUserExpanded(true);
verify(privateLayout).setOnExpandedVisibleListener(onExpandedVisibleRunner);
}
@@ -169,7 +177,8 @@ public class StatusBarRemoteInputCallbackTest extends SysuiTestCase {
enr, mock(View.class), false, onExpandedVisibleRunner);
// THEN
- verify(mGroupExpansionManager, never()).toggleGroupExpansion(any());
+ verify(mGroupExpansionManager, never()).toggleGroupExpansion(any(NotificationEntry.class));
+ verify(mGroupExpansionManager, never()).toggleGroupExpansion(any(NotifEntryAdapter.class));
verify(enr).setUserExpanded(true);
verify(privateLayout).setOnExpandedVisibleListener(onExpandedVisibleRunner);
}
@@ -193,7 +202,8 @@ public class StatusBarRemoteInputCallbackTest extends SysuiTestCase {
enr, mock(View.class), false, onExpandedVisibleRunner);
// THEN
- verify(mGroupExpansionManager, never()).toggleGroupExpansion(any());
+ verify(mGroupExpansionManager, never()).toggleGroupExpansion(any(NotificationEntry.class));
+ verify(mGroupExpansionManager, never()).toggleGroupExpansion(any(NotifEntryAdapter.class));
verify(enr).setUserExpanded(true);
verify(privateLayout).setOnExpandedVisibleListener(onExpandedVisibleRunner);
}
@@ -207,9 +217,11 @@ public class StatusBarRemoteInputCallbackTest extends SysuiTestCase {
final ExpandableNotificationRow enr = mock(ExpandableNotificationRow.class);
final NotificationContentView privateLayout = mock(NotificationContentView.class);
final NotificationEntry enrEntry = mock(NotificationEntry.class);
+ final NotifEntryAdapter enrEntryAdapter = mock(NotifEntryAdapter.class);
when(enr.getPrivateLayout()).thenReturn(privateLayout);
when(enr.getEntry()).thenReturn(enrEntry);
+ when(enr.getEntryAdapter()).thenReturn(enrEntryAdapter);
when(enr.isChildInGroup()).thenReturn(true);
when(enr.areChildrenExpanded()).thenReturn(false);
@@ -218,7 +230,11 @@ public class StatusBarRemoteInputCallbackTest extends SysuiTestCase {
enr, mock(View.class), false, onExpandedVisibleRunner);
// THEN
- verify(mGroupExpansionManager).toggleGroupExpansion(enrEntry);
+ if (NotificationBundleUi.isEnabled()) {
+ verify(mGroupExpansionManager).toggleGroupExpansion(enrEntryAdapter);
+ } else {
+ verify(mGroupExpansionManager).toggleGroupExpansion(enrEntry);
+ }
verify(enr, never()).setUserExpanded(anyBoolean());
verify(privateLayout, never()).setOnExpandedVisibleListener(any());
}
@@ -244,6 +260,7 @@ public class StatusBarRemoteInputCallbackTest extends SysuiTestCase {
// THEN
verify(mGroupExpansionManager, never()).toggleGroupExpansion(enrEntry);
+ verify(mGroupExpansionManager, never()).toggleGroupExpansion(any(NotifEntryAdapter.class));
verify(enr, never()).setUserExpanded(anyBoolean());
verify(privateLayout, never()).setOnExpandedVisibleListener(any());
}
@@ -272,7 +289,8 @@ public class StatusBarRemoteInputCallbackTest extends SysuiTestCase {
verify(enr).toggleExpansionState();
verify(privateLayout).setOnExpandedVisibleListener(onExpandedVisibleRunner);
verify(enr, never()).setUserExpanded(anyBoolean());
- verify(mGroupExpansionManager, never()).toggleGroupExpansion(any());
+ verify(mGroupExpansionManager, never()).toggleGroupExpansion(any(NotificationEntry.class));
+ verify(mGroupExpansionManager, never()).toggleGroupExpansion(any(NotifEntryAdapter.class));
}
@Test
@@ -299,7 +317,8 @@ public class StatusBarRemoteInputCallbackTest extends SysuiTestCase {
verify(enr, never()).toggleExpansionState();
verify(privateLayout, never()).setOnExpandedVisibleListener(onExpandedVisibleRunner);
verify(enr, never()).setUserExpanded(anyBoolean());
- verify(mGroupExpansionManager, never()).toggleGroupExpansion(any());
+ verify(mGroupExpansionManager, never()).toggleGroupExpansion(any(NotificationEntry.class));
+ verify(mGroupExpansionManager, never()).toggleGroupExpansion(any(NotifEntryAdapter.class));
}
@Test
@@ -326,7 +345,8 @@ public class StatusBarRemoteInputCallbackTest extends SysuiTestCase {
verify(enr).toggleExpansionState();
verify(privateLayout).setOnExpandedVisibleListener(onExpandedVisibleRunner);
verify(enr, never()).setUserExpanded(anyBoolean());
- verify(mGroupExpansionManager, never()).toggleGroupExpansion(any());
+ verify(mGroupExpansionManager, never()).toggleGroupExpansion(any(NotificationEntry.class));
+ verify(mGroupExpansionManager, never()).toggleGroupExpansion(any(NotifEntryAdapter.class));
}
@Test
@@ -353,6 +373,7 @@ public class StatusBarRemoteInputCallbackTest extends SysuiTestCase {
verify(enr, never()).toggleExpansionState();
verify(privateLayout, never()).setOnExpandedVisibleListener(onExpandedVisibleRunner);
verify(enr, never()).setUserExpanded(anyBoolean());
- verify(mGroupExpansionManager, never()).toggleGroupExpansion(any());
+ verify(mGroupExpansionManager, never()).toggleGroupExpansion(any(NotificationEntry.class));
+ verify(mGroupExpansionManager, never()).toggleGroupExpansion(any(NotifEntryAdapter.class));
}
}
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 ff1ffccfb2de..22e28d883c97 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
@@ -469,7 +469,7 @@ class ZenModeInteractorTest : SysuiTestCase() {
}
@Test
- @EnableFlags(ModesEmptyShadeFix.FLAG_NAME, Flags.FLAG_MODES_UI, Flags.FLAG_MODES_API)
+ @EnableFlags(ModesEmptyShadeFix.FLAG_NAME, Flags.FLAG_MODES_UI)
fun modesHidingNotifications_onlyIncludesModesWithNotifListSuppression() =
kosmos.runTest {
val modesHidingNotifications by collectLastValue(underTest.modesHidingNotifications)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/theme/HardwareColorRule.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/theme/HardwareColorRule.java
new file mode 100644
index 000000000000..ecd04a47b8ae
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/theme/HardwareColorRule.java
@@ -0,0 +1,39 @@
+/*
+ * 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.theme;
+
+import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+import org.junit.runners.model.Statement;
+
+
+public class HardwareColorRule implements TestRule {
+ public String color = "";
+ public String[] options = {};
+ public boolean isTesting = false;
+
+ @Override
+ public Statement apply(Statement base, Description description) {
+ HardwareColors hardwareColors = description.getAnnotation(HardwareColors.class);
+ if (hardwareColors != null) {
+ color = hardwareColors.color();
+ options = hardwareColors.options();
+ isTesting = true;
+ }
+ return base;
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/theme/HardwareColors.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/theme/HardwareColors.java
new file mode 100644
index 000000000000..0b8df2e2670e
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/theme/HardwareColors.java
@@ -0,0 +1,30 @@
+/*
+ * 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.theme;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.METHOD)
+public @interface HardwareColors {
+ String color();
+ String[] options();
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/theme/ThemeOverlayControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/theme/ThemeOverlayControllerTest.java
index 5cd0846ded7e..9a0b8125fb25 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/theme/ThemeOverlayControllerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/theme/ThemeOverlayControllerTest.java
@@ -64,6 +64,7 @@ import com.android.systemui.broadcast.BroadcastDispatcher;
import com.android.systemui.dump.DumpManager;
import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.flags.Flags;
+import com.android.systemui.flags.SystemPropertiesHelper;
import com.android.systemui.keyguard.WakefulnessLifecycle;
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
import com.android.systemui.monet.DynamicColors;
@@ -77,6 +78,7 @@ import com.android.systemui.util.settings.SecureSettings;
import com.google.common.util.concurrent.MoreExecutors;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
@@ -98,6 +100,9 @@ public class ThemeOverlayControllerTest extends SysuiTestCase {
private static final UserHandle MANAGED_USER_HANDLE = UserHandle.of(100);
private static final UserHandle PRIVATE_USER_HANDLE = UserHandle.of(101);
+ @Rule
+ public HardwareColorRule rule = new HardwareColorRule();
+
@Mock
private JavaAdapter mJavaAdapter;
@Mock
@@ -148,13 +153,17 @@ public class ThemeOverlayControllerTest extends SysuiTestCase {
@Captor
private ArgumentCaptor<ContentObserver> mSettingsObserver;
+ @Mock
+ private SystemPropertiesHelper mSystemProperties;
+
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
+
when(mFeatureFlags.isEnabled(Flags.MONET)).thenReturn(true);
when(mWakefulnessLifecycle.getWakefulness()).thenReturn(WAKEFULNESS_AWAKE);
when(mUiModeManager.getContrast()).thenReturn(0.5f);
- when(mDeviceProvisionedController.isCurrentUserSetup()).thenReturn(true);
+
when(mResources.getColor(eq(android.R.color.system_accent1_500), any()))
.thenReturn(Color.RED);
when(mResources.getColor(eq(android.R.color.system_accent2_500), any()))
@@ -166,11 +175,20 @@ public class ThemeOverlayControllerTest extends SysuiTestCase {
when(mResources.getColor(eq(android.R.color.system_neutral2_500), any()))
.thenReturn(Color.BLACK);
+ when(mResources.getStringArray(com.android.internal.R.array.theming_defaults))
+ .thenReturn(rule.options);
+
+ // should fallback to `*|TONAL_SPOT|home_wallpaper`
+ when(mSystemProperties.get("ro.boot.hardware.color")).thenReturn(rule.color);
+ // will try set hardware colors as boot ONLY if user is not set yet
+ when(mDeviceProvisionedController.isCurrentUserSetup()).thenReturn(!rule.isTesting);
+
mThemeOverlayController = new ThemeOverlayController(mContext,
mBroadcastDispatcher, mBgHandler, mMainExecutor, mBgExecutor, mThemeOverlayApplier,
mSecureSettings, mWallpaperManager, mUserManager, mDeviceProvisionedController,
mUserTracker, mDumpManager, mFeatureFlags, mResources, mWakefulnessLifecycle,
- mJavaAdapter, mKeyguardTransitionInteractor, mUiModeManager, mActivityManager) {
+ mJavaAdapter, mKeyguardTransitionInteractor, mUiModeManager, mActivityManager,
+ mSystemProperties) {
@VisibleForTesting
protected boolean isNightMode() {
return false;
@@ -214,11 +232,58 @@ public class ThemeOverlayControllerTest extends SysuiTestCase {
public void start_checksWallpaper() {
ArgumentCaptor<Runnable> registrationRunnable = ArgumentCaptor.forClass(Runnable.class);
verify(mBgExecutor).execute(registrationRunnable.capture());
+ registrationRunnable.getValue().run();
+ verify(mWallpaperManager).getWallpaperColors(eq(WallpaperManager.FLAG_SYSTEM));
+ }
+
+ @Test
+ @HardwareColors(color = "BLK", options = {
+ "BLK|MONOCHROMATIC|#FF0000",
+ "*|VIBRANT|home_wallpaper"
+ })
+ @EnableFlags(com.android.systemui.Flags.FLAG_HARDWARE_COLOR_STYLES)
+ public void start_checkHardwareColor() {
+ // getWallpaperColors should not be called
+ ArgumentCaptor<Runnable> registrationRunnable = ArgumentCaptor.forClass(Runnable.class);
+ verify(mMainExecutor).execute(registrationRunnable.capture());
+ registrationRunnable.getValue().run();
+ verify(mWallpaperManager, never()).getWallpaperColors(anyInt());
+
+ assertThat(mThemeOverlayController.mThemeStyle).isEqualTo(Style.MONOCHROMATIC);
+ assertThat(mThemeOverlayController.mCurrentColors.get(0).getMainColors().get(
+ 0).toArgb()).isEqualTo(Color.RED);
+ }
+
+ @Test
+ @HardwareColors(color = "", options = {
+ "BLK|MONOCHROMATIC|#FF0000",
+ "*|VIBRANT|home_wallpaper"
+ })
+ @EnableFlags(com.android.systemui.Flags.FLAG_HARDWARE_COLOR_STYLES)
+ public void start_wildcardColor() {
+ // getWallpaperColors will be called because we srt wildcard to `home_wallpaper`
+ ArgumentCaptor<Runnable> registrationRunnable = ArgumentCaptor.forClass(Runnable.class);
+ verify(mMainExecutor).execute(registrationRunnable.capture());
+ registrationRunnable.getValue().run();
+ verify(mWallpaperManager).getWallpaperColors(eq(WallpaperManager.FLAG_SYSTEM));
+ assertThat(mThemeOverlayController.mThemeStyle).isEqualTo(Style.VIBRANT);
+ }
+
+ @Test
+ @HardwareColors(color = "NONEXISTENT", options = {})
+ @EnableFlags(com.android.systemui.Flags.FLAG_HARDWARE_COLOR_STYLES)
+ public void start_fallbackColor() {
+ // getWallpaperColors will be called because we default color source is `home_wallpaper`
+ ArgumentCaptor<Runnable> registrationRunnable = ArgumentCaptor.forClass(Runnable.class);
+ verify(mMainExecutor).execute(registrationRunnable.capture());
registrationRunnable.getValue().run();
verify(mWallpaperManager).getWallpaperColors(eq(WallpaperManager.FLAG_SYSTEM));
+
+ assertThat(mThemeOverlayController.mThemeStyle).isEqualTo(Style.TONAL_SPOT);
}
+
@Test
public void onWallpaperColorsChanged_setsTheme_whenForeground() {
// Should ask for a new theme when wallpaper colors change
@@ -287,9 +352,9 @@ public class ThemeOverlayControllerTest extends SysuiTestCase {
WallpaperColors mainColors = new WallpaperColors(Color.valueOf(Color.RED),
Color.valueOf(Color.BLUE), null);
- String jsonString =
- "{\"android.theme.customization.system_palette\":\"override.package.name\","
- + "\"android.theme.customization.color_source\":\"preset\"}";
+ String jsonString = createJsonString(TestColorSource.preset, "override.package.name",
+ "TONAL_SPOT");
+
when(mSecureSettings.getStringForUser(
eq(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES), anyInt()))
.thenReturn(jsonString);
@@ -313,11 +378,7 @@ public class ThemeOverlayControllerTest extends SysuiTestCase {
WallpaperColors mainColors = new WallpaperColors(Color.valueOf(Color.RED),
Color.valueOf(Color.BLUE), null);
- String jsonString =
- "{\"android.theme.customization.color_source\":\"home_wallpaper\","
- + "\"android.theme.customization.system_palette\":\"A16B00\","
- + "\"android.theme.customization.accent_color\":\"A16B00\","
- + "\"android.theme.customization.color_index\":\"2\"}";
+ String jsonString = createJsonString(TestColorSource.home_wallpaper);
when(mSecureSettings.getStringForUser(
eq(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES), anyInt()))
@@ -348,11 +409,7 @@ public class ThemeOverlayControllerTest extends SysuiTestCase {
WallpaperColors mainColors = new WallpaperColors(Color.valueOf(Color.RED),
Color.valueOf(Color.BLUE), null);
- String jsonString =
- "{\"android.theme.customization.color_source\":\"home_wallpaper\","
- + "\"android.theme.customization.system_palette\":\"A16B00\","
- + "\"android.theme.customization.accent_color\":\"A16B00\","
- + "\"android.theme.customization.color_index\":\"2\"}";
+ String jsonString = createJsonString(TestColorSource.home_wallpaper);
when(mSecureSettings.getStringForUser(
eq(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES), anyInt()))
@@ -381,11 +438,7 @@ public class ThemeOverlayControllerTest extends SysuiTestCase {
// Should ask for a new theme when wallpaper colors change
WallpaperColors mainColors = new WallpaperColors(Color.valueOf(Color.RED),
Color.valueOf(Color.BLUE), null);
- String jsonString =
- "{\"android.theme.customization.color_source\":\"lock_wallpaper\","
- + "\"android.theme.customization.system_palette\":\"A16B00\","
- + "\"android.theme.customization.accent_color\":\"A16B00\","
- + "\"android.theme.customization.color_index\":\"2\"}";
+ String jsonString = createJsonString(TestColorSource.lock_wallpaper);
when(mSecureSettings.getStringForUser(
eq(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES), anyInt()))
.thenReturn(jsonString);
@@ -404,11 +457,7 @@ public class ThemeOverlayControllerTest extends SysuiTestCase {
// Should ask for a new theme when wallpaper colors change
WallpaperColors mainColors = new WallpaperColors(Color.valueOf(Color.RED),
Color.valueOf(Color.BLUE), null);
- String jsonString =
- "{\"android.theme.customization.color_source\":\"lock_wallpaper\","
- + "\"android.theme.customization.system_palette\":\"A16B00\","
- + "\"android.theme.customization.accent_color\":\"A16B00\","
- + "\"android.theme.customization.color_index\":\"2\"}";
+ String jsonString = createJsonString(TestColorSource.lock_wallpaper);
when(mSecureSettings.getStringForUser(
eq(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES), anyInt()))
.thenReturn(jsonString);
@@ -455,8 +504,8 @@ public class ThemeOverlayControllerTest extends SysuiTestCase {
@Test
public void onSettingChanged_invalidStyle() {
when(mDeviceProvisionedController.isUserSetup(anyInt())).thenReturn(true);
- String jsonString = "{\"android.theme.customization.system_palette\":\"A16B00\","
- + "\"android.theme.customization.theme_style\":\"some_invalid_name\"}";
+ String jsonString = createJsonString(TestColorSource.home_wallpaper, "A16B00",
+ "some_invalid_name");
when(mSecureSettings.getStringForUser(
eq(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES), anyInt()))
@@ -473,11 +522,7 @@ public class ThemeOverlayControllerTest extends SysuiTestCase {
WallpaperColors mainColors = new WallpaperColors(Color.valueOf(Color.RED),
Color.valueOf(Color.BLUE), null);
- String jsonString =
- "{\"android.theme.customization.color_source\":\"home_wallpaper\","
- + "\"android.theme.customization.system_palette\":\"A16B00\","
- + "\"android.theme.customization.accent_color\":\"A16B00\","
- + "\"android.theme.customization.color_index\":\"2\"}";
+ String jsonString = createJsonString(TestColorSource.home_wallpaper);
when(mSecureSettings.getStringForUser(
eq(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES), anyInt()))
@@ -506,11 +551,7 @@ public class ThemeOverlayControllerTest extends SysuiTestCase {
// Should ask for a new theme when wallpaper colors change
WallpaperColors mainColors = new WallpaperColors(Color.valueOf(Color.RED),
Color.valueOf(Color.BLUE), null);
- String jsonString =
- "{\"android.theme.customization.color_source\":\"home_wallpaper\","
- + "\"android.theme.customization.system_palette\":\"A16B00\","
- + "\"android.theme.customization.accent_color\":\"A16B00\","
- + "\"android.theme.customization.color_index\":\"2\"}";
+ String jsonString = createJsonString(TestColorSource.home_wallpaper);
when(mSecureSettings.getStringForUser(
eq(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES), anyInt()))
.thenReturn(jsonString);
@@ -537,11 +578,7 @@ public class ThemeOverlayControllerTest extends SysuiTestCase {
// Should ask for a new theme when wallpaper colors change
WallpaperColors mainColors = new WallpaperColors(Color.valueOf(Color.RED),
Color.valueOf(Color.BLUE), null);
- String jsonString =
- "{\"android.theme.customization.color_source\":\"home_wallpaper\","
- + "\"android.theme.customization.system_palette\":\"A16B00\","
- + "\"android.theme.customization.accent_color\":\"A16B00\","
- + "\"android.theme.customization.color_index\":\"2\"}";
+ String jsonString = createJsonString(TestColorSource.home_wallpaper);
when(mSecureSettings.getStringForUser(
eq(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES), anyInt()))
.thenReturn(jsonString);
@@ -570,11 +607,7 @@ public class ThemeOverlayControllerTest extends SysuiTestCase {
WallpaperColors mainColors = new WallpaperColors(Color.valueOf(Color.RED),
Color.valueOf(Color.BLUE), null);
- String jsonString =
- "{\"android.theme.customization.color_source\":\"home_wallpaper\","
- + "\"android.theme.customization.system_palette\":\"A16B00\","
- + "\"android.theme.customization.accent_color\":\"A16B00\","
- + "\"android.theme.customization.color_index\":\"2\"}";
+ String jsonString = createJsonString(TestColorSource.home_wallpaper);
when(mSecureSettings.getStringForUser(
eq(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES), anyInt()))
@@ -599,7 +632,6 @@ public class ThemeOverlayControllerTest extends SysuiTestCase {
}
-
@Test
@EnableFlags(com.android.systemui.shared.Flags.FLAG_NEW_CUSTOMIZATION_PICKER_UI)
public void onWallpaperColorsChanged_homeWallpaperWithSameColor_shouldKeepThemeAndReapply() {
@@ -608,11 +640,7 @@ public class ThemeOverlayControllerTest extends SysuiTestCase {
WallpaperColors mainColors = new WallpaperColors(Color.valueOf(Color.RED),
Color.valueOf(0xffa16b00), null);
- String jsonString =
- "{\"android.theme.customization.color_source\":\"home_wallpaper\","
- + "\"android.theme.customization.system_palette\":\"A16B00\","
- + "\"android.theme.customization.accent_color\":\"A16B00\","
- + "\"android.theme.customization.color_index\":\"2\"}";
+ String jsonString = createJsonString(TestColorSource.home_wallpaper);
when(mSecureSettings.getStringForUser(
eq(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES), anyInt()))
@@ -642,11 +670,7 @@ public class ThemeOverlayControllerTest extends SysuiTestCase {
WallpaperColors mainColors = new WallpaperColors(Color.valueOf(Color.RED),
Color.valueOf(Color.BLUE), null);
- String jsonString =
- "{\"android.theme.customization.color_source\":\"home_wallpaper\","
- + "\"android.theme.customization.system_palette\":\"A16B00\","
- + "\"android.theme.customization.accent_color\":\"A16B00\","
- + "\"android.theme.customization.color_index\":\"2\"}";
+ String jsonString = createJsonString(TestColorSource.home_wallpaper);
when(mSecureSettings.getStringForUser(
eq(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES), anyInt()))
@@ -676,11 +700,7 @@ public class ThemeOverlayControllerTest extends SysuiTestCase {
WallpaperColors mainColors = new WallpaperColors(Color.valueOf(Color.RED),
Color.valueOf(Color.BLUE), null);
- String jsonString =
- "{\"android.theme.customization.color_source\":\"home_wallpaper\","
- + "\"android.theme.customization.system_palette\":\"A16B00\","
- + "\"android.theme.customization.accent_color\":\"A16B00\","
- + "\"android.theme.customization.color_index\":\"2\"}";
+ String jsonString = createJsonString(TestColorSource.home_wallpaper);
when(mSecureSettings.getStringForUser(
eq(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES), anyInt()))
@@ -711,11 +731,7 @@ public class ThemeOverlayControllerTest extends SysuiTestCase {
WallpaperColors mainColors = new WallpaperColors(Color.valueOf(Color.RED),
Color.valueOf(0xffa16b00), null);
- String jsonString =
- "{\"android.theme.customization.color_source\":\"home_wallpaper\","
- + "\"android.theme.customization.system_palette\":\"A16B00\","
- + "\"android.theme.customization.accent_color\":\"A16B00\","
- + "\"android.theme.customization.color_index\":\"2\"}";
+ String jsonString = createJsonString(TestColorSource.home_wallpaper);
when(mSecureSettings.getStringForUser(
eq(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES), anyInt()))
@@ -745,11 +761,7 @@ public class ThemeOverlayControllerTest extends SysuiTestCase {
WallpaperColors mainColors = new WallpaperColors(Color.valueOf(Color.RED),
Color.valueOf(Color.BLUE), null);
- String jsonString =
- "{\"android.theme.customization.color_source\":\"home_wallpaper\","
- + "\"android.theme.customization.system_palette\":\"A16B00\","
- + "\"android.theme.customization.accent_color\":\"A16B00\","
- + "\"android.theme.customization.color_index\":\"2\"}";
+ String jsonString = createJsonString(TestColorSource.home_wallpaper);
when(mSecureSettings.getStringForUser(
eq(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES), anyInt()))
@@ -886,7 +898,8 @@ public class ThemeOverlayControllerTest extends SysuiTestCase {
mBroadcastDispatcher, mBgHandler, executor, executor, mThemeOverlayApplier,
mSecureSettings, mWallpaperManager, mUserManager, mDeviceProvisionedController,
mUserTracker, mDumpManager, mFeatureFlags, mResources, mWakefulnessLifecycle,
- mJavaAdapter, mKeyguardTransitionInteractor, mUiModeManager, mActivityManager) {
+ mJavaAdapter, mKeyguardTransitionInteractor, mUiModeManager, mActivityManager,
+ mSystemProperties) {
@VisibleForTesting
protected boolean isNightMode() {
return false;
@@ -926,7 +939,8 @@ public class ThemeOverlayControllerTest extends SysuiTestCase {
mBroadcastDispatcher, mBgHandler, executor, executor, mThemeOverlayApplier,
mSecureSettings, mWallpaperManager, mUserManager, mDeviceProvisionedController,
mUserTracker, mDumpManager, mFeatureFlags, mResources, mWakefulnessLifecycle,
- mJavaAdapter, mKeyguardTransitionInteractor, mUiModeManager, mActivityManager) {
+ mJavaAdapter, mKeyguardTransitionInteractor, mUiModeManager, mActivityManager,
+ mSystemProperties) {
@VisibleForTesting
protected boolean isNightMode() {
return false;
@@ -992,7 +1006,7 @@ public class ThemeOverlayControllerTest extends SysuiTestCase {
clearInvocations(mThemeOverlayApplier);
// Device went to sleep and second set of colors was applied.
- mainColors = new WallpaperColors(Color.valueOf(Color.BLUE),
+ mainColors = new WallpaperColors(Color.valueOf(Color.BLUE),
Color.valueOf(Color.RED), null);
mColorsListener.getValue().onColorsChanged(mainColors, WallpaperManager.FLAG_SYSTEM,
USER_SYSTEM);
@@ -1018,7 +1032,7 @@ public class ThemeOverlayControllerTest extends SysuiTestCase {
clearInvocations(mThemeOverlayApplier);
// Device went to sleep and second set of colors was applied.
- mainColors = new WallpaperColors(Color.valueOf(Color.BLUE),
+ mainColors = new WallpaperColors(Color.valueOf(Color.BLUE),
Color.valueOf(Color.RED), null);
mColorsListener.getValue().onColorsChanged(mainColors, WallpaperManager.FLAG_SYSTEM,
USER_SYSTEM);
@@ -1034,8 +1048,9 @@ public class ThemeOverlayControllerTest extends SysuiTestCase {
WallpaperColors mainColors = new WallpaperColors(Color.valueOf(Color.RED),
Color.valueOf(Color.BLUE), null);
- String jsonString =
- "{\"android.theme.customization.system_palette\":\"00FF00\"}";
+ String jsonString = createJsonString(TestColorSource.home_wallpaper, "00FF00",
+ "TONAL_SPOT");
+
when(mSecureSettings.getStringForUser(
eq(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES), anyInt()))
.thenReturn(jsonString);
@@ -1115,4 +1130,25 @@ public class ThemeOverlayControllerTest extends SysuiTestCase {
+ DynamicColors.getCustomColorsMapped(false).size() * 2)
).setResourceValue(any(String.class), eq(TYPE_INT_COLOR_ARGB8), anyInt(), eq(null));
}
+
+ private enum TestColorSource {
+ preset,
+ home_wallpaper,
+ lock_wallpaper
+ }
+
+ private String createJsonString(TestColorSource colorSource, String seedColorHex,
+ String style) {
+ return "{\"android.theme.customization.color_source\":\"" + colorSource.toString() + "\","
+ + "\"android.theme.customization.system_palette\":\"" + seedColorHex + "\","
+ + "\"android.theme.customization.accent_color\":\"" + seedColorHex + "\","
+ + "\"android.theme.customization.color_index\":\"2\","
+ + "\"android.theme.customization.theme_style\":\"" + style + "\"}";
+ }
+
+ private String createJsonString(TestColorSource colorSource) {
+ return createJsonString(colorSource, "A16B00", "TONAL_SPOT");
+ }
+
+
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioSharingStreamSliderViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioSharingStreamSliderViewModelTest.kt
index e484d8090c64..04ab98889755 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioSharingStreamSliderViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioSharingStreamSliderViewModelTest.kt
@@ -19,21 +19,17 @@ package com.android.systemui.volume.panel.component.volume.slider.ui.viewmodel
import android.bluetooth.BluetoothDevice
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
-import com.android.internal.logging.uiEventLogger
import com.android.settingslib.bluetooth.CachedBluetoothDevice
import com.android.systemui.SysuiTestCase
import com.android.systemui.common.shared.model.Icon
-import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.haptics.slider.sliderHapticsViewModelFactory
-import com.android.systemui.kosmos.testScope
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.kosmos.collectLastValue
+import com.android.systemui.kosmos.runCurrent
+import com.android.systemui.kosmos.runTest
import com.android.systemui.res.R
import com.android.systemui.testKosmos
import com.android.systemui.volume.data.repository.audioSharingRepository
-import com.android.systemui.volume.domain.interactor.audioSharingInteractor
import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.test.runCurrent
-import kotlinx.coroutines.test.runTest
-import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.mock
@@ -43,47 +39,30 @@ import org.mockito.kotlin.mock
class AudioSharingStreamSliderViewModelTest : SysuiTestCase() {
private val kosmos = testKosmos()
- private val testScope = kosmos.testScope
- private lateinit var stream: AudioSharingStreamSliderViewModel
-
- @Before
- fun setUp() {
- stream = audioSharingStreamSliderViewModel()
- }
-
- private fun audioSharingStreamSliderViewModel(): AudioSharingStreamSliderViewModel {
- return AudioSharingStreamSliderViewModel(
- testScope.backgroundScope,
- context,
- kosmos.audioSharingInteractor,
- kosmos.uiEventLogger,
- kosmos.sliderHapticsViewModelFactory,
- )
- }
+ private val underTest: AudioSharingStreamSliderViewModel =
+ with(kosmos) { audioSharingStreamSliderViewModelFactory.create(applicationCoroutineScope) }
@Test
fun slider_media_inAudioSharing() =
- with(kosmos) {
- testScope.runTest {
- val audioSharingSlider by collectLastValue(stream.slider)
+ kosmos.runTest {
+ val audioSharingSlider by collectLastValue(underTest.slider)
- val bluetoothDevice: BluetoothDevice = mock {}
- val cachedDevice: CachedBluetoothDevice = mock {
- on { groupId }.thenReturn(123)
- on { device }.thenReturn(bluetoothDevice)
- on { name }.thenReturn("my headset 2")
- }
- audioSharingRepository.setSecondaryDevice(cachedDevice)
+ val bluetoothDevice: BluetoothDevice = mock {}
+ val cachedDevice: CachedBluetoothDevice = mock {
+ on { groupId }.thenReturn(123)
+ on { device }.thenReturn(bluetoothDevice)
+ on { name }.thenReturn("my headset 2")
+ }
+ audioSharingRepository.setSecondaryDevice(cachedDevice)
- audioSharingRepository.setInAudioSharing(true)
- audioSharingRepository.setSecondaryGroupId(123)
+ audioSharingRepository.setInAudioSharing(true)
+ audioSharingRepository.setSecondaryGroupId(123)
- runCurrent()
+ runCurrent()
- assertThat(audioSharingSlider!!.label).isEqualTo("my headset 2")
- assertThat(audioSharingSlider!!.icon)
- .isEqualTo(Icon.Resource(R.drawable.ic_volume_media_bt, null))
- }
+ assertThat(audioSharingSlider!!.label).isEqualTo("my headset 2")
+ assertThat(audioSharingSlider!!.icon)
+ .isEqualTo(Icon.Resource(R.drawable.ic_volume_media_bt, null))
}
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/wallpapers/data/repository/WallpaperRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/wallpapers/data/repository/WallpaperRepositoryImplTest.kt
index 7c166de81502..cc6a7b93eef3 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/wallpapers/data/repository/WallpaperRepositoryImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/wallpapers/data/repository/WallpaperRepositoryImplTest.kt
@@ -42,7 +42,6 @@ import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
-import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.any
@@ -53,7 +52,6 @@ import org.mockito.kotlin.whenever
@SmallTest
@RunWith(AndroidJUnit4::class)
class WallpaperRepositoryImplTest : SysuiTestCase() {
-
private var isWallpaperSupported = true
private val kosmos =
testKosmos().apply {
@@ -293,12 +291,9 @@ class WallpaperRepositoryImplTest : SysuiTestCase() {
Intent(Intent.ACTION_WALLPAPER_CHANGED),
)
assertThat(latest).isTrue()
- assertThat(underTest.sendLockscreenLayoutJob).isNotNull()
- assertThat(underTest.sendLockscreenLayoutJob!!.isActive).isEqualTo(true)
}
@Test
- @Ignore("ag/31591766")
@EnableFlags(SharedFlags.FLAG_EXTENDED_WALLPAPER_EFFECTS)
fun shouldSendNotificationLayout_setNotExtendedEffectsWallpaper_cancelSendLayoutJob() =
testScope.runTest {
@@ -315,8 +310,6 @@ class WallpaperRepositoryImplTest : SysuiTestCase() {
Intent(Intent.ACTION_WALLPAPER_CHANGED),
)
assertThat(latest).isTrue()
- assertThat(underTest.sendLockscreenLayoutJob).isNotNull()
- assertThat(underTest.sendLockscreenLayoutJob!!.isActive).isEqualTo(true)
whenever(kosmos.wallpaperManager.getWallpaperInfoForUser(any()))
.thenReturn(UNSUPPORTED_WP)
@@ -327,7 +320,6 @@ class WallpaperRepositoryImplTest : SysuiTestCase() {
runCurrent()
assertThat(latest).isFalse()
- assertThat(underTest.sendLockscreenLayoutJob?.isCancelled).isEqualTo(true)
}
private companion object {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/wallpapers/domain/interactor/WallpaperFocalAreaInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/wallpapers/domain/interactor/WallpaperFocalAreaInteractorTest.kt
index cd6e18a69c4d..31a611cc984b 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/wallpapers/domain/interactor/WallpaperFocalAreaInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/wallpapers/domain/interactor/WallpaperFocalAreaInteractorTest.kt
@@ -30,13 +30,8 @@ import com.android.systemui.kosmos.testScope
import com.android.systemui.res.R
import com.android.systemui.shade.data.repository.ShadeRepository
import com.android.systemui.shade.data.repository.shadeRepository
-import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository
-import com.android.systemui.statusbar.notification.data.repository.setActiveNotifs
-import com.android.systemui.statusbar.notification.domain.interactor.activeNotificationsInteractor
import com.android.systemui.testKosmos
-import com.android.systemui.wallpapers.data.repository.fakeWallpaperFocalAreaRepository
import com.android.systemui.wallpapers.data.repository.wallpaperFocalAreaRepository
-import com.android.systemui.wallpapers.data.repository.wallpaperRepository
import com.android.systemui.wallpapers.ui.viewmodel.wallpaperFocalAreaViewModel
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -77,40 +72,26 @@ class WallpaperFocalAreaInteractorTest : SysuiTestCase() {
.thenReturn(2f)
underTest =
WallpaperFocalAreaInteractor(
- applicationScope = testScope.backgroundScope,
context = kosmos.mockedContext,
- wallpaperFocalAreaRepository = kosmos.fakeWallpaperFocalAreaRepository,
+ wallpaperFocalAreaRepository = kosmos.wallpaperFocalAreaRepository,
shadeRepository = kosmos.shadeRepository,
- activeNotificationsInteractor = kosmos.activeNotificationsInteractor,
- wallpaperRepository = kosmos.wallpaperRepository,
)
}
- private fun overrideMockedResources(overrideResources: OverrideResources) {
- val displayMetrics =
- DisplayMetrics().apply {
- widthPixels = overrideResources.screenWidth
- heightPixels = overrideResources.screenHeight
- density = 2f
- }
- whenever(mockedResources.displayMetrics).thenReturn(displayMetrics)
- whenever(mockedResources.getBoolean(R.bool.center_align_focal_area_shape))
- .thenReturn(overrideResources.centerAlignFocalArea)
- }
-
@Test
fun focalAreaBounds_withoutNotifications_inHandheldDevices() =
testScope.runTest {
overrideMockedResources(
+ mockedResources,
OverrideResources(
screenWidth = 1000,
screenHeight = 2000,
centerAlignFocalArea = false,
- )
+ ),
)
val bounds by collectLastValue(underTest.wallpaperFocalAreaBounds)
kosmos.shadeRepository.setShadeLayoutWide(false)
- kosmos.activeNotificationListRepository.setActiveNotifs(0)
+
kosmos.wallpaperFocalAreaRepository.setShortcutAbsoluteTop(1800F)
kosmos.wallpaperFocalAreaRepository.setNotificationDefaultTop(400F)
kosmos.wallpaperFocalAreaRepository.setNotificationStackAbsoluteBottom(400F)
@@ -122,15 +103,15 @@ class WallpaperFocalAreaInteractorTest : SysuiTestCase() {
fun focalAreaBounds_withNotifications_inHandheldDevices() =
testScope.runTest {
overrideMockedResources(
+ mockedResources,
OverrideResources(
screenWidth = 1000,
screenHeight = 2000,
centerAlignFocalArea = false,
- )
+ ),
)
val bounds by collectLastValue(underTest.wallpaperFocalAreaBounds)
kosmos.shadeRepository.setShadeLayoutWide(false)
- kosmos.activeNotificationListRepository.setActiveNotifs(1)
kosmos.wallpaperFocalAreaRepository.setShortcutAbsoluteTop(1800F)
kosmos.wallpaperFocalAreaRepository.setNotificationDefaultTop(400F)
kosmos.wallpaperFocalAreaRepository.setNotificationStackAbsoluteBottom(600F)
@@ -139,58 +120,38 @@ class WallpaperFocalAreaInteractorTest : SysuiTestCase() {
}
@Test
- fun focalAreaBounds_withNotifications_inUnfoldLandscape() =
+ fun focalAreaBounds_inUnfoldLandscape() =
testScope.runTest {
overrideMockedResources(
+ mockedResources,
OverrideResources(
screenWidth = 2000,
screenHeight = 1600,
centerAlignFocalArea = false,
- )
+ ),
)
val bounds by collectLastValue(underTest.wallpaperFocalAreaBounds)
kosmos.shadeRepository.setShadeLayoutWide(true)
- kosmos.activeNotificationListRepository.setActiveNotifs(1)
- kosmos.wallpaperFocalAreaRepository.setShortcutAbsoluteTop(1400F)
- kosmos.wallpaperFocalAreaRepository.setNotificationDefaultTop(400F)
- kosmos.wallpaperFocalAreaRepository.setNotificationStackAbsoluteBottom(600F)
-
- assertThat(bounds).isEqualTo(RectF(500f, 600F, 1000F, 1100F))
- }
-
- @Test
- fun focalAreaBounds_withoutNotifications_inUnfoldLandscape() =
- testScope.runTest {
- overrideMockedResources(
- OverrideResources(
- screenWidth = 2000,
- screenHeight = 1600,
- centerAlignFocalArea = false,
- )
- )
- val bounds by collectLastValue(underTest.wallpaperFocalAreaBounds)
- kosmos.shadeRepository.setShadeLayoutWide(true)
- kosmos.activeNotificationListRepository.setActiveNotifs(0)
kosmos.wallpaperFocalAreaRepository.setShortcutAbsoluteTop(1400F)
kosmos.wallpaperFocalAreaRepository.setNotificationDefaultTop(400F)
kosmos.wallpaperFocalAreaRepository.setNotificationStackAbsoluteBottom(400F)
- assertThat(bounds).isEqualTo(RectF(1000f, 600F, 1500F, 1100F))
+ assertThat(bounds).isEqualTo(RectF(600f, 600F, 1400F, 1100F))
}
@Test
fun focalAreaBounds_withNotifications_inUnfoldPortrait() =
testScope.runTest {
overrideMockedResources(
+ mockedResources,
OverrideResources(
screenWidth = 1600,
screenHeight = 2000,
centerAlignFocalArea = false,
- )
+ ),
)
val bounds by collectLastValue(underTest.wallpaperFocalAreaBounds)
kosmos.shadeRepository.setShadeLayoutWide(false)
- kosmos.activeNotificationListRepository.setActiveNotifs(1)
kosmos.wallpaperFocalAreaRepository.setShortcutAbsoluteTop(1800F)
kosmos.wallpaperFocalAreaRepository.setNotificationDefaultTop(400F)
kosmos.wallpaperFocalAreaRepository.setNotificationStackAbsoluteBottom(600F)
@@ -202,15 +163,15 @@ class WallpaperFocalAreaInteractorTest : SysuiTestCase() {
fun focalAreaBounds_withoutNotifications_inUnfoldPortrait() =
testScope.runTest {
overrideMockedResources(
+ mockedResources,
OverrideResources(
screenWidth = 1600,
screenHeight = 2000,
centerAlignFocalArea = false,
- )
+ ),
)
val bounds by collectLastValue(underTest.wallpaperFocalAreaBounds)
kosmos.shadeRepository.setShadeLayoutWide(false)
- kosmos.activeNotificationListRepository.setActiveNotifs(0)
kosmos.wallpaperFocalAreaRepository.setShortcutAbsoluteTop(1800F)
kosmos.wallpaperFocalAreaRepository.setNotificationDefaultTop(400F)
kosmos.wallpaperFocalAreaRepository.setNotificationStackAbsoluteBottom(600F)
@@ -219,18 +180,18 @@ class WallpaperFocalAreaInteractorTest : SysuiTestCase() {
}
@Test
- fun focalAreaBounds_withNotifications_inTabletLandscape() =
+ fun focalAreaBounds_inTabletLandscape() =
testScope.runTest {
overrideMockedResources(
+ mockedResources,
OverrideResources(
screenWidth = 3000,
screenHeight = 2000,
centerAlignFocalArea = true,
- )
+ ),
)
val bounds by collectLastValue(underTest.wallpaperFocalAreaBounds)
kosmos.shadeRepository.setShadeLayoutWide(true)
- kosmos.activeNotificationListRepository.setActiveNotifs(1)
kosmos.wallpaperFocalAreaRepository.setShortcutAbsoluteTop(1800F)
kosmos.wallpaperFocalAreaRepository.setNotificationDefaultTop(200F)
kosmos.wallpaperFocalAreaRepository.setNotificationStackAbsoluteBottom(200F)
@@ -239,35 +200,16 @@ class WallpaperFocalAreaInteractorTest : SysuiTestCase() {
}
@Test
- fun focalAreaBounds_withoutNotifications_inTabletLandscape() =
- testScope.runTest {
- overrideMockedResources(
- OverrideResources(
- screenWidth = 3000,
- screenHeight = 2000,
- centerAlignFocalArea = true,
- )
- )
- val bounds by collectLastValue(underTest.wallpaperFocalAreaBounds)
- kosmos.shadeRepository.setShadeLayoutWide(true)
- kosmos.activeNotificationListRepository.setActiveNotifs(0)
- kosmos.wallpaperFocalAreaRepository.setShortcutAbsoluteTop(1800F)
- kosmos.wallpaperFocalAreaRepository.setNotificationDefaultTop(400F)
- kosmos.wallpaperFocalAreaRepository.setNotificationStackAbsoluteBottom(400F)
-
- assertThat(bounds).isEqualTo(RectF(1000f, 600F, 2000F, 1400F))
- }
-
- @Test
fun onTap_inFocalBounds() =
testScope.runTest {
kosmos.wallpaperFocalAreaRepository.setTapPosition(PointF(0F, 0F))
overrideMockedResources(
+ mockedResources,
OverrideResources(
screenWidth = 1000,
screenHeight = 2000,
centerAlignFocalArea = false,
- )
+ ),
)
kosmos.wallpaperFocalAreaRepository.setWallpaperFocalAreaBounds(
RectF(250f, 700F, 750F, 1400F)
@@ -287,11 +229,12 @@ class WallpaperFocalAreaInteractorTest : SysuiTestCase() {
testScope.runTest {
kosmos.wallpaperFocalAreaRepository.setTapPosition(PointF(0F, 0F))
overrideMockedResources(
+ mockedResources,
OverrideResources(
screenWidth = 1000,
screenHeight = 2000,
centerAlignFocalArea = false,
- )
+ ),
)
kosmos.wallpaperFocalAreaViewModel = mock()
kosmos.wallpaperFocalAreaRepository.setWallpaperFocalAreaBounds(
@@ -309,4 +252,21 @@ class WallpaperFocalAreaInteractorTest : SysuiTestCase() {
val screenHeight: Int,
val centerAlignFocalArea: Boolean,
)
+
+ companion object {
+ fun overrideMockedResources(
+ mockedResources: Resources,
+ overrideResources: OverrideResources,
+ ) {
+ val displayMetrics =
+ DisplayMetrics().apply {
+ widthPixels = overrideResources.screenWidth
+ heightPixels = overrideResources.screenHeight
+ density = 2f
+ }
+ whenever(mockedResources.displayMetrics).thenReturn(displayMetrics)
+ whenever(mockedResources.getBoolean(R.bool.center_align_focal_area_shape))
+ .thenReturn(overrideResources.centerAlignFocalArea)
+ }
+ }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/wallpapers/ui/viewmodel/WallpaperFocalAreaViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/wallpapers/ui/viewmodel/WallpaperFocalAreaViewModelTest.kt
new file mode 100644
index 000000000000..3cd20721a15b
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/wallpapers/ui/viewmodel/WallpaperFocalAreaViewModelTest.kt
@@ -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 com.android.systemui.wallpapers.ui.viewmodel
+
+import android.content.mockedContext
+import android.content.res.Resources
+import android.graphics.RectF
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
+import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN
+import com.android.systemui.keyguard.shared.model.TransitionState
+import com.android.systemui.keyguard.shared.model.TransitionStep
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.shade.data.repository.shadeRepository
+import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository
+import com.android.systemui.statusbar.notification.data.repository.setActiveNotifs
+import com.android.systemui.testKosmos
+import com.android.systemui.wallpapers.data.repository.wallpaperFocalAreaRepository
+import com.android.systemui.wallpapers.domain.interactor.WallpaperFocalAreaInteractor
+import com.android.systemui.wallpapers.domain.interactor.WallpaperFocalAreaInteractorTest.Companion.overrideMockedResources
+import com.android.systemui.wallpapers.domain.interactor.WallpaperFocalAreaInteractorTest.OverrideResources
+import com.android.systemui.wallpapers.domain.interactor.wallpaperFocalAreaInteractor
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
+
+@ExperimentalCoroutinesApi
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class WallpaperFocalAreaViewModelTest : SysuiTestCase() {
+ private val kosmos = testKosmos()
+ private val testScope = kosmos.testScope
+ private lateinit var mockedResources: Resources
+ lateinit var underTest: WallpaperFocalAreaViewModel
+
+ @Before
+ fun setup() {
+ MockitoAnnotations.initMocks(this)
+ mockedResources = mock<Resources>()
+ overrideMockedResources(
+ mockedResources,
+ OverrideResources(screenWidth = 1000, screenHeight = 2000, centerAlignFocalArea = false),
+ )
+ whenever(kosmos.mockedContext.resources).thenReturn(mockedResources)
+ whenever(
+ mockedResources.getFloat(
+ Resources.getSystem()
+ .getIdentifier(
+ /* name= */ "config_wallpaperMaxScale",
+ /* defType= */ "dimen",
+ /* defPackage= */ "android",
+ )
+ )
+ )
+ .thenReturn(2f)
+ kosmos.wallpaperFocalAreaInteractor =
+ WallpaperFocalAreaInteractor(
+ context = kosmos.mockedContext,
+ wallpaperFocalAreaRepository = kosmos.wallpaperFocalAreaRepository,
+ shadeRepository = kosmos.shadeRepository,
+ )
+ underTest =
+ WallpaperFocalAreaViewModel(
+ wallpaperFocalAreaInteractor = kosmos.wallpaperFocalAreaInteractor,
+ keyguardTransitionInteractor = kosmos.keyguardTransitionInteractor,
+ )
+ }
+
+ @Test
+ fun focalAreaBoundsSent_whenFinishTransitioningToLockscreen() =
+ testScope.runTest {
+ overrideMockedResources(
+ mockedResources,
+ OverrideResources(
+ screenWidth = 1600,
+ screenHeight = 2000,
+ centerAlignFocalArea = false,
+ ),
+ )
+ val bounds by collectLastValue(underTest.wallpaperFocalAreaBounds)
+
+ kosmos.fakeKeyguardTransitionRepository.sendTransitionSteps(
+ listOf(
+ TransitionStep(transitionState = TransitionState.STARTED, to = LOCKSCREEN),
+ TransitionStep(transitionState = TransitionState.FINISHED, to = LOCKSCREEN),
+ ),
+ testScope,
+ )
+
+ setTestFocalAreaBounds()
+
+ assertThat(bounds).isEqualTo(RectF(400F, 510F, 1200F, 700F))
+ }
+
+ @Test
+ fun focalAreaBoundsNotSent_whenNotFinishTransitioningToLockscreen() =
+ testScope.runTest {
+ overrideMockedResources(
+ mockedResources,
+ OverrideResources(
+ screenWidth = 1600,
+ screenHeight = 2000,
+ centerAlignFocalArea = false,
+ ),
+ )
+ val bounds by collectLastValue(underTest.wallpaperFocalAreaBounds)
+
+ kosmos.fakeKeyguardTransitionRepository.sendTransitionSteps(
+ listOf(TransitionStep(transitionState = TransitionState.STARTED, to = LOCKSCREEN)),
+ testScope,
+ )
+ setTestFocalAreaBounds()
+
+ assertThat(bounds).isEqualTo(null)
+ }
+
+ private fun setTestFocalAreaBounds() {
+ kosmos.shadeRepository.setShadeLayoutWide(false)
+ kosmos.activeNotificationListRepository.setActiveNotifs(0)
+ kosmos.wallpaperFocalAreaRepository.setShortcutAbsoluteTop(400F)
+ kosmos.wallpaperFocalAreaRepository.setNotificationDefaultTop(20F)
+ kosmos.wallpaperFocalAreaRepository.setNotificationStackAbsoluteBottom(20F)
+ }
+}
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockLogger.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockLogger.kt
index 9a837446a802..3ed321e48cd3 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockLogger.kt
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockLogger.kt
@@ -83,6 +83,13 @@ class ClockLogger(private val view: View?, buffer: MessageBuffer, tag: String) :
}
}
+ fun updateAxes(lsFVar: String, aodFVar: String) {
+ i({ "updateAxes(LS = $str1, AOD = $str2)" }) {
+ str1 = lsFVar
+ str2 = aodFVar
+ }
+ }
+
fun addView(child: View) {
d({ "addView($str1 @$int1)" }) {
str1 = child::class.simpleName!!
@@ -90,6 +97,14 @@ class ClockLogger(private val view: View?, buffer: MessageBuffer, tag: String) :
}
}
+ fun animateDoze() {
+ d("animateDoze()")
+ }
+
+ fun animateCharge() {
+ d("animateCharge()")
+ }
+
fun animateFidget(x: Float, y: Float) {
d({ "animateFidget($str1, $str2)" }) {
str1 = x.toString()
diff --git a/packages/SystemUI/res-keyguard/drawable/pin_bouncer_confirm.xml b/packages/SystemUI/res-keyguard/drawable/pin_bouncer_confirm.xml
new file mode 100644
index 000000000000..a27e29f1beb6
--- /dev/null
+++ b/packages/SystemUI/res-keyguard/drawable/pin_bouncer_confirm.xml
@@ -0,0 +1,31 @@
+<!--
+ ~ Copyright (C) 2025 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="40dp"
+ android:height="40dp"
+ android:viewportHeight="40"
+ android:viewportWidth="40">
+ <group>
+ <clip-path android:pathData="M8,12h24.5v15.5h-24.5z" />
+ <path
+ android:fillColor="#000000"
+ android:pathData="M30.75,12C29.79,12 29,12.79 29,13.75V25.75C29,26.71 29.79,27.5 30.75,27.5C31.71,27.5 32.5,26.71 32.5,25.75V13.75C32.5,12.79 31.71,12 30.75,12Z" />
+ <path
+ android:fillColor="#000000"
+ android:pathData="M20.98,12.92C20.3,12.24 19.19,12.24 18.51,12.92C17.83,13.6 17.83,14.71 18.51,15.39L21.12,18H9.75C8.79,18 8,18.79 8,19.75C8,20.71 8.79,21.5 9.75,21.5H21.11L18.51,24.1C18.18,24.43 18,24.87 18,25.34C18,25.81 18.18,26.25 18.52,26.58C18.86,26.92 19.31,27.09 19.75,27.09C20.19,27.09 20.65,26.92 20.99,26.58L26.61,20.96C27.28,20.29 27.28,19.21 26.61,18.55L20.98,12.92Z" />
+ </group>
+</vector>
diff --git a/packages/SystemUI/res-keyguard/drawable/pin_bouncer_delete_filled.xml b/packages/SystemUI/res-keyguard/drawable/pin_bouncer_delete_filled.xml
new file mode 100644
index 000000000000..86f95bc97169
--- /dev/null
+++ b/packages/SystemUI/res-keyguard/drawable/pin_bouncer_delete_filled.xml
@@ -0,0 +1,25 @@
+<!--
+ ~ Copyright (C) 2025 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="40dp"
+ android:height="40dp"
+ android:viewportHeight="40"
+ android:viewportWidth="40">
+ <path
+ android:fillColor="#000000"
+ android:pathData="M22.167,21.9L25.531,25.265C25.795,25.502 26.112,25.621 26.481,25.621C26.851,25.621 27.167,25.502 27.431,25.265C27.669,25.001 27.788,24.684 27.788,24.315C27.788,23.919 27.669,23.589 27.431,23.325L24.067,20L27.392,16.675C27.656,16.411 27.788,16.094 27.788,15.725C27.788,15.356 27.656,15.039 27.392,14.775C27.128,14.511 26.798,14.379 26.402,14.379C26.033,14.379 25.729,14.511 25.492,14.775L22.167,18.1L18.802,14.735C18.538,14.498 18.222,14.379 17.852,14.379C17.483,14.379 17.166,14.498 16.902,14.735C16.665,14.999 16.546,15.329 16.546,15.725C16.546,16.094 16.665,16.411 16.902,16.675L20.267,20L16.902,23.325C16.665,23.589 16.546,23.906 16.546,24.275C16.546,24.644 16.665,24.961 16.902,25.225C17.166,25.489 17.483,25.621 17.852,25.621C18.248,25.621 18.578,25.489 18.842,25.225L22.167,21.9ZM14.012,32.667C13.59,32.667 13.181,32.574 12.785,32.39C12.416,32.179 12.099,31.915 11.835,31.598L4.394,21.623C4.024,21.148 3.84,20.607 3.84,20C3.84,19.393 4.024,18.852 4.394,18.377L11.835,8.402C12.073,8.085 12.39,7.835 12.785,7.65C13.181,7.439 13.59,7.333 14.012,7.333H32.142C32.907,7.333 33.554,7.597 34.081,8.125C34.609,8.653 34.873,9.286 34.873,10.025V29.975C34.873,30.714 34.609,31.347 34.081,31.875C33.554,32.403 32.907,32.667 32.142,32.667H14.012Z" />
+</vector>
diff --git a/packages/SystemUI/res-keyguard/drawable/pin_bouncer_delete_outline.xml b/packages/SystemUI/res-keyguard/drawable/pin_bouncer_delete_outline.xml
new file mode 100644
index 000000000000..7f551f4d3c60
--- /dev/null
+++ b/packages/SystemUI/res-keyguard/drawable/pin_bouncer_delete_outline.xml
@@ -0,0 +1,31 @@
+<!--
+ ~ Copyright (C) 2025 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="40dp"
+ android:height="40dp"
+ android:viewportHeight="40"
+ android:viewportWidth="40">
+ <group>
+ <clip-path android:pathData="M5,7h29.89v25h-29.89z" />
+ <path
+ android:fillColor="#000000"
+ android:pathData="M30.96,32H15.59C14.21,32 12.89,31.34 12.06,30.24L5.78,21.86C4.74,20.47 4.74,18.54 5.78,17.15L12.06,8.77C12.89,7.67 14.21,7 15.59,7H30.96C33.13,7 34.89,8.76 34.89,10.93V28.08C34.89,30.25 33.13,32.01 30.96,32.01V32ZM14.46,28.44C14.73,28.79 15.15,29 15.59,29H30.96C31.47,29 31.89,28.58 31.89,28.07V10.93C31.89,10.42 31.47,10 30.96,10H15.59C15.15,10 14.73,10.21 14.46,10.56L8.18,18.94C7.93,19.27 7.93,19.72 8.18,20.05L14.46,28.43V28.44Z" />
+ <path
+ android:fillColor="#000000"
+ android:pathData="M22.46,21.27L25.36,24.17C25.6,24.43 25.89,24.56 26.25,24.56C26.61,24.56 26.9,24.43 27.14,24.17C27.4,23.93 27.53,23.64 27.53,23.28C27.53,22.92 27.4,22.63 27.14,22.39L24.24,19.49L27.14,16.59C27.38,16.35 27.49,16.06 27.49,15.7C27.49,15.34 27.37,15.05 27.14,14.81C26.91,14.57 26.61,14.46 26.25,14.46C25.89,14.46 25.59,14.58 25.33,14.81L22.46,17.71L19.56,14.81C19.32,14.55 19.03,14.42 18.67,14.42C18.31,14.42 18.02,14.55 17.78,14.81C17.52,15.05 17.39,15.34 17.39,15.7C17.39,16.06 17.52,16.35 17.78,16.59L20.68,19.49L17.78,22.39C17.52,22.63 17.39,22.92 17.39,23.28C17.39,23.64 17.52,23.93 17.78,24.17C18.02,24.41 18.31,24.52 18.67,24.52C19.03,24.52 19.32,24.4 19.56,24.17L22.46,21.27Z" />
+ </group>
+</vector>
diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_bouncer_message_area.xml b/packages/SystemUI/res-keyguard/layout/keyguard_bouncer_message_area.xml
index 0b35559148af..87d06bfde743 100644
--- a/packages/SystemUI/res-keyguard/layout/keyguard_bouncer_message_area.xml
+++ b/packages/SystemUI/res-keyguard/layout/keyguard_bouncer_message_area.xml
@@ -21,7 +21,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/keyguard_lock_padding"
- android:importantForAccessibility="no"
+ android:accessibilityLiveRegion="polite"
android:ellipsize="marquee"
android:focusable="false"
android:gravity="center"
diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_password_motion_layout.xml b/packages/SystemUI/res-keyguard/layout/keyguard_password_motion_layout.xml
index f231df2f1a10..c7f320c69113 100644
--- a/packages/SystemUI/res-keyguard/layout/keyguard_password_motion_layout.xml
+++ b/packages/SystemUI/res-keyguard/layout/keyguard_password_motion_layout.xml
@@ -67,6 +67,7 @@
<com.android.systemui.bouncer.ui.BouncerMessageView
android:id="@+id/bouncer_message_view"
android:screenReaderFocusable="true"
+ android:accessibilityLiveRegion="polite"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical" />
diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_password_view.xml b/packages/SystemUI/res-keyguard/layout/keyguard_password_view.xml
index 04457229d573..9359838238af 100644
--- a/packages/SystemUI/res-keyguard/layout/keyguard_password_view.xml
+++ b/packages/SystemUI/res-keyguard/layout/keyguard_password_view.xml
@@ -32,6 +32,7 @@
<com.android.systemui.bouncer.ui.BouncerMessageView
android:id="@+id/bouncer_message_view"
android:screenReaderFocusable="true"
+ android:accessibilityLiveRegion="polite"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_pattern_motion_layout.xml b/packages/SystemUI/res-keyguard/layout/keyguard_pattern_motion_layout.xml
index b184344f2f24..6cbe96a8cb50 100644
--- a/packages/SystemUI/res-keyguard/layout/keyguard_pattern_motion_layout.xml
+++ b/packages/SystemUI/res-keyguard/layout/keyguard_pattern_motion_layout.xml
@@ -68,6 +68,7 @@
<com.android.systemui.bouncer.ui.BouncerMessageView
android:id="@+id/bouncer_message_view"
android:screenReaderFocusable="true"
+ android:accessibilityLiveRegion="polite"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical" />
diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_pattern_view.xml b/packages/SystemUI/res-keyguard/layout/keyguard_pattern_view.xml
index 0e15ff66f3ee..cf388875a174 100644
--- a/packages/SystemUI/res-keyguard/layout/keyguard_pattern_view.xml
+++ b/packages/SystemUI/res-keyguard/layout/keyguard_pattern_view.xml
@@ -36,6 +36,7 @@
<com.android.systemui.bouncer.ui.BouncerMessageView
android:id="@+id/bouncer_message_view"
android:screenReaderFocusable="true"
+ android:accessibilityLiveRegion="polite"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical" />
diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_pin_motion_layout.xml b/packages/SystemUI/res-keyguard/layout/keyguard_pin_motion_layout.xml
index f6ac02aee657..33eab179c3f7 100644
--- a/packages/SystemUI/res-keyguard/layout/keyguard_pin_motion_layout.xml
+++ b/packages/SystemUI/res-keyguard/layout/keyguard_pin_motion_layout.xml
@@ -75,6 +75,7 @@
<com.android.systemui.bouncer.ui.BouncerMessageView
android:id="@+id/bouncer_message_view"
android:screenReaderFocusable="true"
+ android:accessibilityLiveRegion="polite"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical" />
diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_pin_view.xml b/packages/SystemUI/res-keyguard/layout/keyguard_pin_view.xml
index ba4da794d777..fd5eeb8b9408 100644
--- a/packages/SystemUI/res-keyguard/layout/keyguard_pin_view.xml
+++ b/packages/SystemUI/res-keyguard/layout/keyguard_pin_view.xml
@@ -33,6 +33,7 @@
<com.android.systemui.bouncer.ui.BouncerMessageView
android:id="@+id/bouncer_message_view"
android:screenReaderFocusable="true"
+ android:accessibilityLiveRegion="polite"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical" />
diff --git a/packages/SystemUI/res-keyguard/values/dimens.xml b/packages/SystemUI/res-keyguard/values/dimens.xml
index bfb37a0d97a7..6d446453d9f7 100644
--- a/packages/SystemUI/res-keyguard/values/dimens.xml
+++ b/packages/SystemUI/res-keyguard/values/dimens.xml
@@ -31,6 +31,10 @@
<!-- height for the keyguard pin input field -->
<dimen name="keyguard_pin_field_height">56dp</dimen>
+ <dimen name="keyguard_pattern_dot_size">16dp</dimen>
+ <dimen name="keyguard_pattern_activated_dot_size">24dp</dimen>
+ <dimen name="keyguard_pattern_stroke_width">32dp</dimen>
+
<!-- height for the keyguard password input field -->
<dimen name="keyguard_password_field_height">56dp</dimen>
diff --git a/packages/SystemUI/res/layout/media_session_view.xml b/packages/SystemUI/res/layout/media_session_view.xml
index 109e63c6167a..4472373f99a6 100644
--- a/packages/SystemUI/res/layout/media_session_view.xml
+++ b/packages/SystemUI/res/layout/media_session_view.xml
@@ -93,7 +93,7 @@
android:layout_width="0dp"
android:layout_height="0dp"
android:orientation="vertical"
- app:layout_constraintGuide_end="@dimen/qs_media_session_collapsed_guideline" />
+ app:layout_constraintGuide_end="@dimen/qs_media_session_collapsed_legacy_guideline" />
<!-- App icon -->
<com.android.internal.widget.CachingIconView
diff --git a/packages/SystemUI/res/layout/notification_2025_hybrid.xml b/packages/SystemUI/res/layout/notification_2025_hybrid.xml
index 8fd10fb3ddb8..8c34cd4165e0 100644
--- a/packages/SystemUI/res/layout/notification_2025_hybrid.xml
+++ b/packages/SystemUI/res/layout/notification_2025_hybrid.xml
@@ -29,7 +29,6 @@
android:layout_height="wrap_content"
android:singleLine="true"
android:textAppearance="@*android:style/TextAppearance.DeviceDefault.Notification.Title"
- android:textSize="@*android:dimen/notification_2025_title_text_size"
android:paddingEnd="4dp"
/>
<TextView
diff --git a/packages/SystemUI/res/layout/notification_2025_hybrid_conversation.xml b/packages/SystemUI/res/layout/notification_2025_hybrid_conversation.xml
index 35f2ef901bdd..a338e4c70cfa 100644
--- a/packages/SystemUI/res/layout/notification_2025_hybrid_conversation.xml
+++ b/packages/SystemUI/res/layout/notification_2025_hybrid_conversation.xml
@@ -54,7 +54,6 @@
android:singleLine="true"
android:paddingEnd="4dp"
android:textAppearance="@*android:style/TextAppearance.DeviceDefault.Notification.Title"
- android:textSize="@*android:dimen/notification_2025_title_text_size"
/>
<TextView
diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml
index 09aa2241e42b..8d10e393b5ca 100644
--- a/packages/SystemUI/res/values/config.xml
+++ b/packages/SystemUI/res/values/config.xml
@@ -119,7 +119,7 @@
<!-- Tiles native to System UI. Order should match "quick_settings_tiles_default" -->
<string name="quick_settings_tiles_stock" translatable="false">
- internet,bt,flashlight,dnd,alarm,airplane,controls,wallet,rotation,battery,cast,screenrecord,mictoggle,cameratoggle,location,hotspot,inversion,saver,dark,work,night,reverse,reduce_brightness,qr_code_scanner,onehanded,color_correction,dream,font_scaling,record_issue,hearing_devices,notes
+ internet,bt,flashlight,dnd,alarm,airplane,controls,wallet,rotation,battery,cast,screenrecord,mictoggle,cameratoggle,location,hotspot,inversion,saver,dark,work,night,reverse,reduce_brightness,qr_code_scanner,onehanded,color_correction,dream,font_scaling,record_issue,hearing_devices,notes,desktopeffects
</string>
<!-- The tiles to display in QuickSettings -->
@@ -175,6 +175,9 @@
<!-- Minimum display time for a heads up notification if throttling is enabled, in milliseconds. -->
<integer name="heads_up_notification_minimum_time_with_throttling">500</integer>
+ <!-- Minimum display time for a heads up notification that was shown from a user action (like tapping on a different part of the UI), in milliseconds. -->
+ <integer name="heads_up_notification_minimum_time_for_user_initiated">3000</integer>
+
<!-- Display time for a sticky heads up notification, in milliseconds. -->
<integer name="sticky_heads_up_notification_time">60000</integer>
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 3b1e609d86e2..8cd4c1bb3533 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -203,7 +203,7 @@
<!-- Size of the view displaying the mobile signal icon in the status bar. This value should
match the core/status_bar_system_icon_size and change to sp unit -->
<dimen name="status_bar_mobile_signal_size">15sp</dimen>
- <dimen name="status_bar_mobile_signal_size_updated">14sp</dimen>
+ <dimen name="status_bar_mobile_signal_size_updated">12sp</dimen>
<!-- Size of the view displaying the mobile signal icon in the status bar. This value should
match the viewport height of mobile signal drawables such as ic_lte_mobiledata -->
<dimen name="status_bar_mobile_type_size">16sp</dimen>
@@ -1310,7 +1310,8 @@
<dimen name="qs_media_seekbar_progress_amplitude">1.5dp</dimen>
<dimen name="qs_media_seekbar_progress_phase">8dp</dimen>
<dimen name="qs_media_seekbar_progress_stroke_width">2dp</dimen>
- <dimen name="qs_media_session_collapsed_guideline">144dp</dimen>
+ <dimen name="qs_media_session_collapsed_legacy_guideline">144dp</dimen>
+ <dimen name="qs_media_session_collapsed_guideline">168dp</dimen>
<!-- Size of Smartspace media recommendations cards in the QSPanel carousel -->
<dimen name="qs_media_rec_default_width">380dp</dimen>
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index e077b41a6f59..c0eea15b043b 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -2341,7 +2341,9 @@
<!-- User visible title for the keyboard shortcut that enters split screen with current app on the left [CHAR LIMIT=70] -->
<string name="system_multitasking_lhs">Use split screen with app on the left</string>
<!-- User visible title for the keyboard shortcut that switches from split screen to full screen [CHAR LIMIT=70] -->
- <string name="system_multitasking_full_screen">Switch to full screen</string>
+ <string name="system_multitasking_full_screen">Use full screen</string>
+ <!-- User visible title for the keyboard shortcut that switches to desktop view [CHAR LIMIT=70] -->
+ <string name="system_multitasking_desktop_view">Use desktop view</string>
<!-- User visible title for the keyboard shortcut that switches to app on right or below while using split screen [CHAR LIMIT=70] -->
<string name="system_multitasking_splitscreen_focus_rhs">Switch to app on right or below while using split screen</string>
<!-- User visible title for the keyboard shortcut that switches to app on left or above while using split screen [CHAR LIMIT=70] -->
diff --git a/packages/SystemUI/res/values/tiles_states_strings.xml b/packages/SystemUI/res/values/tiles_states_strings.xml
index d885e00fbe82..faf06f3d39f0 100644
--- a/packages/SystemUI/res/values/tiles_states_strings.xml
+++ b/packages/SystemUI/res/values/tiles_states_strings.xml
@@ -358,4 +358,14 @@
<item>Off</item>
<item>On</item>
</string-array>
+
+ <!-- State names for desktop effects tile: unavailable, off, on.
+ This subtitle is shown when the tile is in that particular state but does not set its own
+ subtitle, so some of these may never appear on screen. They should still be translated as
+ if they could appear. [CHAR LIMIT=32] -->
+ <string-array name="tile_states_desktopeffects">
+ <item>Unavailable</item>
+ <item>Off</item>
+ <item>On</item>
+ </string-array>
</resources> \ No newline at end of file
diff --git a/packages/SystemUI/res/xml/media_session_collapsed.xml b/packages/SystemUI/res/xml/media_session_collapsed.xml
index 66c54a389c8e..b5efd04eeba9 100644
--- a/packages/SystemUI/res/xml/media_session_collapsed.xml
+++ b/packages/SystemUI/res/xml/media_session_collapsed.xml
@@ -64,6 +64,13 @@
app:layout_constraintBottom_toBottomOf="@+id/album_art" />
<Constraint
+ android:id="@+id/action_button_guideline"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:orientation="vertical"
+ app:layout_constraintGuide_end="@dimen/qs_media_session_collapsed_legacy_guideline" />
+
+ <Constraint
android:id="@+id/header_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardMessageAreaController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardMessageAreaController.java
index f528ec8af134..860a496ef18b 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardMessageAreaController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardMessageAreaController.java
@@ -20,22 +20,16 @@ import android.content.res.ColorStateList;
import android.content.res.Configuration;
import android.hardware.biometrics.BiometricSourceType;
import android.os.SystemClock;
-import android.text.Editable;
-import android.text.TextUtils;
-import android.text.TextWatcher;
import android.util.Log;
import android.util.Pair;
import android.view.View;
import androidx.annotation.Nullable;
-import androidx.annotation.VisibleForTesting;
import com.android.systemui.statusbar.policy.ConfigurationController;
import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener;
import com.android.systemui.util.ViewController;
-import java.lang.ref.WeakReference;
-
import javax.inject.Inject;
/**
@@ -54,39 +48,8 @@ public class KeyguardMessageAreaController<T extends KeyguardMessageArea>
private Pair<BiometricSourceType, Long> mMessageBiometricSource = null;
private static final Long SKIP_SHOWING_FACE_MESSAGE_AFTER_FP_MESSAGE_MS = 3500L;
- /**
- * Delay before speaking an accessibility announcement. Used to prevent
- * lift-to-type from interrupting itself.
- */
- private static final long ANNOUNCEMENT_DELAY = 250;
private final KeyguardUpdateMonitor mKeyguardUpdateMonitor;
private final ConfigurationController mConfigurationController;
- private final AnnounceRunnable mAnnounceRunnable;
- private final TextWatcher mTextWatcher = new TextWatcher() {
- @Override
- public void afterTextChanged(Editable editable) {
- CharSequence msg = editable;
- if (!TextUtils.isEmpty(msg)) {
- mView.removeCallbacks(mAnnounceRunnable);
- mAnnounceRunnable.setTextToAnnounce(msg);
- mView.postDelayed(() -> {
- if (msg == mView.getText()) {
- mAnnounceRunnable.run();
- }
- }, ANNOUNCEMENT_DELAY);
- }
- }
-
- @Override
- public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
- /* no-op */
- }
-
- @Override
- public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
- /* no-op */
- }
- };
private KeyguardUpdateMonitorCallback mInfoCallback = new KeyguardUpdateMonitorCallback() {
public void onFinishedGoingToSleep(int why) {
@@ -122,7 +85,6 @@ public class KeyguardMessageAreaController<T extends KeyguardMessageArea>
mKeyguardUpdateMonitor = keyguardUpdateMonitor;
mConfigurationController = configurationController;
- mAnnounceRunnable = new AnnounceRunnable(mView);
}
@Override
@@ -131,14 +93,12 @@ public class KeyguardMessageAreaController<T extends KeyguardMessageArea>
mKeyguardUpdateMonitor.registerCallback(mInfoCallback);
mView.setSelected(mKeyguardUpdateMonitor.isDeviceInteractive());
mView.onThemeChanged();
- mView.addTextChangedListener(mTextWatcher);
}
@Override
protected void onViewDetached() {
mConfigurationController.removeCallback(mConfigurationListener);
mKeyguardUpdateMonitor.removeCallback(mInfoCallback);
- mView.removeTextChangedListener(mTextWatcher);
}
/**
@@ -232,30 +192,4 @@ public class KeyguardMessageAreaController<T extends KeyguardMessageArea>
view, mKeyguardUpdateMonitor, mConfigurationController);
}
}
-
- /**
- * Runnable used to delay accessibility announcements.
- */
- @VisibleForTesting
- public static class AnnounceRunnable implements Runnable {
- private final WeakReference<View> mHost;
- private CharSequence mTextToAnnounce;
-
- AnnounceRunnable(View host) {
- mHost = new WeakReference<>(host);
- }
-
- /** Sets the text to announce. */
- public void setTextToAnnounce(CharSequence textToAnnounce) {
- mTextToAnnounce = textToAnnounce;
- }
-
- @Override
- public void run() {
- final View host = mHost.get();
- if (host != null && host.isVisibleToUser()) {
- host.announceForAccessibility(mTextToAnnounce);
- }
- }
- }
}
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPatternView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPatternView.java
index 5d63c2a92ba8..4a4cb7a232c5 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardPatternView.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPatternView.java
@@ -43,6 +43,8 @@ import com.android.internal.widget.LockPatternView;
import com.android.settingslib.animation.AppearAnimationCreator;
import com.android.settingslib.animation.AppearAnimationUtils;
import com.android.settingslib.animation.DisappearAnimationUtils;
+import com.android.systemui.Flags;
+import com.android.systemui.bouncer.shared.constants.PatternBouncerConstants.ColorId;
import com.android.systemui.res.R;
import com.android.systemui.statusbar.policy.DevicePostureController.DevicePostureInt;
@@ -227,6 +229,18 @@ public class KeyguardPatternView extends KeyguardInputView
super.onFinishInflate();
mLockPatternView = findViewById(R.id.lockPatternView);
+ if (Flags.bouncerUiRevamp2()) {
+ mLockPatternView.setDotColors(mContext.getColor(ColorId.dotColor), mContext.getColor(
+ ColorId.activatedDotColor));
+ mLockPatternView.setColors(mContext.getColor(ColorId.pathColor), 0, 0);
+ mLockPatternView.setDotSizes(
+ getResources().getDimensionPixelSize(R.dimen.keyguard_pattern_dot_size),
+ getResources().getDimensionPixelSize(
+ R.dimen.keyguard_pattern_activated_dot_size));
+ mLockPatternView.setPathWidth(
+ getResources().getDimensionPixelSize(R.dimen.keyguard_pattern_stroke_width));
+ mLockPatternView.setKeepDotActivated(true);
+ }
mEcaView = findViewById(R.id.keyguard_selector_fade_container);
}
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputView.java
index fbe9edfd6680..04d4c2a3cdf9 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputView.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputView.java
@@ -41,6 +41,7 @@ import androidx.annotation.CallSuper;
import com.android.app.animation.Interpolators;
import com.android.internal.widget.LockscreenCredential;
+import com.android.systemui.Flags;
import com.android.systemui.res.R;
import java.util.ArrayList;
@@ -178,7 +179,15 @@ public abstract class KeyguardPinBasedInputView extends KeyguardAbsKeyInputView
mOkButton = findViewById(R.id.key_enter);
+ if (Flags.bouncerUiRevamp2()) {
+ mOkButton.setImageResource(R.drawable.pin_bouncer_confirm);
+ }
mDeleteButton = findViewById(R.id.delete_button);
+ if (Flags.bouncerUiRevamp2()) {
+ mDeleteButton.setDrawableForTransparentMode(R.drawable.pin_bouncer_delete_filled);
+ mDeleteButton.setDefaultDrawable(R.drawable.pin_bouncer_delete_outline);
+ mDeleteButton.setImageResource(R.drawable.pin_bouncer_delete_outline);
+ }
mDeleteButton.setVisibility(View.VISIBLE);
mButtons[0] = findViewById(R.id.key0);
diff --git a/packages/SystemUI/src/com/android/keyguard/NumPadAnimator.java b/packages/SystemUI/src/com/android/keyguard/NumPadAnimator.java
index 2f74158107f2..69e4fd7c3d53 100644
--- a/packages/SystemUI/src/com/android/keyguard/NumPadAnimator.java
+++ b/packages/SystemUI/src/com/android/keyguard/NumPadAnimator.java
@@ -15,6 +15,7 @@
*/
package com.android.keyguard;
+import android.animation.Animator;
import android.animation.AnimatorSet;
import android.animation.ArgbEvaluator;
import android.animation.ValueAnimator;
@@ -33,6 +34,9 @@ import com.android.systemui.Flags;
import com.android.systemui.bouncer.shared.constants.PinBouncerConstants.Animation;
import com.android.systemui.bouncer.shared.constants.PinBouncerConstants.Color;
+import java.util.ArrayList;
+import java.util.List;
+
/**
* Provides background color and radius animations for key pad buttons.
*/
@@ -141,6 +145,7 @@ class NumPadAnimator {
mExpandAnimator.addUpdateListener(
anim -> mBackground.setCornerRadius((float) anim.getAnimatedValue()));
+ List<Animator> expandAnimators = new ArrayList<>();
ValueAnimator expandBackgroundColorAnimator = ValueAnimator.ofObject(new ArgbEvaluator(),
mNormalBackgroundColor, mPressedBackgroundColor);
expandBackgroundColorAnimator.setDuration(Animation.expansionColorDuration);
@@ -162,10 +167,27 @@ class NumPadAnimator {
}
});
+ expandAnimators.add(mExpandAnimator);
+ expandAnimators.add(expandBackgroundColorAnimator);
+ expandAnimators.add(expandTextColorAnimator);
+
+ if (Flags.bouncerUiRevamp2()) {
+ ValueAnimator expandTextScaleAnimator = ValueAnimator.ofFloat(
+ Animation.normalTextScaleX, Animation.pressedTextScaleX);
+ expandTextScaleAnimator.setInterpolator(Animation.expansionInterpolator);
+ expandTextScaleAnimator.setDuration(Animation.expansionDuration);
+ expandTextScaleAnimator.addUpdateListener(valueAnimator -> {
+ if (mDigitTextView != null) {
+ mDigitTextView.setTextScaleX((Float) valueAnimator.getAnimatedValue());
+ }
+ });
+ expandAnimators.add(expandTextScaleAnimator);
+ }
+
mExpandAnimatorSet = new AnimatorSet();
- mExpandAnimatorSet.playTogether(mExpandAnimator,
- expandBackgroundColorAnimator, expandTextColorAnimator);
+ mExpandAnimatorSet.playTogether(expandAnimators);
+ List<Animator> contractAnimators = new ArrayList<>();
mContractAnimator = ValueAnimator.ofFloat(1f, 0f);
mContractAnimator.setStartDelay(Animation.contractionStartDelay);
mContractAnimator.setDuration(Animation.contractionDuration);
@@ -195,9 +217,24 @@ class NumPadAnimator {
}
});
+ contractAnimators.add(mContractAnimator);
+ contractAnimators.add(contractBackgroundColorAnimator);
+ contractAnimators.add(contractTextColorAnimator);
+
+ if (Flags.bouncerUiRevamp2()) {
+ ValueAnimator contractTextScaleAnimator = ValueAnimator.ofFloat(
+ Animation.pressedTextScaleX, Animation.normalTextScaleX);
+ contractTextScaleAnimator.setInterpolator(Animation.contractionRadiusInterpolator);
+ contractTextScaleAnimator.setDuration(Animation.contractionDuration);
+ contractTextScaleAnimator.addUpdateListener(valueAnimator -> {
+ if (mDigitTextView != null) {
+ mDigitTextView.setTextScaleX((Float) valueAnimator.getAnimatedValue());
+ }
+ });
+ contractAnimators.add(contractTextScaleAnimator);
+ }
mContractAnimatorSet = new AnimatorSet();
- mContractAnimatorSet.playTogether(mContractAnimator,
- contractBackgroundColorAnimator, contractTextColorAnimator);
+ mContractAnimatorSet.playTogether(contractAnimators);
}
}
diff --git a/packages/SystemUI/src/com/android/keyguard/NumPadButton.java b/packages/SystemUI/src/com/android/keyguard/NumPadButton.java
index 0ff93236a856..584ebb50520a 100644
--- a/packages/SystemUI/src/com/android/keyguard/NumPadButton.java
+++ b/packages/SystemUI/src/com/android/keyguard/NumPadButton.java
@@ -25,6 +25,7 @@ import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.accessibility.AccessibilityNodeInfo;
+import androidx.annotation.DrawableRes;
import androidx.annotation.Nullable;
import com.android.systemui.Flags;
@@ -42,6 +43,12 @@ public class NumPadButton extends AlphaOptimizedImageButton implements NumPadAni
private int mStyleAttr;
private boolean mIsTransparentMode;
+ @DrawableRes
+ private int mDrawableForTransparentMode = 0;
+
+ @DrawableRes
+ private int mDefaultDrawable = 0;
+
public NumPadButton(Context context, AttributeSet attrs) {
super(context, attrs);
mStyleAttr = attrs.getStyleAttribute();
@@ -123,8 +130,14 @@ public class NumPadButton extends AlphaOptimizedImageButton implements NumPadAni
mIsTransparentMode = isTransparentMode;
if (isTransparentMode) {
+ if (mDrawableForTransparentMode != 0) {
+ setImageResource(mDrawableForTransparentMode);
+ }
setBackgroundColor(getResources().getColor(android.R.color.transparent));
} else {
+ if (mDefaultDrawable != 0) {
+ setImageResource(mDefaultDrawable);
+ }
Drawable bgDrawable = getContext().getDrawable(R.drawable.num_pad_key_background);
if (Flags.bouncerUiRevamp2() && bgDrawable != null) {
bgDrawable.setTint(Color.actionBg);
@@ -154,4 +167,19 @@ public class NumPadButton extends AlphaOptimizedImageButton implements NumPadAni
super.onInitializeAccessibilityNodeInfo(info);
info.setTextEntryKey(true);
}
+
+ /**
+ * Drawable to use when transparent mode is enabled
+ */
+ public void setDrawableForTransparentMode(@DrawableRes int drawableResId) {
+ mDrawableForTransparentMode = drawableResId;
+ }
+
+ /**
+ * Drawable to use when transparent mode is not enabled.
+ */
+ public void setDefaultDrawable(@DrawableRes int drawableResId) {
+ mDefaultDrawable = drawableResId;
+ setImageResource(mDefaultDrawable);
+ }
}
diff --git a/packages/SystemUI/src/com/android/keyguard/NumPadKey.java b/packages/SystemUI/src/com/android/keyguard/NumPadKey.java
index b152ff348e22..56aadc342424 100644
--- a/packages/SystemUI/src/com/android/keyguard/NumPadKey.java
+++ b/packages/SystemUI/src/com/android/keyguard/NumPadKey.java
@@ -148,7 +148,7 @@ public class NumPadKey extends ViewGroup implements NumPadAnimationListener {
if (bouncerUiRevamp2()) {
mDigitText.setTypeface(
- Typeface.create(FontStyles.GSF_LABEL_LARGE_EMPHASIZED, Typeface.NORMAL));
+ Typeface.create(FontStyles.GSF_LABEL_SMALL_EMPHASIZED, Typeface.NORMAL));
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationSettings.java b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationSettings.java
index c14d28d1c08d..3b6f8f87a1a8 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationSettings.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationSettings.java
@@ -174,13 +174,14 @@ class WindowMagnificationSettings implements MagnificationGestureDetector.OnGest
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
// Notify the service to update the magnifier scale only when the progress changed is
- // triggered by user interaction on seekbar
- if (fromUser) {
- final float scale = transformProgressToScale(progress);
- // We don't need to update the persisted scale when the seekbar progress is
- // changing. The update should be triggered when the changing is ended.
- mCallback.onMagnifierScale(scale, /* updatePersistence= */ false);
+ // triggered by user interaction on seekbar.
+ if (!fromUser) {
+ return;
}
+ final float scale = transformProgressToScale(progress);
+ // We don't need to update the persisted scale when the seekbar progress is
+ // changing. The update should be triggered when the changing is ended.
+ mCallback.onMagnifierScale(scale, /* updatePersistence= */ false);
}
@Override
@@ -195,7 +196,7 @@ class WindowMagnificationSettings implements MagnificationGestureDetector.OnGest
@Override
public void onUserInteractionFinalized(SeekBar seekBar, @ControlUnitType int control) {
- // Update the Settings persisted scale only when user interaction with seekbar ends
+ // Update the Settings persisted scale only when user interaction with seekbar ends.
final int progress = seekBar.getProgress();
final float scale = transformProgressToScale(progress);
mCallback.onMagnifierScale(scale, /* updatePersistence= */ true);
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/fontscaling/FontScalingDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/accessibility/fontscaling/FontScalingDialogDelegate.kt
index eaf541d7b559..76b5d823e0b6 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/fontscaling/FontScalingDialogDelegate.kt
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/fontscaling/FontScalingDialogDelegate.kt
@@ -88,7 +88,7 @@ constructor(
dialog.setPositiveButton(
R.string.quick_settings_done,
/* onClick = */ null,
- /* dismissOnClick = */ true
+ /* dismissOnClick = */ true,
)
}
@@ -102,7 +102,7 @@ constructor(
labelArray[i] =
context.resources.getString(
com.android.settingslib.R.string.font_scale_percentage,
- (strEntryValues[i].toFloat() * 100).roundToInt()
+ (strEntryValues[i].toFloat() * 100).roundToInt(),
)
}
seekBarWithIconButtonsView.setProgressStateLabels(labelArray)
@@ -132,7 +132,7 @@ constructor(
override fun onUserInteractionFinalized(
seekBar: SeekBar,
- @ControlUnitType control: Int
+ @ControlUnitType control: Int,
) {
if (control == ControlUnitType.BUTTON) {
// The seekbar progress is changed by icon buttons
@@ -216,7 +216,7 @@ constructor(
!systemSettings.putStringForUser(
Settings.System.FONT_SCALE,
strEntryValues[lastProgress.get()],
- userTracker.userId
+ userTracker.userId,
)
) {
title.post { doneButton.isEnabled = true }
@@ -228,13 +228,13 @@ constructor(
if (
secureSettings.getStringForUser(
Settings.Secure.ACCESSIBILITY_FONT_SCALING_HAS_BEEN_CHANGED,
- userTracker.userId
+ userTracker.userId,
) != ON
) {
secureSettings.putStringForUser(
Settings.Secure.ACCESSIBILITY_FONT_SCALING_HAS_BEEN_CHANGED,
ON,
- userTracker.userId
+ userTracker.userId,
)
}
}
@@ -249,7 +249,7 @@ constructor(
title.setTextSize(
TypedValue.COMPLEX_UNIT_PX,
- previewConfigContext.resources.getDimension(R.dimen.dialog_title_text_size)
+ previewConfigContext.resources.getDimension(R.dimen.dialog_title_text_size),
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/activity/data/model/AppVisibilityModel.kt b/packages/SystemUI/src/com/android/systemui/activity/data/model/AppVisibilityModel.kt
new file mode 100644
index 000000000000..2d21d655561f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/activity/data/model/AppVisibilityModel.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.activity.data.model
+
+/** Describes an app's previous and current visibility to the user. */
+data class AppVisibilityModel(
+ /** True if the app is currently visible to the user and false otherwise. */
+ val isAppCurrentlyVisible: Boolean = false,
+ /**
+ * The last time this app became visible to the user, in
+ * [com.android.systemui.util.time.SystemClock.currentTimeMillis] units. Null if the app hasn't
+ * become visible since the flow started collection.
+ */
+ val lastAppVisibleTime: Long? = null,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/activity/data/repository/ActivityManagerRepository.kt b/packages/SystemUI/src/com/android/systemui/activity/data/repository/ActivityManagerRepository.kt
index 94614b70beda..11831eabf19d 100644
--- a/packages/SystemUI/src/com/android/systemui/activity/data/repository/ActivityManagerRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/activity/data/repository/ActivityManagerRepository.kt
@@ -18,9 +18,11 @@ package com.android.systemui.activity.data.repository
import android.app.ActivityManager
import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND
+import com.android.systemui.activity.data.model.AppVisibilityModel
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.log.core.Logger
+import com.android.systemui.util.time.SystemClock
import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
import javax.inject.Inject
import kotlin.coroutines.CoroutineContext
@@ -29,9 +31,23 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.onStart
+import kotlinx.coroutines.flow.scan
/** Repository for interfacing with [ActivityManager]. */
interface ActivityManagerRepository {
+
+ /**
+ * Given a UID, creates a flow that emits details about when the process with the given UID was
+ * and is visible to the user.
+ *
+ * @param identifyingLogTag a tag identifying who created this flow, used for logging.
+ */
+ fun createAppVisibilityFlow(
+ creationUid: Int,
+ logger: Logger,
+ identifyingLogTag: String,
+ ): Flow<AppVisibilityModel>
+
/**
* Given a UID, creates a flow that emits true when the process with the given UID is visible to
* the user and false otherwise.
@@ -50,8 +66,38 @@ class ActivityManagerRepositoryImpl
@Inject
constructor(
@Background private val backgroundContext: CoroutineContext,
+ private val systemClock: SystemClock,
private val activityManager: ActivityManager,
) : ActivityManagerRepository {
+
+ override fun createAppVisibilityFlow(
+ creationUid: Int,
+ logger: Logger,
+ identifyingLogTag: String,
+ ): Flow<AppVisibilityModel> {
+ return createIsAppVisibleFlow(creationUid, logger, identifyingLogTag)
+ .distinctUntilChanged()
+ .scan(initial = AppVisibilityModel()) {
+ oldState: AppVisibilityModel,
+ newIsVisible: Boolean ->
+ if (newIsVisible) {
+ val lastAppVisibleTime = systemClock.currentTimeMillis()
+ logger.d({ "$str1: Setting lastAppVisibleTime=$long1" }) {
+ str1 = identifyingLogTag
+ long1 = lastAppVisibleTime
+ }
+ AppVisibilityModel(
+ isAppCurrentlyVisible = true,
+ lastAppVisibleTime = lastAppVisibleTime,
+ )
+ } else {
+ // Reset the current status while maintaining the lastAppVisibleTime
+ oldState.copy(isAppCurrentlyVisible = false)
+ }
+ }
+ .distinctUntilChanged()
+ }
+
override fun createIsAppVisibleFlow(
creationUid: Int,
logger: Logger,
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/shared/constants/KeyguardBouncerConstants.kt b/packages/SystemUI/src/com/android/systemui/bouncer/shared/constants/KeyguardBouncerConstants.kt
index e949dc6a1935..3ef50f68cba4 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/shared/constants/KeyguardBouncerConstants.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/shared/constants/KeyguardBouncerConstants.kt
@@ -87,6 +87,14 @@ private fun <T> c(old: T, new: T): T {
}
}
+object PatternBouncerConstants {
+ object ColorId {
+ @JvmField val dotColor = colors.materialColorOnSurfaceVariant
+ @JvmField val activatedDotColor = colors.materialColorOnPrimary
+ @JvmField val pathColor = colors.materialColorPrimary
+ }
+}
+
object PinBouncerConstants {
@JvmField
val pinShapes = c(old = R.array.bouncer_pin_shapes, new = R.array.updated_bouncer_pin_shapes)
@@ -126,5 +134,8 @@ object PinBouncerConstants {
@JvmField
val contractionColorInterpolator =
c(old = Interpolators.LINEAR, new = Interpolators.STANDARD)!!
+
+ const val pressedTextScaleX = 1.35f
+ const val normalTextScaleX = 1.0f
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/binder/KeyguardBouncerViewBinder.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/binder/KeyguardBouncerViewBinder.kt
index 434a9ce58c3b..7d8945a5b4a7 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/binder/KeyguardBouncerViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/binder/KeyguardBouncerViewBinder.kt
@@ -191,7 +191,6 @@ object KeyguardBouncerViewBinder {
.filter { it == EXPANSION_VISIBLE }
.collect {
securityContainerController.onResume(KeyguardSecurityView.SCREEN_ON)
- view.announceForAccessibility(securityContainerController.title)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/common/ui/view/SeekBarWithIconButtonsView.java b/packages/SystemUI/src/com/android/systemui/common/ui/view/SeekBarWithIconButtonsView.java
index 82bce0b5338a..257a5a4d9061 100644
--- a/packages/SystemUI/src/com/android/systemui/common/ui/view/SeekBarWithIconButtonsView.java
+++ b/packages/SystemUI/src/com/android/systemui/common/ui/view/SeekBarWithIconButtonsView.java
@@ -286,7 +286,8 @@ public class SeekBarWithIconButtonsView extends LinearLayout {
/**
* Notification that the user interaction with SeekBarWithIconButtonsView is finalized. This
- * would be triggered after user ends dragging on the slider or clicks icon buttons.
+ * would be triggered after user ends dragging on the slider or clicks icon buttons. This is
+ * not called if the progress change was not initiated by the user.
*
* @param seekBar The SeekBar in which the user ends interaction with
* @param control The last user interacted control unit. It would be
@@ -318,10 +319,14 @@ public class SeekBarWithIconButtonsView extends LinearLayout {
seekBar, OnSeekBarWithIconButtonsChangeListener.ControlUnitType.BUTTON);
} else {
mOnSeekBarChangeListener.onProgressChanged(seekBar, progress, fromUser);
- if (!mSeekByTouch) {
+ if (!mSeekByTouch && fromUser) {
// Accessibility users could change the progress of the seekbar without
- // touching the seekbar or clicking the buttons. We will consider the
- // interaction has finished in this case.
+ // touching the seekbar or clicking the buttons. In this, {@code fromUser}
+ // will be true, and we will consider the interaction to be finished.
+ // The seekbar progress could be changed when {@code fromUser} is false
+ // when magnification scale is set by pinch-to-zoom, keyboard control, or
+ // other services. In this case, we don't need to take finalized actions
+ // for the progress change.
mOnSeekBarChangeListener.onUserInteractionFinalized(
seekBar,
OnSeekBarWithIconButtonsChangeListener.ControlUnitType.SLIDER);
diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSceneRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSceneRepository.kt
index 0a9bd4214a12..bf4445ba18db 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSceneRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSceneRepository.kt
@@ -151,5 +151,7 @@ constructor(
override fun instantlyShowOverlay(overlay: OverlayKey) = Unit
override fun instantlyHideOverlay(overlay: OverlayKey) = Unit
+
+ override fun freezeAndAnimateToCurrentState() = Unit
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java
index 8bff090959ab..3c68e3a09f02 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java
@@ -74,7 +74,6 @@ import com.android.systemui.statusbar.NotificationShadeWindowController;
import com.android.systemui.statusbar.SysuiStatusBarStateController;
import com.android.systemui.statusbar.dagger.CentralSurfacesModule;
import com.android.systemui.statusbar.dagger.StartCentralSurfacesModule;
-import com.android.systemui.statusbar.notification.dagger.NotificationStackModule;
import com.android.systemui.statusbar.notification.dagger.ReferenceNotificationsModule;
import com.android.systemui.statusbar.notification.headsup.HeadsUpModule;
import com.android.systemui.statusbar.phone.CentralSurfaces;
@@ -170,7 +169,6 @@ import javax.inject.Named;
WallpaperModule.class,
ShortcutHelperModule.class,
ContextualEducationModule.class,
- NotificationStackModule.class,
})
public abstract class ReferenceSystemUIModule {
diff --git a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
index ebe228dab05a..26501596aa1a 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
@@ -45,7 +45,6 @@ class FlagDependencies @Inject constructor(featureFlags: FeatureFlagsClassic, ha
// Internal notification backend dependencies
crossAppPoliteNotifications dependsOn politeNotifications
vibrateWhileUnlockedToken dependsOn politeNotifications
- modesUi dependsOn modesApi
// Internal notification frontend dependencies
NotificationAvalancheSuppression.token dependsOn VisualInterruptionRefactor.token
@@ -71,9 +70,6 @@ class FlagDependencies @Inject constructor(featureFlags: FeatureFlagsClassic, ha
private inline val modesUi
get() = FlagToken(android.app.Flags.FLAG_MODES_UI, android.app.Flags.modesUi())
- private inline val modesApi
- get() = FlagToken(android.app.Flags.FLAG_MODES_API, android.app.Flags.modesApi())
-
private inline val communalHub
get() = FlagToken(FLAG_COMMUNAL_HUB, communalHub())
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/source/MultitaskingShortcutsSource.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/source/MultitaskingShortcutsSource.kt
index 464201f6ec12..b787fc2a2b17 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/source/MultitaskingShortcutsSource.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/source/MultitaskingShortcutsSource.kt
@@ -19,6 +19,7 @@ package com.android.systemui.keyboard.shortcut.data.source
import android.content.Context
import android.content.res.Resources
import android.view.KeyEvent.KEYCODE_D
+import android.view.KeyEvent.KEYCODE_DPAD_DOWN
import android.view.KeyEvent.KEYCODE_DPAD_LEFT
import android.view.KeyEvent.KEYCODE_DPAD_RIGHT
import android.view.KeyEvent.KEYCODE_DPAD_UP
@@ -73,6 +74,15 @@ constructor(@Main private val resources: Resources, @Application private val con
command(META_META_ON or META_CTRL_ON, KEYCODE_DPAD_UP)
}
)
+ if (DesktopModeStatus.canEnterDesktopMode(context)) {
+ // Switch to desktop view
+ // - Meta + Ctrl + Down arrow
+ add(
+ shortcutInfo(resources.getString(R.string.system_multitasking_desktop_view)) {
+ command(META_META_ON or META_CTRL_ON, KEYCODE_DPAD_DOWN)
+ }
+ )
+ }
if (enableMoveToNextDisplayShortcut()) {
// Move a window to the next display:
// - Meta + Ctrl + D
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/LockscreenSceneTransitionRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/LockscreenSceneTransitionRepository.kt
index 80bdc65f9b97..f69229213690 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/LockscreenSceneTransitionRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/LockscreenSceneTransitionRepository.kt
@@ -25,11 +25,17 @@ import kotlinx.coroutines.flow.MutableStateFlow
class LockscreenSceneTransitionRepository @Inject constructor() {
/**
- * This [KeyguardState] will indicate which sub state within KTF should be navigated to when the
- * next transition into the Lockscreen scene is started. It will be consumed exactly once and
- * after that the state will be set back to [DEFAULT_STATE].
+ * This [KeyguardState] will indicate which sub-state within KTF should be navigated to next.
+ *
+ * This can be either starting a transition to the `Lockscreen` scene or cancelling a transition
+ * from the `Lockscreen` scene and returning back to it.
+ *
+ * A `null` value means that no explicit target state was set and therefore the [DEFAULT_STATE]
+ * should be used.
+ *
+ * Once consumed, this state should be reset to `null`.
*/
- val nextLockscreenTargetState: MutableStateFlow<KeyguardState> = MutableStateFlow(DEFAULT_STATE)
+ val nextLockscreenTargetState: MutableStateFlow<KeyguardState?> = MutableStateFlow(null)
companion object {
val DEFAULT_STATE = KeyguardState.LOCKSCREEN
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAlternateBouncerTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAlternateBouncerTransitionInteractor.kt
index bf0f25ff089e..a3796ab5ee27 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAlternateBouncerTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAlternateBouncerTransitionInteractor.kt
@@ -118,26 +118,19 @@ constructor(
powerInteractor.isAwake,
keyguardInteractor.isAodAvailable,
communalSceneInteractor.isIdleOnCommunal,
- communalInteractor.editModeOpen,
+ keyguardInteractor.isDreaming,
keyguardInteractor.isKeyguardOccluded,
)
.filterRelevantKeyguardStateAnd {
- (isAlternateBouncerShowing, isPrimaryBouncerShowing, _, _, _) ->
+ (isAlternateBouncerShowing, isPrimaryBouncerShowing, _, _, _, _) ->
!isAlternateBouncerShowing && !isPrimaryBouncerShowing
}
- .collect {
- (
- _,
- _,
- isAwake,
- isAodAvailable,
- isIdleOnCommunal,
- isCommunalEditMode,
- isOccluded) ->
+ .collect { (_, _, isAwake, isAodAvailable, isIdleOnCommunal, isDreaming, isOccluded)
+ ->
// When unlocking over glanceable hub to enter edit mode, transitioning directly
// to GONE prevents the lockscreen flash. Let listenForAlternateBouncerToGone
// handle it.
- if (isCommunalEditMode) return@collect
+ if (communalInteractor.editModeOpen.value) return@collect
val hubV2 = communalSettingsInteractor.isV2FlagEnabled()
val to =
if (!isAwake) {
@@ -150,8 +143,10 @@ constructor(
if (!hubV2 && isIdleOnCommunal) {
if (SceneContainerFlag.isEnabled) return@collect
KeyguardState.GLANCEABLE_HUB
- } else if (isOccluded) {
+ } else if (isOccluded && !isDreaming) {
KeyguardState.OCCLUDED
+ } else if (hubV2 && isDreaming) {
+ KeyguardState.DREAMING
} else if (hubV2 && isIdleOnCommunal) {
if (SceneContainerFlag.isEnabled) return@collect
KeyguardState.GLANCEABLE_HUB
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt
index 0700ec639153..6f5f662d6fa3 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt
@@ -159,7 +159,6 @@ constructor(
val isKeyguardOccludedLegacy = keyguardInteractor.isKeyguardOccluded.value
val primaryBouncerShowing = keyguardInteractor.primaryBouncerShowing.value
val isKeyguardGoingAway = keyguardInteractor.isKeyguardGoingAway.value
- val canStartDreaming = dreamManager.canStartDreaming(false)
if (!deviceEntryInteractor.isLockscreenEnabled()) {
if (!SceneContainerFlag.isEnabled) {
@@ -192,13 +191,6 @@ constructor(
if (!SceneContainerFlag.isEnabled) {
transitionToGlanceableHub()
}
- } else if (canStartDreaming) {
- // If we're waking up to dream, transition directly to dreaming without
- // showing the lockscreen.
- startTransitionTo(
- KeyguardState.DREAMING,
- ownerReason = "moving from doze to dream",
- )
} else {
startTransitionTo(KeyguardState.LOCKSCREEN)
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractor.kt
index 673fa9730c53..63cf4f72e415 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractor.kt
@@ -24,7 +24,6 @@ import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.keyguard.data.repository.KeyguardClockRepository
import com.android.systemui.keyguard.shared.model.ClockSize
import com.android.systemui.keyguard.shared.model.ClockSizeSetting
-import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.model.KeyguardState.AOD
import com.android.systemui.keyguard.shared.model.KeyguardState.DOZING
import com.android.systemui.keyguard.shared.model.KeyguardState.GONE
@@ -33,12 +32,13 @@ import com.android.systemui.media.controls.domain.pipeline.interactor.MediaCarou
import com.android.systemui.plugins.clocks.ClockController
import com.android.systemui.plugins.clocks.ClockId
import com.android.systemui.scene.shared.flag.SceneContainerFlag
-import com.android.systemui.shade.domain.interactor.ShadeInteractor
+import com.android.systemui.shade.domain.interactor.ShadeModeInteractor
import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor
import com.android.systemui.statusbar.notification.domain.interactor.HeadsUpNotificationInteractor
import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUiAod
import com.android.systemui.statusbar.notification.promoted.domain.interactor.AODPromotedNotificationInteractor
import com.android.systemui.util.kotlin.combine
+import com.android.systemui.utils.coroutines.flow.flatMapLatestConflated
import com.android.systemui.wallpapers.domain.interactor.WallpaperFocalAreaInteractor
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
@@ -61,7 +61,7 @@ constructor(
mediaCarouselInteractor: MediaCarouselInteractor,
activeNotificationsInteractor: ActiveNotificationsInteractor,
aodPromotedNotificationInteractor: AODPromotedNotificationInteractor,
- shadeInteractor: ShadeInteractor,
+ shadeModeInteractor: ShadeModeInteractor,
keyguardInteractor: KeyguardInteractor,
keyguardTransitionInteractor: KeyguardTransitionInteractor,
headsUpNotificationInteractor: HeadsUpNotificationInteractor,
@@ -70,8 +70,13 @@ constructor(
private val wallpaperFocalAreaInteractor: WallpaperFocalAreaInteractor,
) {
private val isOnAod: Flow<Boolean> =
- keyguardTransitionInteractor.currentKeyguardState.map { it == KeyguardState.AOD }
+ keyguardTransitionInteractor.currentKeyguardState.map { it == AOD }
+ /**
+ * The clock size setting explicitly selected by the user. When it is `SMALL`, the large clock
+ * is never shown. When it is `DYNAMIC`, the clock size gets determined based on a combination
+ * of system signals.
+ */
val selectedClockSize: StateFlow<ClockSizeSetting> = keyguardClockRepository.selectedClockSize
val currentClockId: Flow<ClockId> = keyguardClockRepository.currentClockId
@@ -103,36 +108,46 @@ constructor(
activeNotificationsInteractor.areAnyNotificationsPresent
}
- val clockSize: StateFlow<ClockSize> =
+ private val dynamicClockSize: Flow<ClockSize> =
if (SceneContainerFlag.isEnabled) {
combine(
- shadeInteractor.isShadeLayoutWide,
- areAnyNotificationsPresent,
- mediaCarouselInteractor.hasActiveMediaOrRecommendation,
- keyguardInteractor.isDozing,
- isOnAod,
- ) { isShadeLayoutWide, hasNotifs, hasMedia, isDozing, isOnAod ->
- return@combine when {
- keyguardClockRepository.shouldForceSmallClock && !isOnAod -> ClockSize.SMALL
- !isShadeLayoutWide && (hasNotifs || hasMedia) -> ClockSize.SMALL
- !isShadeLayoutWide -> ClockSize.LARGE
- hasMedia && !isDozing -> ClockSize.SMALL
- else -> ClockSize.LARGE
- }
+ shadeModeInteractor.isShadeLayoutWide,
+ areAnyNotificationsPresent,
+ mediaCarouselInteractor.hasActiveMediaOrRecommendation,
+ keyguardInteractor.isDozing,
+ isOnAod,
+ ) { isShadeLayoutWide, hasNotifs, hasMedia, isDozing, isOnAod ->
+ when {
+ keyguardClockRepository.shouldForceSmallClock && !isOnAod -> ClockSize.SMALL
+ !isShadeLayoutWide && (hasNotifs || hasMedia) -> ClockSize.SMALL
+ !isShadeLayoutWide -> ClockSize.LARGE
+ hasMedia && !isDozing -> ClockSize.SMALL
+ else -> ClockSize.LARGE
}
- .stateIn(
- scope = applicationScope,
- started = SharingStarted.WhileSubscribed(),
- initialValue = ClockSize.LARGE,
- )
+ }
} else {
keyguardClockRepository.clockSize
}
+ val clockSize: StateFlow<ClockSize> =
+ selectedClockSize
+ .flatMapLatestConflated { selectedSize ->
+ if (selectedSize == ClockSizeSetting.SMALL) {
+ flowOf(ClockSize.SMALL)
+ } else {
+ dynamicClockSize
+ }
+ }
+ .stateIn(
+ scope = applicationScope,
+ started = SharingStarted.Eagerly,
+ initialValue = ClockSize.LARGE,
+ )
+
val clockShouldBeCentered: Flow<Boolean> =
if (SceneContainerFlag.isEnabled) {
combine(
- shadeInteractor.isShadeLayoutWide,
+ shadeModeInteractor.isShadeLayoutWide,
areAnyNotificationsPresent,
isAodPromotedNotificationPresent,
isOnAod,
@@ -156,7 +171,7 @@ constructor(
}
} else {
combine(
- shadeInteractor.isShadeLayoutWide,
+ shadeModeInteractor.isShadeLayoutWide,
areAnyNotificationsPresent,
isAodPromotedNotificationPresent,
keyguardInteractor.dozeTransitionModel,
@@ -203,7 +218,7 @@ constructor(
val renderedClockId: ClockId
get() {
- return clock?.let { clock -> clock.config.id }
+ return clock?.config?.id
?: run {
Log.e(TAG, "No clock is available")
"MISSING_CLOCK_ID"
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/scenetransition/LockscreenSceneTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/scenetransition/LockscreenSceneTransitionInteractor.kt
index 5f821022d580..1b70ff84f09d 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/scenetransition/LockscreenSceneTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/scenetransition/LockscreenSceneTransitionInteractor.kt
@@ -119,7 +119,8 @@ constructor(
} else {
val targetState =
if (idle.currentScene == Scenes.Lockscreen) {
- transitionInteractor.startedKeyguardTransitionStep.value.from
+ repository.nextLockscreenTargetState.value
+ ?: transitionInteractor.startedKeyguardTransitionStep.value.from
} else {
UNDEFINED
}
@@ -197,11 +198,11 @@ constructor(
TransitionInfo(
ownerName = this::class.java.simpleName,
from = UNDEFINED,
- to = repository.nextLockscreenTargetState.value,
+ to = repository.nextLockscreenTargetState.value ?: DEFAULT_STATE,
animator = null,
modeOnCanceled = TransitionModeOnCanceled.RESET,
)
- repository.nextLockscreenTargetState.value = DEFAULT_STATE
+ repository.nextLockscreenTargetState.value = null
startTransition(newTransition)
}
@@ -215,7 +216,7 @@ constructor(
animator = null,
modeOnCanceled = TransitionModeOnCanceled.RESET,
)
- repository.nextLockscreenTargetState.value = DEFAULT_STATE
+ repository.nextLockscreenTargetState.value = null
startTransition(newTransition)
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt
index 8e385385b8c4..da87e38daa9b 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt
@@ -370,6 +370,14 @@ object KeyguardRootViewBinder {
repeatOnLifecycle(Lifecycle.State.STARTED) {
if (wallpaperFocalAreaViewModel.hasFocalArea.value) {
launch {
+ wallpaperFocalAreaViewModel.wallpaperFocalAreaBounds.collect {
+ wallpaperFocalAreaBounds ->
+ wallpaperFocalAreaViewModel.setFocalAreaBounds(
+ wallpaperFocalAreaBounds
+ )
+ }
+ }
+ launch {
wallpaperFocalAreaViewModel.wallpaperFocalAreaBounds
.filterNotNull()
.collect { wallpaperFocalAreaViewModel.setFocalAreaBounds(it) }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModel.kt
index aed86648e3cf..0a087404c075 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModel.kt
@@ -26,7 +26,6 @@ import com.android.systemui.keyguard.domain.interactor.BurnInInteractor
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
import com.android.systemui.keyguard.shared.model.BurnInModel
-import com.android.systemui.keyguard.shared.model.ClockSize
import com.android.systemui.keyguard.shared.model.Edge
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.ui.StateToValue
@@ -35,6 +34,7 @@ import com.android.systemui.shade.ShadeDisplayAware
import javax.inject.Inject
import kotlin.math.max
import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
@@ -51,6 +51,7 @@ import kotlinx.coroutines.flow.stateIn
* Models UI state for elements that need to apply anti-burn-in tactics when showing in AOD
* (always-on display).
*/
+@OptIn(ExperimentalCoroutinesApi::class)
@SysUISingleton
class AodBurnInViewModel
@Inject
@@ -184,10 +185,9 @@ constructor(
keyguardClockViewModel.currentClock.value
?.config
?.useAlternateSmartspaceAODTransition == true
- // Only scale large non-weather clocks
- // elements in large weather clock will translate the same as smartspace
- val useScaleOnly =
- (!useAltAod) && keyguardClockViewModel.clockSize.value == ClockSize.LARGE
+ // Only scale large non-weather clocks elements in large weather clock will translate
+ // the same as smartspace
+ val useScaleOnly = (!useAltAod) && keyguardClockViewModel.isLargeClockVisible.value
val burnInY = MathUtils.lerp(0, burnIn.translationY, interpolated).toInt()
val translationY = max(params.topInset - params.minViewY, burnInY)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DozingToDreamingTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DozingToDreamingTransitionViewModel.kt
index 9018c58a7e36..e6a85c6860c5 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DozingToDreamingTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DozingToDreamingTransitionViewModel.kt
@@ -39,6 +39,4 @@ constructor(animationFlow: KeyguardTransitionAnimationFlow) {
)
val lockscreenAlpha: Flow<Float> = transitionAnimation.immediatelyTransitionTo(0f)
- // Notifications should not be shown while transitioning to dream.
- val notificationAlpha = transitionAnimation.immediatelyTransitionTo(0f)
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt
index 88fdc83fa7a0..cf5cc264be8d 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt
@@ -18,7 +18,6 @@ package com.android.systemui.keyguard.ui.viewmodel
import android.content.Context
import android.content.res.Resources
-import androidx.annotation.VisibleForTesting
import androidx.constraintlayout.helper.widget.Layer
import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor
import com.android.systemui.customization.R as customR
@@ -27,11 +26,10 @@ import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor
import com.android.systemui.keyguard.shared.model.ClockSize
-import com.android.systemui.keyguard.shared.model.ClockSizeSetting
import com.android.systemui.plugins.clocks.ClockPreviewConfig
import com.android.systemui.scene.shared.flag.SceneContainerFlag
import com.android.systemui.shade.ShadeDisplayAware
-import com.android.systemui.shade.domain.interactor.ShadeInteractor
+import com.android.systemui.shade.domain.interactor.ShadeModeInteractor
import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerAlwaysOnDisplayViewModel
import com.android.systemui.statusbar.ui.SystemBarUtilsProxy
import javax.inject.Inject
@@ -47,11 +45,11 @@ import kotlinx.coroutines.flow.stateIn
class KeyguardClockViewModel
@Inject
constructor(
- val context: Context,
+ private val context: Context,
keyguardClockInteractor: KeyguardClockInteractor,
@Application private val applicationScope: CoroutineScope,
aodNotificationIconViewModel: NotificationIconContainerAlwaysOnDisplayViewModel,
- @get:VisibleForTesting val shadeInteractor: ShadeInteractor,
+ private val shadeModeInteractor: ShadeModeInteractor,
private val systemBarUtils: SystemBarUtilsProxy,
@ShadeDisplayAware configurationInteractor: ConfigurationInteractor,
// TODO: b/374267505 - Use ShadeDisplayAware resources here.
@@ -59,17 +57,7 @@ constructor(
) {
var burnInLayer: Layer? = null
- val clockSize: StateFlow<ClockSize> =
- combine(keyguardClockInteractor.selectedClockSize, keyguardClockInteractor.clockSize) {
- selectedSize,
- clockSize ->
- if (selectedSize == ClockSizeSetting.SMALL) ClockSize.SMALL else clockSize
- }
- .stateIn(
- scope = applicationScope,
- started = SharingStarted.Eagerly,
- initialValue = ClockSize.LARGE,
- )
+ val clockSize: StateFlow<ClockSize> = keyguardClockInteractor.clockSize
val isLargeClockVisible: StateFlow<Boolean> =
clockSize
@@ -118,7 +106,7 @@ constructor(
combine(
isLargeClockVisible,
clockShouldBeCentered,
- shadeInteractor.isShadeLayoutWide,
+ shadeModeInteractor.isShadeLayoutWide,
currentClock,
) { isLargeClockVisible, clockShouldBeCentered, isShadeLayoutWide, currentClock ->
if (currentClock?.config?.useCustomClockScene == true) {
@@ -163,7 +151,7 @@ constructor(
fun getSmallClockTopMargin(): Int {
return ClockPreviewConfig(
context,
- shadeInteractor.isShadeLayoutWide.value,
+ shadeModeInteractor.isShadeLayoutWide.value,
SceneContainerFlag.isEnabled,
)
.getSmallClockTopPadding(systemBarUtils.getStatusBarHeaderHeightKeyguard())
@@ -172,7 +160,7 @@ constructor(
val smallClockTopMargin =
combine(
configurationInteractor.onAnyConfigurationChange,
- shadeInteractor.isShadeLayoutWide,
+ shadeModeInteractor.isShadeLayoutWide,
) { _, _ ->
getSmallClockTopMargin()
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardMediaViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardMediaViewModel.kt
index ba03c48c65e9..e70d696a207f 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardMediaViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardMediaViewModel.kt
@@ -21,6 +21,7 @@ import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
import com.android.systemui.lifecycle.ExclusiveActivatable
import com.android.systemui.lifecycle.Hydrator
import com.android.systemui.media.controls.domain.pipeline.interactor.MediaCarouselInteractor
+import com.android.systemui.shade.domain.interactor.ShadeModeInteractor
import com.android.systemui.utils.coroutines.flow.flatMapLatestConflated
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
@@ -31,6 +32,7 @@ class KeyguardMediaViewModel
constructor(
mediaCarouselInteractor: MediaCarouselInteractor,
keyguardInteractor: KeyguardInteractor,
+ shadeModeInteractor: ShadeModeInteractor,
) : ExclusiveActivatable() {
private val hydrator = Hydrator("KeyguardMediaViewModel.hydrator")
@@ -54,6 +56,12 @@ constructor(
mediaCarouselInteractor.hasActiveMediaOrRecommendation.value,
)
+ val isShadeLayoutWide: Boolean by
+ hydrator.hydratedStateOf(
+ traceName = "isShadeLayoutWide",
+ source = shadeModeInteractor.isShadeLayoutWide,
+ )
+
override suspend fun onActivated(): Nothing {
hydrator.activate()
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModel.kt
index 3e3a89a55f66..ecebaee62862 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModel.kt
@@ -17,8 +17,9 @@
package com.android.systemui.keyguard.ui.viewmodel
import android.content.res.Resources
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Alignment
import com.android.app.tracing.coroutines.launchTraced as launch
-import com.android.internal.annotations.VisibleForTesting
import com.android.systemui.biometrics.AuthController
import com.android.systemui.customization.R as customR
import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor
@@ -30,86 +31,121 @@ import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.transition.KeyguardTransitionAnimationCallback
import com.android.systemui.keyguard.shared.transition.KeyguardTransitionAnimationCallbackDelegator
import com.android.systemui.lifecycle.ExclusiveActivatable
+import com.android.systemui.lifecycle.Hydrator
import com.android.systemui.res.R
-import com.android.systemui.shade.domain.interactor.ShadeInteractor
+import com.android.systemui.shade.domain.interactor.ShadeModeInteractor
+import com.android.systemui.shade.shared.model.ShadeMode
import com.android.systemui.unfold.domain.interactor.UnfoldTransitionInteractor
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
-import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.coroutineScope
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.stateIn
class LockscreenContentViewModel
@AssistedInject
constructor(
- clockInteractor: KeyguardClockInteractor,
- private val interactor: KeyguardBlueprintInteractor,
+ private val clockInteractor: KeyguardClockInteractor,
+ interactor: KeyguardBlueprintInteractor,
private val authController: AuthController,
val touchHandling: KeyguardTouchHandlingViewModel,
- private val shadeInteractor: ShadeInteractor,
- private val unfoldTransitionInteractor: UnfoldTransitionInteractor,
- private val deviceEntryInteractor: DeviceEntryInteractor,
- private val transitionInteractor: KeyguardTransitionInteractor,
+ shadeModeInteractor: ShadeModeInteractor,
+ unfoldTransitionInteractor: UnfoldTransitionInteractor,
+ deviceEntryInteractor: DeviceEntryInteractor,
+ transitionInteractor: KeyguardTransitionInteractor,
private val keyguardTransitionAnimationCallbackDelegator:
KeyguardTransitionAnimationCallbackDelegator,
@Assisted private val keyguardTransitionAnimationCallback: KeyguardTransitionAnimationCallback,
) : ExclusiveActivatable() {
- @VisibleForTesting val clockSize = clockInteractor.clockSize
+
+ private val hydrator = Hydrator("LockscreenContentViewModel.hydrator")
val isUdfpsVisible: Boolean
get() = authController.isUdfpsSupported
- val isShadeLayoutWide: StateFlow<Boolean> = shadeInteractor.isShadeLayoutWide
+ /** Where to place the notifications stack on the lockscreen. */
+ val notificationsPlacement: NotificationsPlacement by
+ hydrator.hydratedStateOf(
+ traceName = "notificationsPlacement",
+ initialValue = NotificationsPlacement.BelowClock,
+ source =
+ combine(shadeModeInteractor.shadeMode, clockInteractor.clockSize) {
+ shadeMode,
+ clockSize ->
+ if (shadeMode is ShadeMode.Split) {
+ NotificationsPlacement.BesideClock(alignment = Alignment.TopEnd)
+ } else if (clockSize == ClockSize.SMALL) {
+ NotificationsPlacement.BelowClock
+ } else {
+ NotificationsPlacement.BesideClock(alignment = Alignment.TopStart)
+ }
+ },
+ )
- private val _unfoldTranslations = MutableStateFlow(UnfoldTranslations())
/** Amount of horizontal translation that should be applied to elements in the scene. */
- val unfoldTranslations: StateFlow<UnfoldTranslations> = _unfoldTranslations.asStateFlow()
+ val unfoldTranslations: UnfoldTranslations by
+ hydrator.hydratedStateOf(
+ traceName = "unfoldTranslations",
+ initialValue = UnfoldTranslations(),
+ source =
+ combine(
+ unfoldTransitionInteractor.unfoldTranslationX(isOnStartSide = true),
+ unfoldTransitionInteractor.unfoldTranslationX(isOnStartSide = false),
+ ::UnfoldTranslations,
+ ),
+ )
- private val _isContentVisible = MutableStateFlow(true)
/** Whether the content of the scene UI should be shown. */
- val isContentVisible: StateFlow<Boolean> = _isContentVisible.asStateFlow()
+ val isContentVisible: Boolean by
+ hydrator.hydratedStateOf(
+ traceName = "isContentVisible",
+ initialValue = true,
+ // Content is visible unless we're OCCLUDED. Currently, we don't have nice animations
+ // into and out of OCCLUDED, so the lockscreen/AOD content is hidden immediately upon
+ // entering/exiting OCCLUDED.
+ source = transitionInteractor.transitionValue(KeyguardState.OCCLUDED).map { it == 0f },
+ )
+
+ /** Indicates whether lockscreen notifications should be rendered. */
+ val areNotificationsVisible: Boolean by
+ hydrator.hydratedStateOf(
+ traceName = "areNotificationsVisible",
+ initialValue = false,
+ // Content is visible unless we're OCCLUDED. Currently, we don't have nice animations
+ // into and out of OCCLUDED, so the lockscreen/AOD content is hidden immediately upon
+ // entering/exiting OCCLUDED.
+ source =
+ combine(clockInteractor.clockSize, shadeModeInteractor.isShadeLayoutWide) {
+ clockSize,
+ isShadeLayoutWide ->
+ clockSize == ClockSize.SMALL || isShadeLayoutWide
+ },
+ )
/** @see DeviceEntryInteractor.isBypassEnabled */
- val isBypassEnabled: StateFlow<Boolean>
- get() = deviceEntryInteractor.isBypassEnabled
+ val isBypassEnabled: Boolean by
+ hydrator.hydratedStateOf(
+ traceName = "isBypassEnabled",
+ source = deviceEntryInteractor.isBypassEnabled,
+ )
+
+ val blueprintId: String by
+ hydrator.hydratedStateOf(
+ traceName = "blueprintId",
+ initialValue = interactor.getCurrentBlueprint().id,
+ source = interactor.blueprint.map { it.id }.distinctUntilChanged(),
+ )
override suspend fun onActivated(): Nothing {
coroutineScope {
try {
+ launch { hydrator.activate() }
+
keyguardTransitionAnimationCallbackDelegator.delegate =
keyguardTransitionAnimationCallback
- launch {
- combine(
- unfoldTransitionInteractor.unfoldTranslationX(isOnStartSide = true),
- unfoldTransitionInteractor.unfoldTranslationX(isOnStartSide = false),
- ) { start, end ->
- UnfoldTranslations(start = start, end = end)
- }
- .collect { _unfoldTranslations.value = it }
- }
-
- launch {
- transitionInteractor
- .transitionValue(KeyguardState.OCCLUDED)
- .map { it > 0f }
- .collect { fullyOrPartiallyOccluded ->
- // Content is visible unless we're OCCLUDED. Currently, we don't have
- // nice
- // animations into and out of OCCLUDED, so the lockscreen/AOD content is
- // hidden immediately upon entering/exiting OCCLUDED.
- _isContentVisible.value = !fullyOrPartiallyOccluded
- }
- }
awaitCancellation()
} finally {
@@ -118,16 +154,8 @@ constructor(
}
}
- /** Returns a flow that indicates whether lockscreen notifications should be rendered. */
- fun areNotificationsVisible(): Flow<Boolean> {
- return combine(clockSize, shadeInteractor.isShadeLayoutWide) { clockSize, isShadeLayoutWide
- ->
- clockSize == ClockSize.SMALL || isShadeLayoutWide
- }
- }
-
fun getSmartSpacePaddingTop(resources: Resources): Int {
- return if (clockSize.value == ClockSize.LARGE) {
+ return if (clockInteractor.clockSize.value == ClockSize.LARGE) {
resources.getDimensionPixelSize(customR.dimen.keyguard_smartspace_top_offset) +
resources.getDimensionPixelSize(R.dimen.keyguard_clock_top_margin)
} else {
@@ -135,17 +163,6 @@ constructor(
}
}
- fun blueprintId(scope: CoroutineScope): StateFlow<String> {
- return interactor.blueprint
- .map { it.id }
- .distinctUntilChanged()
- .stateIn(
- scope = scope,
- started = SharingStarted.WhileSubscribed(),
- initialValue = interactor.getCurrentBlueprint().id,
- )
- }
-
data class UnfoldTranslations(
/**
@@ -162,6 +179,15 @@ constructor(
val end: Float = 0f,
)
+ /** Where to place the notifications stack on the lockscreen. */
+ sealed interface NotificationsPlacement {
+ /** Show notifications below the lockscreen clock. */
+ data object BelowClock : NotificationsPlacement
+
+ /** Show notifications side-by-side with the clock. */
+ data class BesideClock(val alignment: Alignment) : NotificationsPlacement
+ }
+
@AssistedFactory
interface Factory {
fun create(
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataManager.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataManager.kt
index c3182bf7a320..1466d8b4288e 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataManager.kt
@@ -143,6 +143,12 @@ interface MediaDataManager {
* place immediately.
*/
override fun onSmartspaceMediaDataRemoved(key: String, immediately: Boolean) {}
+
+ /**
+ * Called whenever the current active media notification changes. Should only be used if
+ * [SceneContainerFlag] is disabled
+ */
+ override fun onCurrentActiveMediaChanged(key: String?, data: MediaData?) {}
}
companion object {
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt
index 1464849156dc..59f98d83e149 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt
@@ -1434,6 +1434,9 @@ class MediaDataProcessor(
* place immediately.
*/
fun onSmartspaceMediaDataRemoved(key: String, immediately: Boolean = true) {}
+
+ /** Called whenever the current active media notification changes */
+ fun onCurrentActiveMediaChanged(key: String?, data: MediaData?) {}
}
/**
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt
index 173b600de06b..93c4bafe4273 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt
@@ -87,6 +87,7 @@ import com.android.systemui.shared.system.SysUiStatsLog.SMART_SPACE_CARD_REPORTE
import com.android.systemui.shared.system.SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__DREAM_OVERLAY as SSPACE_CARD_REPORTED__DREAM_OVERLAY
import com.android.systemui.shared.system.SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__LOCKSCREEN as SSPACE_CARD_REPORTED__LOCKSCREEN
import com.android.systemui.shared.system.SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__SHADE
+import com.android.systemui.statusbar.featurepods.media.domain.interactor.MediaControlChipInteractor
import com.android.systemui.statusbar.notification.collection.provider.OnReorderingAllowedListener
import com.android.systemui.statusbar.notification.collection.provider.VisualStabilityProvider
import com.android.systemui.statusbar.policy.ConfigurationController
@@ -155,6 +156,7 @@ constructor(
private val mediaCarouselViewModel: MediaCarouselViewModel,
private val mediaViewControllerFactory: Provider<MediaViewController>,
private val deviceEntryInteractor: DeviceEntryInteractor,
+ private val mediaControlChipInteractor: MediaControlChipInteractor,
) : Dumpable {
/** The current width of the carousel */
var currentCarouselWidth: Int = 0
@@ -957,6 +959,9 @@ constructor(
}
}
mediaCarouselScrollHandler.onPlayersChanged()
+ mediaControlChipInteractor.updateMediaControlChipModelLegacy(
+ MediaPlayerData.getFirstActiveMediaData()
+ )
MediaPlayerData.updateVisibleMediaPlayers()
// Automatically scroll to the active player if needed
if (shouldScrollToKey) {
@@ -1015,6 +1020,9 @@ constructor(
)
updatePageIndicator()
mediaCarouselScrollHandler.onPlayersChanged()
+ mediaControlChipInteractor.updateMediaControlChipModelLegacy(
+ MediaPlayerData.getFirstActiveMediaData()
+ )
mediaFrame.requiresRemeasuring = true
onUiExecutionEnd?.run()
}
@@ -1023,6 +1031,9 @@ constructor(
updatePlayer(key, data, isSsReactivated, curVisibleMediaKey, existingPlayer)
updatePageIndicator()
mediaCarouselScrollHandler.onPlayersChanged()
+ mediaControlChipInteractor.updateMediaControlChipModelLegacy(
+ MediaPlayerData.getFirstActiveMediaData()
+ )
mediaFrame.requiresRemeasuring = true
onUiExecutionEnd?.run()
}
@@ -1036,6 +1047,9 @@ constructor(
}
updatePageIndicator()
mediaCarouselScrollHandler.onPlayersChanged()
+ mediaControlChipInteractor.updateMediaControlChipModelLegacy(
+ MediaPlayerData.getFirstActiveMediaData()
+ )
mediaFrame.requiresRemeasuring = true
onUiExecutionEnd?.run()
}
@@ -1194,6 +1208,9 @@ constructor(
mediaContent.removeView(removed.recommendationViewHolder?.recommendations)
removed.onDestroy()
mediaCarouselScrollHandler.onPlayersChanged()
+ mediaControlChipInteractor.updateMediaControlChipModelLegacy(
+ MediaPlayerData.getFirstActiveMediaData()
+ )
updatePageIndicator()
if (dismissMediaData) {
@@ -1928,6 +1945,16 @@ internal object MediaPlayerData {
fun visiblePlayerKeys() = visibleMediaPlayers.values
+ /** Returns the [MediaData] associated with the first mediaPlayer in the mediaCarousel. */
+ fun getFirstActiveMediaData(): MediaData? {
+ mediaPlayers.entries.forEach { entry ->
+ if (!entry.key.isSsMediaRec && entry.key.data.active) {
+ return entry.key.data
+ }
+ }
+ return null
+ }
+
/** Returns the index of the first non-timeout media. */
fun firstActiveMediaIndex(): Int {
mediaPlayers.entries.forEachIndexed { index, e ->
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 c1778119a3fd..2b36872dbe36 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
@@ -1040,13 +1040,19 @@ constructor(
expandedLayout.load(context, R.xml.media_recommendations_expanded)
}
}
- readjustPlayPauseWidth()
+ readjustUIUpdateConstraints()
refreshState()
}
- private fun readjustPlayPauseWidth() {
+ private fun readjustUIUpdateConstraints() {
// TODO: move to xml file when flag is removed.
if (Flags.mediaControlsUiUpdate()) {
+ collapsedLayout.setGuidelineEnd(
+ R.id.action_button_guideline,
+ context.resources.getDimensionPixelSize(
+ R.dimen.qs_media_session_collapsed_guideline
+ ),
+ )
collapsedLayout.constrainWidth(
R.id.actionPlayPause,
context.resources.getDimensionPixelSize(R.dimen.qs_media_action_play_pause_width),
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/util/MediaViewModelListUpdateCallback.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/util/MediaViewModelListUpdateCallback.kt
index 709723fa9480..6022b7b1fc13 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/util/MediaViewModelListUpdateCallback.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/util/MediaViewModelListUpdateCallback.kt
@@ -18,6 +18,7 @@ package com.android.systemui.media.controls.ui.util
import androidx.recyclerview.widget.ListUpdateCallback
import com.android.systemui.media.controls.ui.viewmodel.MediaCommonViewModel
+import kotlin.math.min
/** A [ListUpdateCallback] to apply media events needed to reach the new state. */
class MediaViewModelListUpdateCallback(
@@ -46,7 +47,7 @@ class MediaViewModelListUpdateCallback(
}
override fun onChanged(position: Int, count: Int, payload: Any?) {
- for (i in position until position + count) {
+ for (i in position until min(position + count, new.size)) {
onUpdated(new[i], position)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapterBase.java
index f5e62323e769..c58ba377fb68 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapterBase.java
@@ -20,23 +20,16 @@ import static com.android.settingslib.media.MediaDevice.SelectionBehavior.SELECT
import static com.android.settingslib.media.MediaDevice.SelectionBehavior.SELECTION_BEHAVIOR_NONE;
import static com.android.settingslib.media.MediaDevice.SelectionBehavior.SELECTION_BEHAVIOR_TRANSFER;
-import android.annotation.DrawableRes;
-import android.annotation.StringRes;
import android.content.Context;
-import android.content.res.ColorStateList;
-import android.graphics.drawable.AnimatedVectorDrawable;
import android.graphics.drawable.Drawable;
+import android.text.TextUtils;
import android.util.Log;
import android.view.View;
-import android.view.ViewGroup;
-import android.widget.CheckBox;
-import android.widget.TextView;
import androidx.annotation.DoNotInline;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
-import androidx.core.widget.CompoundButtonCompat;
import androidx.recyclerview.widget.RecyclerView;
import com.android.internal.annotations.VisibleForTesting;
@@ -49,23 +42,64 @@ import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
/**
- * Adapter for media output dialog.
+ * A parent RecyclerView adapter for the media output dialog device list. This class doesn't
+ * manipulate the layout directly.
*/
-public class MediaOutputAdapter extends MediaOutputBaseAdapter {
+public abstract class MediaOutputAdapterBase extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
+ record OngoingSessionStatus(boolean host) {}
- private static final String TAG = "MediaOutputAdapter";
+ record GroupStatus(Boolean selected, Boolean deselectable) {}
+
+ enum ConnectionState {
+ CONNECTED,
+ CONNECTING,
+ DISCONNECTED,
+ }
+
+ protected final MediaSwitchingController mController;
+ private int mCurrentActivePosition;
+ private boolean mIsDragging;
+ private static final String TAG = "MediaOutputAdapterBase";
private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
- private static final float DEVICE_DISABLED_ALPHA = 0.5f;
- private static final float DEVICE_ACTIVE_ALPHA = 1f;
- protected List<MediaItem> mMediaItemList = new CopyOnWriteArrayList<>();
+ protected final List<MediaItem> mMediaItemList = new CopyOnWriteArrayList<>();
private boolean mShouldGroupSelectedMediaItems = Flags.enableOutputSwitcherDeviceGrouping();
- public MediaOutputAdapter(MediaSwitchingController controller) {
- super(controller);
+ public MediaOutputAdapterBase(MediaSwitchingController controller) {
+ mController = controller;
+ mCurrentActivePosition = -1;
+ mIsDragging = false;
setHasStableIds(true);
}
- @Override
+ boolean isCurrentlyConnected(MediaDevice device) {
+ return TextUtils.equals(device.getId(),
+ mController.getCurrentConnectedMediaDevice().getId())
+ || (mController.getSelectedMediaDevice().size() == 1
+ && isDeviceIncluded(mController.getSelectedMediaDevice(), device));
+ }
+
+ boolean isDeviceIncluded(List<MediaDevice> deviceList, MediaDevice targetDevice) {
+ for (MediaDevice device : deviceList) {
+ if (TextUtils.equals(device.getId(), targetDevice.getId())) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ boolean isDragging() {
+ return mIsDragging;
+ }
+
+ void setIsDragging(boolean isDragging) {
+ mIsDragging = isDragging;
+ }
+
+ int getCurrentActivePosition() {
+ return mCurrentActivePosition;
+ }
+
+ /** Refreshes the RecyclerView dataset and forces re-render. */
public void updateItems() {
mMediaItemList.clear();
mMediaItemList.addAll(mController.getMediaItemList());
@@ -79,47 +113,6 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter {
}
@Override
- public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup,
- int viewType) {
- super.onCreateViewHolder(viewGroup, viewType);
- switch (viewType) {
- case MediaItem.MediaItemType.TYPE_GROUP_DIVIDER:
- return new MediaGroupDividerViewHolder(mHolderView);
- case MediaItem.MediaItemType.TYPE_PAIR_NEW_DEVICE:
- case MediaItem.MediaItemType.TYPE_DEVICE:
- default:
- return new MediaDeviceViewHolder(mHolderView);
- }
- }
-
- @Override
- public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) {
- if (position >= mMediaItemList.size()) {
- if (DEBUG) {
- Log.d(TAG, "Incorrect position: " + position + " list size: "
- + mMediaItemList.size());
- }
- return;
- }
- MediaItem currentMediaItem = mMediaItemList.get(position);
- switch (currentMediaItem.getMediaItemType()) {
- case MediaItem.MediaItemType.TYPE_GROUP_DIVIDER:
- ((MediaGroupDividerViewHolder) viewHolder).onBind(currentMediaItem.getTitle());
- break;
- case MediaItem.MediaItemType.TYPE_PAIR_NEW_DEVICE:
- ((MediaDeviceViewHolder) viewHolder).onBindPairNewDevice();
- break;
- case MediaItem.MediaItemType.TYPE_DEVICE:
- ((MediaDeviceViewHolder) viewHolder).onBind(
- currentMediaItem,
- position);
- break;
- default:
- Log.d(TAG, "Incorrect position: " + position);
- }
- }
-
- @Override
public long getItemId(int position) {
if (position >= mMediaItemList.size()) {
Log.d(TAG, "Incorrect position for item id: " + position);
@@ -145,15 +138,17 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter {
return mMediaItemList.size();
}
- class MediaDeviceViewHolder extends MediaDeviceBaseViewHolder {
+ abstract class MediaDeviceViewHolderBase extends RecyclerView.ViewHolder {
+
+ Context mContext;
- MediaDeviceViewHolder(View view) {
+ MediaDeviceViewHolderBase(View view, Context context) {
super(view);
+ mContext = context;
}
- void onBind(MediaItem mediaItem, int position) {
+ void renderItem(MediaItem mediaItem, int position) {
MediaDevice device = mediaItem.getMediaDevice().get();
- super.onBind(device, position);
boolean isMutingExpectedDeviceExist = mController.hasMutingExpectedDevice();
final boolean currentlyConnected = isCurrentlyConnected(device);
boolean isSelected = isDeviceIncluded(mController.getSelectedMediaDevice(), device);
@@ -198,7 +193,6 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter {
if (mCurrentActivePosition == position) {
mCurrentActivePosition = -1;
}
- mItemLayout.setVisibility(View.VISIBLE);
if (mController.isAnyDeviceTransferring()) {
if (device.getState() == MediaDeviceState.STATE_CONNECTING) {
@@ -265,37 +259,15 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter {
}
}
- private void renderDeviceItem(boolean hideGroupItem, MediaDevice device,
+ protected abstract void renderDeviceItem(boolean hideGroupItem, MediaDevice device,
ConnectionState connectionState, boolean restrictVolumeAdjustment,
GroupStatus groupStatus, OngoingSessionStatus ongoingSessionStatus,
View.OnClickListener clickListener, boolean deviceDisabled, String subtitle,
- Drawable deviceStatusIcon) {
- if (hideGroupItem) {
- mItemLayout.setVisibility(View.GONE);
- return;
- }
- updateTitle(device.getName());
- updateTitleIcon(device, connectionState, restrictVolumeAdjustment);
- updateSeekBar(device, connectionState, restrictVolumeAdjustment,
- getDeviceItemContentDescription(device));
- updateEndArea(device, connectionState, groupStatus, ongoingSessionStatus);
- updateLoadingIndicator(connectionState);
- updateFullItemClickListener(clickListener);
- updateContentAlpha(deviceDisabled);
- updateSubtitle(subtitle);
- updateDeviceStatusIcon(deviceStatusIcon);
- updateItemBackground(connectionState);
- }
+ Drawable deviceStatusIcon);
- private void renderDeviceGroupItem() {
- String sessionName = mController.getSessionName() == null ? ""
- : mController.getSessionName().toString();
- updateTitle(sessionName);
- updateUnmutedVolumeIcon(null /* device */);
- updateGroupSeekBar(getGroupItemContentDescription(sessionName));
- updateEndAreaForDeviceGroup();
- updateItemBackground(ConnectionState.CONNECTED);
- }
+ protected abstract void renderDeviceGroupItem();
+
+ protected abstract void disableSeekBar();
private OngoingSessionStatus getOngoingSessionStatus(MediaDevice device) {
return device.hasOngoingSession() ? new OngoingSessionStatus(
@@ -322,95 +294,6 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter {
return !mController.getSelectableMediaDevice().isEmpty();
}
- /** Renders the right side round pill button / checkbox. */
- private void updateEndArea(@NonNull MediaDevice device, ConnectionState connectionState,
- @Nullable GroupStatus groupStatus,
- @Nullable OngoingSessionStatus ongoingSessionStatus) {
- boolean showEndArea = false;
- boolean isCheckbox = false;
- // If both group status and the ongoing session status are present, only the ongoing
- // session controls are displayed. The current layout design doesn't allow both group
- // and ongoing session controls to be rendered simultaneously.
- if (ongoingSessionStatus != null && connectionState == ConnectionState.CONNECTED) {
- showEndArea = true;
- updateEndAreaForOngoingSession(device, ongoingSessionStatus.host());
- } else if (groupStatus != null && shouldShowGroupCheckbox(groupStatus)) {
- showEndArea = true;
- isCheckbox = true;
- updateEndAreaForGroupCheckBox(device, groupStatus);
- }
- updateEndAreaVisibility(showEndArea, isCheckbox);
- }
-
- private boolean shouldShowGroupCheckbox(@NonNull GroupStatus groupStatus) {
- if (Flags.enableOutputSwitcherDeviceGrouping()) {
- return isGroupCheckboxEnabled(groupStatus);
- }
- return true;
- }
-
- private boolean isGroupCheckboxEnabled(@NonNull GroupStatus groupStatus) {
- boolean disabled = groupStatus.selected() && !groupStatus.deselectable();
- return !disabled;
- }
-
- public void setCheckBoxColor(CheckBox checkBox, int color) {
- int[][] states = {{android.R.attr.state_checked}, {}};
- int[] colors = {color, color};
- CompoundButtonCompat.setButtonTintList(checkBox, new
- ColorStateList(states, colors));
- }
-
- private void updateContentAlpha(boolean deviceDisabled) {
- float alphaValue = deviceDisabled ? DEVICE_DISABLED_ALPHA : DEVICE_ACTIVE_ALPHA;
- mTitleIcon.setAlpha(alphaValue);
- mTitleText.setAlpha(alphaValue);
- mSubTitleText.setAlpha(alphaValue);
- mStatusIcon.setAlpha(alphaValue);
- }
-
- private void updateEndAreaForDeviceGroup() {
- updateEndAreaWithIcon(
- v -> {
- mShouldGroupSelectedMediaItems = false;
- notifyDataSetChanged();
- },
- R.drawable.media_output_item_expand_group,
- R.string.accessibility_expand_group);
- updateEndAreaVisibility(true /* showEndArea */, false /* isCheckbox */);
- }
-
- private void updateEndAreaForOngoingSession(@NonNull MediaDevice device, boolean isHost) {
- updateEndAreaWithIcon(
- v -> mController.tryToLaunchInAppRoutingIntent(device.getId(), v),
- isHost ? R.drawable.media_output_status_edit_session
- : R.drawable.ic_sound_bars_anim,
- R.string.accessibility_open_application);
- }
-
- private void updateEndAreaWithIcon(View.OnClickListener clickListener,
- @DrawableRes int iconDrawableId,
- @StringRes int accessibilityStringId) {
- updateEndAreaColor(mController.getColorSeekbarProgress());
- mEndClickIcon.setImageTintList(
- ColorStateList.valueOf(mController.getColorItemContent()));
- mEndClickIcon.setOnClickListener(clickListener);
- mEndTouchArea.setOnClickListener(v -> mEndClickIcon.performClick());
- Drawable drawable = mContext.getDrawable(iconDrawableId);
- mEndClickIcon.setImageDrawable(drawable);
- if (drawable instanceof AnimatedVectorDrawable) {
- ((AnimatedVectorDrawable) drawable).start();
- }
- if (Flags.enableOutputSwitcherDeviceGrouping()) {
- mEndClickIcon.setContentDescription(mContext.getString(accessibilityStringId));
- }
- }
-
- public void updateEndAreaColor(int color) {
- mEndTouchArea.setBackgroundTintList(
- ColorStateList.valueOf(color));
- }
-
@Nullable
private View.OnClickListener getClickListenerBasedOnSelectionBehavior(
@NonNull MediaDevice device) {
@@ -427,57 +310,12 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter {
}
}
- void updateDeviceStatusIcon(@Nullable Drawable deviceStatusIcon) {
- if (deviceStatusIcon == null) {
- mStatusIcon.setVisibility(View.GONE);
- } else {
- mStatusIcon.setImageDrawable(deviceStatusIcon);
- mStatusIcon.setImageTintList(
- ColorStateList.valueOf(mController.getColorItemContent()));
- if (deviceStatusIcon instanceof AnimatedVectorDrawable) {
- ((AnimatedVectorDrawable) deviceStatusIcon).start();
- }
- mStatusIcon.setVisibility(View.VISIBLE);
- }
- }
-
- public void updateEndAreaForGroupCheckBox(@NonNull MediaDevice device,
- @NonNull GroupStatus groupStatus) {
- boolean isEnabled = isGroupCheckboxEnabled(groupStatus);
- mEndTouchArea.setOnClickListener(
- isEnabled ? (v) -> mCheckBox.performClick() : null);
- mEndTouchArea.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
- updateEndAreaColor(groupStatus.selected() ? mController.getColorSeekbarProgress()
- : mController.getColorItemBackground());
- mEndTouchArea.setContentDescription(getDeviceItemContentDescription(device));
- mCheckBox.setOnCheckedChangeListener(null);
- mCheckBox.setChecked(groupStatus.selected());
- mCheckBox.setOnCheckedChangeListener(
- isEnabled ? (buttonView, isChecked) -> onGroupActionTriggered(
- !groupStatus.selected(), device) : null);
- mCheckBox.setEnabled(isEnabled);
- setCheckBoxColor(mCheckBox, mController.getColorItemContent());
- }
-
- private void updateFullItemClickListener(@Nullable View.OnClickListener listener) {
- mContainerLayout.setOnClickListener(listener);
- updateIconAreaClickListener(listener);
- }
-
- /** Binds a ViewHolder for a "Connect a device" item. */
- void onBindPairNewDevice() {
- mTitleText.setTextColor(mController.getColorItemContent());
- mCheckBox.setVisibility(View.GONE);
- updateTitle(mContext.getText(R.string.media_output_dialog_pairing_new));
- updateItemBackground(ConnectionState.DISCONNECTED);
- final Drawable addDrawable = mContext.getDrawable(R.drawable.ic_add);
- mTitleIcon.setImageDrawable(addDrawable);
- mTitleIcon.setImageTintList(
- ColorStateList.valueOf(mController.getColorItemContent()));
- mContainerLayout.setOnClickListener(mController::launchBluetoothPairing);
+ protected void onExpandGroupButtonClicked() {
+ mShouldGroupSelectedMediaItems = false;
+ notifyDataSetChanged();
}
- private void onGroupActionTriggered(boolean isChecked, MediaDevice device) {
+ protected void onGroupActionTriggered(boolean isChecked, MediaDevice device) {
disableSeekBar();
if (isChecked && isDeviceIncluded(mController.getSelectableMediaDevice(), device)) {
mController.addDeviceToPlayMedia(device);
@@ -523,32 +361,18 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter {
notifyDataSetChanged();
}
- private String getDeviceItemContentDescription(@NonNull MediaDevice device) {
+ protected String getDeviceItemContentDescription(@NonNull MediaDevice device) {
return mContext.getString(
device.getDeviceType() == MediaDevice.MediaDeviceType.TYPE_BLUETOOTH_DEVICE
? R.string.accessibility_bluetooth_name
: R.string.accessibility_cast_name, device.getName());
}
- private String getGroupItemContentDescription(String sessionName) {
+ protected String getGroupItemContentDescription(String sessionName) {
return mContext.getString(R.string.accessibility_cast_name, sessionName);
}
}
- class MediaGroupDividerViewHolder extends RecyclerView.ViewHolder {
- final TextView mTitleText;
-
- MediaGroupDividerViewHolder(@NonNull View itemView) {
- super(itemView);
- mTitleText = itemView.requireViewById(R.id.title);
- }
-
- void onBind(String groupDividerTitle) {
- mTitleText.setTextColor(mController.getColorItemContent());
- mTitleText.setText(groupDividerTitle);
- }
- }
-
@RequiresApi(34)
private static class Api34Impl {
@DoNotInline
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapterLegacy.java
index f97b3d3d5e38..565b2e41f75a 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapterLegacy.java
@@ -18,14 +18,18 @@ package com.android.systemui.media.dialog;
import android.animation.Animator;
import android.animation.ValueAnimator;
-import android.app.WallpaperColors;
+import android.annotation.DrawableRes;
+import android.annotation.StringRes;
import android.content.Context;
import android.content.res.ColorStateList;
+import android.graphics.drawable.AnimatedVectorDrawable;
import android.graphics.drawable.ClipDrawable;
+import android.graphics.drawable.Drawable;
import android.graphics.drawable.GradientDrawable;
import android.graphics.drawable.Icon;
import android.graphics.drawable.LayerDrawable;
import android.text.TextUtils;
+import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@@ -40,6 +44,7 @@ import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
+import androidx.core.widget.CompoundButtonCompat;
import androidx.recyclerview.widget.RecyclerView;
import com.android.media.flags.Flags;
@@ -48,82 +53,67 @@ import com.android.settingslib.media.MediaDevice;
import com.android.settingslib.utils.ThreadUtils;
import com.android.systemui.res.R;
-import java.util.List;
-
/**
- * Base adapter for media output dialog.
+ * A RecyclerView adapter for the legacy UI media output dialog device list.
*/
-public abstract class MediaOutputBaseAdapter extends
- RecyclerView.Adapter<RecyclerView.ViewHolder> {
-
- record OngoingSessionStatus(boolean host) {}
-
- record GroupStatus(Boolean selected, Boolean deselectable) {}
-
- enum ConnectionState {
- CONNECTED,
- CONNECTING,
- DISCONNECTED,
- }
-
- protected final MediaSwitchingController mController;
+public class MediaOutputAdapterLegacy extends MediaOutputAdapterBase {
+ private static final String TAG = "MediaOutputAdapterL";
+ private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
private static final int UNMUTE_DEFAULT_VOLUME = 2;
-
- Context mContext;
+ private static final float DEVICE_DISABLED_ALPHA = 0.5f;
+ private static final float DEVICE_ACTIVE_ALPHA = 1f;
View mHolderView;
- boolean mIsDragging;
- int mCurrentActivePosition;
private boolean mIsInitVolumeFirstTime;
- public MediaOutputBaseAdapter(MediaSwitchingController controller) {
- mController = controller;
- mIsDragging = false;
- mCurrentActivePosition = -1;
+ public MediaOutputAdapterLegacy(MediaSwitchingController controller) {
+ super(controller);
mIsInitVolumeFirstTime = true;
}
- /**
- * Refresh current dataset
- */
- public abstract void updateItems();
-
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup,
int viewType) {
- mContext = viewGroup.getContext();
- mHolderView = LayoutInflater.from(mContext).inflate(MediaItem.getMediaLayoutId(viewType),
- viewGroup, false);
- return null;
- }
-
- void updateColorScheme(WallpaperColors wallpaperColors, boolean isDarkTheme) {
- mController.setCurrentColorScheme(wallpaperColors, isDarkTheme);
- }
+ Context context = viewGroup.getContext();
+ mHolderView = LayoutInflater.from(viewGroup.getContext()).inflate(
+ MediaItem.getMediaLayoutId(viewType),
+ viewGroup, false);
- boolean isCurrentlyConnected(MediaDevice device) {
- return TextUtils.equals(device.getId(),
- mController.getCurrentConnectedMediaDevice().getId())
- || (mController.getSelectedMediaDevice().size() == 1
- && isDeviceIncluded(mController.getSelectedMediaDevice(), device));
+ switch (viewType) {
+ case MediaItem.MediaItemType.TYPE_GROUP_DIVIDER:
+ return new MediaGroupDividerViewHolderLegacy(mHolderView);
+ case MediaItem.MediaItemType.TYPE_PAIR_NEW_DEVICE:
+ case MediaItem.MediaItemType.TYPE_DEVICE:
+ default:
+ return new MediaDeviceViewHolderLegacy(mHolderView, context);
+ }
}
- boolean isDeviceIncluded(List<MediaDevice> deviceList, MediaDevice targetDevice) {
- for (MediaDevice device : deviceList) {
- if (TextUtils.equals(device.getId(), targetDevice.getId())) {
- return true;
+ @Override
+ public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) {
+ if (position >= getItemCount()) {
+ if (DEBUG) {
+ Log.d(TAG, "Incorrect position: " + position + " list size: "
+ + getItemCount());
}
+ return;
+ }
+ MediaItem currentMediaItem = mMediaItemList.get(position);
+ switch (currentMediaItem.getMediaItemType()) {
+ case MediaItem.MediaItemType.TYPE_GROUP_DIVIDER:
+ ((MediaGroupDividerViewHolderLegacy) viewHolder).onBind(
+ currentMediaItem.getTitle());
+ break;
+ case MediaItem.MediaItemType.TYPE_PAIR_NEW_DEVICE:
+ ((MediaDeviceViewHolderLegacy) viewHolder).onBindPairNewDevice();
+ break;
+ case MediaItem.MediaItemType.TYPE_DEVICE:
+ ((MediaDeviceViewHolderLegacy) viewHolder).onBindDevice(currentMediaItem, position);
+ break;
+ default:
+ Log.d(TAG, "Incorrect position: " + position);
}
- return false;
- }
-
- boolean isDragging() {
- return mIsDragging;
- }
-
- int getCurrentActivePosition() {
- return mCurrentActivePosition;
}
public MediaSwitchingController getController() {
@@ -133,7 +123,7 @@ public abstract class MediaOutputBaseAdapter extends
/**
* ViewHolder for binding device view.
*/
- abstract class MediaDeviceBaseViewHolder extends RecyclerView.ViewHolder {
+ class MediaDeviceViewHolderLegacy extends MediaDeviceViewHolderBase {
private static final int ANIM_DURATION = 500;
@@ -158,8 +148,8 @@ public abstract class MediaOutputBaseAdapter extends
private ValueAnimator mVolumeAnimator;
private int mLatestUpdateVolume = -1;
- MediaDeviceBaseViewHolder(View view) {
- super(view);
+ MediaDeviceViewHolderLegacy(View view, Context context) {
+ super(view, context);
mContainerLayout = view.requireViewById(R.id.device_container);
mItemLayout = view.requireViewById(R.id.item_layout);
mTitleText = view.requireViewById(R.id.title);
@@ -180,8 +170,10 @@ public abstract class MediaOutputBaseAdapter extends
initAnimator();
}
- void onBind(MediaDevice device, int position) {
+ void onBindDevice(MediaItem mediaItem, int position) {
+ MediaDevice device = mediaItem.getMediaDevice().get();
mDeviceId = device.getId();
+ mItemLayout.setVisibility(View.VISIBLE);
mCheckBox.setVisibility(View.GONE);
mStatusIcon.setVisibility(View.GONE);
mEndTouchArea.setVisibility(View.GONE);
@@ -196,6 +188,54 @@ public abstract class MediaOutputBaseAdapter extends
mSeekBar.setProgressTintList(
ColorStateList.valueOf(mController.getColorSeekbarProgress()));
enableFocusPropertyForView(mContainerLayout);
+ renderItem(mediaItem, position);
+ }
+
+ /** Binds a ViewHolder for a "Connect a device" item. */
+ void onBindPairNewDevice() {
+ mTitleText.setTextColor(mController.getColorItemContent());
+ mCheckBox.setVisibility(View.GONE);
+ updateTitle(mContext.getText(R.string.media_output_dialog_pairing_new));
+ updateItemBackground(ConnectionState.DISCONNECTED);
+ final Drawable addDrawable = mContext.getDrawable(R.drawable.ic_add);
+ mTitleIcon.setImageDrawable(addDrawable);
+ mTitleIcon.setImageTintList(
+ ColorStateList.valueOf(mController.getColorItemContent()));
+ mContainerLayout.setOnClickListener(mController::launchBluetoothPairing);
+ }
+
+ @Override
+ protected void renderDeviceItem(boolean hideGroupItem, MediaDevice device,
+ ConnectionState connectionState, boolean restrictVolumeAdjustment,
+ GroupStatus groupStatus, OngoingSessionStatus ongoingSessionStatus,
+ View.OnClickListener clickListener, boolean deviceDisabled, String subtitle,
+ Drawable deviceStatusIcon) {
+ if (hideGroupItem) {
+ mItemLayout.setVisibility(View.GONE);
+ return;
+ }
+ updateTitle(device.getName());
+ updateTitleIcon(device, connectionState, restrictVolumeAdjustment);
+ updateSeekBar(device, connectionState, restrictVolumeAdjustment,
+ getDeviceItemContentDescription(device));
+ updateEndArea(device, connectionState, groupStatus, ongoingSessionStatus);
+ updateLoadingIndicator(connectionState);
+ updateFullItemClickListener(clickListener);
+ updateContentAlpha(deviceDisabled);
+ updateSubtitle(subtitle);
+ updateDeviceStatusIcon(deviceStatusIcon);
+ updateItemBackground(connectionState);
+ }
+
+ @Override
+ protected void renderDeviceGroupItem() {
+ String sessionName = mController.getSessionName() == null ? ""
+ : mController.getSessionName().toString();
+ updateTitle(sessionName);
+ updateUnmutedVolumeIcon(null /* device */);
+ updateGroupSeekBar(getGroupItemContentDescription(sessionName));
+ updateEndAreaForDeviceGroup();
+ updateItemBackground(ConnectionState.CONNECTED);
}
void updateTitle(CharSequence title) {
@@ -303,7 +343,7 @@ public abstract class MediaOutputBaseAdapter extends
private void initializeSeekbarVolume(
@Nullable MediaDevice device, int currentVolume,
boolean isCurrentSeekbarInvisible) {
- if (!mIsDragging) {
+ if (!isDragging()) {
if (mSeekBar.getVolume() != currentVolume && (mLatestUpdateVolume == -1
|| currentVolume == mLatestUpdateVolume)) {
// Update only if volume of device and value of volume bar doesn't match.
@@ -459,6 +499,132 @@ public abstract class MediaOutputBaseAdapter extends
: R.drawable.media_output_icon_volume;
}
+ private void updateContentAlpha(boolean deviceDisabled) {
+ float alphaValue = deviceDisabled ? DEVICE_DISABLED_ALPHA : DEVICE_ACTIVE_ALPHA;
+ mTitleIcon.setAlpha(alphaValue);
+ mTitleText.setAlpha(alphaValue);
+ mSubTitleText.setAlpha(alphaValue);
+ mStatusIcon.setAlpha(alphaValue);
+ }
+
+ private void updateDeviceStatusIcon(@Nullable Drawable deviceStatusIcon) {
+ if (deviceStatusIcon == null) {
+ mStatusIcon.setVisibility(View.GONE);
+ } else {
+ mStatusIcon.setImageDrawable(deviceStatusIcon);
+ mStatusIcon.setImageTintList(
+ ColorStateList.valueOf(mController.getColorItemContent()));
+ if (deviceStatusIcon instanceof AnimatedVectorDrawable) {
+ ((AnimatedVectorDrawable) deviceStatusIcon).start();
+ }
+ mStatusIcon.setVisibility(View.VISIBLE);
+ }
+ }
+
+
+ /** Renders the right side round pill button / checkbox. */
+ private void updateEndArea(@NonNull MediaDevice device, ConnectionState connectionState,
+ @Nullable GroupStatus groupStatus,
+ @Nullable OngoingSessionStatus ongoingSessionStatus) {
+ boolean showEndArea = false;
+ boolean isCheckbox = false;
+ // If both group status and the ongoing session status are present, only the ongoing
+ // session controls are displayed. The current layout design doesn't allow both group
+ // and ongoing session controls to be rendered simultaneously.
+ if (ongoingSessionStatus != null && connectionState == ConnectionState.CONNECTED) {
+ showEndArea = true;
+ updateEndAreaForOngoingSession(device, ongoingSessionStatus.host());
+ } else if (groupStatus != null && shouldShowGroupCheckbox(groupStatus)) {
+ showEndArea = true;
+ isCheckbox = true;
+ updateEndAreaForGroupCheckBox(device, groupStatus);
+ }
+ updateEndAreaVisibility(showEndArea, isCheckbox);
+ }
+
+ private void updateEndAreaForDeviceGroup() {
+ updateEndAreaWithIcon(
+ v -> {
+ onExpandGroupButtonClicked();
+ },
+ R.drawable.media_output_item_expand_group,
+ R.string.accessibility_expand_group);
+ updateEndAreaVisibility(true /* showEndArea */, false /* isCheckbox */);
+ }
+
+ private void updateEndAreaForOngoingSession(@NonNull MediaDevice device, boolean isHost) {
+ updateEndAreaWithIcon(
+ v -> mController.tryToLaunchInAppRoutingIntent(device.getId(), v),
+ isHost ? R.drawable.media_output_status_edit_session
+ : R.drawable.ic_sound_bars_anim,
+ R.string.accessibility_open_application);
+ }
+
+ private void updateEndAreaWithIcon(View.OnClickListener clickListener,
+ @DrawableRes int iconDrawableId,
+ @StringRes int accessibilityStringId) {
+ updateEndAreaColor(mController.getColorSeekbarProgress());
+ mEndClickIcon.setImageTintList(
+ ColorStateList.valueOf(mController.getColorItemContent()));
+ mEndClickIcon.setOnClickListener(clickListener);
+ mEndTouchArea.setOnClickListener(v -> mEndClickIcon.performClick());
+ Drawable drawable = mContext.getDrawable(iconDrawableId);
+ mEndClickIcon.setImageDrawable(drawable);
+ if (drawable instanceof AnimatedVectorDrawable) {
+ ((AnimatedVectorDrawable) drawable).start();
+ }
+ if (Flags.enableOutputSwitcherDeviceGrouping()) {
+ mEndClickIcon.setContentDescription(mContext.getString(accessibilityStringId));
+ }
+ }
+
+ private void updateEndAreaForGroupCheckBox(@NonNull MediaDevice device,
+ @NonNull GroupStatus groupStatus) {
+ boolean isEnabled = isGroupCheckboxEnabled(groupStatus);
+ mEndTouchArea.setOnClickListener(
+ isEnabled ? (v) -> mCheckBox.performClick() : null);
+ mEndTouchArea.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
+ updateEndAreaColor(groupStatus.selected() ? mController.getColorSeekbarProgress()
+ : mController.getColorItemBackground());
+ mEndTouchArea.setContentDescription(getDeviceItemContentDescription(device));
+ mCheckBox.setOnCheckedChangeListener(null);
+ mCheckBox.setChecked(groupStatus.selected());
+ mCheckBox.setOnCheckedChangeListener(
+ isEnabled ? (buttonView, isChecked) -> onGroupActionTriggered(
+ !groupStatus.selected(), device) : null);
+ mCheckBox.setEnabled(isEnabled);
+ setCheckBoxColor(mCheckBox, mController.getColorItemContent());
+ }
+
+ private void setCheckBoxColor(CheckBox checkBox, int color) {
+ int[][] states = {{android.R.attr.state_checked}, {}};
+ int[] colors = {color, color};
+ CompoundButtonCompat.setButtonTintList(checkBox, new
+ ColorStateList(states, colors));
+ }
+
+ private boolean shouldShowGroupCheckbox(@NonNull GroupStatus groupStatus) {
+ if (Flags.enableOutputSwitcherDeviceGrouping()) {
+ return isGroupCheckboxEnabled(groupStatus);
+ }
+ return true;
+ }
+
+ private boolean isGroupCheckboxEnabled(@NonNull GroupStatus groupStatus) {
+ boolean disabled = groupStatus.selected() && !groupStatus.deselectable();
+ return !disabled;
+ }
+
+ private void updateEndAreaColor(int color) {
+ mEndTouchArea.setBackgroundTintList(
+ ColorStateList.valueOf(color));
+ }
+
+ private void updateFullItemClickListener(@Nullable View.OnClickListener listener) {
+ mContainerLayout.setOnClickListener(listener);
+ updateIconAreaClickListener(listener);
+ }
+
void updateIconAreaClickListener(@Nullable View.OnClickListener listener) {
mIconAreaLayout.setOnClickListener(listener);
}
@@ -498,6 +664,7 @@ public abstract class MediaOutputBaseAdapter extends
});
}
+ @Override
protected void disableSeekBar() {
mSeekBar.setEnabled(false);
mSeekBar.setOnTouchListener((v, event) -> true);
@@ -589,7 +756,7 @@ public abstract class MediaOutputBaseAdapter extends
int currentVolume = MediaOutputSeekbar.scaleProgressToVolume(
seekBar.getProgress());
mStartFromMute = (currentVolume == 0);
- mIsDragging = true;
+ setIsDragging(true);
}
@Override
@@ -604,11 +771,25 @@ public abstract class MediaOutputBaseAdapter extends
}
mTitleIcon.setVisibility(View.VISIBLE);
mVolumeValueText.setVisibility(View.GONE);
- mIsDragging = false;
+ setIsDragging(false);
}
protected boolean shouldHandleProgressChanged() {
return mMediaDevice != null;
}
};
}
+
+ class MediaGroupDividerViewHolderLegacy extends RecyclerView.ViewHolder {
+ final TextView mTitleText;
+
+ MediaGroupDividerViewHolderLegacy(@NonNull View itemView) {
+ super(itemView);
+ mTitleText = itemView.requireViewById(R.id.title);
+ }
+
+ void onBind(String groupDividerTitle) {
+ mTitleText.setTextColor(mController.getColorItemContent());
+ mTitleText.setText(groupDividerTitle);
+ }
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java
index 64256f97fd78..d791361d555f 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java
@@ -105,7 +105,7 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog
private boolean mIsLeBroadcastCallbackRegistered;
private boolean mDismissing;
- MediaOutputBaseAdapter mAdapter;
+ MediaOutputAdapterBase mAdapter;
protected Executor mExecutor;
@@ -342,7 +342,7 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog
WallpaperColors wallpaperColors = WallpaperColors.fromBitmap(icon.getBitmap());
colorSetUpdated = !wallpaperColors.equals(mWallpaperColors);
if (colorSetUpdated) {
- mAdapter.updateColorScheme(wallpaperColors, isDarkThemeOn);
+ mMediaSwitchingController.setCurrentColorScheme(wallpaperColors, isDarkThemeOn);
updateButtonBackgroundColorFilter();
updateDialogBackgroundColor();
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialog.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialog.java
index 9b5b872a00db..9ade9e275ca1 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialog.java
@@ -245,7 +245,7 @@ public class MediaOutputBroadcastDialog extends MediaOutputBaseDialog {
broadcastSender,
mediaSwitchingController, /* includePlaybackAndAppMetadata */
true);
- mAdapter = new MediaOutputAdapter(mMediaSwitchingController);
+ mAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController);
// TODO(b/226710953): Move the part to MediaOutputBaseDialog for every class
// that extends MediaOutputBaseDialog
if (!aboveStatusbar) {
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java
index c9af7b322811..2e602be4556e 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java
@@ -53,7 +53,7 @@ public class MediaOutputDialog extends MediaOutputBaseDialog {
super(context, broadcastSender, mediaSwitchingController, includePlaybackAndAppMetadata);
mDialogTransitionAnimator = dialogTransitionAnimator;
mUiEventLogger = uiEventLogger;
- mAdapter = new MediaOutputAdapter(mMediaSwitchingController);
+ mAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController);
if (!aboveStatusbar) {
getWindow().setType(WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY);
}
diff --git a/packages/SystemUI/src/com/android/systemui/modes/shared/ModesUi.kt b/packages/SystemUI/src/com/android/systemui/modes/shared/ModesUi.kt
index a0663d72a076..e293e202633e 100644
--- a/packages/SystemUI/src/com/android/systemui/modes/shared/ModesUi.kt
+++ b/packages/SystemUI/src/com/android/systemui/modes/shared/ModesUi.kt
@@ -25,7 +25,7 @@ object ModesUi {
/** Is the refactor enabled */
@JvmStatic
inline val isEnabled
- get() = Flags.modesApi() && Flags.modesUi()
+ get() = Flags.modesUi()
/**
* Called to ensure code is only run when the flag is enabled. This protects users from the
diff --git a/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModel.kt b/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModel.kt
index c7b165415aea..c43c1a999fcb 100644
--- a/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModel.kt
@@ -20,12 +20,15 @@ import androidx.compose.runtime.getValue
import com.android.app.tracing.coroutines.launchTraced as launch
import com.android.systemui.lifecycle.ExclusiveActivatable
import com.android.systemui.lifecycle.Hydrator
+import com.android.systemui.media.controls.domain.pipeline.interactor.MediaCarouselInteractor
import com.android.systemui.scene.domain.interactor.SceneInteractor
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.shade.domain.interactor.ShadeInteractor
import com.android.systemui.shade.ui.viewmodel.ShadeHeaderViewModel
+import com.android.systemui.statusbar.disableflags.domain.interactor.DisableFlagsInteractor
import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor
import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel
+import com.android.systemui.utils.coroutines.flow.flatMapLatestConflated
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.awaitCancellation
@@ -33,6 +36,7 @@ import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.flowOf
/**
* Models UI state used to render the content of the notifications shade overlay.
@@ -47,6 +51,8 @@ constructor(
val notificationsPlaceholderViewModelFactory: NotificationsPlaceholderViewModel.Factory,
val sceneInteractor: SceneInteractor,
private val shadeInteractor: ShadeInteractor,
+ disableFlagsInteractor: DisableFlagsInteractor,
+ mediaCarouselInteractor: MediaCarouselInteractor,
activeNotificationsInteractor: ActiveNotificationsInteractor,
) : ExclusiveActivatable() {
@@ -69,6 +75,22 @@ constructor(
),
)
+ val showMedia: Boolean by
+ hydrator.hydratedStateOf(
+ traceName = "showMedia",
+ initialValue =
+ disableFlagsInteractor.disableFlags.value.isQuickSettingsEnabled() &&
+ mediaCarouselInteractor.hasActiveMediaOrRecommendation.value,
+ source =
+ disableFlagsInteractor.disableFlags.flatMapLatestConflated {
+ if (it.isQuickSettingsEnabled()) {
+ mediaCarouselInteractor.hasActiveMediaOrRecommendation
+ } else {
+ flowOf(false)
+ }
+ },
+ )
+
override suspend fun onActivated(): Nothing {
coroutineScope {
launch { hydrator.activate() }
diff --git a/packages/SystemUI/src/com/android/systemui/privacy/PrivacyDialogV2.kt b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyDialogV2.kt
index 57d40638b8df..9117afb1de6f 100644
--- a/packages/SystemUI/src/com/android/systemui/privacy/PrivacyDialogV2.kt
+++ b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyDialogV2.kt
@@ -39,6 +39,11 @@ import androidx.annotation.DrawableRes
import androidx.annotation.WorkerThread
import androidx.core.view.ViewCompat
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat
+import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat
+import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_COLLAPSE
+import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_EXPAND
+import androidx.core.view.accessibility.AccessibilityViewCommand
+import com.android.systemui.Flags
import com.android.systemui.animation.ViewHierarchyAnimator
import com.android.systemui.res.R
import com.android.systemui.statusbar.phone.SystemUIDialog
@@ -282,49 +287,95 @@ class PrivacyDialogV2(
val expandToggle =
itemHeader.findViewById<ImageView>(R.id.privacy_dialog_item_header_expand_toggle)!!
- expandToggle.setImageResource(R.drawable.privacy_dialog_expand_toggle_down)
expandToggle.visibility = View.VISIBLE
-
- ViewCompat.replaceAccessibilityAction(
- itemCard,
- AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_CLICK,
- context.getString(R.string.privacy_dialog_expand_action),
- null,
- )
-
val expandedLayout =
itemCard.findViewById<View>(R.id.privacy_dialog_item_header_expanded_layout)!!
expandedLayout.setOnClickListener {
// Stop clicks from propagating
}
- itemCard.setOnClickListener {
- if (expandedLayout.visibility == View.VISIBLE) {
- expandedLayout.visibility = View.GONE
- expandToggle.setImageResource(R.drawable.privacy_dialog_expand_toggle_down)
- ViewCompat.replaceAccessibilityAction(
- it!!,
- AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_CLICK,
- context.getString(R.string.privacy_dialog_expand_action),
- null,
- )
- } else {
- expandedLayout.visibility = View.VISIBLE
- expandToggle.setImageResource(R.drawable.privacy_dialog_expand_toggle_up)
- ViewCompat.replaceAccessibilityAction(
- it!!,
- AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_CLICK,
- context.getString(R.string.privacy_dialog_collapse_action),
- null,
- )
+ if (Flags.expandCollapsePrivacyDialog()) {
+ updateExpansion(ACTION_COLLAPSE, itemCard, expandedLayout, expandToggle)
+
+ itemCard.setOnClickListener {
+ if (expandedLayout.visibility == View.VISIBLE) {
+ updateExpansion(ACTION_COLLAPSE, it!!, expandedLayout, expandToggle)
+ } else {
+ updateExpansion(ACTION_EXPAND, it!!, expandedLayout, expandToggle)
+ }
}
- ViewHierarchyAnimator.animateNextUpdate(
- rootView = window!!.decorView,
- excludedViews = setOf(expandedLayout),
+ } else {
+ expandToggle.setImageResource(R.drawable.privacy_dialog_expand_toggle_down)
+ ViewCompat.replaceAccessibilityAction(
+ itemCard,
+ AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_CLICK,
+ context.getString(R.string.privacy_dialog_expand_action),
+ null,
)
+
+ itemCard.setOnClickListener {
+ if (expandedLayout.visibility == View.VISIBLE) {
+ expandedLayout.visibility = View.GONE
+ expandToggle.setImageResource(R.drawable.privacy_dialog_expand_toggle_down)
+ ViewCompat.replaceAccessibilityAction(
+ it!!,
+ AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_CLICK,
+ context.getString(R.string.privacy_dialog_expand_action),
+ null,
+ )
+ } else {
+ expandedLayout.visibility = View.VISIBLE
+ expandToggle.setImageResource(R.drawable.privacy_dialog_expand_toggle_up)
+ ViewCompat.replaceAccessibilityAction(
+ it!!,
+ AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_CLICK,
+ context.getString(R.string.privacy_dialog_collapse_action),
+ null,
+ )
+ }
+ ViewHierarchyAnimator.animateNextUpdate(
+ rootView = window!!.decorView,
+ excludedViews = setOf(expandedLayout),
+ )
+ }
}
}
+ private fun updateExpansion(
+ newState: AccessibilityActionCompat,
+ itemCard: View,
+ expandedLayout: View,
+ expandToggle: ImageView,
+ ) {
+ expandedLayout.visibility = if (newState == ACTION_COLLAPSE) View.GONE else View.VISIBLE
+ expandToggle.setImageResource(
+ if (newState == ACTION_COLLAPSE) R.drawable.privacy_dialog_expand_toggle_down
+ else R.drawable.privacy_dialog_expand_toggle_up
+ )
+ val accessibilityString =
+ context.getString(
+ if (newState == ACTION_COLLAPSE) R.string.privacy_dialog_expand_action
+ else R.string.privacy_dialog_collapse_action
+ )
+ ViewCompat.replaceAccessibilityAction(
+ itemCard,
+ AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_CLICK,
+ accessibilityString,
+ null,
+ )
+ val expandCollapseAccessibilityListener =
+ AccessibilityViewCommand { view: View, _: AccessibilityViewCommand.CommandArguments? ->
+ view.callOnClick()
+ }
+ ViewCompat.replaceAccessibilityAction(
+ itemCard,
+ if (newState == ACTION_COLLAPSE) ACTION_EXPAND else ACTION_COLLAPSE,
+ accessibilityString,
+ expandCollapseAccessibilityListener,
+ )
+ ViewCompat.removeAccessibilityAction(itemCard, newState.id)
+ }
+
private fun updateIconView(iconView: ImageView, indicatorIcon: Drawable, active: Boolean) {
indicatorIcon.setTint(getForegroundColor(active))
val backgroundIcon = getMutableDrawable(R.drawable.privacy_dialog_background_circle)
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/SubtitleArrayMapping.kt b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/SubtitleArrayMapping.kt
index c9a0635021da..61a8fa3d2a6e 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/SubtitleArrayMapping.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/SubtitleArrayMapping.kt
@@ -55,6 +55,7 @@ object SubtitleArrayMapping {
subtitleIdsMap["font_scaling"] = R.array.tile_states_font_scaling
subtitleIdsMap["hearing_devices"] = R.array.tile_states_hearing_devices
subtitleIdsMap["notes"] = R.array.tile_states_notes
+ subtitleIdsMap["desktopeffects"] = R.array.tile_states_desktopeffects
}
/** Get the subtitle resource id of the given tile */
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateLegacy.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateLegacy.java
index c6bcab48fa68..75cb8ddca484 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateLegacy.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateLegacy.java
@@ -589,8 +589,10 @@ public class InternetDialogDelegateLegacy implements
}
mSecondaryMobileNetworkLayout = mDialogView.findViewById(
R.id.secondary_mobile_network_layout);
- mSecondaryMobileNetworkLayout.setOnClickListener(
- this::onClickConnectedSecondarySub);
+ if (mCanConfigMobileData) {
+ mSecondaryMobileNetworkLayout.setOnClickListener(
+ this::onClickConnectedSecondarySub);
+ }
mSecondaryMobileNetworkLayout.setBackground(mBackgroundOn);
TextView mSecondaryMobileTitleText = mDialogView.requireViewById(
@@ -623,6 +625,8 @@ public class InternetDialogDelegateLegacy implements
mDialogView.requireViewById(R.id.secondary_settings_icon);
mSecondaryMobileSettingsIcon.setColorFilter(
dialog.getContext().getColor(R.color.connected_network_primary_color));
+ mSecondaryMobileSettingsIcon.setVisibility(mCanConfigMobileData ?
+ View.VISIBLE : View.INVISIBLE);
// set secondary visual for default data sub
mMobileNetworkLayout.setBackground(mBackgroundOff);
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 caa7bbae0420..e357f63479dc 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
@@ -163,4 +163,12 @@ constructor(
fun setTransitionState(transitionState: Flow<ObservableTransitionState>?) {
_transitionState.value = transitionState
}
+
+ /**
+ * If currently in a transition between contents, cancel that transition and go back to the
+ * pre-transition state.
+ */
+ fun freezeAndAnimateToCurrentState() {
+ dataSource.freezeAndAnimateToCurrentState()
+ }
}
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 e9e7deca0abf..01180859b1d2 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
@@ -234,6 +234,10 @@ constructor(
* The change is animated. Therefore, it will be some time before the UI will switch to the
* desired scene. Once enough of the transition has occurred, the [currentScene] will become
* [toScene] (unless the transition is canceled by user action or another call to this method).
+ *
+ * If [forceSettleToTargetScene] is `true` and the target scene is the same as the current
+ * scene, any current transition will be canceled and an animation to the target scene will be
+ * started.
*/
@JvmOverloads
fun changeScene(
@@ -241,9 +245,19 @@ constructor(
loggingReason: String,
transitionKey: TransitionKey? = null,
sceneState: Any? = null,
+ forceSettleToTargetScene: Boolean = false,
) {
val currentSceneKey = currentScene.value
val resolvedScene = sceneFamilyResolvers.get()[toScene]?.resolvedScene?.value ?: toScene
+
+ if (resolvedScene == currentSceneKey && forceSettleToTargetScene) {
+ logger.logSceneChangeCancellation(scene = resolvedScene, sceneState = sceneState)
+ onSceneAboutToChangeListener.forEach {
+ it.onSceneAboutToChange(resolvedScene, sceneState)
+ }
+ repository.freezeAndAnimateToCurrentState()
+ }
+
if (
!validateSceneChange(
from = currentSceneKey,
@@ -523,14 +537,32 @@ constructor(
}
if (from == to) {
+ logger.logSceneChangeRejection(
+ from = from,
+ to = to,
+ originalChangeReason = loggingReason,
+ rejectionReason = "${from.debugName} is the same as ${to.debugName}",
+ )
return false
}
if (to !in repository.allContentKeys) {
+ logger.logSceneChangeRejection(
+ from = from,
+ to = to,
+ originalChangeReason = loggingReason,
+ rejectionReason = "${to.debugName} isn't present in allContentKeys",
+ )
return false
}
if (disabledContentInteractor.isDisabled(to)) {
+ logger.logSceneChangeRejection(
+ from = from,
+ to = to,
+ originalChangeReason = loggingReason,
+ rejectionReason = "${to.debugName} is currently disabled",
+ )
return false
}
@@ -580,14 +612,58 @@ constructor(
}
if (to != null && disabledContentInteractor.isDisabled(to)) {
+ logger.logSceneChangeRejection(
+ from = from,
+ to = to,
+ originalChangeReason = loggingReason,
+ rejectionReason = "${to.debugName} is currently disabled",
+ )
return false
}
- val isFromValid = (from == null) || (from in currentOverlays.value)
- val isToValid =
- (to == null) || (to !in currentOverlays.value && to in repository.allContentKeys)
+ return when {
+ to != null && from != null && to == from -> {
+ logger.logSceneChangeRejection(
+ from = from,
+ to = to,
+ originalChangeReason = loggingReason,
+ rejectionReason = "${from.debugName} is the same as ${to.debugName}",
+ )
+ false
+ }
- return isFromValid && isToValid && from != to
+ to != null && to !in repository.allContentKeys -> {
+ logger.logSceneChangeRejection(
+ from = from,
+ to = to,
+ originalChangeReason = loggingReason,
+ rejectionReason = "${to.debugName} is not in allContentKeys",
+ )
+ false
+ }
+
+ from != null && from !in currentOverlays.value -> {
+ logger.logSceneChangeRejection(
+ from = from,
+ to = to,
+ originalChangeReason = loggingReason,
+ rejectionReason = "${from.debugName} is not a current overlay",
+ )
+ false
+ }
+
+ to != null && to in currentOverlays.value -> {
+ logger.logSceneChangeRejection(
+ from = from,
+ to = to,
+ originalChangeReason = loggingReason,
+ rejectionReason = "${to.debugName} is already a current overlay",
+ )
+ false
+ }
+
+ else -> true
+ }
}
/** Returns a flow indicating if the currently visible scene can be resolved from [family]. */
diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/resolver/HomeSceneFamilyResolver.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/resolver/HomeSceneFamilyResolver.kt
index 140b231593bd..aab37d433e4f 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/domain/resolver/HomeSceneFamilyResolver.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/domain/resolver/HomeSceneFamilyResolver.kt
@@ -16,7 +16,6 @@
package com.android.systemui.scene.domain.resolver
-import android.util.Log
import com.android.compose.animation.scene.SceneKey
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
@@ -84,7 +83,7 @@ constructor(
isDreamingWithOverlay: Boolean,
isAbleToDream: Boolean,
): SceneKey {
- val result = when {
+ return when {
// Dream can run even if Keyguard is disabled, thus it has the highest priority here.
isDreamingWithOverlay && isAbleToDream -> Scenes.Dream
!isKeyguardEnabled -> Scenes.Gone
@@ -93,21 +92,9 @@ constructor(
!isUnlocked -> Scenes.Lockscreen
else -> Scenes.Gone
}
- Log.d(TAG, "homeScene emitting $result, values:")
- Log.d(TAG, " isKeyguardEnabled=$isKeyguardEnabled")
- Log.d(TAG, " canSwipeToEnter=$canSwipeToEnter")
- Log.d(TAG, " isDeviceEntered=$isDeviceEntered" )
- Log.d(TAG, " isUnlocked=$isUnlocked")
- Log.d(TAG, " isDreamingWithOverlay=$isDreamingWithOverlay")
- Log.d(TAG, " isAbleToDream=$isAbleToDream")
- Log.d(TAG, "")
- return result
}
companion object {
-
- private const val TAG = "HomeSceneFamilyResolver"
-
val homeScenes =
setOf(
Scenes.Gone,
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 94e32fcb9ac6..218ad477c45e 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
@@ -90,6 +90,7 @@ import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine
@@ -553,6 +554,7 @@ constructor(
targetSceneKey = Scenes.Lockscreen,
loggingReason = "device is starting to sleep",
sceneState = keyguardInteractor.asleepKeyguardState.value,
+ freezeAndAnimateToCurrentState = true,
)
} else {
val canSwipeToEnter = deviceEntryInteractor.canSwipeToEnter.value
@@ -610,15 +612,24 @@ constructor(
private fun handleShadeTouchability() {
applicationScope.launch {
- shadeInteractor.isShadeTouchable
- .distinctUntilChanged()
- .filter { !it }
- .collect {
- switchToScene(
- targetSceneKey = Scenes.Lockscreen,
- loggingReason = "device became non-interactive (SceneContainerStartable)",
- )
+ repeatWhen(deviceEntryInteractor.isDeviceEntered.map { !it }) {
+ // Run logic only when the device isn't entered.
+ repeatWhen(
+ sceneInteractor.transitionState.map { !it.isTransitioning(to = Scenes.Gone) }
+ ) {
+ // Run logic only when not transitioning to gone.
+ shadeInteractor.isShadeTouchable
+ .distinctUntilChanged()
+ .filter { !it }
+ .collect {
+ switchToScene(
+ targetSceneKey = Scenes.Lockscreen,
+ loggingReason =
+ "device became non-interactive (SceneContainerStartable)",
+ )
+ }
}
+ }
}
}
@@ -923,11 +934,13 @@ constructor(
targetSceneKey: SceneKey,
loggingReason: String,
sceneState: Any? = null,
+ freezeAndAnimateToCurrentState: Boolean = false,
) {
sceneInteractor.changeScene(
toScene = targetSceneKey,
loggingReason = loggingReason,
sceneState = sceneState,
+ forceSettleToTargetScene = freezeAndAnimateToCurrentState,
)
}
@@ -1013,6 +1026,14 @@ constructor(
}
}
+ private suspend fun repeatWhen(condition: Flow<Boolean>, block: suspend () -> Unit) {
+ condition.distinctUntilChanged().collectLatest { conditionMet ->
+ if (conditionMet) {
+ block()
+ }
+ }
+ }
+
companion object {
private const val TAG = "SceneContainerStartable"
}
diff --git a/packages/SystemUI/src/com/android/systemui/scene/shared/logger/SceneLogger.kt b/packages/SystemUI/src/com/android/systemui/scene/shared/logger/SceneLogger.kt
index d00585858ccb..73c71f6088e1 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/shared/logger/SceneLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/shared/logger/SceneLogger.kt
@@ -16,6 +16,7 @@
package com.android.systemui.scene.shared.logger
+import com.android.compose.animation.scene.ContentKey
import com.android.compose.animation.scene.ObservableTransitionState
import com.android.compose.animation.scene.OverlayKey
import com.android.compose.animation.scene.SceneKey
@@ -74,6 +75,50 @@ class SceneLogger @Inject constructor(@SceneFrameworkLog private val logBuffer:
)
}
+ fun logSceneChangeCancellation(scene: SceneKey, sceneState: Any?) {
+ logBuffer.log(
+ tag = TAG,
+ level = LogLevel.INFO,
+ messageInitializer = {
+ str1 = scene.debugName
+ str2 = sceneState?.toString()
+ },
+ messagePrinter = { "CANCELED scene change. scene: $str1, sceneState: $str2" },
+ )
+ }
+
+ fun logSceneChangeRejection(
+ from: ContentKey?,
+ to: ContentKey?,
+ originalChangeReason: String,
+ rejectionReason: String,
+ ) {
+ logBuffer.log(
+ tag = TAG,
+ level = LogLevel.INFO,
+ messageInitializer = {
+ str1 = "${from?.debugName ?: "<none>"} → ${to?.debugName ?: "<none>"}"
+ str2 = rejectionReason
+ str3 = originalChangeReason
+ bool1 = to is OverlayKey
+ },
+ messagePrinter = {
+ buildString {
+ append("REJECTED ")
+ append(
+ if (bool1) {
+ "overlay "
+ } else {
+ "scene "
+ }
+ )
+ append("change $str1 because \"$str2\" ")
+ append("(original change reason: \"$str3\")")
+ }
+ },
+ )
+ }
+
fun logSceneTransition(transitionState: ObservableTransitionState) {
when (transitionState) {
is ObservableTransitionState.Transition -> {
diff --git a/packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneDataSource.kt b/packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneDataSource.kt
index daf2d7f698b6..42c4b24a72d3 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneDataSource.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneDataSource.kt
@@ -83,4 +83,10 @@ interface SceneDataSource {
/** Asks for [overlay] to be instantly hidden, without an animated transition of any kind. */
fun instantlyHideOverlay(overlay: OverlayKey)
+
+ /**
+ * If currently in a transition between contents, cancel that transition and go back to the
+ * pre-transition state.
+ */
+ fun freezeAndAnimateToCurrentState()
}
diff --git a/packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneDataSourceDelegator.kt b/packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneDataSourceDelegator.kt
index dcb699539760..d6dce38d0bbf 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneDataSourceDelegator.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneDataSourceDelegator.kt
@@ -82,6 +82,10 @@ class SceneDataSourceDelegator(applicationScope: CoroutineScope, config: SceneCo
delegateMutable.value.instantlyHideOverlay(overlay)
}
+ override fun freezeAndAnimateToCurrentState() {
+ delegateMutable.value.freezeAndAnimateToCurrentState()
+ }
+
/**
* Binds the current, dependency injection provided [SceneDataSource] to the given object.
*
@@ -120,5 +124,7 @@ class SceneDataSourceDelegator(applicationScope: CoroutineScope, config: SceneCo
override fun instantlyShowOverlay(overlay: OverlayKey) = Unit
override fun instantlyHideOverlay(overlay: OverlayKey) = Unit
+
+ override fun freezeAndAnimateToCurrentState() = Unit
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayAwareModule.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayAwareModule.kt
index f926d39760fe..96b224fbd4f3 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayAwareModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayAwareModule.kt
@@ -42,12 +42,12 @@ 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.notification.stack.NotificationStackRebindingHider
+import com.android.systemui.statusbar.notification.stack.NotificationStackRebindingHiderImpl
import com.android.systemui.statusbar.phone.ConfigurationControllerImpl
import com.android.systemui.statusbar.phone.ConfigurationForwarder
import com.android.systemui.statusbar.policy.ConfigurationController
-import dagger.BindsOptionalOf
import dagger.Module
import dagger.Provides
import dagger.multibindings.ClassKey
@@ -67,7 +67,7 @@ import javax.inject.Qualifier
* By using this dedicated module, we ensure the notification shade window always utilizes the
* correct display context and resources, regardless of the display it's on.
*/
-@Module(includes = [OptionalShadeDisplayAwareBindings::class, ShadeDisplayPolicyModule::class])
+@Module(includes = [ShadeDisplayPolicyModule::class])
object ShadeDisplayAwareModule {
/** Creates a new context for the shade window. */
@@ -242,17 +242,6 @@ object ShadeDisplayAwareModule {
}
}
- @Provides
- @IntoMap
- @ClassKey(ShadeDisplaysInteractor::class)
- fun provideShadeDisplaysInteractor(impl: Provider<ShadeDisplaysInteractor>): CoreStartable {
- return if (ShadeWindowGoesAround.isEnabled) {
- impl.get()
- } else {
- CoreStartable.NOP
- }
- }
-
/**
* Provided for making classes easier to test. In tests, a custom method to wait for the next
* frame can be easily provided.
@@ -264,11 +253,25 @@ object ShadeDisplayAwareModule {
fun provideShadeOnDefaultDisplayWhenLocked(): Boolean = true
}
+/** Module that should be included only if the shade window [WindowRootView] is available. */
@Module
-internal interface OptionalShadeDisplayAwareBindings {
- @BindsOptionalOf fun bindOptionalOfWindowRootView(): WindowRootView
+object ShadeDisplayAwareWithShadeWindowModule {
+ @Provides
+ @IntoMap
+ @ClassKey(ShadeDisplaysInteractor::class)
+ fun provideShadeDisplaysInteractor(impl: Provider<ShadeDisplaysInteractor>): CoreStartable {
+ return if (ShadeWindowGoesAround.isEnabled) {
+ impl.get()
+ } else {
+ CoreStartable.NOP
+ }
+ }
- @BindsOptionalOf fun bindOptionalOShadeExpandedStateInteractor(): ShadeExpandedStateInteractor
+ @Provides
+ @SysUISingleton
+ fun bindNotificationStackRebindingHider(
+ impl: NotificationStackRebindingHiderImpl
+ ): NotificationStackRebindingHider = impl
}
/**
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayChangeLatencyTracker.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayChangeLatencyTracker.kt
index 13b540aa54ba..5fda998dac2d 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayChangeLatencyTracker.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayChangeLatencyTracker.kt
@@ -24,8 +24,6 @@ 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.data.repository.ShadeDisplaysRepository
-import com.android.systemui.util.kotlin.getOrNull
-import java.util.Optional
import java.util.concurrent.CancellationException
import javax.inject.Inject
import kotlin.time.Duration.Companion.seconds
@@ -51,22 +49,13 @@ import kotlinx.coroutines.withTimeout
class ShadeDisplayChangeLatencyTracker
@Inject
constructor(
- optionalShadeRootView: Optional<WindowRootView>,
+ private val shadeRootView: WindowRootView,
@ShadeDisplayAware private val configurationRepository: ConfigurationRepository,
private val latencyTracker: LatencyTracker,
@Background private val bgScope: CoroutineScope,
private val choreographerUtils: ChoreographerUtils,
) {
- private val shadeRootView =
- optionalShadeRootView.getOrNull()
- ?: error(
- """
- ShadeRootView must be provided for ShadeDisplayChangeLatencyTracker to work.
- If it is not, it means this is being instantiated in a SystemUI variant that shouldn't.
- """
- .trimIndent()
- )
/**
* We need to keep this always up to date eagerly to avoid delays receiving the new display ID.
*/
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt
index 7d4b0ed6304c..c44e066aad3a 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt
@@ -54,7 +54,12 @@ import javax.inject.Provider
/** Module for classes related to the notification shade. */
@Module(
includes =
- [StartShadeModule::class, ShadeViewProviderModule::class, WindowRootViewBlurModule::class]
+ [
+ StartShadeModule::class,
+ ShadeViewProviderModule::class,
+ WindowRootViewBlurModule::class,
+ ShadeDisplayAwareWithShadeWindowModule::class,
+ ]
)
abstract class ShadeModule {
companion object {
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 e746274a39c1..9a5c96824e77 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
@@ -39,9 +39,7 @@ import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotif
import com.android.systemui.statusbar.notification.row.NotificationRebindingTracker
import com.android.systemui.statusbar.notification.stack.NotificationStackRebindingHider
import com.android.systemui.statusbar.phone.ConfigurationForwarder
-import com.android.systemui.util.kotlin.getOrNull
import com.android.window.flags.Flags
-import java.util.Optional
import javax.inject.Inject
import kotlin.coroutines.CoroutineContext
import kotlin.time.Duration.Companion.seconds
@@ -63,17 +61,14 @@ constructor(
@Background private val bgScope: CoroutineScope,
@Main private val mainThreadContext: CoroutineContext,
private val shadeDisplayChangeLatencyTracker: ShadeDisplayChangeLatencyTracker,
- shadeExpandedInteractor: Optional<ShadeExpandedStateInteractor>,
+ private val shadeExpandedInteractor: ShadeExpandedStateInteractor,
private val shadeExpansionIntent: ShadeExpansionIntent,
private val activeNotificationsInteractor: ActiveNotificationsInteractor,
private val notificationRebindingTracker: NotificationRebindingTracker,
- notificationStackRebindingHider: Optional<NotificationStackRebindingHider>,
+ private val notificationStackRebindingHider: NotificationStackRebindingHider,
@ShadeDisplayAware private val configForwarder: ConfigurationForwarder,
) : CoreStartable {
- private val shadeExpandedInteractor = requireOptional(shadeExpandedInteractor)
- private val notificationStackRebindingHider = requireOptional(notificationStackRebindingHider)
-
private val hasActiveNotifications: Boolean
get() = activeNotificationsInteractor.areAnyNotificationsPresentValue
@@ -224,24 +219,5 @@ constructor(
const val TAG = "ShadeDisplaysInteractor"
const val COLLAPSE_EXPAND_REASON = "Shade window move"
val TIMEOUT = 1.seconds
-
- /**
- * [ShadeDisplaysInteractor] is bound in the SystemUI module for all variants, but needs
- * some specific dependencies to be bound from each variant (e.g.
- * [ShadeExpandedStateInteractor] or [NotificationStackRebindingHider]). When those are not
- * bound, this class is not expected to be instantiated, and trying to instantiate it would
- * crash.
- */
- inline fun <reified T> requireOptional(optional: Optional<T>): T {
- return optional.getOrNull()
- ?: error(
- """
- ${T::class.java.simpleName} 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()
- )
- }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImpl.kt
index 9d81be2091c2..e8b5d5bdf7df 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImpl.kt
@@ -16,7 +16,6 @@
package com.android.systemui.shade.domain.interactor
-import android.util.Log
import com.android.app.tracing.coroutines.flow.flowName
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
@@ -39,7 +38,6 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn
/** The non-empty [ShadeInteractor] implementation. */
@@ -100,31 +98,17 @@ constructor(
override val isShadeTouchable: Flow<Boolean> =
combine(
- powerInteractor.isAsleep.onEach {
- Log.d(TAG, "isShadeTouchable: upstream isAsleep=$it")
- },
- keyguardTransitionInteractor
- .isInTransition(Edge.create(to = KeyguardState.AOD))
- .onEach { Log.d(TAG, "isShadeTouchable: upstream isTransitioningToAod=$it") },
- keyguardRepository.dozeTransitionModel
- .map { it.to == DozeStateModel.DOZE_PULSING }
- .onEach { Log.d(TAG, "isShadeTouchable: upstream isPulsing=$it") },
+ powerInteractor.isAsleep,
+ keyguardTransitionInteractor.isInTransition(Edge.create(to = KeyguardState.AOD)),
+ keyguardRepository.dozeTransitionModel.map { it.to == DozeStateModel.DOZE_PULSING },
) { isAsleep, isTransitioningToAod, isPulsing ->
- val downstream =
- when {
- // If the device is transitioning to AOD, only accept touches if
- // still animating.
- isTransitioningToAod -> dozeParams.shouldControlScreenOff()
- // If the device is asleep, only accept touches if there's a pulse
- isAsleep -> isPulsing
- else -> true
- }
- Log.d(TAG, "isShadeTouchable emitting $downstream, values:")
- Log.d(TAG, " isAsleep=$isAsleep")
- Log.d(TAG, " isTransitioningToAod=$isTransitioningToAod")
- Log.d(TAG, " isPulsing=$isPulsing")
- Log.d(TAG, "")
- downstream
+ when {
+ // If the device is transitioning to AOD, only accept touches if still animating.
+ isTransitioningToAod -> dozeParams.shouldControlScreenOff()
+ // If the device is asleep, only accept touches if there's a pulse
+ isAsleep -> isPulsing
+ else -> true
+ }
}
override val isExpandToQsEnabled: Flow<Boolean> =
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractor.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractor.kt
index 8f4e8701cad8..1ab0b93da175 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractor.kt
@@ -22,6 +22,7 @@ import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.log.table.TableLogBuffer
import com.android.systemui.log.table.logDiffsForTable
import com.android.systemui.scene.domain.SceneFrameworkTableLog
+import com.android.systemui.scene.shared.flag.SceneContainerFlag
import com.android.systemui.shade.data.repository.ShadeRepository
import com.android.systemui.shade.shared.model.ShadeMode
import com.android.systemui.shared.settings.data.repository.SecureSettingsRepository
@@ -32,6 +33,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.stateIn
/**
@@ -89,10 +91,14 @@ constructor(
) : ShadeModeInteractor {
private val isDualShadeEnabled: Flow<Boolean> =
- secureSettingsRepository.boolSetting(
- Settings.Secure.DUAL_SHADE,
- defaultValue = DUAL_SHADE_ENABLED_DEFAULT,
- )
+ if (SceneContainerFlag.isEnabled) {
+ secureSettingsRepository.boolSetting(
+ Settings.Secure.DUAL_SHADE,
+ defaultValue = DUAL_SHADE_ENABLED_DEFAULT,
+ )
+ } else {
+ flowOf(false)
+ }
override val isShadeLayoutWide: StateFlow<Boolean> = repository.isShadeLayoutWide
diff --git a/packages/SystemUI/src/com/android/systemui/shade/shared/flag/ShadeWindowGoesAround.kt b/packages/SystemUI/src/com/android/systemui/shade/shared/flag/ShadeWindowGoesAround.kt
index c23ff5302b3c..dc444ffc2a34 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/shared/flag/ShadeWindowGoesAround.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/shared/flag/ShadeWindowGoesAround.kt
@@ -16,6 +16,7 @@
package com.android.systemui.shade.shared.flag
+import android.window.DesktopExperienceFlags
import com.android.systemui.Flags
import com.android.systemui.flags.FlagToken
import com.android.systemui.flags.RefactorFlagUtils
@@ -30,10 +31,26 @@ object ShadeWindowGoesAround {
val token: FlagToken
get() = FlagToken(FLAG_NAME, isEnabled)
+ /**
+ * This is defined as [DesktopExperienceFlags] to make it possible to enable it together with
+ * all the other desktop experience flags from the dev settings.
+ *
+ * Alternatively, using adb:
+ * ```bash
+ * adb shell aflags enable com.android.window.flags.show_desktop_experience_dev_option && \
+ * adb shell setprop persist.wm.debug.desktop_experience_devopts 1
+ * ```
+ */
+ val FLAG =
+ DesktopExperienceFlags.DesktopExperienceFlag(
+ Flags::shadeWindowGoesAround,
+ /* shouldOverrideByDevOption= */ true,
+ )
+
/** Is the refactor enabled */
@JvmStatic
inline val isEnabled: Boolean
- get() = Flags.shadeWindowGoesAround()
+ get() = FLAG.isTrue
/**
* Called to ensure code is only run when the flag is enabled. This protects users from the
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractor.kt
index b1af811178e4..d8c3e2546a8f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractor.kt
@@ -16,6 +16,7 @@
package com.android.systemui.statusbar.chips.notification.domain.interactor
+import com.android.systemui.activity.data.model.AppVisibilityModel
import com.android.systemui.activity.data.repository.ActivityManagerRepository
import com.android.systemui.log.LogBuffer
import com.android.systemui.log.core.Logger
@@ -30,8 +31,6 @@ import dagger.assisted.AssistedInject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.flatMapLatest
-import kotlinx.coroutines.flow.map
/**
* Interactor representing a single notification's status bar chip.
@@ -53,6 +52,7 @@ constructor(
@StatusBarChipsLog private val logBuffer: LogBuffer,
) {
private val key = startingModel.key
+ private val uid = startingModel.uid
private val logger = Logger(logBuffer, "Notif".pad())
// [StatusBarChipLogTag] recommends a max tag length of 20, so [extraLogTag] should NOT be the
// top-level tag. It should instead be provided as the first string in each log message.
@@ -88,28 +88,36 @@ constructor(
}
return
}
+
+ if (model.uid != uid) {
+ logger.e({
+ "$str1: received model with different uid, which shouldn't happen. " +
+ "Original UID: $int1, New UID: $int2. " +
+ "Proceeding as usual, but app visibility changes will be for *old* UID."
+ }) {
+ str1 = extraLogTag
+ int1 = uid
+ int2 = model.uid
+ }
+ }
_notificationModel.value = model
}
- private val uid: Flow<Int> = _notificationModel.map { it.uid }
-
- /** True if the application managing the notification is visible to the user. */
- private val isAppVisible: Flow<Boolean> =
- uid.flatMapLatest { currentUid ->
- activityManagerRepository.createIsAppVisibleFlow(currentUid, logger, extraLogTag)
- }
+ /** Details about when the app managing the notification was & is visible to the user. */
+ private val appVisibility: Flow<AppVisibilityModel> =
+ activityManagerRepository.createAppVisibilityFlow(uid, logger, extraLogTag)
/**
* Emits this notification's status bar chip, or null if this notification shouldn't show a
* status bar chip.
*/
val notificationChip: Flow<NotificationChipModel?> =
- combine(_notificationModel, isAppVisible) { notif, isAppVisible ->
- notif.toNotificationChipModel(isAppVisible)
+ combine(_notificationModel, appVisibility) { notif, appVisibility ->
+ notif.toNotificationChipModel(appVisibility)
}
private fun ActiveNotificationModel.toNotificationChipModel(
- isVisible: Boolean
+ appVisibility: AppVisibilityModel
): NotificationChipModel? {
val promotedContent = this.promotedContent
if (promotedContent == null) {
@@ -134,11 +142,13 @@ constructor(
}
return NotificationChipModel(
- key,
- appName,
- statusBarChipIconView,
- promotedContent,
- isVisible,
+ key = key,
+ appName = appName,
+ statusBarChipIconView = statusBarChipIconView,
+ promotedContent = promotedContent,
+ creationTime = creationTime,
+ isAppVisible = appVisibility.isAppCurrentlyVisible,
+ lastAppVisibleTime = appVisibility.lastAppVisibleTime,
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractor.kt
index c26d10311f1e..edb44185459c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractor.kt
@@ -32,6 +32,7 @@ import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotif
import com.android.systemui.util.kotlin.pairwise
import com.android.systemui.util.time.SystemClock
import javax.inject.Inject
+import kotlin.math.max
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
@@ -39,9 +40,11 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onEach
/** An interactor for the notification chips shown in the status bar. */
@SysUISingleton
@@ -132,9 +135,6 @@ constructor(
}
interactor.setNotification(notif)
}
- logger.d({ "Interactors: $str1" }) {
- str1 = promotedNotificationInteractorMap.keys.joinToString(separator = " /// ")
- }
promotedNotificationInteractors.value =
promotedNotificationInteractorMap.values.toList()
}
@@ -145,26 +145,23 @@ constructor(
* Emits all notifications that are eligible to show as chips in the status bar. This is
* different from which chips will *actually* show, see [shownNotificationChips] for that.
*/
- private val allNotificationChips: Flow<List<NotificationChipModel>> =
+ val allNotificationChips: Flow<List<NotificationChipModel>> =
if (StatusBarNotifChips.isEnabled) {
// For all our current interactors...
- promotedNotificationInteractors.flatMapLatest { intrs ->
- // Stable-sort the promoted notifications by when they first appeared so that:
- // 1) The chips don't switch places if the older chip gets a notification update.
- // 2) The chips don't switch places when the second chip is tapped. (Whichever
- // notification is showing heads-up is considered to be the top notification, which
- // means tapping the second chip would move it to be the first chip if we didn't
- // sort by appearance time here.)
- // 3) Older chips get hidden if there's not enough room for all chips.
- val interactors = intrs.sortedByDescending { it.creationTime }
+ // TODO(b/364653005): When a promoted notification is added or removed, each individual
+ // interactor's [notificationChip] flow becomes un-collected then re-collected, which
+ // can cause some flows to remove then add callbacks when they don't need to. Is there a
+ // better structure for this? Maybe Channels or a StateFlow with a short timeout?
+ promotedNotificationInteractors.flatMapLatest { interactors ->
if (interactors.isNotEmpty()) {
// Combine each interactor's [notificationChip] flow...
val allNotificationChips: List<Flow<NotificationChipModel?>> =
interactors.map { interactor -> interactor.notificationChip }
combine(allNotificationChips) {
- // ... and emit just the non-null chips
- it.filterNotNull()
- }
+ // ... and emit just the non-null & sorted chips
+ it.filterNotNull().sortedWith(chipComparator)
+ }
+ .logSort()
} else {
flowOf(emptyList())
}
@@ -181,4 +178,35 @@ constructor(
// out-of-sync (like a timer that's slightly off)
chipsList.filter { !it.isAppVisible }
}
+
+ /*
+ Stable sort the promoted notifications by two criteria:
+ Criteria #1: Whichever app was most recently visible has higher ranking.
+ - Reasoning: If a user opened the app to see additional information, that's
+ likely the most important ongoing notification.
+ Criteria #2: Whichever notification first appeared more recently has higher ranking.
+ - Reasoning: Older chips get hidden if there's not enough room for all chips.
+ This semi-stable ordering ensures:
+ 1) The chips don't switch places if the older chip gets a notification update.
+ 2) The chips don't switch places when the second chip is tapped. (Whichever
+ notification is showing heads-up is considered to be the top notification, which
+ means tapping the second chip would move it to be the first chip if we didn't
+ sort by appearance time here.)
+ */
+ private val chipComparator =
+ compareByDescending<NotificationChipModel> {
+ max(it.creationTime, it.lastAppVisibleTime ?: Long.MIN_VALUE)
+ }
+
+ private fun Flow<List<NotificationChipModel>>.logSort(): Flow<List<NotificationChipModel>> {
+ return this.distinctUntilChanged().onEach { chips ->
+ val logString =
+ chips.joinToString {
+ "{key=${it.key}. " +
+ "lastVisibleAppTime=${it.lastAppVisibleTime}. " +
+ "creationTime=${it.creationTime}}"
+ }
+ logger.d({ "Sorted chips: $str1" }) { str1 = logString }
+ }
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/model/NotificationChipModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/model/NotificationChipModel.kt
index 97c37628f2e1..1f2079d83e6f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/model/NotificationChipModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/model/NotificationChipModel.kt
@@ -26,6 +26,13 @@ data class NotificationChipModel(
val appName: String,
val statusBarChipIconView: StatusBarIconView?,
val promotedContent: PromotedNotificationContentModel,
+ /** The time when the notification first appeared as promoted. */
+ val creationTime: Long,
/** True if the app managing this notification is currently visible to the user. */
val isAppVisible: Boolean,
+ /**
+ * The time when the app managing this notification last appeared as visible, or null if the app
+ * hasn't become visible since the notification became promoted.
+ */
+ val lastAppVisibleTime: Long?,
)
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 3ecbdf82f2cb..11e9fd56288f 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
@@ -35,6 +35,7 @@ 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 com.android.systemui.statusbar.phone.ongoingcall.StatusBarChipsModernization
+import com.android.systemui.util.time.SystemClock
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
@@ -51,6 +52,7 @@ constructor(
@Application private val applicationScope: CoroutineScope,
private val notifChipsInteractor: StatusBarNotificationChipsInteractor,
headsUpNotificationInteractor: HeadsUpNotificationInteractor,
+ private val systemClock: SystemClock,
) {
/**
* A flow modeling the notification chips that should be shown. Emits an empty list if there are
@@ -158,16 +160,38 @@ constructor(
clickBehavior,
)
}
+
when (this.promotedContent.time.mode) {
PromotedNotificationContentModel.When.Mode.BasicTime -> {
- return OngoingActivityChipModel.Active.ShortTimeDelta(
- this.key,
- icon,
- colors,
- time = this.promotedContent.time.time,
- onClickListenerLegacy,
- clickBehavior,
- )
+ return if (
+ this.promotedContent.time.time >=
+ systemClock.currentTimeMillis() + FUTURE_TIME_THRESHOLD_MILLIS
+ ) {
+ OngoingActivityChipModel.Active.ShortTimeDelta(
+ this.key,
+ icon,
+ colors,
+ time = this.promotedContent.time.time,
+ onClickListenerLegacy,
+ clickBehavior,
+ )
+ } else {
+ // Don't show a `when` time that's close to now or in the past because it's
+ // likely that the app didn't intentionally set the `when` time to be shown in
+ // the status bar chip.
+ // TODO(b/393369213): If a notification sets a `when` time in the future and
+ // then that time comes and goes, the chip *will* start showing times in the
+ // past. Not going to fix this right now because the Compose implementation
+ // automatically handles this for us and we're hoping to launch the notification
+ // chips at the same time as the Compose chips.
+ return OngoingActivityChipModel.Active.IconOnly(
+ this.key,
+ icon,
+ colors,
+ onClickListenerLegacy,
+ clickBehavior,
+ )
+ }
}
PromotedNotificationContentModel.When.Mode.CountUp -> {
return OngoingActivityChipModel.Active.Timer(
@@ -204,4 +228,12 @@ constructor(
)
)
}
+
+ companion object {
+ /**
+ * Notifications must have a `when` time of at least 1 minute in the future in order for the
+ * status bar chip to show the time.
+ */
+ private const val FUTURE_TIME_THRESHOLD_MILLIS = 60 * 1000
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/ChipContent.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/ChipContent.kt
index 2501aa59c375..5242feac898b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/ChipContent.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/ChipContent.kt
@@ -37,7 +37,9 @@ import androidx.compose.ui.unit.constrain
import androidx.compose.ui.unit.dp
import com.android.systemui.res.R
import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel
+import com.android.systemui.statusbar.chips.ui.viewmodel.formatTimeRemainingData
import com.android.systemui.statusbar.chips.ui.viewmodel.rememberChronometerState
+import com.android.systemui.statusbar.chips.ui.viewmodel.rememberTimeRemainingState
import kotlin.math.min
@Composable
@@ -119,7 +121,26 @@ fun ChipContent(viewModel: OngoingActivityChipModel.Active, modifier: Modifier =
}
is OngoingActivityChipModel.Active.ShortTimeDelta -> {
- // TODO(b/372657935): Implement ShortTimeDelta content in compose.
+ val timeRemainingState = rememberTimeRemainingState(futureTimeMillis = viewModel.time)
+
+ timeRemainingState.timeRemainingData?.let {
+ val text = formatTimeRemainingData(it)
+ Text(
+ text = text,
+ style = textStyle,
+ color = textColor,
+ softWrap = false,
+ modifier =
+ modifier.hideTextIfDoesNotFit(
+ text = text,
+ textStyle = textStyle,
+ textMeasurer = textMeasurer,
+ maxTextWidth = maxTextWidth,
+ startPadding = startPadding,
+ endPadding = endPadding,
+ ),
+ )
+ }
}
is OngoingActivityChipModel.Active.IconOnly -> {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/TimeRemainingState.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/TimeRemainingState.kt
new file mode 100644
index 000000000000..eb6ebcaa5796
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/TimeRemainingState.kt
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.chips.ui.viewmodel
+
+import android.os.SystemClock
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.res.stringResource
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.compose.LocalLifecycleOwner
+import androidx.lifecycle.repeatOnLifecycle
+import kotlin.time.Duration
+import kotlin.time.Duration.Companion.hours
+import kotlin.time.Duration.Companion.minutes
+import kotlin.time.DurationUnit
+import kotlin.time.toDuration
+import kotlinx.coroutines.delay
+
+/**
+ * Manages state and updates for the duration remaining between now and a given time in the future.
+ */
+class TimeRemainingState(private val timeSource: TimeSource, private val futureTimeMillis: Long) {
+ private var durationRemaining by mutableStateOf(Duration.ZERO)
+ private var startTimeMillis: Long = 0
+
+ /**
+ * [Pair] representing the time unit and its value.
+ *
+ * @property first the string resource ID corresponding to the time unit (e.g., minutes, hours).
+ * @property second the time value of the duration unit. Null if time is less than a minute or
+ * past.
+ */
+ val timeRemainingData by derivedStateOf { getTimeRemainingData(durationRemaining) }
+
+ suspend fun run() {
+ startTimeMillis = timeSource.getCurrentTime()
+ while (true) {
+ val currentTime = timeSource.getCurrentTime()
+ durationRemaining =
+ (futureTimeMillis - currentTime).toDuration(DurationUnit.MILLISECONDS)
+ // No need to update if duration is more than 1 minute in the past. Because, we will
+ // stop displaying anything.
+ if (durationRemaining.inWholeMilliseconds < -1.minutes.inWholeMilliseconds) {
+ break
+ }
+ val delaySkewMillis = (currentTime - startTimeMillis) % 1000L
+ delay(calculateNextUpdateDelay(durationRemaining) - delaySkewMillis)
+ }
+ }
+
+ private fun calculateNextUpdateDelay(duration: Duration): Long {
+ val durationAbsolute = duration.absoluteValue
+ return when {
+ durationAbsolute.inWholeHours < 1 -> {
+ 1000 + ((durationAbsolute.inWholeMilliseconds % 1.minutes.inWholeMilliseconds))
+ }
+ durationAbsolute.inWholeHours < 24 -> {
+ 1000 + (durationAbsolute.inWholeMilliseconds % 1.hours.inWholeMilliseconds)
+ }
+ else -> 1000 + (durationAbsolute.inWholeMilliseconds % 24.hours.inWholeMilliseconds)
+ }
+ }
+}
+
+/** Remember and manage the TimeRemainingState */
+@Composable
+fun rememberTimeRemainingState(
+ futureTimeMillis: Long,
+ timeSource: TimeSource = remember { TimeSource { SystemClock.elapsedRealtime() } },
+): TimeRemainingState {
+
+ val state =
+ remember(timeSource, futureTimeMillis) { TimeRemainingState(timeSource, futureTimeMillis) }
+ val lifecycleOwner = LocalLifecycleOwner.current
+ LaunchedEffect(lifecycleOwner, timeSource, futureTimeMillis) {
+ lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { state.run() }
+ }
+
+ return state
+}
+
+private fun getTimeRemainingData(duration: Duration): Pair<Int, Long?>? {
+ return when {
+ duration.inWholeMinutes <= -1 -> null
+ duration.inWholeMinutes < 1 -> Pair(com.android.internal.R.string.now_string_shortest, null)
+ duration.inWholeHours < 1 ->
+ Pair(com.android.internal.R.string.duration_minutes_medium, duration.inWholeMinutes)
+ duration.inWholeDays < 1 ->
+ Pair(com.android.internal.R.string.duration_hours_medium, duration.inWholeHours)
+ else -> null
+ }
+}
+
+/** Formats the time remaining data into a user-readable string. */
+@Composable
+fun formatTimeRemainingData(resourcePair: Pair<Int, Long?>): String {
+ return resourcePair.let { (resourceId, time) ->
+ when (time) {
+ null -> stringResource(resourceId)
+ else -> stringResource(resourceId, time.toInt())
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/media/MediaControlChipStartable.kt b/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/media/MediaControlChipStartable.kt
new file mode 100644
index 000000000000..e7bc052114eb
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/media/MediaControlChipStartable.kt
@@ -0,0 +1,43 @@
+/*
+ * 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.featurepods.media
+
+import com.android.systemui.CoreStartable
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.statusbar.featurepods.media.domain.interactor.MediaControlChipInteractor
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+
+/**
+ * A [CoreStartable] that initializes and starts the media control chip functionality. The media
+ * chip is limited to large screen devices currently. Therefore, this [CoreStartable] should not be
+ * used for phones or smaller form factor devices.
+ */
+@SysUISingleton
+class MediaControlChipStartable
+@Inject
+constructor(
+ @Background val bgScope: CoroutineScope,
+ private val mediaControlChipInteractor: MediaControlChipInteractor,
+) : CoreStartable {
+
+ override fun start() {
+ bgScope.launch { mediaControlChipInteractor.initialize() }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/media/domain/interactor/MediaControlChipInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/media/domain/interactor/MediaControlChipInteractor.kt
index e3e77e16be6d..f439bb297de0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/media/domain/interactor/MediaControlChipInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/media/domain/interactor/MediaControlChipInteractor.kt
@@ -22,13 +22,15 @@ import com.android.systemui.media.controls.data.repository.MediaFilterRepository
import com.android.systemui.media.controls.shared.model.MediaCommonModel
import com.android.systemui.media.controls.shared.model.MediaData
import com.android.systemui.res.R
+import com.android.systemui.scene.shared.flag.SceneContainerFlag
import com.android.systemui.statusbar.featurepods.media.shared.model.MediaControlChipModel
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
/**
@@ -37,6 +39,8 @@ import kotlinx.coroutines.flow.stateIn
* Provides a [StateFlow] of [MediaControlChipModel] representing the current state of the media
* control chip. Emits a new [MediaControlChipModel] when there is an active media session and the
* corresponding user preference is found, otherwise emits null.
+ *
+ * This functionality is only enabled on large screen devices.
*/
@SysUISingleton
class MediaControlChipInteractor
@@ -45,30 +49,57 @@ constructor(
@Background private val backgroundScope: CoroutineScope,
mediaFilterRepository: MediaFilterRepository,
) {
- private val currentMediaControls: StateFlow<List<MediaCommonModel.MediaControl>> =
- mediaFilterRepository.currentMedia
- .map { mediaList -> mediaList.filterIsInstance<MediaCommonModel.MediaControl>() }
- .stateIn(
- scope = backgroundScope,
- started = SharingStarted.WhileSubscribed(),
- initialValue = emptyList(),
- )
+ private val isEnabled = MutableStateFlow(false)
+
+ private val mediaControlChipModelForScene: Flow<MediaControlChipModel?> =
+ combine(mediaFilterRepository.currentMedia, mediaFilterRepository.selectedUserEntries) {
+ mediaList,
+ userEntries ->
+ mediaList
+ .filterIsInstance<MediaCommonModel.MediaControl>()
+ .mapNotNull { userEntries[it.mediaLoadedModel.instanceId] }
+ .firstOrNull { it.active }
+ ?.toMediaControlChipModel()
+ }
+
+ /**
+ * A flow of [MediaControlChipModel] representing the current state of the media controls chip.
+ * This flow emits null when no active media is playing or when playback information is
+ * unavailable. This flow is only active when [SceneContainerFlag] is disabled.
+ */
+ private val mediaControlChipModelLegacy = MutableStateFlow<MediaControlChipModel?>(null)
+
+ fun updateMediaControlChipModelLegacy(mediaData: MediaData?) {
+ if (!SceneContainerFlag.isEnabled) {
+ mediaControlChipModelLegacy.value = mediaData?.toMediaControlChipModel()
+ }
+ }
+
+ private val _mediaControlChipModel: Flow<MediaControlChipModel?> =
+ if (SceneContainerFlag.isEnabled) {
+ mediaControlChipModelForScene
+ } else {
+ mediaControlChipModelLegacy
+ }
/** The currently active [MediaControlChipModel] */
- val mediaControlModel: StateFlow<MediaControlChipModel?> =
- combine(currentMediaControls, mediaFilterRepository.selectedUserEntries) {
- mediaControls,
- userEntries ->
- mediaControls
- .mapNotNull { userEntries[it.mediaLoadedModel.instanceId] }
- .firstOrNull { it.active }
- ?.toMediaControlChipModel()
+ val mediaControlChipModel: StateFlow<MediaControlChipModel?> =
+ combine(_mediaControlChipModel, isEnabled) { mediaControlChipModel, isEnabled ->
+ if (isEnabled) {
+ mediaControlChipModel
+ } else {
+ null
+ }
}
- .stateIn(
- scope = backgroundScope,
- started = SharingStarted.WhileSubscribed(),
- initialValue = null,
- )
+ .stateIn(backgroundScope, SharingStarted.WhileSubscribed(), null)
+
+ /**
+ * The media control chip may not be enabled on all form factors, so only the relevant form
+ * factors should initialize the interactor. This must be called from a CoreStartable.
+ */
+ fun initialize() {
+ isEnabled.value = true
+ }
}
private fun MediaData.toMediaControlChipModel(): MediaControlChipModel {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/media/ui/viewmodel/MediaControlChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/media/ui/viewmodel/MediaControlChipViewModel.kt
index 19acb2e9839c..7f0f6078f391 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/media/ui/viewmodel/MediaControlChipViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/media/ui/viewmodel/MediaControlChipViewModel.kt
@@ -55,8 +55,8 @@ constructor(
* whenever the underlying [MediaControlChipModel] changes.
*/
override val chip: StateFlow<PopupChipModel> =
- mediaControlChipInteractor.mediaControlModel
- .map { mediaControlModel -> toPopupChipModel(mediaControlModel) }
+ mediaControlChipInteractor.mediaControlChipModel
+ .map { mediaControlChipModel -> toPopupChipModel(mediaControlChipModel) }
.stateIn(
backgroundScope,
SharingStarted.WhileSubscribed(),
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/BundleEntry.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/BundleEntry.java
index 37485feed792..0e3f103c152e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/BundleEntry.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/BundleEntry.java
@@ -16,11 +16,74 @@
package com.android.systemui.statusbar.notification.collection;
+import static android.app.NotificationChannel.NEWS_ID;
+import static android.app.NotificationChannel.PROMOTIONS_ID;
+import static android.app.NotificationChannel.RECS_ID;
+import static android.app.NotificationChannel.SOCIAL_MEDIA_ID;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+
+import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
+
+import java.util.List;
+
/**
* Abstract class to represent notification section bundled by AI.
*/
public class BundleEntry extends PipelineEntry {
+ private final String mKey;
+ private final BundleEntryAdapter mEntryAdapter;
+
+ // TODO (b/389839319): implement the row
+ private ExpandableNotificationRow mRow;
+
+ public BundleEntry(String key) {
+ mKey = key;
+ mEntryAdapter = new BundleEntryAdapter();
+ }
+
+ @VisibleForTesting
+ public BundleEntryAdapter getEntryAdapter() {
+ return mEntryAdapter;
+ }
+
public class BundleEntryAdapter implements EntryAdapter {
+
+ /**
+ * TODO (b/394483200): convert to PipelineEntry.ROOT_ENTRY when pipeline is migrated?
+ */
+ @Override
+ public GroupEntry getParent() {
+ return GroupEntry.ROOT_ENTRY;
+ }
+
+ @Override
+ public boolean isTopLevelEntry() {
+ return true;
+ }
+
+ @Override
+ public String getKey() {
+ return mKey;
+ }
+
+ @Override
+ public ExpandableNotificationRow getRow() {
+ return mRow;
+ }
+
+ @Nullable
+ @Override
+ public EntryAdapter getGroupRoot() {
+ return this;
+ }
}
+
+ public static final List<BundleEntry> ROOT_BUNDLES = List.of(
+ new BundleEntry(PROMOTIONS_ID),
+ new BundleEntry(SOCIAL_MEDIA_ID),
+ new BundleEntry(NEWS_ID),
+ new BundleEntry(RECS_ID));
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/EntryAdapter.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/EntryAdapter.java
index b12b1c538a32..4df81c97e21e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/EntryAdapter.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/EntryAdapter.java
@@ -16,8 +16,52 @@
package com.android.systemui.statusbar.notification.collection;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
+
/**
* Adapter interface for UI to get relevant info.
*/
public interface EntryAdapter {
+
+ /**
+ * Gets the parent of this entry, or null if the entry's view is not attached
+ */
+ @Nullable PipelineEntry getParent();
+
+ /**
+ * Returns whether the entry is attached and appears at the top level of the shade
+ */
+ boolean isTopLevelEntry();
+
+ /**
+ * @return the unique identifier for this entry
+ */
+ @NonNull String getKey();
+
+ /**
+ * Gets the view that this entry is backing.
+ */
+ @NonNull
+ ExpandableNotificationRow getRow();
+
+ /**
+ * Gets the EntryAdapter that is the nearest root of the collection of rows the given entry
+ * belongs to. If the given entry is a BundleEntry or an isolated child of a BundleEntry, the
+ * BundleEntry will be returned. If the given notification is a group summary NotificationEntry,
+ * or a child of a group summary, the summary NotificationEntry will be returned, even if that
+ * summary belongs to a BundleEntry. If the entry is a notification that does not belong to any
+ * group or bundle grouping, null will be returned.
+ */
+ @Nullable
+ EntryAdapter getGroupRoot();
+
+ /**
+ * Returns whether the entry is attached to the current shade list
+ */
+ default boolean isAttached() {
+ return getParent() != null;
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifInflaterImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifInflaterImpl.java
index fc47dc1ed81a..8f3c357a277a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifInflaterImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifInflaterImpl.java
@@ -24,6 +24,7 @@ import com.android.systemui.statusbar.notification.collection.inflation.NotifInf
import com.android.systemui.statusbar.notification.collection.inflation.NotificationRowBinderImpl;
import com.android.systemui.statusbar.notification.row.NotifInflationErrorManager;
import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder;
+import com.android.systemui.statusbar.notification.shared.NotificationBundleUi;
import javax.inject.Inject;
@@ -78,7 +79,7 @@ public class NotifInflaterImpl implements NotifInflater {
requireBinder().inflateViews(
entry,
params,
- wrapInflationCallback(callback));
+ wrapInflationCallback(entry, callback));
} catch (InflationException e) {
mLogger.logInflationException(entry, e);
mNotifErrorManager.setInflationError(entry, e);
@@ -101,17 +102,26 @@ public class NotifInflaterImpl implements NotifInflater {
}
private NotificationRowContentBinder.InflationCallback wrapInflationCallback(
+ final NotificationEntry entry,
InflationCallback callback) {
return new NotificationRowContentBinder.InflationCallback() {
@Override
public void handleInflationException(
NotificationEntry entry,
Exception e) {
+ if (NotificationBundleUi.isEnabled()) {
+ handleInflationException(e);
+ } else {
+ mNotifErrorManager.setInflationError(entry, e);
+ }
+ }
+ @Override
+ public void handleInflationException(Exception e) {
mNotifErrorManager.setInflationError(entry, e);
}
@Override
- public void onAsyncInflationFinished(NotificationEntry entry) {
+ public void onAsyncInflationFinished() {
mNotifErrorManager.clearInflationError(entry);
if (callback != null) {
callback.onInflationFinished(entry, entry.getRowController());
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java
index 7dd82a6b5198..90f9525c7683 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java
@@ -29,6 +29,8 @@ import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_NOTIFICAT
import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_PEEK;
import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_STATUS_BAR;
+import static com.android.systemui.statusbar.notification.collection.BundleEntry.ROOT_BUNDLES;
+import static com.android.systemui.statusbar.notification.collection.GroupEntry.ROOT_ENTRY;
import static com.android.systemui.statusbar.notification.collection.NotifCollection.REASON_NOT_CANCELED;
import static com.android.systemui.statusbar.notification.stack.NotificationPriorityBucketKt.BUCKET_ALERTING;
@@ -107,6 +109,7 @@ public final class NotificationEntry extends ListEntry {
private final String mKey;
private StatusBarNotification mSbn;
private Ranking mRanking;
+ private final NotifEntryAdapter mEntryAdapter;
/*
* Bookkeeping members
@@ -268,9 +271,48 @@ public final class NotificationEntry extends ListEntry {
mKey = sbn.getKey();
setSbn(sbn);
setRanking(ranking);
+ mEntryAdapter = new NotifEntryAdapter();
}
public class NotifEntryAdapter implements EntryAdapter {
+ @Override
+ public PipelineEntry getParent() {
+ return NotificationEntry.this.getParent();
+ }
+
+ @Override
+ public boolean isTopLevelEntry() {
+ return getParent() != null
+ && (getParent() == ROOT_ENTRY || ROOT_BUNDLES.contains(getParent()));
+ }
+
+ @Override
+ public String getKey() {
+ return NotificationEntry.this.getKey();
+ }
+
+ @Override
+ public ExpandableNotificationRow getRow() {
+ return NotificationEntry.this.getRow();
+ }
+
+ @Nullable
+ @Override
+ public EntryAdapter getGroupRoot() {
+ // TODO (b/395857098): for backwards compatibility this will return null if called
+ // on a group summary that's not in a bundles, but it should return itself.
+ if (isTopLevelEntry() || getParent() == null) {
+ return null;
+ }
+ if (NotificationEntry.this.getParent().getSummary() != null) {
+ return NotificationEntry.this.getParent().getSummary().mEntryAdapter;
+ }
+ return null;
+ }
+ }
+
+ public EntryAdapter getEntryAdapter() {
+ return mEntryAdapter;
}
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/PipelineEntry.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/PipelineEntry.java
index efedfef5cbe9..c5a479180329 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/PipelineEntry.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/PipelineEntry.java
@@ -19,5 +19,5 @@ package com.android.systemui.statusbar.notification.collection;
/**
* Class to represent a notification, group, or bundle in the pipeline.
*/
-public class PipelineEntry {
+public abstract class PipelineEntry {
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/ColorizedFgsCoordinator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/ColorizedFgsCoordinator.java
index 733b986b5422..9df4bf4af4e8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/ColorizedFgsCoordinator.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/ColorizedFgsCoordinator.java
@@ -23,6 +23,7 @@ import android.app.Notification;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import com.android.systemui.dagger.qualifiers.Application;
import com.android.systemui.statusbar.notification.collection.ListEntry;
import com.android.systemui.statusbar.notification.collection.NotifPipeline;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
@@ -31,29 +32,50 @@ import com.android.systemui.statusbar.notification.collection.listbuilder.plugga
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifPromoter;
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner;
import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUi;
+import com.android.systemui.statusbar.notification.promoted.domain.interactor.PromotedNotificationsInteractor;
import com.android.systemui.statusbar.notification.stack.NotificationPriorityBucketKt;
+import com.android.systemui.util.kotlin.JavaAdapterKt;
-import com.google.common.primitives.Booleans;
+import kotlinx.coroutines.CoroutineScope;
+
+import java.util.Collections;
+import java.util.List;
import javax.inject.Inject;
/**
* Handles sectioning for foreground service notifications.
- * Puts non-min colorized foreground service notifications into the FGS section. See
- * {@link NotifCoordinators} for section ordering priority.
+ * Puts non-min colorized foreground service notifications into the FGS section. See
+ * {@link NotifCoordinators} for section ordering priority.
*/
@CoordinatorScope
public class ColorizedFgsCoordinator implements Coordinator {
private static final String TAG = "ColorizedCoordinator";
+ private final PromotedNotificationsInteractor mPromotedNotificationsInteractor;
+ private final CoroutineScope mMainScope;
+
+ private List<String> mOrderedPromotedNotifKeys = Collections.emptyList();
@Inject
- public ColorizedFgsCoordinator() {
+ public ColorizedFgsCoordinator(
+ @Application CoroutineScope mainScope,
+ PromotedNotificationsInteractor promotedNotificationsInteractor
+ ) {
+ mPromotedNotificationsInteractor = promotedNotificationsInteractor;
+ mMainScope = mainScope;
}
@Override
- public void attach(NotifPipeline pipeline) {
+ public void attach(@NonNull NotifPipeline pipeline) {
if (PromotedNotificationUi.isEnabled()) {
pipeline.addPromoter(mPromotedOngoingPromoter);
+
+ JavaAdapterKt.collectFlow(mMainScope,
+ mPromotedNotificationsInteractor.getOrderedChipNotificationKeys(),
+ (List<String> keys) -> {
+ mOrderedPromotedNotifKeys = keys;
+ mNotifSectioner.invalidateList("updated mOrderedPromotedNotifKeys");
+ });
}
}
@@ -82,12 +104,24 @@ public class ColorizedFgsCoordinator implements Coordinator {
return false;
}
- private NotifComparator mPreferPromoted = new NotifComparator("PreferPromoted") {
+ /** get the sort key for any entry in the ongoing section */
+ private int getSortKey(@Nullable NotificationEntry entry) {
+ if (entry == null) return Integer.MAX_VALUE;
+ // Order all promoted notif keys first, using their order in the list
+ final int index = mOrderedPromotedNotifKeys.indexOf(entry.getKey());
+ if (index >= 0) return index;
+ // Next, prioritize promoted ongoing over other notifications
+ return isPromotedOngoing(entry) ? Integer.MAX_VALUE - 1 : Integer.MAX_VALUE;
+ }
+
+ private final NotifComparator mOngoingComparator = new NotifComparator(
+ "OngoingComparator") {
@Override
public int compare(@NonNull ListEntry o1, @NonNull ListEntry o2) {
- return -1 * Booleans.compare(
- isPromotedOngoing(o1.getRepresentativeEntry()),
- isPromotedOngoing(o2.getRepresentativeEntry()));
+ return Integer.compare(
+ getSortKey(o1.getRepresentativeEntry()),
+ getSortKey(o2.getRepresentativeEntry())
+ );
}
};
@@ -95,7 +129,7 @@ public class ColorizedFgsCoordinator implements Coordinator {
@Override
public NotifComparator getComparator() {
if (PromotedNotificationUi.isEnabled()) {
- return mPreferPromoted;
+ return mOngoingComparator;
} else {
return null;
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/HighPriorityProvider.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/HighPriorityProvider.java
index d47fe20911f9..2e3ab926ad57 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/HighPriorityProvider.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/HighPriorityProvider.java
@@ -27,6 +27,7 @@ import com.android.systemui.statusbar.notification.collection.ListEntry;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager;
import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier;
+import com.android.systemui.statusbar.notification.shared.NotificationBundleUi;
import java.util.List;
@@ -129,21 +130,42 @@ public class HighPriorityProvider {
>= NotificationManager.IMPORTANCE_DEFAULT);
}
+ /**
+ * Returns whether the given ListEntry has a high priority child or is in a group with a child
+ * that's high priority
+ */
private boolean hasHighPriorityChild(ListEntry entry, boolean allowImplicit) {
- if (entry instanceof NotificationEntry
- && !mGroupMembershipManager.isGroupSummary((NotificationEntry) entry)) {
- return false;
- }
-
- List<NotificationEntry> children = mGroupMembershipManager.getChildren(entry);
- if (children != null) {
- for (NotificationEntry child : children) {
- if (child != entry && isHighPriority(child, allowImplicit)) {
- return true;
+ if (NotificationBundleUi.isEnabled()) {
+ GroupEntry representativeGroupEntry = null;
+ if (entry instanceof GroupEntry) {
+ representativeGroupEntry = (GroupEntry) entry;
+ } else if (entry instanceof NotificationEntry){
+ final NotificationEntry notificationEntry = entry.getRepresentativeEntry();
+ if (notificationEntry.getParent() != null
+ && notificationEntry.getParent().getSummary() != null
+ && notificationEntry.getParent().getSummary() == notificationEntry) {
+ representativeGroupEntry = notificationEntry.getParent();
}
}
+ return representativeGroupEntry != null &&
+ representativeGroupEntry.getChildren().stream().anyMatch(
+ childEntry -> isHighPriority(childEntry, allowImplicit));
+
+ } else {
+ if (entry instanceof NotificationEntry
+ && !mGroupMembershipManager.isGroupSummary((NotificationEntry) entry)) {
+ return false;
+ }
+ List<NotificationEntry> children = mGroupMembershipManager.getChildren(entry);
+ if (children != null) {
+ for (NotificationEntry child : children) {
+ if (child != entry && isHighPriority(child, allowImplicit)) {
+ return true;
+ }
+ }
+ }
+ return false;
}
- return false;
}
private boolean hasHighPriorityCharacteristics(NotificationEntry entry) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManager.java
index 30386ab46382..ea369463da51 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManager.java
@@ -16,6 +16,7 @@
package com.android.systemui.statusbar.notification.collection.render;
+import com.android.systemui.statusbar.notification.collection.EntryAdapter;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
@@ -38,6 +39,20 @@ public interface GroupExpansionManager {
boolean isGroupExpanded(NotificationEntry entry);
/**
+ * Whether the parent associated with this notification is expanded.
+ * If this notification is not part of a group or bundle, it will always return false.
+ */
+ boolean isGroupExpanded(EntryAdapter entry);
+
+ /**
+ * Set whether the group/bundle associated with this notification is expanded or not.
+ */
+ void setGroupExpanded(EntryAdapter entry, boolean expanded);
+
+ /** @return group/bundle expansion state after toggling. */
+ boolean toggleGroupExpansion(EntryAdapter entry);
+
+ /**
* Set whether the group associated with this notification is expanded or not.
*/
void setGroupExpanded(NotificationEntry entry, boolean expanded);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerImpl.java
index d1aff80b4e7c..16b98e20498a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerImpl.java
@@ -23,11 +23,13 @@ import androidx.annotation.NonNull;
import com.android.systemui.Dumpable;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dump.DumpManager;
+import com.android.systemui.statusbar.notification.collection.EntryAdapter;
import com.android.systemui.statusbar.notification.collection.GroupEntry;
import com.android.systemui.statusbar.notification.collection.ListEntry;
import com.android.systemui.statusbar.notification.collection.NotifPipeline;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeRenderListListener;
+import com.android.systemui.statusbar.notification.shared.NotificationBundleUi;
import java.io.PrintWriter;
import java.util.ArrayList;
@@ -55,6 +57,8 @@ public class GroupExpansionManagerImpl implements GroupExpansionManager, Dumpabl
*/
private final Set<NotificationEntry> mExpandedGroups = new HashSet<>();
+ private final Set<EntryAdapter> mExpandedCollections = new HashSet<>();
+
@Inject
public GroupExpansionManagerImpl(DumpManager dumpManager,
GroupMembershipManager groupMembershipManager) {
@@ -63,11 +67,17 @@ public class GroupExpansionManagerImpl implements GroupExpansionManager, Dumpabl
}
/**
- * Cleanup entries from mExpandedGroups that no longer exist in the pipeline.
+ * Cleanup entries from internal tracking that no longer exist in the pipeline.
*/
private final OnBeforeRenderListListener mNotifTracker = (entries) -> {
- if (mExpandedGroups.isEmpty()) {
- return; // nothing to do
+ if (NotificationBundleUi.isEnabled()) {
+ if (mExpandedCollections.isEmpty()) {
+ return; // nothing to do
+ }
+ } else {
+ if (mExpandedGroups.isEmpty()) {
+ return; // nothing to do
+ }
}
final Set<NotificationEntry> renderingSummaries = new HashSet<>();
@@ -77,10 +87,25 @@ public class GroupExpansionManagerImpl implements GroupExpansionManager, Dumpabl
}
}
- // If a group is in mExpandedGroups but not in the pipeline entries, collapse it.
- final var groupsToRemove = setDifference(mExpandedGroups, renderingSummaries);
- for (NotificationEntry entry : groupsToRemove) {
- setGroupExpanded(entry, false);
+ if (NotificationBundleUi.isEnabled()) {
+ for (EntryAdapter entryAdapter : mExpandedCollections) {
+ boolean isInPipeline = false;
+ for (NotificationEntry entry : renderingSummaries) {
+ if (entry.getKey().equals(entryAdapter.getKey())) {
+ isInPipeline = true;
+ break;
+ }
+ }
+ if (!isInPipeline) {
+ setGroupExpanded(entryAdapter, false);
+ }
+ }
+ } else {
+ // If a group is in mExpandedGroups but not in the pipeline entries, collapse it.
+ final var groupsToRemove = setDifference(mExpandedGroups, renderingSummaries);
+ for (NotificationEntry entry : groupsToRemove) {
+ setGroupExpanded(entry, false);
+ }
}
};
@@ -96,11 +121,13 @@ public class GroupExpansionManagerImpl implements GroupExpansionManager, Dumpabl
@Override
public boolean isGroupExpanded(NotificationEntry entry) {
+ NotificationBundleUi.assertInLegacyMode();
return mExpandedGroups.contains(mGroupMembershipManager.getGroupSummary(entry));
}
@Override
public void setGroupExpanded(NotificationEntry entry, boolean expanded) {
+ NotificationBundleUi.assertInLegacyMode();
NotificationEntry groupSummary = mGroupMembershipManager.getGroupSummary(entry);
if (entry.getParent() == null) {
if (expanded) {
@@ -127,14 +154,61 @@ public class GroupExpansionManagerImpl implements GroupExpansionManager, Dumpabl
@Override
public boolean toggleGroupExpansion(NotificationEntry entry) {
+ NotificationBundleUi.assertInLegacyMode();
+ setGroupExpanded(entry, !isGroupExpanded(entry));
+ return isGroupExpanded(entry);
+ }
+
+ @Override
+ public boolean isGroupExpanded(EntryAdapter entry) {
+ NotificationBundleUi.assertInNewMode();
+ return mExpandedCollections.contains(mGroupMembershipManager.getGroupRoot(entry));
+ }
+
+ @Override
+ public void setGroupExpanded(EntryAdapter entry, boolean expanded) {
+ NotificationBundleUi.assertInNewMode();
+ EntryAdapter groupParent = mGroupMembershipManager.getGroupRoot(entry);
+ if (!entry.isAttached()) {
+ if (expanded) {
+ Log.wtf(TAG, "Cannot expand group that is not attached");
+ } else {
+ // The entry is no longer attached, but we still want to make sure we don't have
+ // a stale expansion state.
+ groupParent = entry;
+ }
+ }
+
+ boolean changed;
+ if (expanded) {
+ changed = mExpandedCollections.add(groupParent);
+ } else {
+ changed = mExpandedCollections.remove(groupParent);
+ }
+
+ // Only notify listeners if something changed.
+ if (changed) {
+ sendOnGroupExpandedChange(entry, expanded);
+ }
+ }
+
+ @Override
+ public boolean toggleGroupExpansion(EntryAdapter entry) {
+ NotificationBundleUi.assertInNewMode();
setGroupExpanded(entry, !isGroupExpanded(entry));
return isGroupExpanded(entry);
}
@Override
public void collapseGroups() {
- for (NotificationEntry entry : new ArrayList<>(mExpandedGroups)) {
- setGroupExpanded(entry, false);
+ if (NotificationBundleUi.isEnabled()) {
+ for (EntryAdapter entry : new ArrayList<>(mExpandedCollections)) {
+ setGroupExpanded(entry, false);
+ }
+ } else {
+ for (NotificationEntry entry : new ArrayList<>(mExpandedGroups)) {
+ setGroupExpanded(entry, false);
+ }
}
}
@@ -145,9 +219,21 @@ public class GroupExpansionManagerImpl implements GroupExpansionManager, Dumpabl
for (NotificationEntry entry : mExpandedGroups) {
pw.println(" * " + entry.getKey());
}
+ pw.println(" mExpandedCollection: " + mExpandedCollections.size());
+ for (EntryAdapter entry : mExpandedCollections) {
+ pw.println(" * " + entry.getKey());
+ }
}
private void sendOnGroupExpandedChange(NotificationEntry entry, boolean expanded) {
+ NotificationBundleUi.assertInLegacyMode();
+ for (OnGroupExpansionChangeListener listener : mOnGroupChangeListeners) {
+ listener.onGroupExpansionChange(entry.getRow(), expanded);
+ }
+ }
+
+ private void sendOnGroupExpandedChange(EntryAdapter entry, boolean expanded) {
+ NotificationBundleUi.assertInNewMode();
for (OnGroupExpansionChangeListener listener : mOnGroupChangeListeners) {
listener.onGroupExpansionChange(entry.getRow(), expanded);
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManager.java
index 3158782e6fea..69267e5d9e55 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManager.java
@@ -19,6 +19,7 @@ package com.android.systemui.statusbar.notification.collection.render;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import com.android.systemui.statusbar.notification.collection.EntryAdapter;
import com.android.systemui.statusbar.notification.collection.ListEntry;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
@@ -29,6 +30,13 @@ import java.util.List;
* generally assumes that the notification is attached (aka its parent is not null).
*/
public interface GroupMembershipManager {
+
+ /**
+ * @return whether a given entry is the root (GroupEntry or BundleEntry) in a collection which
+ * has children
+ */
+ boolean isGroupRoot(@NonNull EntryAdapter entry);
+
/**
* @return whether a given notification is the summary in a group which has children
*/
@@ -42,16 +50,15 @@ public interface GroupMembershipManager {
NotificationEntry getGroupSummary(@NonNull NotificationEntry entry);
/**
- * Similar to {@link #getGroupSummary(NotificationEntry)} but doesn't get the visual summary
- * but the logical summary, i.e when a child is isolated, it still returns the summary as if
- * it wasn't isolated.
- * TODO: remove this when migrating to the new pipeline, this is taken care of in the
- * dismissal logic built into NotifCollection
+ * Gets the EntryAdapter that is the nearest root of the collection of rows the given entry
+ * belongs to. If the given entry is a BundleEntry or an isolated child of a BundleEntry, the
+ * BundleEntry will be returned. If the given notification is a group summary NotificationEntry,
+ * or a child of a group summary, the summary NotificationEntry will be returned, even if that
+ * summary belongs to a BundleEntry. If the entry is a notification that does not belong to any
+ * group or bundle grouping, null will be returned.
*/
@Nullable
- default NotificationEntry getLogicalGroupSummary(@NonNull NotificationEntry entry) {
- return getGroupSummary(entry);
- }
+ EntryAdapter getGroupRoot(@NonNull EntryAdapter entry);
/**
* @return whether a given notification is a child in a group
@@ -59,9 +66,10 @@ public interface GroupMembershipManager {
boolean isChildInGroup(@NonNull NotificationEntry entry);
/**
- * Whether this is the only child in a group
+ * @return whether a given notification is a child in a group. The group may be a notification
+ * group or a bundle.
*/
- boolean isOnlyChildInGroup(@NonNull NotificationEntry entry);
+ boolean isChildInGroup(@NonNull EntryAdapter entry);
/**
* Get the children that are in the summary's group, not including those isolated.
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManagerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManagerImpl.java
index da1247953c4c..80a9f8adf8f3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManagerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManagerImpl.java
@@ -22,9 +22,11 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.statusbar.notification.collection.EntryAdapter;
import com.android.systemui.statusbar.notification.collection.GroupEntry;
import com.android.systemui.statusbar.notification.collection.ListEntry;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
+import com.android.systemui.statusbar.notification.shared.NotificationBundleUi;
import java.util.List;
@@ -41,6 +43,7 @@ public class GroupMembershipManagerImpl implements GroupMembershipManager {
@Override
public boolean isGroupSummary(@NonNull NotificationEntry entry) {
+ NotificationBundleUi.assertInLegacyMode();
if (entry.getParent() == null) {
// The entry is not attached, so it doesn't count.
return false;
@@ -49,33 +52,47 @@ public class GroupMembershipManagerImpl implements GroupMembershipManager {
return entry.getParent().getSummary() == entry;
}
+ @Override
+ public boolean isGroupRoot(@NonNull EntryAdapter entry) {
+ NotificationBundleUi.assertInNewMode();
+ return entry == entry.getGroupRoot();
+ }
+
@Nullable
@Override
public NotificationEntry getGroupSummary(@NonNull NotificationEntry entry) {
+ NotificationBundleUi.assertInLegacyMode();
if (isTopLevelEntry(entry) || entry.getParent() == null) {
return null;
}
return entry.getParent().getSummary();
}
+ @Nullable
+ @Override
+ public EntryAdapter getGroupRoot(@NonNull EntryAdapter entry) {
+ NotificationBundleUi.assertInNewMode();
+ return entry.getGroupRoot();
+ }
+
@Override
public boolean isChildInGroup(@NonNull NotificationEntry entry) {
+ NotificationBundleUi.assertInLegacyMode();
// An entry is a child if it's not a summary or top level entry, but it is attached.
return !isGroupSummary(entry) && !isTopLevelEntry(entry) && entry.getParent() != null;
}
@Override
- public boolean isOnlyChildInGroup(@NonNull NotificationEntry entry) {
- if (entry.getParent() == null) {
- return false; // The entry is not attached.
- }
-
- return !isGroupSummary(entry) && entry.getParent().getChildren().size() == 1;
+ public boolean isChildInGroup(@NonNull EntryAdapter entry) {
+ NotificationBundleUi.assertInNewMode();
+ // An entry is a child if it's not a group root or top level entry, but it is attached.
+ return entry.isAttached() && entry != getGroupRoot(entry) && !entry.isTopLevelEntry();
}
@Nullable
@Override
public List<NotificationEntry> getChildren(@NonNull ListEntry entry) {
+ NotificationBundleUi.assertInLegacyMode();
if (entry instanceof GroupEntry) {
return ((GroupEntry) entry).getChildren();
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationStackOptionalModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationStackOptionalModule.kt
deleted file mode 100644
index bcaf1878a869..000000000000
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationStackOptionalModule.kt
+++ /dev/null
@@ -1,41 +0,0 @@
-/*
- * 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.dagger
-
-import com.android.systemui.statusbar.notification.stack.NotificationStackRebindingHider
-import com.android.systemui.statusbar.notification.stack.NotificationStackRebindingHiderImpl
-import dagger.Binds
-import dagger.BindsOptionalOf
-import dagger.Module
-
-/**
- * This is meant to be bound in SystemUI variants with [NotificationStackScrollLayoutController].
- */
-@Module
-interface NotificationStackModule {
- @Binds
- fun bindNotificationStackRebindingHider(
- impl: NotificationStackRebindingHiderImpl
- ): NotificationStackRebindingHider
-}
-
-/** This is meant to be used by all SystemUI variants, also those without NSSL. */
-@Module
-interface NotificationStackOptionalModule {
- @BindsOptionalOf
- fun bindOptionalOfNotificationStackRebindingHider(): NotificationStackRebindingHider
-}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java
index 34f4969127e3..53d5dbc58677 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java
@@ -121,7 +121,6 @@ import javax.inject.Provider;
NotificationMemoryModule.class,
NotificationStatsLoggerModule.class,
NotificationsLogModule.class,
- NotificationStackOptionalModule.class,
})
public interface NotificationsModule {
@Binds
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImpl.java
index be61dc95fe20..7d74a496853f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImpl.java
@@ -46,6 +46,7 @@ import com.android.systemui.shade.ShadeDisplayAware;
import com.android.systemui.shade.domain.interactor.ShadeInteractor;
import com.android.systemui.statusbar.StatusBarState;
import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips;
+import com.android.systemui.statusbar.notification.collection.EntryAdapter;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.collection.coordinator.HeadsUpCoordinator;
import com.android.systemui.statusbar.notification.collection.provider.OnReorderingAllowedListener;
@@ -55,6 +56,7 @@ import com.android.systemui.statusbar.notification.collection.render.GroupMember
import com.android.systemui.statusbar.notification.data.repository.HeadsUpRepository;
import com.android.systemui.statusbar.notification.data.repository.HeadsUpRowRepository;
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
+import com.android.systemui.statusbar.notification.shared.NotificationBundleUi;
import com.android.systemui.statusbar.notification.shared.NotificationThrottleHun;
import com.android.systemui.statusbar.phone.ExpandHeadsUpOnInlineReply;
import com.android.systemui.statusbar.phone.KeyguardBypassController;
@@ -118,7 +120,8 @@ public class HeadsUpManagerImpl
@VisibleForTesting
final ArrayMap<String, HeadsUpEntry> mHeadsUpEntryMap = new ArrayMap<>();
private final HeadsUpManagerLogger mLogger;
- private final int mMinimumDisplayTime;
+ private final int mMinimumDisplayTimeDefault;
+ private final int mMinimumDisplayTimeForUserInitiated;
private final int mStickyForSomeTimeAutoDismissTime;
private final int mAutoDismissTime;
private final DelayableExecutor mExecutor;
@@ -215,9 +218,11 @@ public class HeadsUpManagerImpl
mGroupMembershipManager = groupMembershipManager;
mVisualStabilityProvider = visualStabilityProvider;
Resources resources = context.getResources();
- mMinimumDisplayTime = NotificationThrottleHun.isEnabled()
+ mMinimumDisplayTimeDefault = NotificationThrottleHun.isEnabled()
? resources.getInteger(R.integer.heads_up_notification_minimum_time_with_throttling)
: resources.getInteger(R.integer.heads_up_notification_minimum_time);
+ mMinimumDisplayTimeForUserInitiated = resources.getInteger(
+ R.integer.heads_up_notification_minimum_time_for_user_initiated);
mStickyForSomeTimeAutoDismissTime = resources.getInteger(
R.integer.sticky_heads_up_notification_time);
mAutoDismissTime = resources.getInteger(R.integer.heads_up_notification_decay);
@@ -871,14 +876,24 @@ public class HeadsUpManagerImpl
if (!hasPinnedHeadsUp() || topEntry == null) {
return null;
} else {
+ ExpandableNotificationRow topRow = topEntry.getRow();
if (topEntry.rowIsChildInGroup()) {
- final NotificationEntry groupSummary =
- mGroupMembershipManager.getGroupSummary(topEntry);
- if (groupSummary != null) {
- topEntry = groupSummary;
+ if (NotificationBundleUi.isEnabled()) {
+ final EntryAdapter adapter = mGroupMembershipManager.getGroupRoot(
+ topRow.getEntryAdapter());
+ if (adapter != null) {
+ topRow = adapter.getRow();
+ }
+ } else {
+ final NotificationEntry groupSummary =
+ mGroupMembershipManager.getGroupSummary(topEntry);
+ if (groupSummary != null) {
+ topEntry = groupSummary;
+ topRow = topEntry.getRow();
+ }
}
}
- ExpandableNotificationRow topRow = topEntry.getRow();
+
int[] tmpArray = new int[2];
topRow.getLocationOnScreen(tmpArray);
int minX = tmpArray[0];
@@ -1358,7 +1373,12 @@ public class HeadsUpManagerImpl
final long now = mSystemClock.elapsedRealtime();
if (updateEarliestRemovalTime) {
- mEarliestRemovalTime = now + mMinimumDisplayTime;
+ if (StatusBarNotifChips.isEnabled()
+ && mPinnedStatus.getValue() == PinnedStatus.PinnedByUser) {
+ mEarliestRemovalTime = now + mMinimumDisplayTimeForUserInitiated;
+ } else {
+ mEarliestRemovalTime = now + mMinimumDisplayTimeDefault;
+ }
}
if (updatePostTime) {
@@ -1377,7 +1397,7 @@ public class HeadsUpManagerImpl
final long now = mSystemClock.elapsedRealtime();
return NotificationThrottleHun.isEnabled()
? Math.max(finishTime, mEarliestRemovalTime) - now
- : Math.max(finishTime - now, mMinimumDisplayTime);
+ : Math.max(finishTime - now, mMinimumDisplayTimeDefault);
};
scheduleAutoRemovalCallback(finishTimeCalculator, "updateEntry (not sticky)");
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/domain/interactor/NotificationIconsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/domain/interactor/NotificationIconsInteractor.kt
index f02edee399eb..18a1afa17720 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/domain/interactor/NotificationIconsInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/domain/interactor/NotificationIconsInteractor.kt
@@ -21,6 +21,7 @@ import com.android.systemui.statusbar.data.repository.NotificationListenerSettin
import com.android.systemui.statusbar.notification.data.repository.NotificationsKeyguardViewStateRepository
import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor
import com.android.systemui.statusbar.notification.domain.interactor.HeadsUpNotificationIconInteractor
+import com.android.systemui.statusbar.notification.promoted.domain.interactor.AODPromotedNotificationInteractor
import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel
import com.android.wm.shell.bubbles.Bubbles
import java.util.Optional
@@ -30,6 +31,7 @@ import kotlin.jvm.optionals.getOrNull
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.flowOn
/** Domain logic related to notification icons. */
@@ -39,8 +41,21 @@ constructor(
private val activeNotificationsInteractor: ActiveNotificationsInteractor,
private val bubbles: Optional<Bubbles>,
private val headsUpNotificationIconInteractor: HeadsUpNotificationIconInteractor,
+ private val aodPromotedNotificationInteractor: AODPromotedNotificationInteractor,
private val keyguardViewStateRepository: NotificationsKeyguardViewStateRepository,
) {
+ private val aodPromotedKeyToHide: Flow<String?> =
+ combine(
+ aodPromotedNotificationInteractor.content,
+ aodPromotedNotificationInteractor.isPresent,
+ ) { content, isPresent ->
+ when {
+ !isPresent -> null
+ content == null -> null
+ else -> content.identity.key
+ }
+ }
+
/** Returns a subset of all active notifications based on the supplied filtration parameters. */
fun filteredNotifSet(
forceShowHeadsUp: Boolean = false,
@@ -49,12 +64,14 @@ constructor(
showDismissed: Boolean = true,
showRepliedMessages: Boolean = true,
showPulsing: Boolean = true,
+ showAodPromoted: Boolean = true,
): Flow<Set<ActiveNotificationModel>> {
return combine(
activeNotificationsInteractor.topLevelRepresentativeNotifications,
headsUpNotificationIconInteractor.isolatedNotification,
+ if (showAodPromoted) flowOf(null) else aodPromotedKeyToHide,
keyguardViewStateRepository.areNotificationsFullyHidden,
- ) { notifications, isolatedNotifKey, notifsFullyHidden ->
+ ) { notifications, isolatedNotifKey, aodPromotedKeyToHide, notifsFullyHidden ->
notifications
.asSequence()
.filter { model: ActiveNotificationModel ->
@@ -67,6 +84,7 @@ constructor(
showRepliedMessages = showRepliedMessages,
showPulsing = showPulsing,
isolatedNotifKey = isolatedNotifKey,
+ aodPromotedKeyToHide = aodPromotedKeyToHide,
notifsFullyHidden = notifsFullyHidden,
)
}
@@ -83,6 +101,7 @@ constructor(
showRepliedMessages: Boolean,
showPulsing: Boolean,
isolatedNotifKey: String?,
+ aodPromotedKeyToHide: String?,
notifsFullyHidden: Boolean,
): Boolean {
return when {
@@ -93,6 +112,7 @@ constructor(
!showRepliedMessages && model.isLastMessageFromReply -> false
!showAmbient && model.isSuppressedFromStatusBar -> false
!showPulsing && model.isPulsing && !notifsFullyHidden -> false
+ model.key == aodPromotedKeyToHide -> false
bubbles.getOrNull()?.isBubbleExpanded(model.key) == true -> false
else -> true
}
@@ -115,6 +135,7 @@ constructor(
showDismissed = false,
showRepliedMessages = false,
showPulsing = !isBypassEnabled,
+ showAodPromoted = false,
)
}
.flowOn(bgContext)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/people/PeopleNotificationIdentifier.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/people/PeopleNotificationIdentifier.kt
index 691f1f452da8..f755dbb48e1d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/people/PeopleNotificationIdentifier.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/people/PeopleNotificationIdentifier.kt
@@ -27,6 +27,7 @@ import com.android.systemui.statusbar.notification.people.PeopleNotificationIden
import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier.Companion.TYPE_IMPORTANT_PERSON
import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier.Companion.TYPE_NON_PERSON
import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier.Companion.TYPE_PERSON
+import com.android.systemui.statusbar.notification.shared.NotificationBundleUi
import javax.inject.Inject
import kotlin.math.max
@@ -112,14 +113,26 @@ class PeopleNotificationIdentifierImpl @Inject constructor(
if (personExtractor.isPersonNotification(sbn)) TYPE_PERSON else TYPE_NON_PERSON
private fun getPeopleTypeOfSummary(entry: NotificationEntry): Int {
- if (!groupManager.isGroupSummary(entry)) {
- return TYPE_NON_PERSON
+ if (NotificationBundleUi.isEnabled) {
+ if (!entry.sbn.notification.isGroupSummary) {
+ return TYPE_NON_PERSON;
+ }
+
+ return getPeopleTypeForChildList(entry.parent?.children)
+ } else {
+ if (!groupManager.isGroupSummary(entry)) {
+ return TYPE_NON_PERSON
+ }
+
+ return getPeopleTypeForChildList(groupManager.getChildren(entry))
}
+ }
- val childTypes = groupManager.getChildren(entry)
- ?.asSequence()
- ?.map { getPeopleNotificationType(it) }
- ?: return TYPE_NON_PERSON
+ private fun getPeopleTypeForChildList(children: List<NotificationEntry>?): Int {
+ val childTypes = children
+ ?.asSequence()
+ ?.map { getPeopleNotificationType(it) }
+ ?: return TYPE_NON_PERSON
var groupType = TYPE_NON_PERSON
for (childType in childTypes) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/AODPromotedNotification.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/AODPromotedNotification.kt
index e5d2361e8524..893570b7fb51 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/AODPromotedNotification.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/AODPromotedNotification.kt
@@ -426,6 +426,8 @@ private class AODPromotedNotificationViewUpdater(root: View) {
chronometer = chronometerStub?.inflate() as Chronometer
chronometerStub = null
+
+ chronometer?.appendFontFeatureSetting("tnum")
}
private fun inflateOldProgressBar() {
@@ -501,6 +503,10 @@ private fun Notification.ProgressStyle.Point.toSkeleton(): Notification.Progress
}
}
+private fun TextView.appendFontFeatureSetting(newSetting: String) {
+ fontFeatureSettings = (fontFeatureSettings?.let { "$it," } ?: "") + newSetting
+}
+
private enum class AodPromotedNotificationColor(val colorInt: Int) {
Background(android.graphics.Color.BLACK),
PrimaryText(android.graphics.Color.WHITE),
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/AODPromotedNotificationInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/AODPromotedNotificationInteractor.kt
index 393f95d3ad77..4bc685423659 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/AODPromotedNotificationInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/AODPromotedNotificationInteractor.kt
@@ -18,7 +18,6 @@ package com.android.systemui.statusbar.notification.promoted.domain.interactor
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dump.DumpManager
-import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor
import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel
import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel.Style
import com.android.systemui.util.kotlin.FlowDumperImpl
@@ -30,13 +29,12 @@ import kotlinx.coroutines.flow.map
class AODPromotedNotificationInteractor
@Inject
constructor(
- activeNotificationsInteractor: ActiveNotificationsInteractor,
+ promotedNotificationsInteractor: PromotedNotificationsInteractor,
dumpManager: DumpManager,
) : FlowDumperImpl(dumpManager) {
+ /** The content to show as the promoted notification on AOD */
val content: Flow<PromotedNotificationContentModel?> =
- activeNotificationsInteractor.topLevelRepresentativeNotifications.map { notifs ->
- notifs.firstNotNullOfOrNull { it.promotedContent }
- }
+ promotedNotificationsInteractor.topPromotedNotificationContent
val isPresent: Flow<Boolean> =
content
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/PromotedNotificationsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/PromotedNotificationsInteractor.kt
new file mode 100644
index 000000000000..1015cfbefc41
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/PromotedNotificationsInteractor.kt
@@ -0,0 +1,143 @@
+/*
+ * 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.promoted.domain.interactor
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.statusbar.chips.call.domain.interactor.CallChipInteractor
+import com.android.systemui.statusbar.chips.notification.domain.interactor.StatusBarNotificationChipsInteractor
+import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor
+import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel
+import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallModel
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
+
+/**
+ * An interactor that provides details about promoted notification precedence, based on the
+ * presented order of current notification status bar chips.
+ */
+@SysUISingleton
+class PromotedNotificationsInteractor
+@Inject
+constructor(
+ activeNotificationsInteractor: ActiveNotificationsInteractor,
+ callChipInteractor: CallChipInteractor,
+ notifChipsInteractor: StatusBarNotificationChipsInteractor,
+ @Background backgroundDispatcher: CoroutineDispatcher,
+) {
+ /**
+ * This is the ordered list of notifications (and the promoted content) represented as chips in
+ * the status bar.
+ */
+ private val orderedChipNotifications: Flow<List<NotifAndPromotedContent>> =
+ combine(callChipInteractor.ongoingCallState, notifChipsInteractor.allNotificationChips) {
+ callState,
+ notifChips ->
+ buildList {
+ val callData = callState.getNotifData()?.also { add(it) }
+ addAll(
+ notifChips.mapNotNull {
+ when (it.key) {
+ callData?.key -> null // do not re-add the same call
+ else -> NotifAndPromotedContent(it.key, it.promotedContent)
+ }
+ }
+ )
+ }
+ }
+
+ private fun OngoingCallModel.getNotifData(): NotifAndPromotedContent? =
+ when (this) {
+ is OngoingCallModel.InCall -> NotifAndPromotedContent(notificationKey, promotedContent)
+ is OngoingCallModel.InCallWithVisibleApp ->
+ // TODO(b/395989259): support InCallWithVisibleApp when it has notif data
+ null
+ is OngoingCallModel.NoCall -> null
+ }
+
+ /**
+ * The top promoted notification represented by a chip, with the order determined by the order
+ * of the chips, not the notifications.
+ */
+ private val topPromotedChipNotification: Flow<PromotedNotificationContentModel?> =
+ orderedChipNotifications
+ .map { list -> list.firstNotNullOfOrNull { it.promotedContent } }
+ .distinctUntilNewInstance()
+
+ /** This is the top-most promoted notification, which should avoid regular changing. */
+ val topPromotedNotificationContent: Flow<PromotedNotificationContentModel?> =
+ combine(
+ topPromotedChipNotification,
+ activeNotificationsInteractor.topLevelRepresentativeNotifications,
+ ) { topChipNotif, topLevelNotifs ->
+ topChipNotif ?: topLevelNotifs.firstNotNullOfOrNull { it.promotedContent }
+ }
+ // #equals() can be a bit expensive on this object, but this flow will regularly try to
+ // emit the same immutable instance over and over, so just prevent that.
+ .distinctUntilNewInstance()
+
+ /**
+ * This is the ordered list of notifications (and the promoted content) represented as chips in
+ * the status bar. Flows on the background context.
+ */
+ val orderedChipNotificationKeys: Flow<List<String>> =
+ orderedChipNotifications
+ .map { list -> list.map { it.key } }
+ .distinctUntilChanged()
+ .flowOn(backgroundDispatcher)
+
+ /**
+ * Returns flow where all subsequent repetitions of the same object instance are filtered out.
+ */
+ private fun <T> Flow<T>.distinctUntilNewInstance() = distinctUntilChanged { a, b -> a === b }
+
+ /**
+ * A custom pair, but providing clearer semantic names, and implementing equality as being the
+ * same instance of the promoted content model, which allows us to use distinctUntilChanged() on
+ * flows containing this without doing pixel comparisons on the Bitmaps inside Icon objects
+ * provided by the Notification.
+ */
+ private data class NotifAndPromotedContent(
+ val key: String,
+ val promotedContent: PromotedNotificationContentModel?,
+ ) {
+ /**
+ * Define the equals of this object to only check the reference equality of the promoted
+ * content so that we can mark.
+ */
+ override fun equals(other: Any?): Boolean {
+ return when {
+ other == null -> false
+ other === this -> true
+ other !is NotifAndPromotedContent -> return false
+ else -> key == other.key && promotedContent === other.promotedContent
+ }
+ }
+
+ /** Define the hashCode to be very quick, even if it increases collisions. */
+ override fun hashCode(): Int {
+ var result = key.hashCode()
+ result = 31 * result + (promotedContent?.identity?.hashCode() ?: 0)
+ return result
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/AsyncRowInflater.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/AsyncRowInflater.kt
new file mode 100644
index 000000000000..c3b241154e0e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/AsyncRowInflater.kt
@@ -0,0 +1,90 @@
+/*
+ * 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.util.Log
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.annotation.LayoutRes
+import androidx.annotation.UiThread
+import com.android.app.tracing.coroutines.launchTraced
+import com.android.app.tracing.coroutines.withContextTraced
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.dagger.qualifiers.NotifInflation
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
+
+@SysUISingleton
+class AsyncRowInflater
+@Inject
+constructor(
+ @Application private val applicationScope: CoroutineScope,
+ @Main private val mainCoroutineDispatcher: CoroutineDispatcher,
+ @NotifInflation private val inflationCoroutineDispatcher: CoroutineDispatcher,
+) {
+ /**
+ * Inflate the layout on the background thread, and invoke the listener on the main thread when
+ * finished.
+ *
+ * If the inflation fails on the background, it will be retried once on the main thread.
+ */
+ @UiThread
+ fun inflate(
+ context: Context,
+ layoutFactory: LayoutInflater.Factory2,
+ @LayoutRes resId: Int,
+ parent: ViewGroup,
+ listener: OnInflateFinishedListener,
+ ): Job {
+ val inflater = BasicRowInflater(context).apply { factory2 = layoutFactory }
+ return applicationScope.launchTraced("AsyncRowInflater-bg", inflationCoroutineDispatcher) {
+ val view =
+ try {
+ inflater.inflate(resId, parent, false)
+ } catch (ex: RuntimeException) {
+ // Probably a Looper failure, retry on the UI thread
+ Log.w(
+ "AsyncRowInflater",
+ "Failed to inflate resource in the background!" +
+ " Retrying on the UI thread",
+ ex,
+ )
+ null
+ }
+ withContextTraced("AsyncRowInflater-ui", mainCoroutineDispatcher) {
+ // If the inflate failed on the inflation thread, try again on the main thread
+ val finalView = view ?: inflater.inflate(resId, parent, false)
+ // Inform the listener of the completion
+ listener.onInflateFinished(finalView, resId, parent)
+ }
+ }
+ }
+
+ /**
+ * Callback interface (identical to the one from AsyncLayoutInflater) for receiving the inflated
+ * view
+ */
+ interface OnInflateFinishedListener {
+ @UiThread fun onInflateFinished(view: View, @LayoutRes resId: Int, parent: ViewGroup?)
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/BasicRowInflater.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/BasicRowInflater.kt
new file mode 100644
index 000000000000..79d50b8398bc
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/BasicRowInflater.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.notification.row
+
+import android.content.Context
+import android.util.AttributeSet
+import android.view.LayoutInflater
+import android.view.View
+
+/**
+ * A [LayoutInflater] that is copy of
+ * [androidx.asynclayoutinflater.view.AsyncLayoutInflater.BasicInflater]
+ */
+internal class BasicRowInflater(context: Context) : LayoutInflater(context) {
+ override fun cloneInContext(newContext: Context): LayoutInflater {
+ return BasicRowInflater(newContext)
+ }
+
+ @Throws(ClassNotFoundException::class)
+ override fun onCreateView(name: String, attrs: AttributeSet): View {
+ for (prefix in sClassPrefixList) {
+ try {
+ val view = createView(name, prefix, attrs)
+ if (view != null) {
+ return view
+ }
+ } catch (e: ClassNotFoundException) {
+ // In this case we want to let the base class take a crack at it.
+ }
+ }
+
+ return super.onCreateView(name, attrs)
+ }
+
+ companion object {
+ private val sClassPrefixList = arrayOf("android.widget.", "android.webkit.", "android.app.")
+ }
+}
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 9bf07689dbdb..1568e9e66c4c 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
@@ -105,6 +105,7 @@ import com.android.systemui.statusbar.notification.NotificationFadeAware;
import com.android.systemui.statusbar.notification.NotificationTransitionAnimatorController;
import com.android.systemui.statusbar.notification.NotificationUtils;
import com.android.systemui.statusbar.notification.SourceType;
+import com.android.systemui.statusbar.notification.collection.EntryAdapter;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.collection.provider.NotificationDismissibilityProvider;
import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManager;
@@ -120,6 +121,7 @@ import com.android.systemui.statusbar.notification.row.shared.LockscreenOtpRedac
import com.android.systemui.statusbar.notification.row.wrapper.NotificationCompactMessagingTemplateViewWrapper;
import com.android.systemui.statusbar.notification.row.wrapper.NotificationViewWrapper;
import com.android.systemui.statusbar.notification.shared.NotificationAddXOnHoverToDismiss;
+import com.android.systemui.statusbar.notification.shared.NotificationBundleUi;
import com.android.systemui.statusbar.notification.shared.NotificationContentAlphaOptimization;
import com.android.systemui.statusbar.notification.shared.TransparentHeaderFix;
import com.android.systemui.statusbar.notification.stack.AmbientState;
@@ -268,6 +270,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
private String mLoggingKey;
private NotificationGuts mGuts;
private NotificationEntry mEntry;
+ private EntryAdapter mEntryAdapter;
private String mAppName;
private NotificationRebindingTracker mRebindingTracker;
private FalsingManager mFalsingManager;
@@ -390,11 +393,17 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
}
private void toggleExpansionState(View v, boolean shouldLogExpandClickMetric) {
- if (!shouldShowPublic() && (!mIsMinimized || isExpanded())
- && mGroupMembershipManager.isGroupSummary(mEntry)) {
+ boolean isGroupRoot = NotificationBundleUi.isEnabled()
+ ? mGroupMembershipManager.isGroupRoot(mEntryAdapter)
+ : mGroupMembershipManager.isGroupSummary(mEntry);
+ if (!shouldShowPublic() && (!mIsMinimized || isExpanded()) && isGroupRoot) {
mGroupExpansionChanging = true;
- final boolean wasExpanded = mGroupExpansionManager.isGroupExpanded(mEntry);
- boolean nowExpanded = mGroupExpansionManager.toggleGroupExpansion(mEntry);
+ final boolean wasExpanded = NotificationBundleUi.isEnabled()
+ ? mGroupExpansionManager.isGroupExpanded(mEntryAdapter)
+ : mGroupExpansionManager.isGroupExpanded(mEntry);
+ boolean nowExpanded = NotificationBundleUi.isEnabled()
+ ? mGroupExpansionManager.toggleGroupExpansion(mEntryAdapter)
+ : mGroupExpansionManager.toggleGroupExpansion(mEntry);
mOnExpandClickListener.onExpandClicked(mEntry, v, nowExpanded);
if (shouldLogExpandClickMetric) {
mMetricsLogger.action(MetricsEvent.ACTION_NOTIFICATION_GROUP_EXPANDER, nowExpanded);
@@ -910,6 +919,12 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
return mEntry;
}
+ @Nullable
+ public EntryAdapter getEntryAdapter() {
+ NotificationBundleUi.assertInNewMode();
+ return mEntryAdapter;
+ }
+
@Override
public boolean isHeadsUp() {
return mIsHeadsUp;
@@ -2010,11 +2025,25 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
*
* @param context context context of the view
* @param attrs attributes used to initialize parent view
+ * @param user the user the row is associated to
+ */
+ public ExpandableNotificationRow(Context context, AttributeSet attrs, UserHandle user) {
+ this(context, attrs, userContextForEntry(context, user));
+ NotificationBundleUi.assertInNewMode();
+ }
+
+ /**
+ * Constructs an ExpandableNotificationRow. Used by layout inflation (with a custom {@code
+ * AsyncLayoutFactory} in {@link RowInflaterTask}.
+ *
+ * @param context context context of the view
+ * @param attrs attributes used to initialize parent view
* @param entry notification that the row will be associated to (determines the user for the
* ImageResolver)
*/
public ExpandableNotificationRow(Context context, AttributeSet attrs, NotificationEntry entry) {
this(context, attrs, userContextForEntry(context, entry));
+ NotificationBundleUi.assertInLegacyMode();
}
private static Context userContextForEntry(Context base, NotificationEntry entry) {
@@ -2025,6 +2054,13 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
UserHandle.of(entry.getSbn().getNormalizedUserId()), /* flags= */ 0);
}
+ private static Context userContextForEntry(Context base, UserHandle user) {
+ if (base.getUserId() == user.getIdentifier()) {
+ return base;
+ }
+ return base.createContextAsUser(user, /* flags= */ 0);
+ }
+
private ExpandableNotificationRow(Context sysUiContext, AttributeSet attrs,
Context userContext) {
super(sysUiContext, attrs);
@@ -2067,7 +2103,14 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
IStatusBarService statusBarService,
UiEventLogger uiEventLogger,
NotificationRebindingTracker notificationRebindingTracker) {
- mEntry = entry;
+
+ if (NotificationBundleUi.isEnabled()) {
+ // TODO (b/395857098): remove when all usages are migrated
+ mEntryAdapter = entry.getEntryAdapter();
+ mEntry = entry;
+ } else {
+ mEntry = entry;
+ }
mAppName = appName;
mRebindingTracker = notificationRebindingTracker;
if (mMenuRow == null) {
@@ -2740,6 +2783,11 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
invalidateOutline();
mBackgroundNormal.setExpandAnimationSize(params.getWidth(), actualHeight);
+
+ if (Flags.notificationsLaunchRadius()) {
+ mBackgroundNormal.setRadius(params.getTopCornerRadius(),
+ params.getBottomCornerRadius());
+ }
}
public void setExpandAnimationRunning(boolean expandAnimationRunning) {
@@ -2871,7 +2919,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
public void setUserExpanded(boolean userExpanded, boolean allowChildExpansion) {
if (mIsSummaryWithChildren && !shouldShowPublic() && allowChildExpansion
&& !mChildrenContainer.showingAsLowPriority()) {
- final boolean wasExpanded = mGroupExpansionManager.isGroupExpanded(mEntry);
+ final boolean wasExpanded = isGroupExpanded();
mGroupExpansionManager.setGroupExpanded(mEntry, userExpanded);
onExpansionChanged(true /* userAction */, wasExpanded);
return;
@@ -3026,6 +3074,9 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
@Override
public boolean isGroupExpanded() {
+ if (NotificationBundleUi.isEnabled()) {
+ return mGroupExpansionManager.isGroupExpanded(mEntryAdapter);
+ }
return mGroupExpansionManager.isGroupExpanded(mEntry);
}
@@ -3182,12 +3233,20 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
}
public void setSensitive(boolean sensitive, boolean hideSensitive) {
+ if (notificationsRedesignTemplates()
+ && sensitive == mSensitive && hideSensitive == mSensitiveHiddenInGeneral) {
+ return; // nothing has changed
+ }
+
int intrinsicBefore = getIntrinsicHeight();
mSensitive = sensitive;
mSensitiveHiddenInGeneral = hideSensitive;
int intrinsicAfter = getIntrinsicHeight();
if (intrinsicBefore != intrinsicAfter) {
notifyHeightChanged(/* needsAnimation= */ true);
+ } else if (notificationsRedesignTemplates()) {
+ // Just request the correct layout, even if the height hasn't changed
+ getShowingLayout().requestSelectLayout(/* needsAnimation= */ true);
}
}
@@ -3222,11 +3281,14 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
}
boolean oldShowingPublic = mShowingPublic;
mShowingPublic = mSensitive && hideSensitive;
- if (mShowingPublicInitialized && mShowingPublic == oldShowingPublic) {
+ boolean isShowingLayoutNotChanged = mShowingPublic == oldShowingPublic;
+ if (mShowingPublicInitialized && isShowingLayoutNotChanged) {
return;
}
- if (!animated) {
+ final boolean shouldSkipHideSensitiveAnimation =
+ Flags.skipHideSensitiveNotifAnimation() && isShowingLayoutNotChanged;
+ if (!animated || shouldSkipHideSensitiveAnimation) {
if (!NotificationContentAlphaOptimization.isEnabled()
|| mShowingPublic != oldShowingPublic) {
// Don't reset the alpha or cancel the animation if the showing layout doesn't
@@ -3761,7 +3823,9 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
public void onExpandedByGesture(boolean userExpanded) {
int event = MetricsEvent.ACTION_NOTIFICATION_GESTURE_EXPANDER;
- if (mGroupMembershipManager.isGroupSummary(mEntry)) {
+ if (NotificationBundleUi.isEnabled()
+ ? mGroupMembershipManager.isGroupRoot(mEntryAdapter)
+ : mGroupMembershipManager.isGroupSummary(mEntry)) {
event = MetricsEvent.ACTION_NOTIFICATION_GROUP_GESTURE_EXPANDER;
}
mMetricsLogger.action(event, userExpanded);
@@ -3797,7 +3861,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
private void onExpansionChanged(boolean userAction, boolean wasExpanded) {
boolean nowExpanded = isExpanded();
if (mIsSummaryWithChildren && (!mIsMinimized || wasExpanded)) {
- nowExpanded = mGroupExpansionManager.isGroupExpanded(mEntry);
+ nowExpanded = isGroupExpanded();
}
// Note: nowExpanded is going to be true here on the first expansion of minimized groups,
// even though the group itself is not expanded. Use mGroupExpansionManager to get the real
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 e311b53bfa64..0c1dd2e026b6 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
@@ -56,6 +56,7 @@ import com.android.systemui.statusbar.NotificationRemoteInputManager;
import com.android.systemui.statusbar.notification.ConversationNotificationProcessor;
import com.android.systemui.statusbar.notification.InflationException;
import com.android.systemui.statusbar.notification.NmSummarizationUiFlag;
+import com.android.systemui.statusbar.notification.collection.EntryAdapter;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.promoted.PromotedNotificationContentExtractor;
import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUiForceExpanded;
@@ -1457,12 +1458,12 @@ public class NotificationContentInflater implements NotificationRowContentBinder
}
@Override
- public void handleInflationException(NotificationEntry entry, Exception e) {
+ public void handleInflationException(Exception e) {
handleError(e);
}
@Override
- public void onAsyncInflationFinished(NotificationEntry entry) {
+ public void onAsyncInflationFinished() {
mEntry.onInflationTaskFinished();
mRow.onNotificationUpdated();
if (mCallback != null) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java
index 430e5e4f1520..6e638f5de209 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java
@@ -18,22 +18,13 @@ package com.android.systemui.statusbar.notification.row;
import static android.app.AppOpsManager.OP_CAMERA;
import static android.app.AppOpsManager.OP_RECORD_AUDIO;
import static android.app.AppOpsManager.OP_SYSTEM_ALERT_WINDOW;
-import static android.service.notification.Adjustment.KEY_SUMMARIZATION;
-import static android.service.notification.Adjustment.KEY_TYPE;
-import static android.service.notification.NotificationAssistantService.ACTION_NOTIFICATION_ASSISTANT_FEEDBACK_SETTINGS;
-import static android.service.notification.NotificationAssistantService.EXTRA_NOTIFICATION_ADJUSTMENT;
-import static android.service.notification.NotificationAssistantService.EXTRA_NOTIFICATION_KEY;
-import android.annotation.FlaggedApi;
import android.app.INotificationManager;
import android.app.NotificationChannel;
-import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
-import android.content.pm.ActivityInfo;
import android.content.pm.LauncherApps;
import android.content.pm.PackageManager;
-import android.content.pm.ResolveInfo;
import android.content.pm.ShortcutManager;
import android.net.Uri;
import android.os.Bundle;
@@ -41,7 +32,6 @@ import android.os.Handler;
import android.os.UserHandle;
import android.os.UserManager;
import android.provider.Settings;
-import android.service.notification.NotificationAssistantService;
import android.service.notification.StatusBarNotification;
import android.util.ArraySet;
import android.util.IconDrawableFactory;
@@ -81,13 +71,14 @@ import com.android.systemui.statusbar.notification.collection.provider.HighPrior
import com.android.systemui.statusbar.notification.collection.render.NotifGutsViewListener;
import com.android.systemui.statusbar.notification.collection.render.NotifGutsViewManager;
import com.android.systemui.statusbar.notification.headsup.HeadsUpManager;
+import com.android.systemui.statusbar.notification.row.icon.AppIconProvider;
+import com.android.systemui.statusbar.notification.row.icon.NotificationIconStyleProvider;
import com.android.systemui.statusbar.notification.stack.NotificationListContainer;
import com.android.systemui.statusbar.phone.CentralSurfaces;
import com.android.systemui.statusbar.policy.DeviceProvisionedController;
import com.android.systemui.util.kotlin.JavaAdapter;
import com.android.systemui.wmshell.BubblesManager;
-import java.util.List;
import java.util.Optional;
import javax.inject.Inject;
@@ -131,6 +122,8 @@ public class NotificationGutsManager implements NotifGutsViewManager, CoreStarta
private final Optional<BubblesManager> mBubblesManagerOptional;
private Runnable mOpenRunnable;
private final INotificationManager mNotificationManager;
+ private final AppIconProvider mAppIconProvider;
+ private final NotificationIconStyleProvider mIconStyleProvider;
private final PeopleSpaceWidgetManager mPeopleSpaceWidgetManager;
private final UserManager mUserManager;
@@ -154,6 +147,8 @@ public class NotificationGutsManager implements NotifGutsViewManager, CoreStarta
AccessibilityManager accessibilityManager,
HighPriorityProvider highPriorityProvider,
INotificationManager notificationManager,
+ AppIconProvider appIconProvider,
+ NotificationIconStyleProvider iconStyleProvider,
UserManager userManager,
PeopleSpaceWidgetManager peopleSpaceWidgetManager,
LauncherApps launcherApps,
@@ -180,6 +175,8 @@ public class NotificationGutsManager implements NotifGutsViewManager, CoreStarta
mAccessibilityManager = accessibilityManager;
mHighPriorityProvider = highPriorityProvider;
mNotificationManager = notificationManager;
+ mAppIconProvider = appIconProvider;
+ mIconStyleProvider = iconStyleProvider;
mUserManager = userManager;
mPeopleSpaceWidgetManager = peopleSpaceWidgetManager;
mLauncherApps = launcherApps;
@@ -427,6 +424,8 @@ public class NotificationGutsManager implements NotifGutsViewManager, CoreStarta
notificationInfoView.bindNotification(
pmUser,
mNotificationManager,
+ mAppIconProvider,
+ mIconStyleProvider,
mOnUserInteractionCallback,
mChannelEditorDialogController,
packageName,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationInfo.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationInfo.java
index 49b682d0a5d2..661122510c6c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationInfo.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationInfo.java
@@ -16,6 +16,7 @@
package com.android.systemui.statusbar.notification.row;
+import static android.app.Flags.notificationsRedesignThemedAppIcons;
import static android.app.Notification.EXTRA_BUILDER_APPLICATION_INFO;
import static android.app.NotificationChannel.SYSTEM_RESERVED_IDS;
import static android.app.NotificationManager.IMPORTANCE_DEFAULT;
@@ -25,11 +26,13 @@ import static android.service.notification.Adjustment.KEY_SUMMARIZATION;
import static android.service.notification.Adjustment.KEY_TYPE;
import static com.android.app.animation.Interpolators.FAST_OUT_SLOW_IN;
+import static com.android.systemui.Flags.notificationsRedesignGuts;
import static java.lang.annotation.RetentionPolicy.SOURCE;
import android.annotation.IntDef;
import android.annotation.Nullable;
+import android.annotation.SuppressLint;
import android.app.Flags;
import android.app.INotificationManager;
import android.app.Notification;
@@ -70,8 +73,9 @@ import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
import com.android.systemui.Dependency;
import com.android.systemui.res.R;
import com.android.systemui.statusbar.notification.AssistantFeedbackController;
-import com.android.systemui.statusbar.notification.NmSummarizationUiFlag;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
+import com.android.systemui.statusbar.notification.row.icon.AppIconProvider;
+import com.android.systemui.statusbar.notification.row.icon.NotificationIconStyleProvider;
import java.lang.annotation.Retention;
import java.util.List;
@@ -88,6 +92,8 @@ public class NotificationInfo extends LinearLayout implements NotificationGuts.G
private TextView mAutomaticDescriptionView;
private INotificationManager mINotificationManager;
+ private AppIconProvider mAppIconProvider;
+ private NotificationIconStyleProvider mIconStyleProvider;
private OnUserInteractionCallback mOnUserInteractionCallback;
private PackageManager mPm;
private MetricsLogger mMetricsLogger;
@@ -183,6 +189,8 @@ public class NotificationInfo extends LinearLayout implements NotificationGuts.G
public void bindNotification(
PackageManager pm,
INotificationManager iNotificationManager,
+ AppIconProvider appIconProvider,
+ NotificationIconStyleProvider iconStyleProvider,
OnUserInteractionCallback onUserInteractionCallback,
ChannelEditorDialogController channelEditorDialogController,
String pkg,
@@ -200,6 +208,8 @@ public class NotificationInfo extends LinearLayout implements NotificationGuts.G
OnClickListener onCloseClick)
throws RemoteException {
mINotificationManager = iNotificationManager;
+ mAppIconProvider = appIconProvider;
+ mIconStyleProvider = iconStyleProvider;
mMetricsLogger = metricsLogger;
mOnUserInteractionCallback = onUserInteractionCallback;
mChannelEditorDialogController = channelEditorDialogController;
@@ -290,23 +300,39 @@ public class NotificationInfo extends LinearLayout implements NotificationGuts.G
applyAlertingBehavior(behavior, false /* userTriggered */);
}
+ @SuppressLint("WrongThread")
private void bindHeader() {
- // Package name
mPkgIcon = null;
// filled in if missing during notification inflation, which must have happened if
// we have a notification to long press on
ApplicationInfo info =
mSbn.getNotification().extras.getParcelable(EXTRA_BUILDER_APPLICATION_INFO,
ApplicationInfo.class);
- if (info != null) {
- try {
- mAppName = String.valueOf(mPm.getApplicationLabel(info));
- mPkgIcon = mPm.getApplicationIcon(info);
- } catch (Exception ignored) {}
- }
- if (mPkgIcon == null) {
- // app is gone, just show package name and generic icon
- mPkgIcon = mPm.getDefaultActivityIcon();
+ if (notificationsRedesignGuts()) {
+ if (info != null) {
+ try {
+ mAppName = String.valueOf(mPm.getApplicationLabel(info));
+ // The app icon is likely already in the cache, so let's use it
+ boolean withWorkProfileBadge =
+ mIconStyleProvider.shouldShowWorkProfileBadge(mSbn, getContext());
+ mPkgIcon = mAppIconProvider.getOrFetchAppIcon(info.packageName, getContext(),
+ withWorkProfileBadge,
+ /* themed = */ notificationsRedesignThemedAppIcons());
+ } catch (Exception ignored) {
+ }
+ }
+ } else {
+ if (info != null) {
+ try {
+ mAppName = String.valueOf(mPm.getApplicationLabel(info));
+ mPkgIcon = mPm.getApplicationIcon(info);
+ } catch (Exception ignored) {
+ }
+ }
+ if (mPkgIcon == null) {
+ // app is gone, just show package name and generic icon
+ mPkgIcon = mPm.getDefaultActivityIcon();
+ }
}
((ImageView) findViewById(R.id.pkg_icon)).setImageDrawable(mPkgIcon);
((TextView) findViewById(R.id.pkg_name)).setText(mAppName);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinder.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinder.java
index 0be1d5d9d79d..05934e7edfba 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinder.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinder.java
@@ -24,6 +24,7 @@ import android.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
+import com.android.systemui.statusbar.notification.collection.EntryAdapter;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import java.lang.annotation.Retention;
@@ -170,13 +171,29 @@ public interface NotificationRowContentBinder {
* @param entry notification which failed to inflate content
* @param e exception
*/
- void handleInflationException(NotificationEntry entry, Exception e);
+ default void handleInflationException(NotificationEntry entry, Exception e) {
+ handleInflationException(e);
+ }
+
+ /**
+ * Callback for when there is an inflation exception
+ *
+ * @param e exception
+ */
+ void handleInflationException(Exception e);
/**
* Callback for after the content views finish inflating.
*
* @param entry the entry with the content views set
*/
- void onAsyncInflationFinished(NotificationEntry entry);
+ default void onAsyncInflationFinished(NotificationEntry entry) {
+ onAsyncInflationFinished();
+ }
+
+ /**
+ * Callback for after the content views finish inflating.
+ */
+ void onAsyncInflationFinished();
}
}
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 517fc3a06d84..761d3fe91cd0 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
@@ -49,6 +49,7 @@ import com.android.systemui.statusbar.NotificationRemoteInputManager
import com.android.systemui.statusbar.notification.ConversationNotificationProcessor
import com.android.systemui.statusbar.notification.InflationException
import com.android.systemui.statusbar.notification.NmSummarizationUiFlag
+import com.android.systemui.statusbar.notification.collection.EntryAdapter
import com.android.systemui.statusbar.notification.collection.NotificationEntry
import com.android.systemui.statusbar.notification.promoted.PromotedNotificationContentExtractor
import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel
@@ -76,6 +77,7 @@ import com.android.systemui.statusbar.notification.row.shared.NotificationConten
import com.android.systemui.statusbar.notification.row.shared.NotificationRowContentBinderRefactor
import com.android.systemui.statusbar.notification.row.ui.viewbinder.SingleLineViewBinder
import com.android.systemui.statusbar.notification.row.wrapper.NotificationViewWrapper
+import com.android.systemui.statusbar.notification.shared.NotificationBundleUi
import com.android.systemui.statusbar.notification.stack.NotificationChildrenContainer
import com.android.systemui.statusbar.policy.InflatedSmartReplyState
import com.android.systemui.statusbar.policy.InflatedSmartReplyViewHolder
@@ -536,7 +538,7 @@ constructor(
val ident: String = (sbn.packageName + "/0x" + Integer.toHexString(sbn.id))
Log.e(TAG, "couldn't inflate view for notification $ident", e)
callback?.handleInflationException(
- row.entry,
+ if (NotificationBundleUi.isEnabled) entry else row.entry,
InflationException("Couldn't inflate contentViews$e"),
)
@@ -554,11 +556,11 @@ constructor(
logger.logAsyncTaskProgress(entry, "aborted")
}
- override fun handleInflationException(entry: NotificationEntry, e: Exception) {
+ override fun handleInflationException(e: Exception) {
handleError(e)
}
- override fun onAsyncInflationFinished(entry: NotificationEntry) {
+ override fun onAsyncInflationFinished() {
this.entry.onInflationTaskFinished()
row.onNotificationUpdated()
callback?.onAsyncInflationFinished(this.entry)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/PromotedNotificationInfo.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/PromotedNotificationInfo.java
index db25e0889298..6ff711deeb01 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/PromotedNotificationInfo.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/PromotedNotificationInfo.java
@@ -31,6 +31,8 @@ import com.android.internal.logging.UiEventLogger;
import com.android.systemui.res.R;
import com.android.systemui.statusbar.notification.AssistantFeedbackController;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
+import com.android.systemui.statusbar.notification.row.icon.AppIconProvider;
+import com.android.systemui.statusbar.notification.row.icon.NotificationIconStyleProvider;
/**
* The guts of a notification revealed when performing a long press, specifically
@@ -50,6 +52,8 @@ public class PromotedNotificationInfo extends NotificationInfo {
public void bindNotification(
PackageManager pm,
INotificationManager iNotificationManager,
+ AppIconProvider appIconProvider,
+ NotificationIconStyleProvider iconStyleProvider,
OnUserInteractionCallback onUserInteractionCallback,
ChannelEditorDialogController channelEditorDialogController,
String pkg,
@@ -64,11 +68,11 @@ public class PromotedNotificationInfo extends NotificationInfo {
boolean wasShownHighPriority,
AssistantFeedbackController assistantFeedbackController,
MetricsLogger metricsLogger, OnClickListener onCloseClick) throws RemoteException {
- super.bindNotification(pm, iNotificationManager, onUserInteractionCallback,
- channelEditorDialogController, pkg, notificationChannel, entry, onSettingsClick,
- onAppSettingsClick, feedbackClickListener, uiEventLogger, isDeviceProvisioned,
- isNonblockable, wasShownHighPriority, assistantFeedbackController, metricsLogger,
- onCloseClick);
+ super.bindNotification(pm, iNotificationManager, appIconProvider, iconStyleProvider,
+ onUserInteractionCallback, channelEditorDialogController, pkg, notificationChannel,
+ entry, onSettingsClick, onAppSettingsClick, feedbackClickListener, uiEventLogger,
+ isDeviceProvisioned, isNonblockable, wasShownHighPriority,
+ assistantFeedbackController, metricsLogger, onCloseClick);
mNotificationManager = iNotificationManager;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindStage.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindStage.java
index 6883ec575d7e..da361406fa2a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindStage.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindStage.java
@@ -21,10 +21,12 @@ import static com.android.systemui.statusbar.notification.row.NotificationRowCon
import androidx.annotation.NonNull;
import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.statusbar.notification.collection.EntryAdapter;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.BindParams;
import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.InflationCallback;
import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.InflationFlag;
+import com.android.systemui.statusbar.notification.shared.NotificationBundleUi;
import javax.inject.Inject;
@@ -52,7 +54,7 @@ public class RowContentBindStage extends BindStage<RowContentBindParams> {
@Override
protected void executeStage(
- @NonNull NotificationEntry entry,
+ final @NonNull NotificationEntry entry,
@NonNull ExpandableNotificationRow row,
@NonNull StageCallback callback) {
RowContentBindParams params = getStageParams(entry);
@@ -77,15 +79,35 @@ public class RowContentBindStage extends BindStage<RowContentBindParams> {
InflationCallback inflationCallback = new InflationCallback() {
@Override
- public void handleInflationException(NotificationEntry entry, Exception e) {
- mNotifInflationErrorManager.setInflationError(entry, e);
+ public void handleInflationException(NotificationEntry errorEntry, Exception e) {
+ if (NotificationBundleUi.isEnabled()) {
+ mNotifInflationErrorManager.setInflationError(entry, e);
+ } else {
+ mNotifInflationErrorManager.setInflationError(errorEntry, e);
+ }
+ }
+
+ @Override
+ public void handleInflationException(Exception e) {
+
}
@Override
- public void onAsyncInflationFinished(NotificationEntry entry) {
- mNotifInflationErrorManager.clearInflationError(entry);
- getStageParams(entry).clearDirtyContentViews();
- callback.onStageFinished(entry);
+ public void onAsyncInflationFinished(NotificationEntry finishedEntry) {
+ if (NotificationBundleUi.isEnabled()) {
+ mNotifInflationErrorManager.clearInflationError(entry);
+ getStageParams(entry).clearDirtyContentViews();
+ callback.onStageFinished(entry);
+ } else {
+ mNotifInflationErrorManager.clearInflationError(finishedEntry);
+ getStageParams(finishedEntry).clearDirtyContentViews();
+ callback.onStageFinished(finishedEntry);
+ }
+ }
+
+ @Override
+ public void onAsyncInflationFinished() {
+
}
};
mBinder.cancelBind(entry, row);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowInflaterTask.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowInflaterTask.java
index 9f634bef4c5e..3971661fa787 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowInflaterTask.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowInflaterTask.java
@@ -17,6 +17,7 @@
package com.android.systemui.statusbar.notification.row;
import android.content.Context;
+import android.os.UserHandle;
import android.util.AttributeSet;
import android.util.Log;
import android.view.LayoutInflater;
@@ -29,9 +30,12 @@ import androidx.annotation.VisibleForTesting;
import androidx.asynclayoutinflater.view.AsyncLayoutFactory;
import androidx.asynclayoutinflater.view.AsyncLayoutInflater;
+import com.android.systemui.Flags;
import com.android.systemui.res.R;
+import com.android.systemui.settings.UserTracker;
import com.android.systemui.statusbar.InflationTask;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
+import com.android.systemui.statusbar.notification.shared.NotificationBundleUi;
import com.android.systemui.util.time.SystemClock;
import java.util.concurrent.Executor;
@@ -41,7 +45,8 @@ import javax.inject.Inject;
/**
* An inflater task that asynchronously inflates a ExpandableNotificationRow
*/
-public class RowInflaterTask implements InflationTask, AsyncLayoutInflater.OnInflateFinishedListener {
+public class RowInflaterTask implements InflationTask,
+ AsyncLayoutInflater.OnInflateFinishedListener, AsyncRowInflater.OnInflateFinishedListener {
private static final String TAG = "RowInflaterTask";
private static final boolean TRACE_ORIGIN = true;
@@ -52,12 +57,17 @@ public class RowInflaterTask implements InflationTask, AsyncLayoutInflater.OnInf
private Throwable mInflateOrigin;
private final SystemClock mSystemClock;
private final RowInflaterTaskLogger mLogger;
+ private final AsyncRowInflater mAsyncRowInflater;
private long mInflateStartTimeMs;
+ private UserTracker mUserTracker;
@Inject
- public RowInflaterTask(SystemClock systemClock, RowInflaterTaskLogger logger) {
+ public RowInflaterTask(SystemClock systemClock, RowInflaterTaskLogger logger,
+ UserTracker userTracker, AsyncRowInflater asyncRowInflater) {
mSystemClock = systemClock;
mLogger = logger;
+ mUserTracker = userTracker;
+ mAsyncRowInflater = asyncRowInflater;
}
/**
@@ -81,13 +91,19 @@ public class RowInflaterTask implements InflationTask, AsyncLayoutInflater.OnInf
mInflateOrigin = new Throwable("inflate requested here");
}
mListener = listener;
- AsyncLayoutInflater inflater = new AsyncLayoutInflater(context, makeRowInflater(entry));
+ RowAsyncLayoutInflater asyncLayoutFactory = makeRowInflater(entry);
mEntry = entry;
entry.setInflationTask(this);
mLogger.logInflateStart(entry);
mInflateStartTimeMs = mSystemClock.elapsedRealtime();
- inflater.inflate(R.layout.status_bar_notification_row, parent, listenerExecutor, this);
+ if (Flags.useNotifInflationThreadForRow()) {
+ mAsyncRowInflater.inflate(context, asyncLayoutFactory,
+ R.layout.status_bar_notification_row, parent, this);
+ } else {
+ AsyncLayoutInflater inflater = new AsyncLayoutInflater(context, asyncLayoutFactory);
+ inflater.inflate(R.layout.status_bar_notification_row, parent, listenerExecutor, this);
+ }
}
/**
@@ -107,40 +123,8 @@ public class RowInflaterTask implements InflationTask, AsyncLayoutInflater.OnInf
}
private RowAsyncLayoutInflater makeRowInflater(NotificationEntry entry) {
- return new RowAsyncLayoutInflater(entry, mSystemClock, mLogger);
- }
-
- /**
- * A {@link LayoutInflater} that is copy of BasicLayoutInflater.
- */
- private static class BasicRowInflater extends LayoutInflater {
- private static final String[] sClassPrefixList =
- {"android.widget.", "android.webkit.", "android.app."};
- BasicRowInflater(Context context) {
- super(context);
- }
-
- @Override
- public LayoutInflater cloneInContext(Context newContext) {
- return new BasicRowInflater(newContext);
- }
-
- @Override
- protected View onCreateView(String name, AttributeSet attrs) throws ClassNotFoundException {
- for (String prefix : sClassPrefixList) {
- try {
- View view = createView(name, prefix, attrs);
- if (view != null) {
- return view;
- }
- } catch (ClassNotFoundException e) {
- // In this case we want to let the base class take a crack
- // at it.
- }
- }
-
- return super.onCreateView(name, attrs);
- }
+ return new RowAsyncLayoutInflater(
+ entry, mSystemClock, mLogger, mUserTracker.getUserHandle());
}
@VisibleForTesting
@@ -148,12 +132,14 @@ public class RowInflaterTask implements InflationTask, AsyncLayoutInflater.OnInf
private final NotificationEntry mEntry;
private final SystemClock mSystemClock;
private final RowInflaterTaskLogger mLogger;
+ private final UserHandle mTargetUser;
public RowAsyncLayoutInflater(NotificationEntry entry, SystemClock systemClock,
- RowInflaterTaskLogger logger) {
+ RowInflaterTaskLogger logger, UserHandle targetUser) {
mEntry = entry;
mSystemClock = systemClock;
mLogger = logger;
+ mTargetUser = targetUser;
}
@Nullable
@@ -165,8 +151,12 @@ public class RowInflaterTask implements InflationTask, AsyncLayoutInflater.OnInf
}
final long startMs = mSystemClock.elapsedRealtime();
- final ExpandableNotificationRow row =
- new ExpandableNotificationRow(context, attrs, mEntry);
+ ExpandableNotificationRow row = null;
+ if (NotificationBundleUi.isEnabled()) {
+ row = new ExpandableNotificationRow(context, attrs, mTargetUser);
+ } else {
+ row = new ExpandableNotificationRow(context, attrs, mEntry);
+ }
final long elapsedMs = mSystemClock.elapsedRealtime() - startMs;
mLogger.logCreatedRow(mEntry, elapsedMs);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/icon/AppIconProvider.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/icon/AppIconProvider.kt
index 52a0f6f92355..33d943348cb3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/icon/AppIconProvider.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/icon/AppIconProvider.kt
@@ -58,7 +58,7 @@ interface AppIconProvider {
packageName: String,
context: Context,
withWorkProfileBadge: Boolean = false,
- themed: Boolean = true,
+ themed: Boolean = false,
): Drawable
/**
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
index b9352bf64be4..3ee827332877 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
@@ -122,6 +122,7 @@ import com.android.systemui.statusbar.notification.row.ActivatableNotificationVi
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
import com.android.systemui.statusbar.notification.row.ExpandableView;
import com.android.systemui.statusbar.notification.row.StackScrollerDecorView;
+import com.android.systemui.statusbar.notification.shared.NotificationBundleUi;
import com.android.systemui.statusbar.notification.shared.NotificationContentAlphaOptimization;
import com.android.systemui.statusbar.notification.shared.NotificationHeadsUpCycling;
import com.android.systemui.statusbar.notification.shared.NotificationThrottleHun;
@@ -2958,9 +2959,13 @@ public class NotificationStackScrollLayout
}
private boolean isChildInGroup(View child) {
- return child instanceof ExpandableNotificationRow
- && mGroupMembershipManager.isChildInGroup(
- ((ExpandableNotificationRow) child).getEntry());
+ if (child instanceof ExpandableNotificationRow) {
+ ExpandableNotificationRow childRow = (ExpandableNotificationRow) child;
+ return NotificationBundleUi.isEnabled()
+ ? mGroupMembershipManager.isChildInGroup(childRow.getEntryAdapter())
+ : mGroupMembershipManager.isChildInGroup(childRow.getEntry());
+ }
+ return false;
}
/**
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 810d0b43b0dd..888c8cc59439 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
@@ -382,9 +382,15 @@ public class NotificationStackScrollLayoutController implements Dumpable {
// Only animate if in a non-sensitive state (not screen sharing)
boolean shouldAnimate = animate && !isSensitiveContentProtectionActive;
+ mLogger.logUpdateSensitivenessWithAnimation(shouldAnimate,
+ isSensitive,
+ isSensitiveContentProtectionActive,
+ isAnyProfilePublic);
mView.updateSensitiveness(shouldAnimate, isSensitive);
} else {
- mView.updateSensitiveness(animate, mLockscreenUserManager.isAnyProfilePublicMode());
+ boolean anyProfilePublicMode = mLockscreenUserManager.isAnyProfilePublicMode();
+ mLogger.logUpdateSensitivenessWithAnimation(animate, anyProfilePublicMode);
+ mView.updateSensitiveness(animate, anyProfilePublicMode);
}
Trace.endSection();
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLogger.kt
index 3396306412bd..30658710c3c5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLogger.kt
@@ -156,6 +156,44 @@ class NotificationStackScrollLogger @Inject constructor(
{ "removeTransientRow from NSSL: childKey: $str1" }
)
}
+
+ fun logUpdateSensitivenessWithAnimation(
+ shouldAnimate: Boolean,
+ isSensitive: Boolean,
+ isSensitiveContentProtectionActive: Boolean,
+ isAnyProfilePublic: Boolean,
+ ) {
+ notificationRenderBuffer.log(
+ TAG,
+ INFO,
+ {
+ bool1 = shouldAnimate
+ bool2 = isSensitive
+ bool3 = isSensitiveContentProtectionActive
+ bool4 = isAnyProfilePublic
+ },
+ {
+ "updateSensitivenessWithAnimation from NSSL: shouldAnimate=$bool1 " +
+ "isSensitive(hideSensitive)=$bool2 isSensitiveContentProtectionActive=$bool3 " +
+ "isAnyProfilePublic=$bool4"
+ },
+ )
+ }
+
+ fun logUpdateSensitivenessWithAnimation(animate: Boolean, anyProfilePublicMode: Boolean) {
+ notificationRenderBuffer.log(
+ TAG,
+ INFO,
+ {
+ bool1 = animate
+ bool2 = anyProfilePublicMode
+ },
+ {
+ "updateSensitivenessWithAnimation from NSSL: animate=$bool1 " +
+ "anyProfilePublicMode(hideSensitive)=$bool2"
+ },
+ )
+ }
}
-private const val TAG = "NotificationStackScroll" \ No newline at end of file
+private const val TAG = "NotificationStackScroll"
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt
index 6385d53dbc8b..10b665d8ef01 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt
@@ -25,7 +25,7 @@ import com.android.internal.logging.MetricsLogger
import com.android.internal.logging.nano.MetricsProto
import com.android.systemui.common.ui.ConfigurationState
import com.android.systemui.common.ui.view.setImportantForAccessibilityYesNo
-import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.dagger.qualifiers.NotifInflation
import com.android.systemui.lifecycle.repeatWhenAttached
import com.android.systemui.lifecycle.repeatWhenAttachedToWindow
import com.android.systemui.plugins.FalsingManager
@@ -76,7 +76,7 @@ import kotlinx.coroutines.flow.stateIn
class NotificationListViewBinder
@Inject
constructor(
- @Background private val backgroundDispatcher: CoroutineDispatcher,
+ @NotifInflation private val inflationDispatcher: CoroutineDispatcher,
private val hiderTracker: DisplaySwitchNotificationsHiderTracker,
@ShadeDisplayAware private val configuration: ConfigurationState,
private val falsingManager: FalsingManager,
@@ -155,7 +155,7 @@ constructor(
parentView,
attachToRoot = false,
)
- .flowOn(backgroundDispatcher)
+ .flowOn(inflationDispatcher)
.collectLatest { footerView: FooterView ->
traceAsync("bind FooterView") {
parentView.setFooterView(footerView)
@@ -240,7 +240,7 @@ constructor(
parentView,
attachToRoot = false,
)
- .flowOn(backgroundDispatcher)
+ .flowOn(inflationDispatcher)
.collectLatest { emptyShadeView: EmptyShadeView ->
traceAsync("bind EmptyShadeView") {
parentView.setEmptyShadeView(emptyShadeView)
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 2c8c7a1bdd44..54efa4a2bcf2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt
@@ -48,7 +48,6 @@ import com.android.systemui.keyguard.ui.viewmodel.AodToGoneTransitionViewModel
import com.android.systemui.keyguard.ui.viewmodel.AodToLockscreenTransitionViewModel
import com.android.systemui.keyguard.ui.viewmodel.AodToOccludedTransitionViewModel
import com.android.systemui.keyguard.ui.viewmodel.AodToPrimaryBouncerTransitionViewModel
-import com.android.systemui.keyguard.ui.viewmodel.DozingToDreamingTransitionViewModel
import com.android.systemui.keyguard.ui.viewmodel.DozingToGlanceableHubTransitionViewModel
import com.android.systemui.keyguard.ui.viewmodel.DozingToLockscreenTransitionViewModel
import com.android.systemui.keyguard.ui.viewmodel.DozingToOccludedTransitionViewModel
@@ -137,7 +136,6 @@ constructor(
private val aodToLockscreenTransitionViewModel: AodToLockscreenTransitionViewModel,
private val aodToOccludedTransitionViewModel: AodToOccludedTransitionViewModel,
private val aodToPrimaryBouncerTransitionViewModel: AodToPrimaryBouncerTransitionViewModel,
- private val dozingToDreamingTransitionViewModel: DozingToDreamingTransitionViewModel,
dozingToGlanceableHubTransitionViewModel: DozingToGlanceableHubTransitionViewModel,
private val dozingToLockscreenTransitionViewModel: DozingToLockscreenTransitionViewModel,
private val dozingToOccludedTransitionViewModel: DozingToOccludedTransitionViewModel,
@@ -574,7 +572,6 @@ constructor(
aodToLockscreenTransitionViewModel.notificationAlpha,
aodToOccludedTransitionViewModel.lockscreenAlpha(viewState),
aodToPrimaryBouncerTransitionViewModel.notificationAlpha,
- dozingToDreamingTransitionViewModel.notificationAlpha,
dozingToLockscreenTransitionViewModel.lockscreenAlpha,
dozingToOccludedTransitionViewModel.lockscreenAlpha(viewState),
dozingToPrimaryBouncerTransitionViewModel.notificationAlpha,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallback.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallback.java
index 1dc9de489806..05a46cd9fa31 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallback.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallback.java
@@ -54,6 +54,7 @@ import com.android.systemui.statusbar.StatusBarState;
import com.android.systemui.statusbar.SysuiStatusBarStateController;
import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManager;
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
+import com.android.systemui.statusbar.notification.shared.NotificationBundleUi;
import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout;
import com.android.systemui.statusbar.policy.KeyguardStateController;
import com.android.systemui.util.kotlin.JavaAdapter;
@@ -215,7 +216,11 @@ public class StatusBarRemoteInputCallback implements Callback, Callbacks,
if (ExpandHeadsUpOnInlineReply.isEnabled()) {
if (row.isChildInGroup() && !row.areChildrenExpanded()) {
// The group isn't expanded, let's make sure it's visible!
- mGroupExpansionManager.toggleGroupExpansion(row.getEntry());
+ if (NotificationBundleUi.isEnabled()) {
+ mGroupExpansionManager.toggleGroupExpansion(row.getEntryAdapter());
+ } else {
+ mGroupExpansionManager.toggleGroupExpansion(row.getEntry());
+ }
} else if (!row.isChildInGroup()) {
final boolean expandNotification;
if (row.isPinned()) {
@@ -233,7 +238,11 @@ public class StatusBarRemoteInputCallback implements Callback, Callbacks,
} else {
if (row.isChildInGroup() && !row.areChildrenExpanded()) {
// The group isn't expanded, let's make sure it's visible!
- mGroupExpansionManager.toggleGroupExpansion(row.getEntry());
+ if (NotificationBundleUi.isEnabled()) {
+ mGroupExpansionManager.toggleGroupExpansion(row.getEntryAdapter());
+ } else {
+ mGroupExpansionManager.toggleGroupExpansion(row.getEntry());
+ }
}
if (android.app.Flags.compactHeadsUpNotificationReply()
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt
index 788f041b38c0..0eef2e1ca685 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt
@@ -45,6 +45,7 @@ import com.android.systemui.statusbar.pipeline.shared.ui.binder.ModernStatusBarV
import com.android.systemui.statusbar.pipeline.shared.ui.binder.ModernStatusBarViewVisibilityHelper
import com.android.systemui.statusbar.pipeline.shared.ui.binder.StatusBarViewBinderConstants.ALPHA_ACTIVE
import com.android.systemui.statusbar.pipeline.shared.ui.binder.StatusBarViewBinderConstants.ALPHA_INACTIVE
+import com.android.systemui.util.kotlin.pairwiseBy
import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
@@ -131,19 +132,37 @@ object MobileIconBinder {
// Set the icon for the triangle
launch {
- viewModel.icon.distinctUntilChanged().collect { icon ->
- viewModel.verboseLogger?.logBinderReceivedSignalIcon(
- view,
- viewModel.subscriptionId,
- icon,
- )
- if (icon is SignalIconModel.Cellular) {
- iconView.setImageDrawable(mobileDrawable)
- mobileDrawable.level = icon.toSignalDrawableState()
- } else if (icon is SignalIconModel.Satellite) {
- IconViewBinder.bind(icon.icon, iconView)
+ viewModel.icon
+ .pairwiseBy(initialValue = null) { oldIcon, newIcon ->
+ // Make sure we requestLayout if the number of levels changes
+ val shouldRequestLayout =
+ when {
+ oldIcon == null -> true
+ oldIcon is SignalIconModel.Cellular &&
+ newIcon is SignalIconModel.Cellular -> {
+ oldIcon.numberOfLevels != newIcon.numberOfLevels
+ }
+ else -> false
+ }
+ Pair(shouldRequestLayout, newIcon)
+ }
+ .collect { (shouldRequestLayout, newIcon) ->
+ viewModel.verboseLogger?.logBinderReceivedSignalIcon(
+ view,
+ viewModel.subscriptionId,
+ newIcon,
+ )
+ if (newIcon is SignalIconModel.Cellular) {
+ iconView.setImageDrawable(mobileDrawable)
+ mobileDrawable.level = newIcon.toSignalDrawableState()
+ } else if (newIcon is SignalIconModel.Satellite) {
+ IconViewBinder.bind(newIcon.icon, iconView)
+ }
+
+ if (shouldRequestLayout) {
+ iconView.requestLayout()
+ }
}
- }
}
launch {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernStatusBarMobileView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernStatusBarMobileView.kt
index fd5ab135a1ad..4458b224913b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernStatusBarMobileView.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernStatusBarMobileView.kt
@@ -20,18 +20,16 @@ import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import android.widget.ImageView
-import com.android.settingslib.flags.Flags.newStatusBarIcons
import com.android.systemui.res.R
import com.android.systemui.statusbar.StatusBarIconView.getVisibleStateString
+import com.android.systemui.statusbar.core.NewStatusBarIcons
import com.android.systemui.statusbar.pipeline.mobile.ui.MobileViewLogger
import com.android.systemui.statusbar.pipeline.mobile.ui.binder.MobileIconBinder
import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.LocationBasedMobileViewModel
import com.android.systemui.statusbar.pipeline.shared.ui.view.ModernStatusBarView
-class ModernStatusBarMobileView(
- context: Context,
- attrs: AttributeSet?,
-) : ModernStatusBarView(context, attrs) {
+class ModernStatusBarMobileView(context: Context, attrs: AttributeSet?) :
+ ModernStatusBarView(context, attrs) {
var subId: Int = -1
@@ -62,9 +60,7 @@ class ModernStatusBarMobileView(
as ModernStatusBarMobileView)
.also {
// Flag-specific configuration
- if (newStatusBarIcons()) {
- // New icon (with no embedded whitespace) is slightly shorter
- // (but actually taller)
+ if (NewStatusBarIcons.isEnabled) {
val iconView = it.requireViewById<ImageView>(R.id.mobile_signal)
val lp = iconView.layoutParams
lp.height =
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeControllerImpl.java
index 9ad8619faacc..1d1826d532b7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeControllerImpl.java
@@ -17,7 +17,6 @@
package com.android.systemui.statusbar.policy;
import android.app.AlarmManager;
-import android.app.Flags;
import android.app.NotificationManager;
import android.content.BroadcastReceiver;
import android.content.ContentResolver;
@@ -175,11 +174,7 @@ public class ZenModeControllerImpl implements ZenModeController, Dumpable {
@Override
public void setZen(int zen, Uri conditionId, String reason) {
- if (Flags.modesApi()) {
- mNoMan.setZenMode(zen, conditionId, reason, /* fromUser= */ true);
- } else {
- mNoMan.setZenMode(zen, conditionId, reason);
- }
+ mNoMan.setZenMode(zen, conditionId, reason, /* fromUser= */ true);
}
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java
index 28cf78f6777e..9f60fe212567 100644
--- a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java
+++ b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java
@@ -18,8 +18,10 @@ package com.android.systemui.theme;
import static android.util.TypedValue.TYPE_INT_COLOR_ARGB8;
+import static com.android.systemui.Flags.hardwareColorStyles;
import static com.android.systemui.Flags.themeOverlayControllerWakefulnessDeprecation;
import static com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_ASLEEP;
+import static com.android.systemui.monet.ColorScheme.GOOGLE_BLUE;
import static com.android.systemui.theme.ThemeOverlayApplier.COLOR_SOURCE_HOME;
import static com.android.systemui.theme.ThemeOverlayApplier.COLOR_SOURCE_LOCK;
import static com.android.systemui.theme.ThemeOverlayApplier.COLOR_SOURCE_PRESET;
@@ -73,6 +75,7 @@ import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.dump.DumpManager;
import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.flags.Flags;
+import com.android.systemui.flags.SystemPropertiesHelper;
import com.android.systemui.keyguard.WakefulnessLifecycle;
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
import com.android.systemui.keyguard.shared.model.KeyguardState;
@@ -99,6 +102,7 @@ import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
+import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
@@ -136,9 +140,11 @@ public class ThemeOverlayController implements CoreStartable, Dumpable {
private final DeviceProvisionedController mDeviceProvisionedController;
private final Resources mResources;
// Current wallpaper colors associated to a user.
- private final SparseArray<WallpaperColors> mCurrentColors = new SparseArray<>();
+ @VisibleForTesting
+ protected final SparseArray<WallpaperColors> mCurrentColors = new SparseArray<>();
private final WallpaperManager mWallpaperManager;
private final ActivityManager mActivityManager;
+ protected final SystemPropertiesHelper mSystemPropertiesHelper;
@VisibleForTesting
protected ColorScheme mColorScheme;
// If fabricated overlays were already created for the current theme.
@@ -423,7 +429,9 @@ public class ThemeOverlayController implements CoreStartable, Dumpable {
JavaAdapter javaAdapter,
KeyguardTransitionInteractor keyguardTransitionInteractor,
UiModeManager uiModeManager,
- ActivityManager activityManager) {
+ ActivityManager activityManager,
+ SystemPropertiesHelper systemPropertiesHelper
+ ) {
mContext = context;
mIsMonetEnabled = featureFlags.isEnabled(Flags.MONET);
mIsFidelityEnabled = featureFlags.isEnabled(Flags.COLOR_FIDELITY);
@@ -443,6 +451,7 @@ public class ThemeOverlayController implements CoreStartable, Dumpable {
mKeyguardTransitionInteractor = keyguardTransitionInteractor;
mUiModeManager = uiModeManager;
mActivityManager = activityManager;
+ mSystemPropertiesHelper = systemPropertiesHelper;
dumpManager.registerDumpable(TAG, this);
Flow<Boolean> isFinishedInAsleepStateFlow = mKeyguardTransitionInteractor
@@ -498,29 +507,38 @@ public class ThemeOverlayController implements CoreStartable, Dumpable {
mUserTracker.addCallback(mUserTrackerCallback, mMainExecutor);
mDeviceProvisionedController.addCallback(mDeviceProvisionedListener);
+ WallpaperColors systemColor;
+ if (hardwareColorStyles() && !mDeviceProvisionedController.isCurrentUserSetup()) {
+ Pair<Integer, Color> defaultSettings = getThemeSettingsDefaults();
+ mThemeStyle = defaultSettings.first;
+ Color seedColor = defaultSettings.second;
+
+ // we only use the first color anyway, so we can pass only the single color we have
+ systemColor = new WallpaperColors(
+ /*primaryColor*/ seedColor,
+ /*secondaryColor*/ seedColor,
+ /*tertiaryColor*/ seedColor
+ );
+ } else {
+ systemColor = mWallpaperManager.getWallpaperColors(
+ getDefaultWallpaperColorsSource(mUserTracker.getUserId()));
+ }
+
// Upon boot, make sure we have the most up to date colors
Runnable updateColors = () -> {
- WallpaperColors systemColor = mWallpaperManager.getWallpaperColors(
- getDefaultWallpaperColorsSource(mUserTracker.getUserId()));
- Runnable applyColors = () -> {
- if (DEBUG) Log.d(TAG, "Boot colors: " + systemColor);
- mCurrentColors.put(mUserTracker.getUserId(), systemColor);
- reevaluateSystemTheme(false /* forceReload */);
- };
- if (mDeviceProvisionedController.isCurrentUserSetup()) {
- mMainExecutor.execute(applyColors);
- } else {
- applyColors.run();
- }
+ if (DEBUG) Log.d(TAG, "Boot colors: " + systemColor);
+ mCurrentColors.put(mUserTracker.getUserId(), systemColor);
+ reevaluateSystemTheme(false /* forceReload */);
};
// Whenever we're going directly to setup wizard, we need to process colors synchronously,
// otherwise we'll see some jank when the activity is recreated.
if (!mDeviceProvisionedController.isCurrentUserSetup()) {
- updateColors.run();
+ mMainExecutor.execute(updateColors);
} else {
mBgExecutor.execute(updateColors);
}
+
mWallpaperManager.addOnColorsChangedListener(mOnColorsChangedListener, null,
UserHandle.USER_ALL);
@@ -604,7 +622,7 @@ public class ThemeOverlayController implements CoreStartable, Dumpable {
@VisibleForTesting
protected boolean isPrivateProfile(UserHandle userHandle) {
- Context usercontext = mContext.createContextAsUser(userHandle,0);
+ Context usercontext = mContext.createContextAsUser(userHandle, 0);
return usercontext.getSystemService(UserManager.class).isPrivateProfile();
}
@@ -720,6 +738,7 @@ public class ThemeOverlayController implements CoreStartable, Dumpable {
return true;
}
+ @SuppressWarnings("StringCaseLocaleUsage") // Package name is not localized
private void updateThemeOverlays() {
final int currentUser = mUserTracker.getUserId();
final String overlayPackageJson = mSecureSettings.getStringForUser(
@@ -746,7 +765,7 @@ public class ThemeOverlayController implements CoreStartable, Dumpable {
OverlayIdentifier systemPalette = categoryToPackage.get(OVERLAY_CATEGORY_SYSTEM_PALETTE);
if (mIsMonetEnabled && systemPalette != null && systemPalette.getPackageName() != null) {
try {
- String colorString = systemPalette.getPackageName().toLowerCase();
+ String colorString = systemPalette.getPackageName().toLowerCase();
if (!colorString.startsWith("#")) {
colorString = "#" + colorString;
}
@@ -856,6 +875,75 @@ public class ThemeOverlayController implements CoreStartable, Dumpable {
return style;
}
+ protected Pair<Integer, String> getHardwareColorSetting() {
+ String deviceColorProperty = "ro.boot.hardware.color";
+
+ String[] themeData = mResources.getStringArray(
+ com.android.internal.R.array.theming_defaults);
+
+ // Color can be hex (`#FF0000`) or `home_wallpaper`
+ Map<String, Pair<Integer, String>> themeMap = new HashMap<>();
+
+ // extract all theme settings
+ for (String themeEntry : themeData) {
+ String[] themeComponents = themeEntry.split("\\|");
+ if (themeComponents.length != 3) continue;
+ themeMap.put(themeComponents[0],
+ new Pair<>(Style.valueOf(themeComponents[1]), themeComponents[2]));
+ }
+
+ Pair<Integer, String> fallbackTheme = themeMap.get("*");
+ if (fallbackTheme == null) {
+ Log.d(TAG, "Theming wildcard not found. Fallback to TONAL_SPOT|" + COLOR_SOURCE_HOME);
+ fallbackTheme = new Pair<>(Style.TONAL_SPOT, COLOR_SOURCE_HOME);
+ }
+
+ String deviceColorPropertyValue = mSystemPropertiesHelper.get(deviceColorProperty);
+ Pair<Integer, String> selectedTheme = themeMap.get(deviceColorPropertyValue);
+ if (selectedTheme == null) {
+ Log.d(TAG, "Sysprop `" + deviceColorProperty + "` of value '" + deviceColorPropertyValue
+ + "' not found in theming_defaults: " + Arrays.toString(themeData));
+ selectedTheme = fallbackTheme;
+ }
+
+ return selectedTheme;
+ }
+
+ @VisibleForTesting
+ protected Pair<Integer, Color> getThemeSettingsDefaults() {
+
+ Pair<Integer, String> selectedTheme = getHardwareColorSetting();
+
+ // Last fallback color
+ Color defaultSeedColor = Color.valueOf(GOOGLE_BLUE);
+
+ // defaultColor will come from wallpaper or be parsed from a string
+ boolean isWallpaper = selectedTheme.second.equals(COLOR_SOURCE_HOME);
+
+ if (isWallpaper) {
+ WallpaperColors wallpaperColors = mWallpaperManager.getWallpaperColors(
+ getDefaultWallpaperColorsSource(mUserTracker.getUserId()));
+
+ if (wallpaperColors != null) {
+ defaultSeedColor = wallpaperColors.getPrimaryColor();
+ }
+
+ Log.d(TAG, "Default seed color read from home wallpaper: " + Integer.toHexString(
+ defaultSeedColor.toArgb()));
+ } else {
+ try {
+ defaultSeedColor = Color.valueOf(Color.parseColor(selectedTheme.second));
+ Log.d(TAG, "Default seed color read from resource: " + Integer.toHexString(
+ defaultSeedColor.toArgb()));
+ } catch (IllegalArgumentException e) {
+ Log.e(TAG, "Error parsing color: " + selectedTheme.second, e);
+ // defaultSeedColor remains unchanged in this case
+ }
+ }
+
+ return new Pair<>(selectedTheme.first, defaultSeedColor);
+ }
+
@Override
public void dump(@NonNull PrintWriter pw, @NonNull String[] args) {
pw.println("mSystemColors=" + mCurrentColors);
diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/SysUICoroutinesModule.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/SysUICoroutinesModule.kt
index e5c1e7daa25a..79ff38eabc08 100644
--- a/packages/SystemUI/src/com/android/systemui/util/kotlin/SysUICoroutinesModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/SysUICoroutinesModule.kt
@@ -21,6 +21,7 @@ import com.android.systemui.coroutines.newTracingContext
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.dagger.qualifiers.NotifInflation
import com.android.systemui.dagger.qualifiers.UiBackground
import com.android.systemui.util.settings.SettingsSingleThreadBackground
import dagger.Module
@@ -123,4 +124,19 @@ class SysUICoroutinesModule {
): CoroutineContext {
return uiBgCoroutineDispatcher
}
+
+ /** Coroutine dispatcher for background notification inflation. */
+ @Provides
+ @NotifInflation
+ @SysUISingleton
+ fun notifInflationCoroutineDispatcher(
+ @NotifInflation notifInflationExecutor: Executor,
+ @Background bgCoroutineDispatcher: CoroutineDispatcher,
+ ): CoroutineDispatcher {
+ if (com.android.systemui.Flags.useNotifInflationThreadForFooter()) {
+ return notifInflationExecutor.asCoroutineDispatcher()
+ } else {
+ return bgCoroutineDispatcher
+ }
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/util/settings/repository/SecureSettingsForUserRepository.kt b/packages/SystemUI/src/com/android/systemui/util/settings/repository/SecureSettingsForUserRepository.kt
new file mode 100644
index 000000000000..4d6eb4d8f391
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/settings/repository/SecureSettingsForUserRepository.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.util.settings.repository
+
+import android.provider.Settings
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.util.settings.SecureSettings
+import javax.inject.Inject
+import kotlin.coroutines.CoroutineContext
+import kotlinx.coroutines.CoroutineDispatcher
+
+/** Repository observing values of a [Settings.Secure] for the specified user. */
+@SysUISingleton
+class SecureSettingsForUserRepository
+@Inject
+constructor(
+ secureSettings: SecureSettings,
+ @Background backgroundDispatcher: CoroutineDispatcher,
+ @Background backgroundContext: CoroutineContext,
+) : SettingsForUserRepository(secureSettings, backgroundDispatcher, backgroundContext)
diff --git a/packages/SystemUI/src/com/android/systemui/util/settings/repository/SettingsForUserRepository.kt b/packages/SystemUI/src/com/android/systemui/util/settings/repository/SettingsForUserRepository.kt
new file mode 100644
index 000000000000..94b3fd244a92
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/settings/repository/SettingsForUserRepository.kt
@@ -0,0 +1,66 @@
+/*
+ * 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.util.settings.repository
+
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.util.settings.SettingsProxyExt.observerFlow
+import com.android.systemui.util.settings.UserSettingsProxy
+import kotlin.coroutines.CoroutineContext
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onStart
+import kotlinx.coroutines.withContext
+
+/**
+ * Repository observing values of a [UserSettingsProxy] for the specified user. This repository
+ * should be used for any system that tracks the desired user internally (e.g. the Quick Settings
+ * tiles system). In other cases, use a [UserAwareSettingsRepository] instead.
+ */
+abstract class SettingsForUserRepository(
+ private val userSettings: UserSettingsProxy,
+ @Background private val backgroundDispatcher: CoroutineDispatcher,
+ @Background private val backgroundContext: CoroutineContext,
+) {
+ fun boolSettingForUser(
+ userId: Int,
+ name: String,
+ defaultValue: Boolean = false,
+ ): Flow<Boolean> =
+ settingObserver(name, userId) { userSettings.getBoolForUser(name, defaultValue, userId) }
+ .distinctUntilChanged()
+ .flowOn(backgroundDispatcher)
+
+ fun <T> settingObserver(name: String, userId: Int, settingsReader: () -> T): Flow<T> {
+ return userSettings
+ .observerFlow(userId, name)
+ .onStart { emit(Unit) }
+ .map { settingsReader.invoke() }
+ }
+
+ suspend fun setBoolForUser(userId: Int, name: String, value: Boolean) {
+ withContext(backgroundContext) { userSettings.putBoolForUser(name, value, userId) }
+ }
+
+ suspend fun getBoolForUser(userId: Int, name: String, defaultValue: Boolean = false): Boolean {
+ return withContext(backgroundContext) {
+ userSettings.getBoolForUser(name, defaultValue, userId)
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/util/settings/repository/UserAwareSettingsRepository.kt b/packages/SystemUI/src/com/android/systemui/util/settings/repository/UserAwareSettingsRepository.kt
index 73329b467c04..a8068cda685b 100644
--- a/packages/SystemUI/src/com/android/systemui/util/settings/repository/UserAwareSettingsRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/util/settings/repository/UserAwareSettingsRepository.kt
@@ -33,7 +33,8 @@ import kotlinx.coroutines.withContext
/**
* Repository for observing values of a [UserSettingsProxy], for the currently active user. That
* means that when the user is switched and the new user has a different value, the flow will emit
- * the new value.
+ * the new value. For any system that tracks the desired user internally (e.g. the Quick Settings
+ * tiles system), use a [SettingsForUserRepository] instead.
*/
// TODO: b/377244768 - Make internal when UserAwareSecureSettingsRepository can be made internal.
abstract class UserAwareSettingsRepository(
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioSharingStreamSliderViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioSharingStreamSliderViewModel.kt
index e2d2f3f68c6b..3efb2b464a1d 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioSharingStreamSliderViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioSharingStreamSliderViewModel.kt
@@ -16,7 +16,6 @@
package com.android.systemui.volume.panel.component.volume.slider.ui.viewmodel
-import android.content.Context
import com.android.internal.logging.UiEventLogger
import com.android.systemui.Flags
import com.android.systemui.common.shared.model.Icon
@@ -24,6 +23,7 @@ import com.android.systemui.haptics.slider.SliderHapticFeedbackFilter
import com.android.systemui.haptics.slider.compose.ui.SliderHapticsViewModel
import com.android.systemui.res.R
import com.android.systemui.volume.domain.interactor.AudioSharingInteractor
+import com.android.systemui.volume.panel.shared.VolumePanelLogger
import com.android.systemui.volume.panel.ui.VolumePanelUiEvent
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
@@ -34,6 +34,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
@@ -43,21 +44,25 @@ class AudioSharingStreamSliderViewModel
@AssistedInject
constructor(
@Assisted private val coroutineScope: CoroutineScope,
- private val context: Context,
private val audioSharingInteractor: AudioSharingInteractor,
private val uiEventLogger: UiEventLogger,
private val hapticsViewModelFactory: SliderHapticsViewModel.Factory,
+ private val volumePanelLogger: VolumePanelLogger,
) : SliderViewModel {
private val volumeChanges = MutableStateFlow<Int?>(null)
override val slider: StateFlow<SliderState> =
- combine(audioSharingInteractor.volume, audioSharingInteractor.secondaryDevice) {
- volume,
- device ->
+ combine(
+ audioSharingInteractor.volume.distinctUntilChanged().onEach {
+ it?.let(volumePanelLogger::onAudioSharingVolumeUpdateReceived)
+ },
+ audioSharingInteractor.secondaryDevice,
+ ) { volume, device ->
val deviceName = device?.name ?: return@combine SliderState.Empty
if (volume == null) {
SliderState.Empty
} else {
+
State(
value = volume.toFloat(),
valueRange =
@@ -74,13 +79,15 @@ constructor(
init {
volumeChanges
.filterNotNull()
- .onEach { audioSharingInteractor.setStreamVolume(it) }
+ .onEach {
+ volumePanelLogger.onSetAudioSharingVolumeRequested(it)
+ audioSharingInteractor.setStreamVolume(it)
+ }
.launchIn(coroutineScope)
}
override fun onValueChanged(state: SliderState, newValue: Float) {
- val audioViewModel = state as? State
- audioViewModel ?: return
+ if (state !is State) return
volumeChanges.tryEmit(newValue.roundToInt())
}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModel.kt
index 533276413ade..d74a433ad86c 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModel.kt
@@ -26,6 +26,7 @@ import com.android.systemui.haptics.slider.compose.ui.SliderHapticsViewModel
import com.android.systemui.res.R
import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.MediaDeviceSessionInteractor
import com.android.systemui.volume.panel.component.mediaoutput.shared.model.MediaDeviceSession
+import com.android.systemui.volume.panel.shared.VolumePanelLogger
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
@@ -44,17 +45,23 @@ constructor(
private val context: Context,
private val mediaDeviceSessionInteractor: MediaDeviceSessionInteractor,
private val hapticsViewModelFactory: SliderHapticsViewModel.Factory,
+ private val volumePanelLogger: VolumePanelLogger,
) : SliderViewModel {
override val slider: StateFlow<SliderState> =
mediaDeviceSessionInteractor
.playbackInfo(session)
- .mapNotNull { it?.getCurrentState() }
+ .mapNotNull {
+ volumePanelLogger.onVolumeUpdateReceived(session.sessionToken, it.currentVolume)
+ it.getCurrentState()
+ }
.stateIn(coroutineScope, SharingStarted.Eagerly, SliderState.Empty)
override fun onValueChanged(state: SliderState, newValue: Float) {
coroutineScope.launch {
- mediaDeviceSessionInteractor.setSessionVolume(session, newValue.roundToInt())
+ val volume = newValue.roundToInt()
+ volumePanelLogger.onSetVolumeRequested(session.sessionToken, volume)
+ mediaDeviceSessionInteractor.setSessionVolume(session, volume)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/shared/VolumePanelLogger.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/shared/VolumePanelLogger.kt
index 276326cbf430..930199a03a56 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/shared/VolumePanelLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/shared/VolumePanelLogger.kt
@@ -16,6 +16,8 @@
package com.android.systemui.volume.panel.shared
+import android.media.session.MediaSession
+import android.media.session.MediaSession.Token
import com.android.settingslib.volume.shared.model.AudioStream
import com.android.systemui.log.LogBuffer
import com.android.systemui.log.core.LogLevel
@@ -42,7 +44,7 @@ class VolumePanelLogger @Inject constructor(@VolumeLog private val logBuffer: Lo
str1 = key
bool1 = isAvailable
},
- { "$str1 isAvailable=$bool1" }
+ { "$str1 isAvailable=$bool1" },
)
}
@@ -51,7 +53,7 @@ class VolumePanelLogger @Inject constructor(@VolumeLog private val logBuffer: Lo
TAG,
LogLevel.DEBUG,
{ bool1 = globalState.isVisible },
- { "Global state changed: isVisible=$bool1" }
+ { "Global state changed: isVisible=$bool1" },
)
}
@@ -63,7 +65,7 @@ class VolumePanelLogger @Inject constructor(@VolumeLog private val logBuffer: Lo
str1 = audioStream.toString()
int1 = volume
},
- { "Set volume: stream=$str1 volume=$int1" }
+ { "Set volume: stream=$str1 volume=$int1" },
)
}
@@ -75,7 +77,49 @@ class VolumePanelLogger @Inject constructor(@VolumeLog private val logBuffer: Lo
str1 = audioStream.toString()
int1 = volume
},
- { "Volume update received: stream=$str1 volume=$int1" }
+ { "Volume update received: stream=$str1 volume=$int1" },
+ )
+ }
+
+ fun onSetVolumeRequested(sessionToken: MediaSession.Token, volume: Int) {
+ logBuffer.log(
+ TAG,
+ LogLevel.DEBUG,
+ {
+ str1 = sessionToken.toString()
+ int1 = volume
+ },
+ { "Set volume: token=$str1 volume=$int1" },
+ )
+ }
+
+ fun onVolumeUpdateReceived(sessionToken: Token, volume: Int) {
+ logBuffer.log(
+ TAG,
+ LogLevel.DEBUG,
+ {
+ str1 = sessionToken.toString()
+ int1 = volume
+ },
+ { "Volume update received: token=$str1 volume=$int1" },
+ )
+ }
+
+ fun onSetAudioSharingVolumeRequested(volume: Int) {
+ logBuffer.log(
+ TAG,
+ LogLevel.DEBUG,
+ { int1 = volume },
+ { "Set volume: audio-sharing volume=$int1" },
+ )
+ }
+
+ fun onAudioSharingVolumeUpdateReceived(volume: Int) {
+ logBuffer.log(
+ TAG,
+ LogLevel.DEBUG,
+ { int1 = volume },
+ { "Volume update received: audio-sharing volume=$int1" },
)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/wallpapers/data/repository/NoopWallpaperRepository.kt b/packages/SystemUI/src/com/android/systemui/wallpapers/data/repository/NoopWallpaperRepository.kt
index ec74f4f47bc9..300a7e070b6c 100644
--- a/packages/SystemUI/src/com/android/systemui/wallpapers/data/repository/NoopWallpaperRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/wallpapers/data/repository/NoopWallpaperRepository.kt
@@ -17,6 +17,8 @@
package com.android.systemui.wallpapers.data.repository
import android.app.WallpaperInfo
+import android.graphics.PointF
+import android.graphics.RectF
import android.view.View
import com.android.systemui.dagger.SysUISingleton
import javax.inject.Inject
@@ -37,4 +39,8 @@ class NoopWallpaperRepository @Inject constructor() : WallpaperRepository {
override val wallpaperSupportsAmbientMode = flowOf(false)
override var rootView: View? = null
override val shouldSendFocalArea: StateFlow<Boolean> = MutableStateFlow(false).asStateFlow()
+
+ override fun sendLockScreenLayoutChangeCommand(wallpaperFocalAreaBounds: RectF) {}
+
+ override fun sendTapCommand(tapPosition: PointF) {}
}
diff --git a/packages/SystemUI/src/com/android/systemui/wallpapers/data/repository/WallpaperFocalAreaRepository.kt b/packages/SystemUI/src/com/android/systemui/wallpapers/data/repository/WallpaperFocalAreaRepository.kt
index 2c3491b06a90..974468c16578 100644
--- a/packages/SystemUI/src/com/android/systemui/wallpapers/data/repository/WallpaperFocalAreaRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/wallpapers/data/repository/WallpaperFocalAreaRepository.kt
@@ -33,7 +33,8 @@ interface WallpaperFocalAreaRepository {
val wallpaperFocalAreaBounds: StateFlow<RectF>
- val wallpaperFocalAreaTapPosition: StateFlow<PointF>
+ /** It will be true when wallpaper requires focal area info. */
+ val hasFocalArea: StateFlow<Boolean>
/** top of notifications without bcsmartspace in small clock settings */
val notificationDefaultTop: StateFlow<Float>
@@ -51,7 +52,9 @@ interface WallpaperFocalAreaRepository {
}
@SysUISingleton
-class WallpaperFocalAreaRepositoryImpl @Inject constructor() : WallpaperFocalAreaRepository {
+class WallpaperFocalAreaRepositoryImpl
+@Inject
+constructor(val wallpaperRepository: WallpaperRepository) : WallpaperFocalAreaRepository {
private val _shortcutAbsoluteTop = MutableStateFlow(0F)
override val shortcutAbsoluteTop = _shortcutAbsoluteTop.asStateFlow()
@@ -63,13 +66,11 @@ class WallpaperFocalAreaRepositoryImpl @Inject constructor() : WallpaperFocalAre
override val wallpaperFocalAreaBounds: StateFlow<RectF> =
_wallpaperFocalAreaBounds.asStateFlow()
- private val _wallpaperFocalAreaTapPosition = MutableStateFlow(PointF(0F, 0F))
- override val wallpaperFocalAreaTapPosition: StateFlow<PointF> =
- _wallpaperFocalAreaTapPosition.asStateFlow()
-
private val _notificationDefaultTop = MutableStateFlow(0F)
override val notificationDefaultTop: StateFlow<Float> = _notificationDefaultTop.asStateFlow()
+ override val hasFocalArea = wallpaperRepository.shouldSendFocalArea
+
override fun setShortcutAbsoluteTop(top: Float) {
_shortcutAbsoluteTop.value = top
}
@@ -84,9 +85,10 @@ class WallpaperFocalAreaRepositoryImpl @Inject constructor() : WallpaperFocalAre
override fun setWallpaperFocalAreaBounds(bounds: RectF) {
_wallpaperFocalAreaBounds.value = bounds
+ wallpaperRepository.sendLockScreenLayoutChangeCommand(bounds)
}
- override fun setTapPosition(point: PointF) {
- _wallpaperFocalAreaTapPosition.value = point
+ override fun setTapPosition(tapPosition: PointF) {
+ wallpaperRepository.sendTapCommand(tapPosition)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/wallpapers/data/repository/WallpaperRepository.kt b/packages/SystemUI/src/com/android/systemui/wallpapers/data/repository/WallpaperRepository.kt
index a55f76b333d9..b07342c4c76d 100644
--- a/packages/SystemUI/src/com/android/systemui/wallpapers/data/repository/WallpaperRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/wallpapers/data/repository/WallpaperRepository.kt
@@ -21,22 +21,18 @@ import android.app.WallpaperManager
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
+import android.graphics.PointF
+import android.graphics.RectF
import android.os.Bundle
import android.os.UserHandle
import android.provider.Settings
+import android.util.Log
import android.view.View
-import androidx.annotation.VisibleForTesting
-import com.android.app.tracing.coroutines.launchTraced as launch
import com.android.internal.R
import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
-import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
-import com.android.systemui.keyguard.shared.model.Edge
-import com.android.systemui.keyguard.shared.model.KeyguardState
-import com.android.systemui.keyguard.shared.model.TransitionState
import com.android.systemui.res.R as SysUIR
-import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.shared.Flags.ambientAod
import com.android.systemui.shared.Flags.extendedWallpaperEffects
import com.android.systemui.user.data.model.SelectedUserModel
@@ -48,7 +44,6 @@ import com.android.systemui.utils.coroutines.flow.mapLatestConflated
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
@@ -76,6 +71,10 @@ interface WallpaperRepository {
/** some wallpapers require bounds to be sent from keyguard */
val shouldSendFocalArea: StateFlow<Boolean>
+
+ fun sendLockScreenLayoutChangeCommand(wallpaperFocalAreaBounds: RectF)
+
+ fun sendTapCommand(tapPosition: PointF)
}
@SysUISingleton
@@ -86,10 +85,8 @@ constructor(
@Background private val bgDispatcher: CoroutineDispatcher,
broadcastDispatcher: BroadcastDispatcher,
userRepository: UserRepository,
- wallpaperFocalAreaRepository: WallpaperFocalAreaRepository,
private val wallpaperManager: WallpaperManager,
private val context: Context,
- keyguardTransitionInteractor: KeyguardTransitionInteractor,
private val secureSettings: SecureSettings,
) : WallpaperRepository {
private val wallpaperChanged: Flow<Unit> =
@@ -109,9 +106,6 @@ constructor(
// Only update the wallpaper status once the user selection has finished.
.filter { it.selectionStatus == SelectionStatus.SELECTION_COMPLETE }
- @VisibleForTesting var sendLockscreenLayoutJob: Job? = null
- @VisibleForTesting var sendTapInShapeEffectsJob: Job? = null
-
override val wallpaperInfo: StateFlow<WallpaperInfo?> =
if (!wallpaperManager.isWallpaperSupported) {
MutableStateFlow(null).asStateFlow()
@@ -143,77 +137,45 @@ constructor(
override var rootView: View? = null
+ override fun sendLockScreenLayoutChangeCommand(wallpaperFocalAreaBounds: RectF) {
+ if (DEBUG) {
+ Log.d(TAG, "sendLockScreenLayoutChangeCommand $wallpaperFocalAreaBounds")
+ }
+ wallpaperManager.sendWallpaperCommand(
+ /* windowToken = */ rootView?.windowToken,
+ /* action = */ WallpaperManager.COMMAND_LOCKSCREEN_LAYOUT_CHANGED,
+ /* x = */ 0,
+ /* y = */ 0,
+ /* z = */ 0,
+ /* extras = */ Bundle().apply {
+ putFloat("wallpaperFocalAreaLeft", wallpaperFocalAreaBounds.left)
+ putFloat("wallpaperFocalAreaRight", wallpaperFocalAreaBounds.right)
+ putFloat("wallpaperFocalAreaTop", wallpaperFocalAreaBounds.top)
+ putFloat("wallpaperFocalAreaBottom", wallpaperFocalAreaBounds.bottom)
+ },
+ )
+ }
+
+ override fun sendTapCommand(tapPosition: PointF) {
+ if (DEBUG) {
+ Log.d(TAG, "sendTapCommand $tapPosition")
+ }
+
+ wallpaperManager.sendWallpaperCommand(
+ /* windowToken = */ rootView?.windowToken,
+ /* action = */ WallpaperManager.COMMAND_LOCKSCREEN_TAP,
+ /* x = */ tapPosition.x.toInt(),
+ /* y = */ tapPosition.y.toInt(),
+ /* z = */ 0,
+ /* extras = */ Bundle(),
+ )
+ }
+
override val shouldSendFocalArea =
wallpaperInfo
.map {
val focalAreaTarget = context.resources.getString(SysUIR.string.focal_area_target)
val shouldSendNotificationLayout = it?.component?.className == focalAreaTarget
- if (shouldSendNotificationLayout) {
- sendLockscreenLayoutJob =
- scope.launch {
- combine(
- wallpaperFocalAreaRepository.wallpaperFocalAreaBounds,
- keyguardTransitionInteractor
- .transition(
- edge = Edge.create(to = Scenes.Lockscreen),
- edgeWithoutSceneContainer =
- Edge.create(to = KeyguardState.LOCKSCREEN),
- )
- .filter { transitionStep ->
- transitionStep.transitionState ==
- TransitionState.STARTED
- },
- ::Pair,
- )
- .map { (bounds, _) -> bounds }
- .collect { wallpaperFocalAreaBounds ->
- wallpaperManager.sendWallpaperCommand(
- /* windowToken = */ rootView?.windowToken,
- /* action = */ WallpaperManager
- .COMMAND_LOCKSCREEN_LAYOUT_CHANGED,
- /* x = */ 0,
- /* y = */ 0,
- /* z = */ 0,
- /* extras = */ Bundle().apply {
- putFloat(
- "wallpaperFocalAreaLeft",
- wallpaperFocalAreaBounds.left,
- )
- putFloat(
- "wallpaperFocalAreaRight",
- wallpaperFocalAreaBounds.right,
- )
- putFloat(
- "wallpaperFocalAreaTop",
- wallpaperFocalAreaBounds.top,
- )
- putFloat(
- "wallpaperFocalAreaBottom",
- wallpaperFocalAreaBounds.bottom,
- )
- },
- )
- }
- }
-
- sendTapInShapeEffectsJob =
- scope.launch {
- wallpaperFocalAreaRepository.wallpaperFocalAreaTapPosition.collect {
- wallpaperFocalAreaTapPosition ->
- wallpaperManager.sendWallpaperCommand(
- /* windowToken = */ rootView?.windowToken,
- /* action = */ WallpaperManager.COMMAND_LOCKSCREEN_TAP,
- /* x = */ wallpaperFocalAreaTapPosition.x.toInt(),
- /* y = */ wallpaperFocalAreaTapPosition.y.toInt(),
- /* z = */ 0,
- /* extras = */ null,
- )
- }
- }
- } else {
- sendLockscreenLayoutJob?.cancel()
- sendTapInShapeEffectsJob?.cancel()
- }
shouldSendNotificationLayout
}
.stateIn(
@@ -227,4 +189,9 @@ constructor(
wallpaperManager.getWallpaperInfoForUser(selectedUser.userInfo.id)
}
}
+
+ companion object {
+ private val TAG = WallpaperRepositoryImpl::class.simpleName
+ private val DEBUG = true
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/wallpapers/domain/interactor/WallpaperFocalAreaInteractor.kt b/packages/SystemUI/src/com/android/systemui/wallpapers/domain/interactor/WallpaperFocalAreaInteractor.kt
index 187d6c7801c0..09c6cdf0ce22 100644
--- a/packages/SystemUI/src/com/android/systemui/wallpapers/domain/interactor/WallpaperFocalAreaInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/wallpapers/domain/interactor/WallpaperFocalAreaInteractor.kt
@@ -20,60 +20,38 @@ import android.content.Context
import android.content.res.Resources
import android.graphics.PointF
import android.graphics.RectF
+import android.util.Log
import android.util.TypedValue
-import androidx.annotation.VisibleForTesting
import com.android.app.animation.MathUtils
import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.res.R
import com.android.systemui.shade.data.repository.ShadeRepository
-import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor
import com.android.systemui.wallpapers.data.repository.WallpaperFocalAreaRepository
-import com.android.systemui.wallpapers.data.repository.WallpaperRepository
import javax.inject.Inject
import kotlin.math.min
-import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.filter
@SysUISingleton
class WallpaperFocalAreaInteractor
@Inject
constructor(
- @Application private val applicationScope: CoroutineScope,
private val context: Context,
private val wallpaperFocalAreaRepository: WallpaperFocalAreaRepository,
shadeRepository: ShadeRepository,
- activeNotificationsInteractor: ActiveNotificationsInteractor,
- val wallpaperRepository: WallpaperRepository,
) {
- // When there's notifications in splitshade, the focal area should be left aligned
- @VisibleForTesting
- val notificationInShadeWideLayout: Flow<Boolean> =
- combine(
- shadeRepository.isShadeLayoutWide,
- activeNotificationsInteractor.areAnyNotificationsPresent,
- ) { isShadeLayoutWide, areAnyNotificationsPresent: Boolean ->
- when {
- !isShadeLayoutWide -> false
- !areAnyNotificationsPresent -> false
- else -> true
- }
- }
-
- val hasFocalArea = wallpaperRepository.shouldSendFocalArea
+ val hasFocalArea = wallpaperFocalAreaRepository.hasFocalArea
val wallpaperFocalAreaBounds: Flow<RectF> =
combine(
shadeRepository.isShadeLayoutWide,
- notificationInShadeWideLayout,
wallpaperFocalAreaRepository.notificationStackAbsoluteBottom,
wallpaperFocalAreaRepository.shortcutAbsoluteTop,
wallpaperFocalAreaRepository.notificationDefaultTop,
) {
isShadeLayoutWide,
- notificationInShadeWideLayout,
notificationStackAbsoluteBottom,
shortcutAbsoluteTop,
notificationDefaultTop ->
@@ -97,28 +75,21 @@ constructor(
screenBounds.centerY() + screenBounds.height() / 2F / wallpaperZoomedInScale,
)
+ val focalAreaMaxWidthDp = getFocalAreaMaxWidthDp(context)
val maxFocalAreaWidth =
TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
- FOCAL_AREA_MAX_WIDTH_DP.toFloat(),
+ focalAreaMaxWidthDp.toFloat(),
context.resources.displayMetrics,
)
val (left, right) =
- // tablet landscape
- if (context.resources.getBoolean(R.bool.center_align_focal_area_shape)) {
+ // Tablet & unfold foldable landscape
+ if (isShadeLayoutWide) {
Pair(
scaledBounds.centerX() - maxFocalAreaWidth / 2F,
scaledBounds.centerX() + maxFocalAreaWidth / 2F,
)
- // unfold foldable landscape
- } else if (isShadeLayoutWide) {
- if (notificationInShadeWideLayout) {
- Pair(scaledBounds.left, scaledBounds.centerX())
- } else {
- Pair(scaledBounds.centerX(), scaledBounds.right)
- }
- // handheld / portrait
} else {
val focalAreaWidth = min(scaledBounds.width(), maxFocalAreaWidth)
Pair(
@@ -147,8 +118,10 @@ constructor(
wallpaperZoomedInScale
}
val bottom = scaledBounds.bottom - scaledBottomMargin
- RectF(left, top, right, bottom)
+ RectF(left, top, right, bottom).also { Log.d(TAG, "Focal area changes to $it") }
}
+ // Make sure a valid rec
+ .filter { it.width() >= 0 && it.height() >= 0 }
.distinctUntilChanged()
fun setFocalAreaBounds(bounds: RectF) {
@@ -187,8 +160,17 @@ constructor(
return if (scale == 0f) 1f else scale
}
- // A max width for focal area shape effects bounds, to avoid
- // it becoming too large in large screen portrait mode
- const val FOCAL_AREA_MAX_WIDTH_DP = 500
+ // A max width for focal area shape effects bounds, to avoid it becoming too large,
+ // especially in portrait mode
+ const val FOCAL_AREA_MAX_WIDTH_DP_TABLET = 500
+ const val FOCAL_AREA_MAX_WIDTH_DP_FOLDABLE = 400
+
+ fun getFocalAreaMaxWidthDp(context: Context): Int {
+ return if (context.resources.getBoolean(R.bool.center_align_focal_area_shape))
+ FOCAL_AREA_MAX_WIDTH_DP_TABLET
+ else FOCAL_AREA_MAX_WIDTH_DP_FOLDABLE
+ }
+
+ private val TAG = WallpaperFocalAreaInteractor::class.simpleName
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/wallpapers/ui/viewmodel/WallpaperFocalAreaViewModel.kt b/packages/SystemUI/src/com/android/systemui/wallpapers/ui/viewmodel/WallpaperFocalAreaViewModel.kt
index 70a97d473c49..4cd49d03ad36 100644
--- a/packages/SystemUI/src/com/android/systemui/wallpapers/ui/viewmodel/WallpaperFocalAreaViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/wallpapers/ui/viewmodel/WallpaperFocalAreaViewModel.kt
@@ -17,15 +17,41 @@
package com.android.systemui.wallpapers.ui.viewmodel
import android.graphics.RectF
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.shared.model.Edge
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.TransitionState
+import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.wallpapers.domain.interactor.WallpaperFocalAreaInteractor
import javax.inject.Inject
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.map
class WallpaperFocalAreaViewModel
@Inject
-constructor(private val wallpaperFocalAreaInteractor: WallpaperFocalAreaInteractor) {
+constructor(
+ private val wallpaperFocalAreaInteractor: WallpaperFocalAreaInteractor,
+ val keyguardTransitionInteractor: KeyguardTransitionInteractor,
+) {
val hasFocalArea = wallpaperFocalAreaInteractor.hasFocalArea
- val wallpaperFocalAreaBounds = wallpaperFocalAreaInteractor.wallpaperFocalAreaBounds
+ val wallpaperFocalAreaBounds =
+ combine(
+ wallpaperFocalAreaInteractor.wallpaperFocalAreaBounds,
+ keyguardTransitionInteractor
+ .transition(
+ edge = Edge.create(to = Scenes.Lockscreen),
+ edgeWithoutSceneContainer = Edge.create(to = KeyguardState.LOCKSCREEN),
+ )
+ .filter { transitionStep ->
+ // Should not filter by TransitionState.STARTED, it may race with
+ // wakingup command, causing layout change command not be received.
+ transitionStep.transitionState == TransitionState.FINISHED
+ },
+ ::Pair,
+ )
+ .map { (bounds, _) -> bounds }
fun setFocalAreaBounds(bounds: RectF) {
wallpaperFocalAreaInteractor.setFocalAreaBounds(bounds)
diff --git a/packages/SystemUI/src/com/android/systemui/window/ui/WindowRootViewBinder.kt b/packages/SystemUI/src/com/android/systemui/window/ui/WindowRootViewBinder.kt
index 153df7f29737..06532bc0cc2a 100644
--- a/packages/SystemUI/src/com/android/systemui/window/ui/WindowRootViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/window/ui/WindowRootViewBinder.kt
@@ -84,9 +84,16 @@ object WindowRootViewBinder {
combine(viewModel.blurRadius, viewModel.isBlurOpaque, ::Pair)
.filter { it.first >= 0 }
.collect { (blurRadius, isOpaque) ->
- // Expectation is that we schedule only one blur radius value
- // per frame
+ val newBlurRadius = blurRadius.toInt()
+ // Expectation is that we schedule only one frame callback per frame
if (wasUpdateScheduledForThisFrame) {
+ // Update this value so that the frame callback picks up this
+ // value when it runs
+ if (lastScheduledBlurRadius != newBlurRadius) {
+ Log.w(TAG, "Multiple blur values emitted in the same frame")
+ }
+ lastScheduledBlurRadius = newBlurRadius
+ lastScheduleBlurOpaqueness = isOpaque
return@collect
}
TrackTracer.instantForGroup(
@@ -94,7 +101,7 @@ object WindowRootViewBinder {
"preparedBlurRadius",
blurRadius,
)
- lastScheduledBlurRadius = blurRadius.toInt()
+ lastScheduledBlurRadius = newBlurRadius
lastScheduleBlurOpaqueness = isOpaque
wasUpdateScheduledForThisFrame = true
blurUtils.prepareBlur(
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/OnTeardownRuleTest.kt b/packages/SystemUI/tests/src/com/android/systemui/OnTeardownRuleTest.kt
index 8635bb0e8ab2..8635bb0e8ab2 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/OnTeardownRuleTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/OnTeardownRuleTest.kt
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/IMagnificationConnectionTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/IMagnificationConnectionTest.java
index 6d75c4ca3a38..6d75c4ca3a38 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/IMagnificationConnectionTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/IMagnificationConnectionTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationSettingsTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationSettingsTest.java
index 4553f983b898..45b9f4ad2322 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationSettingsTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationSettingsTest.java
@@ -552,12 +552,23 @@ public class WindowMagnificationSettingsTest extends SysuiTestCase {
mockSeekBar,
OnSeekBarWithIconButtonsChangeListener.ControlUnitType.SLIDER);
- // should trigger callback to update magnifier scale and persist the scale
+ // Should trigger callback to update magnifier scale and persist the scale.
verify(mWindowMagnificationSettingsCallback)
.onMagnifierScale(/* scale= */ eq(4f), /* updatePersistence= */ eq(true));
}
@Test
+ public void onSeekbarUserInteractionFinalized_notFromUser_persistedScaleNotUpdated() {
+ OnSeekBarWithIconButtonsChangeListener onChangeListener =
+ mZoomSeekbar.getOnSeekBarWithIconButtonsChangeListener();
+ onChangeListener.onProgressChanged(mZoomSeekbar.getSeekbar(), 30, false);
+
+ // Should not trigger callback to update magnifier scale and persist the scale.
+ verify(mWindowMagnificationSettingsCallback, never())
+ .onMagnifierScale(/* scale= */ anyFloat(), /* updatePersistence= */ eq(true));
+ }
+
+ @Test
public void seekbarProgress_scaleUpdatedAfterSettingPanelOpened_progressAlsoUpdated() {
setupMagnificationCapabilityAndMode(
/* capability= */ ACCESSIBILITY_MAGNIFICATION_MODE_ALL,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/TextAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/TextAnimatorTest.kt
index dcf38800bb01..14a81b3f8bfb 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/animation/TextAnimatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/animation/TextAnimatorTest.kt
@@ -30,7 +30,6 @@ import kotlin.math.ceil
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentCaptor
-import org.mockito.Mockito.eq
import org.mockito.Mockito.inOrder
import org.mockito.Mockito.mock
import org.mockito.Mockito.never
@@ -60,10 +59,11 @@ class TextAnimatorTest : SysuiTestCase() {
val textAnimator =
TextAnimator(layout, TypefaceVariantCacheImpl(typeface, 20)).apply {
this.textInterpolator = textInterpolator
+ this.createAnimator = { valueAnimator }
this.animator = valueAnimator
}
- textAnimator.setTextStyle(weight = 400, animate = true)
+ textAnimator.setTextStyle(TextAnimator.Style("'wght' 400"), TextAnimator.Animation())
// If animation is requested, the base state should be rebased and the target state should
// be updated.
@@ -90,10 +90,11 @@ class TextAnimatorTest : SysuiTestCase() {
val textAnimator =
TextAnimator(layout, TypefaceVariantCacheImpl(typeface, 20)).apply {
this.textInterpolator = textInterpolator
+ this.createAnimator = { valueAnimator }
this.animator = valueAnimator
}
- textAnimator.setTextStyle(weight = 400, animate = false)
+ textAnimator.setTextStyle(TextAnimator.Style("'wght' 400"))
// If animation is not requested, the progress should be 1 which is end of animation and the
// base state is rebased to target state by calling rebase.
@@ -118,23 +119,24 @@ class TextAnimatorTest : SysuiTestCase() {
val textAnimator =
TextAnimator(layout, TypefaceVariantCacheImpl(typeface, 20)).apply {
this.textInterpolator = textInterpolator
+ this.createAnimator = { valueAnimator }
this.animator = valueAnimator
}
textAnimator.setTextStyle(
- weight = 400,
- animate = true,
- onAnimationEnd = animationEndCallback,
+ TextAnimator.Style("'wght' 400"),
+ TextAnimator.Animation(animate = true, onAnimationEnd = animationEndCallback),
)
// Verify animationEnd callback has been added.
val captor = ArgumentCaptor.forClass(AnimatorListenerAdapter::class.java)
- verify(valueAnimator).addListener(captor.capture())
- captor.value.onAnimationEnd(valueAnimator)
+ verify(valueAnimator, times(2)).addListener(captor.capture())
+ for (callback in captor.allValues) {
+ callback.onAnimationEnd(valueAnimator)
+ }
// Verify animationEnd callback has been invoked and removed.
verify(animationEndCallback).run()
- verify(valueAnimator).removeListener(eq(captor.value))
}
@Test
@@ -148,18 +150,20 @@ class TextAnimatorTest : SysuiTestCase() {
val textAnimator =
TextAnimator(layout, TypefaceVariantCacheImpl(typeface, 20)).apply {
this.textInterpolator = textInterpolator
+ this.createAnimator = { valueAnimator }
this.animator = valueAnimator
}
- textAnimator.setTextStyle(weight = 400, animate = true)
+ val animation = TextAnimator.Animation(animate = true)
+ textAnimator.setTextStyle(TextAnimator.Style("'wght' 400"), animation)
val prevTypeface = paint.typeface
- textAnimator.setTextStyle(weight = 700, animate = true)
+ textAnimator.setTextStyle(TextAnimator.Style("'wght' 700"), animation)
assertThat(paint.typeface).isNotSameInstanceAs(prevTypeface)
- textAnimator.setTextStyle(weight = 400, animate = true)
+ textAnimator.setTextStyle(TextAnimator.Style("'wght' 400"), animation)
assertThat(paint.typeface).isSameInstanceAs(prevTypeface)
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt
index b6c63479990e..b6c63479990e 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDialogDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDialogDelegateTest.kt
index b4200b6850c8..b4200b6850c8 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDialogDelegateTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDialogDelegateTest.kt
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayAnimationsControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayAnimationsControllerTest.kt
index 296a0fc2eb40..296a0fc2eb40 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayAnimationsControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayAnimationsControllerTest.kt
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dump/LogEulogizerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/dump/LogEulogizerTest.kt
index ae6b337a3fa0..ae6b337a3fa0 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dump/LogEulogizerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/dump/LogEulogizerTest.kt
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/ui/view/ContextualEduUiCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/education/domain/ui/view/ContextualEduUiCoordinatorTest.kt
index 6cb6fed978b8..6cb6fed978b8 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/ui/view/ContextualEduUiCoordinatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/education/domain/ui/view/ContextualEduUiCoordinatorTest.kt
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModelTest.kt
index 9c7f01495b58..9c7f01495b58 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModelTest.kt
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/lifecycle/HydratorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/lifecycle/HydratorTest.kt
index b0e93fbecbb9..b0e93fbecbb9 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/lifecycle/HydratorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/lifecycle/HydratorTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerTest.kt
index a2bd5ec28f08..aaf5559290df 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerTest.kt
@@ -66,6 +66,7 @@ import com.android.systemui.scene.data.repository.Idle
import com.android.systemui.scene.data.repository.setSceneTransition
import com.android.systemui.scene.domain.interactor.sceneInteractor
import com.android.systemui.scene.shared.model.Scenes
+import com.android.systemui.statusbar.featurepods.media.domain.interactor.mediaControlChipInteractor
import com.android.systemui.statusbar.notification.collection.provider.OnReorderingAllowedListener
import com.android.systemui.statusbar.notification.collection.provider.VisualStabilityProvider
import com.android.systemui.statusbar.policy.ConfigurationController
@@ -203,6 +204,7 @@ class MediaCarouselControllerTest(flags: FlagsParameterization) : SysuiTestCase(
mediaCarouselViewModel = kosmos.mediaCarouselViewModel,
mediaViewControllerFactory = mediaViewControllerFactory,
deviceEntryInteractor = kosmos.deviceEntryInteractor,
+ mediaControlChipInteractor = kosmos.mediaControlChipInteractor,
)
verify(configurationController).addCallback(capture(configListener))
verify(visualStabilityProvider)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java
index 23282b16d8a8..205ccea657df 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java
@@ -84,7 +84,7 @@ public class MediaOutputBaseDialogTest extends SysuiTestCase {
private final Kosmos mKosmos = SysuiTestCaseExtKt.testKosmos(this);
// Mock
- private MediaOutputBaseAdapter mMediaOutputBaseAdapter = mock(MediaOutputBaseAdapter.class);
+ private MediaOutputAdapterBase mMediaOutputBaseAdapter = mock(MediaOutputAdapterBase.class);
private MediaController mMediaController = mock(MediaController.class);
private PlaybackState mPlaybackState = mock(PlaybackState.class);
private MediaSessionManager mMediaSessionManager = mock(MediaSessionManager.class);
@@ -219,7 +219,6 @@ public class MediaOutputBaseDialogTest extends SysuiTestCase {
public void refresh_withIconCompat_iconIsVisible() {
mIconCompat = IconCompat.createWithBitmap(
Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888));
- when(mMediaOutputBaseAdapter.getController()).thenReturn(mMediaSwitchingController);
mMediaOutputBaseDialogImpl.refresh();
final ImageView view = mMediaOutputBaseDialogImpl.mDialogView.requireViewById(
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/QSFragmentComposeTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/QSFragmentComposeTest.kt
index ab78029684d4..ab78029684d4 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/QSFragmentComposeTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/QSFragmentComposeTest.kt
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/sensorprivacy/SensorUseStartedActivityTest.kt b/packages/SystemUI/tests/src/com/android/systemui/sensorprivacy/SensorUseStartedActivityTest.kt
index 1a4749c3196c..1a4749c3196c 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/sensorprivacy/SensorUseStartedActivityTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/sensorprivacy/SensorUseStartedActivityTest.kt
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/QuickSettingsControllerImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerImplTest.java
index 49cbb5a924f1..49cbb5a924f1 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/QuickSettingsControllerImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerImplTest.java
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shadow/DoubleShadowTextClockTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shadow/DoubleShadowTextClockTest.kt
index 89a3d5b5cf0b..89a3d5b5cf0b 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shadow/DoubleShadowTextClockTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shadow/DoubleShadowTextClockTest.kt
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shared/clocks/AnimatableClockViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/AnimatableClockViewTest.kt
index e076418f2630..79e78c9532c6 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shared/clocks/AnimatableClockViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/AnimatableClockViewTest.kt
@@ -20,9 +20,10 @@ import android.view.LayoutInflater
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.app.animation.Interpolators
-import com.android.systemui.customization.R
import com.android.systemui.SysuiTestCase
+import com.android.systemui.animation.FontVariationUtils
import com.android.systemui.animation.TextAnimator
+import com.android.systemui.customization.R
import com.android.systemui.util.mockito.any
import org.junit.Before
import org.junit.Rule
@@ -32,7 +33,9 @@ import org.mockito.Mock
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
import org.mockito.Mockito.verifyNoMoreInteractions
+import org.mockito.Mockito.`when` as whenever
import org.mockito.junit.MockitoJUnit
+import org.mockito.kotlin.eq
@RunWith(AndroidJUnit4::class)
@SmallTest
@@ -46,6 +49,7 @@ class AnimatableClockViewTest : SysuiTestCase() {
@Before
fun setUp() {
val layoutInflater = LayoutInflater.from(context)
+ whenever(mockTextAnimator.fontVariationUtils).thenReturn(FontVariationUtils())
clockView =
layoutInflater.inflate(R.layout.clock_default_small, null) as AnimatableClockView
clockView.textAnimatorFactory = { _, _ -> mockTextAnimator }
@@ -57,18 +61,19 @@ class AnimatableClockViewTest : SysuiTestCase() {
clockView.animateAppearOnLockscreen()
clockView.measure(50, 50)
+ verify(mockTextAnimator).fontVariationUtils
verify(mockTextAnimator).glyphFilter = any()
verify(mockTextAnimator)
.setTextStyle(
- weight = 300,
- textSize = -1.0f,
- color = 200,
- strokeWidth = -1F,
- animate = false,
- duration = 833L,
- interpolator = Interpolators.EMPHASIZED_DECELERATE,
- delay = 0L,
- onAnimationEnd = null
+ eq(TextAnimator.Style(fVar = "'wght' 300", color = 200)),
+ eq(
+ TextAnimator.Animation(
+ animate = false,
+ duration = 833L,
+ interpolator = Interpolators.EMPHASIZED_DECELERATE,
+ onAnimationEnd = null,
+ )
+ ),
)
verifyNoMoreInteractions(mockTextAnimator)
}
@@ -79,30 +84,24 @@ class AnimatableClockViewTest : SysuiTestCase() {
clockView.measure(50, 50)
clockView.animateAppearOnLockscreen()
+ verify(mockTextAnimator, times(2)).fontVariationUtils
verify(mockTextAnimator, times(2)).glyphFilter = any()
verify(mockTextAnimator)
.setTextStyle(
- weight = 100,
- textSize = -1.0f,
- color = 200,
- strokeWidth = -1F,
- animate = false,
- duration = 0L,
- interpolator = null,
- delay = 0L,
- onAnimationEnd = null
+ eq(TextAnimator.Style(fVar = "'wght' 100", color = 200)),
+ eq(TextAnimator.Animation(animate = false, duration = 0)),
)
+
verify(mockTextAnimator)
.setTextStyle(
- weight = 300,
- textSize = -1.0f,
- color = 200,
- strokeWidth = -1F,
- animate = true,
- duration = 833L,
- interpolator = Interpolators.EMPHASIZED_DECELERATE,
- delay = 0L,
- onAnimationEnd = null
+ eq(TextAnimator.Style(fVar = "'wght' 300", color = 200)),
+ eq(
+ TextAnimator.Animation(
+ animate = true,
+ duration = 833L,
+ interpolator = Interpolators.EMPHASIZED_DECELERATE,
+ )
+ ),
)
verifyNoMoreInteractions(mockTextAnimator)
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shared/condition/CombinedConditionTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shared/condition/CombinedConditionTest.kt
index 8418598c256b..8418598c256b 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shared/condition/CombinedConditionTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shared/condition/CombinedConditionTest.kt
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shared/system/UncaughtExceptionPreHandlerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shared/system/UncaughtExceptionPreHandlerTest.kt
index d67ce303c451..d67ce303c451 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shared/system/UncaughtExceptionPreHandlerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shared/system/UncaughtExceptionPreHandlerTest.kt
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/featurepods/popups/ui/viewmodel/StatusBarPopupChipsViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/featurepods/popups/ui/viewmodel/StatusBarPopupChipsViewModelTest.kt
index fcbf0fe9a37a..fcbf0fe9a37a 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/featurepods/popups/ui/viewmodel/StatusBarPopupChipsViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/featurepods/popups/ui/viewmodel/StatusBarPopupChipsViewModelTest.kt
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/layout/StatusBarBoundsProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/layout/StatusBarBoundsProviderTest.kt
index 04319f05f6f9..04319f05f6f9 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/layout/StatusBarBoundsProviderTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/layout/StatusBarBoundsProviderTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryTest.java
index 281ce16b539f..19d1224a9bf3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryTest.java
@@ -28,6 +28,8 @@ import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_AMBIENT;
import static com.android.systemui.statusbar.NotificationEntryHelper.modifyRanking;
import static com.android.systemui.statusbar.NotificationEntryHelper.modifySbn;
+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.assertTrue;
@@ -46,6 +48,7 @@ import android.os.Bundle;
import android.os.UserHandle;
import android.platform.test.annotations.DisableFlags;
import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.SetFlagsRule;
import android.service.notification.NotificationListenerService.Ranking;
import android.service.notification.SnoozeCriterion;
import android.service.notification.StatusBarNotification;
@@ -59,9 +62,12 @@ import com.android.systemui.statusbar.RankingBuilder;
import com.android.systemui.statusbar.SbnBuilder;
import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips;
import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUi;
+import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
+import com.android.systemui.statusbar.notification.shared.NotificationBundleUi;
import com.android.systemui.util.time.FakeSystemClock;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
@@ -83,6 +89,9 @@ public class NotificationEntryTest extends SysuiTestCase {
private NotificationChannel mChannel = Mockito.mock(NotificationChannel.class);
private final FakeSystemClock mClock = new FakeSystemClock();
+ @Rule
+ public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
@Before
public void setup() {
Notification.Builder n = new Notification.Builder(mContext, "")
@@ -444,6 +453,145 @@ public class NotificationEntryTest extends SysuiTestCase {
// no crash, good
}
+ @Test
+ @EnableFlags(NotificationBundleUi.FLAG_NAME)
+ public void getParent_adapter() {
+ GroupEntry ge = new GroupEntryBuilder()
+ .build();
+ Notification notification = new Notification.Builder(mContext, "")
+ .setSmallIcon(R.drawable.ic_person)
+ .build();
+
+ NotificationEntry entry = new NotificationEntryBuilder()
+ .setPkg(TEST_PACKAGE_NAME)
+ .setOpPkg(TEST_PACKAGE_NAME)
+ .setUid(TEST_UID)
+ .setChannel(mChannel)
+ .setId(mId++)
+ .setNotification(notification)
+ .setUser(new UserHandle(ActivityManager.getCurrentUser()))
+ .setParent(ge)
+ .build();
+
+ assertThat(entry.getEntryAdapter().getParent()).isEqualTo(entry.getParent());
+ }
+
+ @Test
+ @EnableFlags(NotificationBundleUi.FLAG_NAME)
+ public void isTopLevelEntry_adapter() {
+ Notification notification = new Notification.Builder(mContext, "")
+ .setSmallIcon(R.drawable.ic_person)
+ .build();
+
+ NotificationEntry entry = new NotificationEntryBuilder()
+ .setPkg(TEST_PACKAGE_NAME)
+ .setOpPkg(TEST_PACKAGE_NAME)
+ .setUid(TEST_UID)
+ .setChannel(mChannel)
+ .setId(mId++)
+ .setNotification(notification)
+ .setUser(new UserHandle(ActivityManager.getCurrentUser()))
+ .setParent(GroupEntry.ROOT_ENTRY)
+ .build();
+
+ assertThat(entry.getEntryAdapter().isTopLevelEntry()).isTrue();
+ }
+
+ @Test
+ @EnableFlags(NotificationBundleUi.FLAG_NAME)
+ public void getKey_adapter() {
+ Notification notification = new Notification.Builder(mContext, "")
+ .setSmallIcon(R.drawable.ic_person)
+ .build();
+
+ NotificationEntry entry = new NotificationEntryBuilder()
+ .setPkg(TEST_PACKAGE_NAME)
+ .setOpPkg(TEST_PACKAGE_NAME)
+ .setUid(TEST_UID)
+ .setChannel(mChannel)
+ .setId(mId++)
+ .setNotification(notification)
+ .setUser(new UserHandle(ActivityManager.getCurrentUser()))
+ .build();
+
+ assertThat(entry.getEntryAdapter().getKey()).isEqualTo(entry.getKey());
+ }
+
+ @Test
+ @EnableFlags(NotificationBundleUi.FLAG_NAME)
+ public void getRow_adapter() {
+ ExpandableNotificationRow row = mock(ExpandableNotificationRow.class);
+ Notification notification = new Notification.Builder(mContext, "")
+ .setSmallIcon(R.drawable.ic_person)
+ .build();
+
+ NotificationEntry entry = new NotificationEntryBuilder()
+ .setPkg(TEST_PACKAGE_NAME)
+ .setOpPkg(TEST_PACKAGE_NAME)
+ .setUid(TEST_UID)
+ .setChannel(mChannel)
+ .setId(mId++)
+ .setNotification(notification)
+ .setUser(new UserHandle(ActivityManager.getCurrentUser()))
+ .build();
+ entry.setRow(row);
+
+ assertThat(entry.getEntryAdapter().getRow()).isEqualTo(entry.getRow());
+ }
+
+ @Test
+ @EnableFlags(NotificationBundleUi.FLAG_NAME)
+ public void getGroupRoot_adapter_groupSummary() {
+ ExpandableNotificationRow row = mock(ExpandableNotificationRow.class);
+ Notification notification = new Notification.Builder(mContext, "")
+ .setSmallIcon(R.drawable.ic_person)
+ .setGroupSummary(true)
+ .setGroup("key")
+ .build();
+
+ NotificationEntry entry = new NotificationEntryBuilder()
+ .setPkg(TEST_PACKAGE_NAME)
+ .setOpPkg(TEST_PACKAGE_NAME)
+ .setUid(TEST_UID)
+ .setChannel(mChannel)
+ .setId(mId++)
+ .setNotification(notification)
+ .setUser(new UserHandle(ActivityManager.getCurrentUser()))
+ .setParent(GroupEntry.ROOT_ENTRY)
+ .build();
+ entry.setRow(row);
+
+ assertThat(entry.getEntryAdapter().getGroupRoot()).isNull();
+ }
+
+ @Test
+ @EnableFlags(NotificationBundleUi.FLAG_NAME)
+ public void getGroupRoot_adapter_groupChild() {
+ Notification notification = new Notification.Builder(mContext, "")
+ .setSmallIcon(R.drawable.ic_person)
+ .setGroupSummary(true)
+ .setGroup("key")
+ .build();
+
+ NotificationEntry parent = new NotificationEntryBuilder()
+ .setParent(GroupEntry.ROOT_ENTRY)
+ .build();
+ GroupEntryBuilder groupEntry = new GroupEntryBuilder()
+ .setSummary(parent);
+
+ NotificationEntry entry = new NotificationEntryBuilder()
+ .setPkg(TEST_PACKAGE_NAME)
+ .setOpPkg(TEST_PACKAGE_NAME)
+ .setUid(TEST_UID)
+ .setChannel(mChannel)
+ .setId(mId++)
+ .setNotification(notification)
+ .setUser(new UserHandle(ActivityManager.getCurrentUser()))
+ .setParent(groupEntry.build())
+ .build();
+
+ assertThat(entry.getEntryAdapter().getGroupRoot()).isEqualTo(parent.getEntryAdapter());
+ }
private Notification.Action createContextualAction(String title) {
return new Notification.Action.Builder(
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorTest.java
index f1edb417a314..f1edb417a314 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorTest.java
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/BigPictureIconManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/BigPictureIconManagerTest.kt
index 99dcd6c9a798..99dcd6c9a798 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/BigPictureIconManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/BigPictureIconManagerTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java
index 77b116e2e465..a6722c5f4c22 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java
@@ -16,6 +16,8 @@
package com.android.systemui.statusbar.notification.row;
+import static android.app.Flags.FLAG_NOTIFICATIONS_REDESIGN_TEMPLATES;
+
import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_ALL;
import static com.android.systemui.statusbar.notification.row.NotificationTestHelper.PKG;
import static com.android.systemui.statusbar.notification.row.NotificationTestHelper.USER_HANDLE;
@@ -29,6 +31,7 @@ import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
@@ -189,6 +192,54 @@ public class ExpandableNotificationRowTest extends SysuiTestCase {
}
@Test
+ @EnableFlags(FLAG_NOTIFICATIONS_REDESIGN_TEMPLATES)
+ public void setSensitive_doesNothingIfCalledAgain() throws Exception {
+ ExpandableNotificationRow row = mNotificationTestHelper.createRow();
+ measureAndLayout(row);
+
+ // GIVEN a mocked public layout
+ NotificationContentView mockPublicLayout = mock(NotificationContentView.class);
+ row.setPublicLayout(mockPublicLayout);
+
+ // GIVEN a sensitive notification row that's currently redacted
+ row.setHideSensitiveForIntrinsicHeight(true);
+ row.setSensitive(true, true);
+ assertThat(row.getShowingLayout()).isSameInstanceAs(row.getPublicLayout());
+ verify(mockPublicLayout).requestSelectLayout(eq(true));
+ clearInvocations(mockPublicLayout);
+
+ // WHEN the row is set to the same sensitive settings
+ row.setSensitive(true, true);
+
+ // VERIFY that the layout is not updated again
+ assertThat(row.getShowingLayout()).isSameInstanceAs(row.getPublicLayout());
+ verify(mockPublicLayout, never()).requestSelectLayout(anyBoolean());
+ }
+
+ @Test
+ @EnableFlags(FLAG_NOTIFICATIONS_REDESIGN_TEMPLATES)
+ public void testSetSensitiveOnNotifRowUpdatesLayout() throws Exception {
+ // GIVEN a sensitive notification row that's currently redacted
+ ExpandableNotificationRow row = mNotificationTestHelper.createRow();
+ measureAndLayout(row);
+ row.setHideSensitiveForIntrinsicHeight(true);
+ row.setSensitive(true, true);
+ assertThat(row.getShowingLayout()).isSameInstanceAs(row.getPublicLayout());
+
+ // GIVEN a mocked private layout
+ NotificationContentView mockPrivateLayout = mock(NotificationContentView.class);
+ row.setPrivateLayout(mockPrivateLayout);
+
+ // WHEN the row is set to no longer be sensitive
+ row.setSensitive(false, true);
+
+ // VERIFY that the layout is updated
+ assertThat(row.getShowingLayout()).isSameInstanceAs(row.getPrivateLayout());
+ verify(mockPrivateLayout).requestSelectLayout(eq(true));
+ }
+
+ @Test
+ @DisableFlags(FLAG_NOTIFICATIONS_REDESIGN_TEMPLATES)
public void testSetSensitiveOnNotifRowNotifiesOfHeightChange() throws Exception {
// GIVEN a sensitive notification row that's currently redacted
ExpandableNotificationRow row = mNotificationTestHelper.createRow();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.kt
index 699e8c30afde..47238fedee4d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.kt
@@ -23,6 +23,7 @@ import android.service.notification.StatusBarNotification
import android.testing.TestableLooper
import android.testing.ViewUtils
import android.view.NotificationHeaderView
+import android.view.NotificationTopLineView
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
@@ -37,6 +38,7 @@ import com.android.systemui.SysuiTestCase
import com.android.systemui.statusbar.notification.FeedbackIcon
import com.android.systemui.statusbar.notification.collection.NotificationEntry
import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier
+import com.android.systemui.statusbar.notification.shared.NotificationBundleUi
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
@@ -82,8 +84,21 @@ class NotificationContentViewTest : SysuiTestCase() {
val mockEntry = createMockNotificationEntry()
row =
spy(
- ExpandableNotificationRow(mContext, /* attrs= */ null, mockEntry).apply {
- entry = mockEntry
+ when (NotificationBundleUi.isEnabled) {
+ true -> {
+ ExpandableNotificationRow(
+ mContext,
+ /* attrs= */ null,
+ UserHandle.CURRENT
+ ).apply {
+ entry = mockEntry
+ }
+ }
+ false -> {
+ ExpandableNotificationRow(mContext, /* attrs= */ null, mockEntry).apply {
+ entry = mockEntry
+ }
+ }
}
)
ViewUtils.attachView(fakeParent)
@@ -270,7 +285,7 @@ class NotificationContentViewTest : SysuiTestCase() {
val icon =
FeedbackIcon(
R.drawable.ic_feedback_alerted,
- R.string.notification_feedback_indicator_alerted
+ R.string.notification_feedback_indicator_alerted,
)
view.setFeedbackIcon(icon)
@@ -291,10 +306,7 @@ class NotificationContentViewTest : SysuiTestCase() {
val mockHeadsUpEB = mock<NotificationExpandButton>()
val mockHeadsUp = createMockNotificationHeaderView(contractedHeight, mockHeadsUpEB)
- val view =
- createContentView(
- isSystemExpanded = false,
- )
+ val view = createContentView(isSystemExpanded = false)
// Update all 3 child forms
view.apply {
@@ -319,12 +331,14 @@ class NotificationContentViewTest : SysuiTestCase() {
private fun createMockNotificationHeaderView(
height: Int,
- mockExpandedEB: NotificationExpandButton
+ mockExpandedEB: NotificationExpandButton,
) =
spy(NotificationHeaderView(mContext, /* attrs= */ null).apply { minimumHeight = height })
.apply {
whenever(this.animate()).thenReturn(mock())
whenever(this.findViewById<View>(R.id.expand_button)).thenReturn(mockExpandedEB)
+ whenever(this.findViewById<View>(R.id.notification_top_line))
+ .thenReturn(mock<NotificationTopLineView>())
}
@Test
@@ -344,7 +358,7 @@ class NotificationContentViewTest : SysuiTestCase() {
isSystemExpanded = false,
contractedView = mockContracted,
expandedView = mockExpanded,
- headsUpView = mockHeadsUp
+ headsUpView = mockHeadsUp,
)
view.setRemoteInputVisible(true)
@@ -373,7 +387,7 @@ class NotificationContentViewTest : SysuiTestCase() {
isSystemExpanded = false,
contractedView = mockContracted,
expandedView = mockExpanded,
- headsUpView = mockHeadsUp
+ headsUpView = mockHeadsUp,
)
view.setRemoteInputVisible(false)
@@ -635,7 +649,7 @@ class NotificationContentViewTest : SysuiTestCase() {
contractedView: View = createViewWithHeight(contractedHeight),
expandedView: View = createViewWithHeight(expandedHeight),
headsUpView: View = createViewWithHeight(contractedHeight),
- row: ExpandableNotificationRow = this.row
+ row: ExpandableNotificationRow = this.row,
): NotificationContentView {
val height = if (isSystemExpanded) expandedHeight else contractedHeight
doReturn(height).whenever(row).intrinsicHeight
@@ -647,7 +661,7 @@ class NotificationContentViewTest : SysuiTestCase() {
setHeights(
/* smallHeight= */ contractedHeight,
/* headsUpMaxHeight= */ contractedHeight,
- /* maxHeight= */ expandedHeight
+ /* maxHeight= */ expandedHeight,
)
contractedChild = contractedView
expandedChild = expandedView
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerWithScenesTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerWithScenesTest.kt
index 1b447525bbf5..3d4c90140adb 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerWithScenesTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerWithScenesTest.kt
@@ -67,6 +67,8 @@ import com.android.systemui.statusbar.notification.collection.provider.HighPrior
import com.android.systemui.statusbar.notification.domain.interactor.activeNotificationsInteractor
import com.android.systemui.statusbar.notification.headsup.mockHeadsUpManager
import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier
+import com.android.systemui.statusbar.notification.row.icon.appIconProvider
+import com.android.systemui.statusbar.notification.row.icon.notificationIconStyleProvider
import com.android.systemui.statusbar.notification.stack.NotificationListContainer
import com.android.systemui.statusbar.notificationLockscreenUserManager
import com.android.systemui.statusbar.policy.deviceProvisionedController
@@ -128,6 +130,8 @@ class NotificationGutsManagerWithScenesTest : SysuiTestCase() {
private val statusBarStateController = kosmos.statusBarStateController
private val headsUpManager = kosmos.mockHeadsUpManager
private val activityStarter = kosmos.activityStarter
+ private val appIconProvider = kosmos.appIconProvider
+ private val iconStyleProvider = kosmos.notificationIconStyleProvider
private val userManager = kosmos.userManager
private val activeNotificationsInteractor = kosmos.activeNotificationsInteractor
private val sceneInteractor = kosmos.sceneInteractor
@@ -174,6 +178,8 @@ class NotificationGutsManagerWithScenesTest : SysuiTestCase() {
accessibilityManager,
highPriorityProvider,
notificationManager,
+ appIconProvider,
+ iconStyleProvider,
userManager,
peopleSpaceWidgetManager,
launcherApps,
@@ -429,6 +435,8 @@ class NotificationGutsManagerWithScenesTest : SysuiTestCase() {
.bindNotification(
any<PackageManager>(),
any<INotificationManager>(),
+ eq(appIconProvider),
+ eq(iconStyleProvider),
eq(onUserInteractionCallback),
eq(channelEditorDialogController),
eq(statusBarNotification.packageName),
@@ -463,6 +471,8 @@ class NotificationGutsManagerWithScenesTest : SysuiTestCase() {
.bindNotification(
any<PackageManager>(),
any<INotificationManager>(),
+ eq(appIconProvider),
+ eq(iconStyleProvider),
eq(onUserInteractionCallback),
eq(channelEditorDialogController),
eq(statusBarNotification.packageName),
@@ -497,6 +507,8 @@ class NotificationGutsManagerWithScenesTest : SysuiTestCase() {
.bindNotification(
any<PackageManager>(),
any<INotificationManager>(),
+ eq(appIconProvider),
+ eq(iconStyleProvider),
eq(onUserInteractionCallback),
eq(channelEditorDialogController),
eq(statusBarNotification.packageName),
@@ -529,8 +541,8 @@ class NotificationGutsManagerWithScenesTest : SysuiTestCase() {
.setChannel(testNotificationChannel)
.build()
row
- } catch (e: Exception) {
- org.junit.Assert.fail()
+ } catch (_: Exception) {
+ Assert.fail()
null
}
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/RowImageInflaterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/RowImageInflaterTest.kt
index 86689cb88569..86689cb88569 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/RowImageInflaterTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/RowImageInflaterTest.kt
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.kt
index 31f8590c0378..31f8590c0378 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java
index 14a1233045bb..10886760b521 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java
@@ -63,6 +63,7 @@ import com.android.systemui.SysuiTestCase;
import com.android.systemui.animation.ShadeInterpolation;
import com.android.systemui.bouncer.shared.constants.KeyguardBouncerConstants;
import com.android.systemui.dock.DockManager;
+import com.android.systemui.flags.DisableSceneContainer;
import com.android.systemui.flags.EnableSceneContainer;
import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository;
@@ -118,10 +119,8 @@ public class ScrimControllerTest extends SysuiTestCase {
@Rule public Expect mExpect = Expect.create();
private final KosmosJavaAdapter mKosmos = new KosmosJavaAdapter(this);
- private final FakeConfigurationController mConfigurationController =
- new FakeConfigurationController();
- private final LargeScreenShadeInterpolator
- mLinearLargeScreenShadeInterpolator = new LinearLargeScreenShadeInterpolator();
+ private FakeConfigurationController mConfigurationController;
+ private LargeScreenShadeInterpolator mLinearLargeScreenShadeInterpolator;
private final TestScope mTestScope = mKosmos.getTestScope();
private final JavaAdapter mJavaAdapter = new JavaAdapter(mTestScope.getBackgroundScope());
@@ -137,6 +136,7 @@ public class ScrimControllerTest extends SysuiTestCase {
private boolean mAlwaysOnEnabled;
private TestableLooper mLooper;
private Context mContext;
+
@Mock private DozeParameters mDozeParameters;
@Mock private LightBarController mLightBarController;
@Mock private DelayedWakeLock.Factory mDelayedWakeLockFactory;
@@ -149,12 +149,11 @@ public class ScrimControllerTest extends SysuiTestCase {
@Mock private PrimaryBouncerToGoneTransitionViewModel mPrimaryBouncerToGoneTransitionViewModel;
@Mock private AlternateBouncerToGoneTransitionViewModel
mAlternateBouncerToGoneTransitionViewModel;
- private final KeyguardTransitionInteractor mKeyguardTransitionInteractor =
- mKosmos.getKeyguardTransitionInteractor();
- private final FakeKeyguardTransitionRepository mKeyguardTransitionRepository =
- mKosmos.getKeyguardTransitionRepository();
@Mock private KeyguardInteractor mKeyguardInteractor;
+ private KeyguardTransitionInteractor mKeyguardTransitionInteractor;
+ private FakeKeyguardTransitionRepository mKeyguardTransitionRepository;
+
// TODO(b/204991468): Use a real PanelExpansionStateManager object once this bug is fixed. (The
// event-dispatch-on-registration pattern caused some of these unit tests to fail.)
@Mock private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
@@ -238,6 +237,9 @@ public class ScrimControllerTest extends SysuiTestCase {
when(mContext.getColor(com.android.internal.R.color.materialColorSurface))
.thenAnswer(invocation -> mSurfaceColor);
+ mConfigurationController = new FakeConfigurationController();
+ mLinearLargeScreenShadeInterpolator = new LinearLargeScreenShadeInterpolator();
+
mScrimBehind = spy(new ScrimView(mContext));
mScrimInFront = new ScrimView(mContext);
mNotificationsScrim = new ScrimView(mContext);
@@ -270,6 +272,9 @@ public class ScrimControllerTest extends SysuiTestCase {
when(mAlternateBouncerToGoneTransitionViewModel.getScrimAlpha())
.thenReturn(emptyFlow());
+ mKeyguardTransitionRepository = mKosmos.getKeyguardTransitionRepository();
+ mKeyguardTransitionInteractor = mKosmos.getKeyguardTransitionInteractor();
+
mScrimController = new ScrimController(
mLightBarController,
mDozeParameters,
@@ -322,6 +327,7 @@ public class ScrimControllerTest extends SysuiTestCase {
}
@Test
+ @DisableSceneContainer
public void transitionToKeyguard() {
mScrimController.legacyTransitionTo(ScrimState.KEYGUARD);
finishAnimationsImmediately();
@@ -337,6 +343,7 @@ public class ScrimControllerTest extends SysuiTestCase {
}
@Test
+ @DisableSceneContainer
public void transitionToShadeLocked() {
mScrimController.legacyTransitionTo(SHADE_LOCKED);
mScrimController.setQsPosition(1f, 0);
@@ -373,6 +380,7 @@ public class ScrimControllerTest extends SysuiTestCase {
}
@Test
+ @DisableSceneContainer
public void transitionToShadeLocked_clippingQs() {
mScrimController.setClipsQsScrim(true);
mScrimController.legacyTransitionTo(SHADE_LOCKED);
@@ -391,6 +399,7 @@ public class ScrimControllerTest extends SysuiTestCase {
}
@Test
+ @DisableSceneContainer
public void transitionToOff() {
mScrimController.legacyTransitionTo(ScrimState.OFF);
finishAnimationsImmediately();
@@ -406,6 +415,7 @@ public class ScrimControllerTest extends SysuiTestCase {
}
@Test
+ @DisableSceneContainer
public void transitionToAod_withRegularWallpaper() {
mScrimController.legacyTransitionTo(ScrimState.AOD);
finishAnimationsImmediately();
@@ -421,6 +431,7 @@ public class ScrimControllerTest extends SysuiTestCase {
}
@Test
+ @DisableSceneContainer
public void transitionToAod_withFrontAlphaUpdates() {
// Assert that setting the AOD front scrim alpha doesn't take effect in a non-AOD state.
mScrimController.legacyTransitionTo(ScrimState.KEYGUARD);
@@ -465,6 +476,7 @@ public class ScrimControllerTest extends SysuiTestCase {
}
@Test
+ @DisableSceneContainer
public void transitionToAod_afterDocked_ignoresAlwaysOnAndUpdatesFrontAlpha() {
// Assert that setting the AOD front scrim alpha doesn't take effect in a non-AOD state.
mScrimController.legacyTransitionTo(ScrimState.KEYGUARD);
@@ -506,6 +518,7 @@ public class ScrimControllerTest extends SysuiTestCase {
}
@Test
+ @DisableSceneContainer
public void transitionToPulsing_withFrontAlphaUpdates() {
// Pre-condition
// Need to go to AoD first because PULSING doesn't change
@@ -551,6 +564,7 @@ public class ScrimControllerTest extends SysuiTestCase {
}
@Test
+ @DisableSceneContainer
public void transitionToKeyguardBouncer() {
mScrimController.legacyTransitionTo(BOUNCER);
finishAnimationsImmediately();
@@ -571,6 +585,7 @@ public class ScrimControllerTest extends SysuiTestCase {
}
@Test
+ @DisableSceneContainer
public void lockscreenToHubTransition_setsBehindScrimAlpha() {
// Start on lockscreen.
mScrimController.legacyTransitionTo(ScrimState.KEYGUARD);
@@ -617,6 +632,7 @@ public class ScrimControllerTest extends SysuiTestCase {
}
@Test
+ @DisableSceneContainer
public void hubToLockscreenTransition_setsViewAlpha() {
// Start on glanceable hub.
mScrimController.legacyTransitionTo(ScrimState.GLANCEABLE_HUB);
@@ -663,6 +679,7 @@ public class ScrimControllerTest extends SysuiTestCase {
}
@Test
+ @DisableSceneContainer
public void transitionToHub() {
mScrimController.setRawPanelExpansionFraction(0f);
mScrimController.setBouncerHiddenFraction(KeyguardBouncerConstants.EXPANSION_HIDDEN);
@@ -677,6 +694,7 @@ public class ScrimControllerTest extends SysuiTestCase {
}
@Test
+ @DisableSceneContainer
public void openBouncerOnHub() {
mScrimController.legacyTransitionTo(ScrimState.GLANCEABLE_HUB);
@@ -706,6 +724,7 @@ public class ScrimControllerTest extends SysuiTestCase {
}
@Test
+ @DisableSceneContainer
public void openShadeOnHub() {
mScrimController.legacyTransitionTo(ScrimState.GLANCEABLE_HUB);
@@ -734,6 +753,7 @@ public class ScrimControllerTest extends SysuiTestCase {
}
@Test
+ @DisableSceneContainer
public void transitionToHubOverDream() {
mScrimController.setRawPanelExpansionFraction(0f);
mScrimController.setBouncerHiddenFraction(KeyguardBouncerConstants.EXPANSION_HIDDEN);
@@ -748,6 +768,7 @@ public class ScrimControllerTest extends SysuiTestCase {
}
@Test
+ @DisableSceneContainer
public void openBouncerOnHubOverDream() {
mScrimController.legacyTransitionTo(ScrimState.GLANCEABLE_HUB_OVER_DREAM);
@@ -777,6 +798,7 @@ public class ScrimControllerTest extends SysuiTestCase {
}
@Test
+ @DisableSceneContainer
public void openShadeOnHubOverDream() {
mScrimController.legacyTransitionTo(ScrimState.GLANCEABLE_HUB_OVER_DREAM);
@@ -805,6 +827,7 @@ public class ScrimControllerTest extends SysuiTestCase {
}
@Test
+ @DisableSceneContainer
public void onThemeChange_bouncerBehindTint_isUpdatedToSurfaceColor() {
assertEquals(BOUNCER.getBehindTint(), 0x112233);
mSurfaceColor = 0x223344;
@@ -813,6 +836,7 @@ public class ScrimControllerTest extends SysuiTestCase {
}
@Test
+ @DisableSceneContainer
public void onThemeChangeWhileClipQsScrim_bouncerBehindTint_remainsBlack() {
mScrimController.setClipsQsScrim(true);
mScrimController.legacyTransitionTo(BOUNCER);
@@ -825,6 +849,7 @@ public class ScrimControllerTest extends SysuiTestCase {
}
@Test
+ @DisableSceneContainer
public void transitionToKeyguardBouncer_clippingQs() {
mScrimController.setClipsQsScrim(true);
mScrimController.legacyTransitionTo(BOUNCER);
@@ -845,6 +870,7 @@ public class ScrimControllerTest extends SysuiTestCase {
}
@Test
+ @DisableSceneContainer
public void disableClipQsScrimWithoutStateTransition_updatesTintAndAlpha() {
mScrimController.setClipsQsScrim(true);
mScrimController.legacyTransitionTo(BOUNCER);
@@ -867,6 +893,7 @@ public class ScrimControllerTest extends SysuiTestCase {
}
@Test
+ @DisableSceneContainer
public void enableClipQsScrimWithoutStateTransition_updatesTintAndAlpha() {
mScrimController.setClipsQsScrim(false);
mScrimController.legacyTransitionTo(BOUNCER);
@@ -889,6 +916,7 @@ public class ScrimControllerTest extends SysuiTestCase {
}
@Test
+ @DisableSceneContainer
public void transitionToBouncer() {
mScrimController.legacyTransitionTo(ScrimState.BOUNCER_SCRIMMED);
finishAnimationsImmediately();
@@ -902,6 +930,7 @@ public class ScrimControllerTest extends SysuiTestCase {
}
@Test
+ @DisableSceneContainer
public void transitionToUnlocked_clippedQs() {
mScrimController.setClipsQsScrim(true);
mScrimController.setRawPanelExpansionFraction(0f);
@@ -960,6 +989,7 @@ public class ScrimControllerTest extends SysuiTestCase {
}
@Test
+ @DisableSceneContainer
public void transitionToUnlocked_nonClippedQs_followsLargeScreensInterpolator() {
mScrimController.setClipsQsScrim(false);
mScrimController.setRawPanelExpansionFraction(0f);
@@ -999,6 +1029,7 @@ public class ScrimControllerTest extends SysuiTestCase {
}
@Test
+ @DisableSceneContainer
public void scrimStateCallback() {
mScrimController.legacyTransitionTo(ScrimState.UNLOCKED);
finishAnimationsImmediately();
@@ -1014,6 +1045,7 @@ public class ScrimControllerTest extends SysuiTestCase {
}
@Test
+ @DisableSceneContainer
public void panelExpansion() {
mScrimController.setRawPanelExpansionFraction(0f);
mScrimController.setRawPanelExpansionFraction(0.5f);
@@ -1036,6 +1068,7 @@ public class ScrimControllerTest extends SysuiTestCase {
}
@Test
+ @DisableSceneContainer
public void qsExpansion() {
reset(mScrimBehind);
mScrimController.setQsPosition(1f, 999 /* value doesn't matter */);
@@ -1048,6 +1081,7 @@ public class ScrimControllerTest extends SysuiTestCase {
}
@Test
+ @DisableSceneContainer
public void qsExpansion_clippingQs() {
reset(mScrimBehind);
mScrimController.setClipsQsScrim(true);
@@ -1061,6 +1095,7 @@ public class ScrimControllerTest extends SysuiTestCase {
}
@Test
+ @DisableSceneContainer
public void qsExpansion_half_clippingQs() {
reset(mScrimBehind);
mScrimController.setClipsQsScrim(true);
@@ -1074,6 +1109,7 @@ public class ScrimControllerTest extends SysuiTestCase {
}
@Test
+ @DisableSceneContainer
public void panelExpansionAffectsAlpha() {
mScrimController.setRawPanelExpansionFraction(0f);
mScrimController.setRawPanelExpansionFraction(0.5f);
@@ -1096,6 +1132,7 @@ public class ScrimControllerTest extends SysuiTestCase {
}
@Test
+ @DisableSceneContainer
public void transitionToUnlockedFromOff() {
// Simulate unlock with fingerprint without AOD
mScrimController.legacyTransitionTo(ScrimState.OFF);
@@ -1118,6 +1155,7 @@ public class ScrimControllerTest extends SysuiTestCase {
}
@Test
+ @DisableSceneContainer
public void transitionToUnlockedFromAod() {
// Simulate unlock with fingerprint
mScrimController.legacyTransitionTo(ScrimState.AOD);
@@ -1140,6 +1178,7 @@ public class ScrimControllerTest extends SysuiTestCase {
}
@Test
+ @DisableSceneContainer
public void scrimBlanksBeforeLeavingAod() {
// Simulate unlock with fingerprint
mScrimController.legacyTransitionTo(ScrimState.AOD);
@@ -1163,6 +1202,7 @@ public class ScrimControllerTest extends SysuiTestCase {
}
@Test
+ @DisableSceneContainer
public void scrimBlankCallbackWhenUnlockingFromPulse() {
boolean[] blanked = {false};
// Simulate unlock with fingerprint
@@ -1181,6 +1221,7 @@ public class ScrimControllerTest extends SysuiTestCase {
}
@Test
+ @DisableSceneContainer
public void blankingNotRequired_leavingAoD() {
// GIVEN display does NOT need blanking
when(mDozeParameters.getDisplayNeedsBlanking()).thenReturn(false);
@@ -1236,6 +1277,7 @@ public class ScrimControllerTest extends SysuiTestCase {
}
@Test
+ @DisableSceneContainer
public void testScrimCallback() {
int[] callOrder = {0, 0, 0};
int[] currentCall = {0};
@@ -1262,12 +1304,14 @@ public class ScrimControllerTest extends SysuiTestCase {
}
@Test
+ @DisableSceneContainer
public void testScrimCallbacksWithoutAmbientDisplay() {
mAlwaysOnEnabled = false;
testScrimCallback();
}
@Test
+ @DisableSceneContainer
public void testScrimCallbackCancelled() {
boolean[] cancelledCalled = {false};
mScrimController.legacyTransitionTo(ScrimState.AOD, new ScrimController.Callback() {
@@ -1281,6 +1325,7 @@ public class ScrimControllerTest extends SysuiTestCase {
}
@Test
+ @DisableSceneContainer
public void testHoldsWakeLock_whenAOD() {
mScrimController.legacyTransitionTo(ScrimState.AOD);
verify(mWakeLock).acquire(anyString());
@@ -1290,6 +1335,7 @@ public class ScrimControllerTest extends SysuiTestCase {
}
@Test
+ @DisableSceneContainer
public void testDoesNotHoldWakeLock_whenUnlocking() {
mScrimController.legacyTransitionTo(ScrimState.UNLOCKED);
finishAnimationsImmediately();
@@ -1297,6 +1343,7 @@ public class ScrimControllerTest extends SysuiTestCase {
}
@Test
+ @DisableSceneContainer
public void testCallbackInvokedOnSameStateTransition() {
mScrimController.legacyTransitionTo(ScrimState.UNLOCKED);
finishAnimationsImmediately();
@@ -1306,6 +1353,7 @@ public class ScrimControllerTest extends SysuiTestCase {
}
@Test
+ @DisableSceneContainer
public void testConservesExpansionOpacityAfterTransition() {
mScrimController.legacyTransitionTo(ScrimState.UNLOCKED);
mScrimController.setRawPanelExpansionFraction(0.5f);
@@ -1323,6 +1371,7 @@ public class ScrimControllerTest extends SysuiTestCase {
}
@Test
+ @DisableSceneContainer
public void testCancelsOldAnimationBeforeBlanking() {
mScrimController.legacyTransitionTo(ScrimState.AOD);
finishAnimationsImmediately();
@@ -1335,6 +1384,7 @@ public class ScrimControllerTest extends SysuiTestCase {
}
@Test
+ @DisableSceneContainer
public void testScrimsAreNotFocusable() {
assertFalse("Behind scrim should not be focusable", mScrimBehind.isFocusable());
assertFalse("Front scrim should not be focusable", mScrimInFront.isFocusable());
@@ -1343,6 +1393,7 @@ public class ScrimControllerTest extends SysuiTestCase {
}
@Test
+ @DisableSceneContainer
public void testEatsTouchEvent() {
HashSet<ScrimState> eatsTouches =
new HashSet<>(Collections.singletonList(ScrimState.AOD));
@@ -1359,6 +1410,7 @@ public class ScrimControllerTest extends SysuiTestCase {
}
@Test
+ @DisableSceneContainer
public void testAnimatesTransitionToAod() {
when(mDozeParameters.shouldControlScreenOff()).thenReturn(false);
ScrimState.AOD.prepare(ScrimState.KEYGUARD);
@@ -1373,6 +1425,7 @@ public class ScrimControllerTest extends SysuiTestCase {
}
@Test
+ @DisableSceneContainer
public void testIsLowPowerMode() {
HashSet<ScrimState> lowPowerModeStates = new HashSet<>(Arrays.asList(
ScrimState.OFF, ScrimState.AOD, ScrimState.PULSING));
@@ -1390,6 +1443,7 @@ public class ScrimControllerTest extends SysuiTestCase {
}
@Test
+ @DisableSceneContainer
public void testScrimsOpaque_whenShadeFullyExpanded() {
mScrimController.legacyTransitionTo(ScrimState.UNLOCKED);
mScrimController.setRawPanelExpansionFraction(1);
@@ -1404,6 +1458,7 @@ public class ScrimControllerTest extends SysuiTestCase {
}
@Test
+ @DisableSceneContainer
public void testScrimsVisible_whenShadeVisible() {
mScrimController.setClipsQsScrim(true);
mScrimController.legacyTransitionTo(ScrimState.UNLOCKED);
@@ -1419,6 +1474,7 @@ public class ScrimControllerTest extends SysuiTestCase {
}
@Test
+ @DisableSceneContainer
public void testDoesntAnimate_whenUnlocking() {
// LightRevealScrim will animate the transition, we should only hide the keyguard scrims.
ScrimState.UNLOCKED.prepare(ScrimState.KEYGUARD);
@@ -1439,6 +1495,7 @@ public class ScrimControllerTest extends SysuiTestCase {
}
@Test
+ @DisableSceneContainer
public void testScrimsVisible_whenShadeVisible_clippingQs() {
mScrimController.setClipsQsScrim(true);
mScrimController.legacyTransitionTo(ScrimState.UNLOCKED);
@@ -1454,6 +1511,7 @@ public class ScrimControllerTest extends SysuiTestCase {
}
@Test
+ @DisableSceneContainer
public void testScrimsVisible_whenShadeVisibleOnLockscreen() {
mScrimController.legacyTransitionTo(ScrimState.KEYGUARD);
mScrimController.setQsPosition(0.25f, 300);
@@ -1465,6 +1523,7 @@ public class ScrimControllerTest extends SysuiTestCase {
}
@Test
+ @DisableSceneContainer
public void testNotificationScrimTransparent_whenOnLockscreen() {
mScrimController.legacyTransitionTo(ScrimState.KEYGUARD);
// even if shade is not pulled down, panel has expansion of 1 on the lockscreen
@@ -1477,6 +1536,7 @@ public class ScrimControllerTest extends SysuiTestCase {
}
@Test
+ @DisableSceneContainer
public void testNotificationScrimVisible_afterOpeningShadeFromLockscreen() {
mScrimController.setRawPanelExpansionFraction(1);
mScrimController.legacyTransitionTo(SHADE_LOCKED);
@@ -1488,6 +1548,7 @@ public class ScrimControllerTest extends SysuiTestCase {
}
@Test
+ @DisableSceneContainer
public void qsExpansion_BehindTint_shadeLocked_bouncerActive_usesBouncerProgress() {
when(mStatusBarKeyguardViewManager.isPrimaryBouncerInTransit()).thenReturn(true);
// clipping doesn't change tested logic but allows to assert scrims more in line with
@@ -1504,6 +1565,7 @@ public class ScrimControllerTest extends SysuiTestCase {
}
@Test
+ @DisableSceneContainer
public void expansionNotificationAlpha_shadeLocked_bouncerActive_usesBouncerInterpolator() {
when(mStatusBarKeyguardViewManager.isPrimaryBouncerInTransit()).thenReturn(true);
@@ -1520,6 +1582,7 @@ public class ScrimControllerTest extends SysuiTestCase {
}
@Test
+ @DisableSceneContainer
public void expansionNotificationAlpha_shadeLocked_bouncerNotActive_usesShadeInterpolator() {
when(mStatusBarKeyguardViewManager.isPrimaryBouncerInTransit()).thenReturn(false);
@@ -1535,6 +1598,7 @@ public class ScrimControllerTest extends SysuiTestCase {
}
@Test
+ @DisableSceneContainer
public void notificationAlpha_unnocclusionAnimating_bouncerNotActive_usesKeyguardNotifAlpha() {
when(mStatusBarKeyguardViewManager.isPrimaryBouncerInTransit()).thenReturn(false);
@@ -1554,6 +1618,7 @@ public class ScrimControllerTest extends SysuiTestCase {
}
@Test
+ @DisableSceneContainer
public void notificationAlpha_inKeyguardState_bouncerActive_usesInvertedBouncerInterpolator() {
when(mStatusBarKeyguardViewManager.isPrimaryBouncerInTransit()).thenReturn(true);
mScrimController.setClipsQsScrim(true);
@@ -1574,6 +1639,7 @@ public class ScrimControllerTest extends SysuiTestCase {
}
@Test
+ @DisableSceneContainer
public void notificationAlpha_inKeyguardState_bouncerNotActive_usesInvertedShadeInterpolator() {
when(mStatusBarKeyguardViewManager.isPrimaryBouncerInTransit()).thenReturn(false);
mScrimController.setClipsQsScrim(true);
@@ -1594,6 +1660,7 @@ public class ScrimControllerTest extends SysuiTestCase {
}
@Test
+ @DisableSceneContainer
public void behindTint_inKeyguardState_bouncerNotActive_usesKeyguardBehindTint() {
when(mStatusBarKeyguardViewManager.isPrimaryBouncerInTransit()).thenReturn(false);
mScrimController.setClipsQsScrim(false);
@@ -1605,6 +1672,7 @@ public class ScrimControllerTest extends SysuiTestCase {
}
@Test
+ @DisableSceneContainer
public void testNotificationTransparency_followsTransitionToFullShade() {
mScrimController.setClipsQsScrim(true);
@@ -1646,6 +1714,7 @@ public class ScrimControllerTest extends SysuiTestCase {
}
@Test
+ @DisableSceneContainer
public void notificationTransparency_followsNotificationScrimProgress() {
mScrimController.legacyTransitionTo(SHADE_LOCKED);
mScrimController.setRawPanelExpansionFraction(1.0f);
@@ -1662,6 +1731,7 @@ public class ScrimControllerTest extends SysuiTestCase {
}
@Test
+ @DisableSceneContainer
public void notificationAlpha_qsNotClipped_alphaMatchesNotificationExpansionProgress() {
mScrimController.setClipsQsScrim(false);
mScrimController.legacyTransitionTo(ScrimState.KEYGUARD);
@@ -1697,6 +1767,7 @@ public class ScrimControllerTest extends SysuiTestCase {
}
@Test
+ @DisableSceneContainer
public void setNotificationsOverScrollAmount_setsTranslationYOnNotificationsScrim() {
int overScrollAmount = 10;
@@ -1706,6 +1777,7 @@ public class ScrimControllerTest extends SysuiTestCase {
}
@Test
+ @DisableSceneContainer
public void setNotificationsOverScrollAmount_doesNotSetTranslationYOnBehindScrim() {
int overScrollAmount = 10;
@@ -1715,6 +1787,7 @@ public class ScrimControllerTest extends SysuiTestCase {
}
@Test
+ @DisableSceneContainer
public void setNotificationsOverScrollAmount_doesNotSetTranslationYOnFrontScrim() {
int overScrollAmount = 10;
@@ -1724,6 +1797,7 @@ public class ScrimControllerTest extends SysuiTestCase {
}
@Test
+ @DisableSceneContainer
public void notificationBoundsTopGetsPassedToKeyguard() {
mScrimController.legacyTransitionTo(SHADE_LOCKED);
mScrimController.setQsPosition(1f, 0);
@@ -1734,6 +1808,7 @@ public class ScrimControllerTest extends SysuiTestCase {
}
@Test
+ @DisableSceneContainer
public void notificationBoundsTopDoesNotGetPassedToKeyguardWhenNotifScrimIsNotVisible() {
mScrimController.setKeyguardOccluded(true);
mScrimController.legacyTransitionTo(ScrimState.KEYGUARD);
@@ -1744,6 +1819,7 @@ public class ScrimControllerTest extends SysuiTestCase {
}
@Test
+ @DisableSceneContainer
public void transitionToDreaming() {
mScrimController.setRawPanelExpansionFraction(0f);
mScrimController.setBouncerHiddenFraction(KeyguardBouncerConstants.EXPANSION_HIDDEN);
@@ -1763,6 +1839,7 @@ public class ScrimControllerTest extends SysuiTestCase {
}
@Test
+ @DisableSceneContainer
public void keyguardGoingAwayUpdateScrims() {
when(mKeyguardStateController.isKeyguardGoingAway()).thenReturn(true);
mScrimController.updateScrims();
@@ -1772,6 +1849,7 @@ public class ScrimControllerTest extends SysuiTestCase {
@Test
+ @DisableSceneContainer
public void setUnOccludingAnimationKeyguard() {
mScrimController.legacyTransitionTo(ScrimState.KEYGUARD);
finishAnimationsImmediately();
@@ -1786,6 +1864,7 @@ public class ScrimControllerTest extends SysuiTestCase {
}
@Test
+ @DisableSceneContainer
public void testHidesScrimFlickerInActivity() {
mScrimController.setKeyguardOccluded(true);
mScrimController.legacyTransitionTo(ScrimState.KEYGUARD);
@@ -1804,6 +1883,7 @@ public class ScrimControllerTest extends SysuiTestCase {
}
@Test
+ @DisableSceneContainer
public void notificationAlpha_inKeyguardState_bouncerNotActive_clipsQsScrimFalse() {
mScrimController.setClipsQsScrim(false);
mScrimController.legacyTransitionTo(ScrimState.KEYGUARD);
@@ -1813,6 +1893,7 @@ public class ScrimControllerTest extends SysuiTestCase {
}
@Test
+ @DisableSceneContainer
public void aodStateSetsFrontScrimToNotBlend() {
mScrimController.legacyTransitionTo(ScrimState.AOD);
assertFalse("Front scrim should not blend with main color",
@@ -1820,6 +1901,7 @@ public class ScrimControllerTest extends SysuiTestCase {
}
@Test
+ @DisableSceneContainer
public void applyState_unlocked_bouncerShowing() {
mScrimController.legacyTransitionTo(ScrimState.UNLOCKED);
mScrimController.setBouncerHiddenFraction(0.99f);
@@ -1829,6 +1911,7 @@ public class ScrimControllerTest extends SysuiTestCase {
}
@Test
+ @DisableSceneContainer
public void ignoreTransitionRequestWhileKeyguardTransitionRunning() {
mScrimController.legacyTransitionTo(ScrimState.UNLOCKED);
mScrimController.mBouncerToGoneTransition.accept(
@@ -1841,6 +1924,7 @@ public class ScrimControllerTest extends SysuiTestCase {
}
@Test
+ @DisableSceneContainer
public void primaryBouncerToGoneOnFinishCallsKeyguardFadedAway() {
when(mKeyguardStateController.isKeyguardFadingAway()).thenReturn(true);
mScrimController.mBouncerToGoneTransition.accept(
@@ -1851,6 +1935,7 @@ public class ScrimControllerTest extends SysuiTestCase {
}
@Test
+ @DisableSceneContainer
public void primaryBouncerToGoneOnFinishCallsLightBarController() {
reset(mLightBarController);
mScrimController.mBouncerToGoneTransition.accept(
@@ -1862,6 +1947,7 @@ public class ScrimControllerTest extends SysuiTestCase {
}
@Test
+ @DisableSceneContainer
public void testDoNotAnimateChangeIfOccludeAnimationPlaying() {
mScrimController.setOccludeAnimationPlaying(true);
mScrimController.legacyTransitionTo(ScrimState.UNLOCKED);
@@ -1870,6 +1956,7 @@ public class ScrimControllerTest extends SysuiTestCase {
}
@Test
+ @DisableSceneContainer
public void testNotifScrimAlpha_1f_afterUnlockFinishedAndExpanded() {
mScrimController.legacyTransitionTo(ScrimState.KEYGUARD);
when(mKeyguardUnlockAnimationController.isPlayingCannedUnlockAnimation()).thenReturn(true);
@@ -1942,9 +2029,9 @@ public class ScrimControllerTest extends SysuiTestCase {
// Check combined scrim visibility.
final int visibility;
- if (scrimToAlpha.values().contains(OPAQUE)) {
+ if (scrimToAlpha.containsValue(OPAQUE)) {
visibility = OPAQUE;
- } else if (scrimToAlpha.values().contains(SEMI_TRANSPARENT)) {
+ } else if (scrimToAlpha.containsValue(SEMI_TRANSPARENT)) {
visibility = SEMI_TRANSPARENT;
} else {
visibility = TRANSPARENT;
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
index dde6e2ee1866..dde6e2ee1866 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/temporarydisplay/TouchableRegionViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TouchableRegionViewControllerTest.kt
index a230f0630d6e..a230f0630d6e 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/temporarydisplay/TouchableRegionViewControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TouchableRegionViewControllerTest.kt
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/unfold/FoldAodAnimationControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/unfold/FoldAodAnimationControllerTest.kt
index 1135a5f86952..1135a5f86952 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/unfold/FoldAodAnimationControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/unfold/FoldAodAnimationControllerTest.kt
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/unfold/UnfoldLatencyTrackerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/unfold/UnfoldLatencyTrackerTest.kt
index a1122c3cbcd2..a1122c3cbcd2 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/unfold/UnfoldLatencyTrackerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/unfold/UnfoldLatencyTrackerTest.kt
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/unfold/progress/PhysicsBasedUnfoldTransitionProgressProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/unfold/progress/PhysicsBasedUnfoldTransitionProgressProviderTest.kt
index c7b685fba455..c7b685fba455 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/unfold/progress/PhysicsBasedUnfoldTransitionProgressProviderTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/unfold/progress/PhysicsBasedUnfoldTransitionProgressProviderTest.kt
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/unfold/progress/UnfoldRemoteFilterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/unfold/progress/UnfoldRemoteFilterTest.kt
index b93c161a7039..b93c161a7039 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/unfold/progress/UnfoldRemoteFilterTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/unfold/progress/UnfoldRemoteFilterTest.kt
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/usb/UsbPermissionActivityTest.kt b/packages/SystemUI/tests/src/com/android/systemui/usb/UsbPermissionActivityTest.kt
index 32c598612aa6..32c598612aa6 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/usb/UsbPermissionActivityTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/usb/UsbPermissionActivityTest.kt
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/user/CreateUserActivityTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/CreateUserActivityTest.kt
index 25ceea951d3c..25ceea951d3c 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/user/CreateUserActivityTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/user/CreateUserActivityTest.kt
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/util/kotlin/FlowUtilTests.kt b/packages/SystemUI/tests/src/com/android/systemui/util/kotlin/FlowUtilTests.kt
index 9440280649dd..9440280649dd 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/util/kotlin/FlowUtilTests.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/util/kotlin/FlowUtilTests.kt
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/util/kotlin/PackageManagerExtComponentEnabledTest.kt b/packages/SystemUI/tests/src/com/android/systemui/util/kotlin/PackageManagerExtComponentEnabledTest.kt
index b3f2113f86ec..b3f2113f86ec 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/util/kotlin/PackageManagerExtComponentEnabledTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/util/kotlin/PackageManagerExtComponentEnabledTest.kt
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/util/sensors/AsyncSensorManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/util/sensors/AsyncSensorManagerTest.java
index c896fc0bfb8a..c896fc0bfb8a 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/util/sensors/AsyncSensorManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/util/sensors/AsyncSensorManagerTest.java
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/wallpapers/GradientColorWallpaperTest.kt b/packages/SystemUI/tests/src/com/android/systemui/wallpapers/GradientColorWallpaperTest.kt
index b4fbaad6ab37..b4fbaad6ab37 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/wallpapers/GradientColorWallpaperTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/wallpapers/GradientColorWallpaperTest.kt
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/activity/data/repository/ActivityManagerRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/activity/data/repository/ActivityManagerRepositoryKosmos.kt
index a6e71333c816..5dc28bea9bc4 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/activity/data/repository/ActivityManagerRepositoryKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/activity/data/repository/ActivityManagerRepositoryKosmos.kt
@@ -17,33 +17,77 @@
package com.android.systemui.activity.data.repository
import android.app.activityManager
+import com.android.systemui.activity.data.model.AppVisibilityModel
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.testDispatcher
import com.android.systemui.log.core.Logger
+import com.android.systemui.util.time.SystemClock
+import com.android.systemui.util.time.fakeSystemClock
+import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
-val Kosmos.activityManagerRepository by Kosmos.Fixture { FakeActivityManagerRepository() }
+val Kosmos.activityManagerRepository by
+ Kosmos.Fixture { FakeActivityManagerRepository(fakeSystemClock) }
val Kosmos.realActivityManagerRepository by
- Kosmos.Fixture { ActivityManagerRepositoryImpl(testDispatcher, activityManager) }
+ Kosmos.Fixture {
+ ActivityManagerRepositoryImpl(testDispatcher, fakeSystemClock, activityManager)
+ }
-class FakeActivityManagerRepository : ActivityManagerRepository {
- private val uidFlows = mutableMapOf<Int, MutableList<MutableStateFlow<Boolean>>>()
+class FakeActivityManagerRepository(private val systemClock: SystemClock) :
+ ActivityManagerRepository {
+ private val isVisibleFlows = mutableMapOf<Int, MutableList<MutableStateFlow<Boolean>>>()
+ private val appVisibilityFlows =
+ mutableMapOf<Int, MutableList<MutableStateFlow<AppVisibilityModel>>>()
var startingIsAppVisibleValue = false
+ override fun createAppVisibilityFlow(
+ creationUid: Int,
+ logger: Logger,
+ identifyingLogTag: String,
+ ): Flow<AppVisibilityModel> {
+ val newFlow =
+ MutableStateFlow(
+ if (startingIsAppVisibleValue) {
+ AppVisibilityModel(
+ isAppCurrentlyVisible = true,
+ lastAppVisibleTime = systemClock.currentTimeMillis(),
+ )
+ } else {
+ AppVisibilityModel(isAppCurrentlyVisible = false, lastAppVisibleTime = null)
+ }
+ )
+ appVisibilityFlows.computeIfAbsent(creationUid) { mutableListOf() }.add(newFlow)
+ return newFlow
+ }
+
override fun createIsAppVisibleFlow(
creationUid: Int,
logger: Logger,
identifyingLogTag: String,
): MutableStateFlow<Boolean> {
val newFlow = MutableStateFlow(startingIsAppVisibleValue)
- uidFlows.computeIfAbsent(creationUid) { mutableListOf() }.add(newFlow)
+ isVisibleFlows.computeIfAbsent(creationUid) { mutableListOf() }.add(newFlow)
return newFlow
}
fun setIsAppVisible(uid: Int, isAppVisible: Boolean) {
- uidFlows[uid]?.forEach { stateFlow -> stateFlow.value = isAppVisible }
+ isVisibleFlows[uid]?.forEach { stateFlow -> stateFlow.value = isAppVisible }
+ appVisibilityFlows[uid]?.forEach { stateFlow ->
+ stateFlow.value =
+ if (isAppVisible) {
+ AppVisibilityModel(
+ isAppCurrentlyVisible = true,
+ lastAppVisibleTime = systemClock.currentTimeMillis(),
+ )
+ } else {
+ AppVisibilityModel(
+ isAppCurrentlyVisible = false,
+ stateFlow.value.lastAppVisibleTime,
+ )
+ }
+ }
}
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractorKosmos.kt
index 4068a2290559..b781f61723f2 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractorKosmos.kt
@@ -20,7 +20,7 @@ import com.android.systemui.keyguard.data.repository.keyguardClockRepository
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.applicationCoroutineScope
import com.android.systemui.media.controls.domain.pipeline.interactor.mediaCarouselInteractor
-import com.android.systemui.shade.domain.interactor.shadeInteractor
+import com.android.systemui.shade.domain.interactor.shadeModeInteractor
import com.android.systemui.statusbar.notification.domain.interactor.activeNotificationsInteractor
import com.android.systemui.statusbar.notification.promoted.domain.interactor.aodPromotedNotificationInteractor
import com.android.systemui.statusbar.notification.stack.domain.interactor.headsUpNotificationInteractor
@@ -32,7 +32,7 @@ val Kosmos.keyguardClockInteractor by
mediaCarouselInteractor = mediaCarouselInteractor,
activeNotificationsInteractor = activeNotificationsInteractor,
aodPromotedNotificationInteractor = aodPromotedNotificationInteractor,
- shadeInteractor = shadeInteractor,
+ shadeModeInteractor = shadeModeInteractor,
keyguardInteractor = keyguardInteractor,
keyguardTransitionInteractor = keyguardTransitionInteractor,
headsUpNotificationInteractor = headsUpNotificationInteractor,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModelKosmos.kt
index c0b39b1df7d5..5dc19a340dd0 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModelKosmos.kt
@@ -22,7 +22,7 @@ import com.android.systemui.common.ui.domain.interactor.configurationInteractor
import com.android.systemui.keyguard.domain.interactor.keyguardClockInteractor
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.applicationCoroutineScope
-import com.android.systemui.shade.domain.interactor.shadeInteractor
+import com.android.systemui.shade.domain.interactor.shadeModeInteractor
import com.android.systemui.statusbar.notification.icon.ui.viewmodel.notificationIconContainerAlwaysOnDisplayViewModel
import com.android.systemui.statusbar.ui.systemBarUtilsProxy
@@ -33,7 +33,7 @@ val Kosmos.keyguardClockViewModel by
keyguardClockInteractor = keyguardClockInteractor,
applicationScope = applicationCoroutineScope,
aodNotificationIconViewModel = notificationIconContainerAlwaysOnDisplayViewModel,
- shadeInteractor = shadeInteractor,
+ shadeModeInteractor = shadeModeInteractor,
systemBarUtils = systemBarUtilsProxy,
configurationInteractor = configurationInteractor,
resources = mainResources,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardMediaViewModelFactoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardMediaViewModelFactoryKosmos.kt
index 16d3fdc26613..345d69aa8df0 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardMediaViewModelFactoryKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardMediaViewModelFactoryKosmos.kt
@@ -19,12 +19,17 @@ package com.android.systemui.keyguard.ui.viewmodel
import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.media.controls.domain.pipeline.interactor.mediaCarouselInteractor
+import com.android.systemui.shade.domain.interactor.shadeModeInteractor
val Kosmos.keyguardMediaViewModelFactory by
Kosmos.Fixture {
object : KeyguardMediaViewModel.Factory {
override fun create(): KeyguardMediaViewModel {
- return KeyguardMediaViewModel(mediaCarouselInteractor, keyguardInteractor)
+ return KeyguardMediaViewModel(
+ mediaCarouselInteractor = mediaCarouselInteractor,
+ keyguardInteractor = keyguardInteractor,
+ shadeModeInteractor = shadeModeInteractor,
+ )
}
}
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelKosmos.kt
index dd13b8b143ae..b751e213152e 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelKosmos.kt
@@ -25,7 +25,7 @@ import com.android.systemui.keyguard.shared.transition.KeyguardTransitionAnimati
import com.android.systemui.keyguard.shared.transition.keyguardTransitionAnimationCallbackDelegator
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.Kosmos.Fixture
-import com.android.systemui.shade.domain.interactor.shadeInteractor
+import com.android.systemui.shade.domain.interactor.shadeModeInteractor
import com.android.systemui.unfold.domain.interactor.unfoldTransitionInteractor
val Kosmos.lockscreenContentViewModelFactory by Fixture {
@@ -38,7 +38,7 @@ val Kosmos.lockscreenContentViewModelFactory by Fixture {
interactor = keyguardBlueprintInteractor,
authController = authController,
touchHandling = keyguardTouchHandlingViewModel,
- shadeInteractor = shadeInteractor,
+ shadeModeInteractor = shadeModeInteractor,
unfoldTransitionInteractor = unfoldTransitionInteractor,
deviceEntryInteractor = deviceEntryInteractor,
transitionInteractor = keyguardTransitionInteractor,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/FakeSceneDataSource.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/FakeSceneDataSource.kt
index 60c0f342b874..f9917ac680e0 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/FakeSceneDataSource.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/FakeSceneDataSource.kt
@@ -44,6 +44,8 @@ class FakeSceneDataSource(initialSceneKey: SceneKey, val testScope: TestScope) :
var pendingOverlays: Set<OverlayKey>? = null
private set
+ var freezeAndAnimateToCurrentStateCallCount = 0
+
override fun changeScene(toScene: SceneKey, transitionKey: TransitionKey?) {
if (_isPaused) {
_pendingScene = toScene
@@ -85,6 +87,10 @@ class FakeSceneDataSource(initialSceneKey: SceneKey, val testScope: TestScope) :
hideOverlay(overlay)
}
+ override fun freezeAndAnimateToCurrentState() {
+ freezeAndAnimateToCurrentStateCallCount++
+ }
+
/**
* Pauses scene and overlay changes.
*
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeDisplayChangeLatencyTrackerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeDisplayChangeLatencyTrackerKosmos.kt
index 67dd0ad896d5..0892e66b8b86 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeDisplayChangeLatencyTrackerKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeDisplayChangeLatencyTrackerKosmos.kt
@@ -27,7 +27,7 @@ import java.util.Optional
val Kosmos.shadeDisplayChangeLatencyTracker by Fixture {
ShadeDisplayChangeLatencyTracker(
- Optional.of(mockShadeRootView),
+ mockShadeRootView,
configurationRepository,
latencyTracker,
testScope.backgroundScope,
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 46314135c574..1397d974cbc5 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
@@ -29,7 +29,6 @@ import com.android.systemui.statusbar.notification.domain.interactor.activeNotif
import com.android.systemui.statusbar.notification.row.notificationRebindingTracker
import com.android.systemui.statusbar.notification.stack.notificationStackRebindingHider
import com.android.systemui.statusbar.policy.configurationController
-import java.util.Optional
import org.mockito.kotlin.any
import org.mockito.kotlin.mock
import org.mockito.kotlin.whenever
@@ -55,11 +54,11 @@ val Kosmos.shadeDisplaysInteractor by
testScope.backgroundScope,
testScope.backgroundScope.coroutineContext,
mockedShadeDisplayChangeLatencyTracker,
- Optional.of(shadeExpandedStateInteractor),
+ shadeExpandedStateInteractor,
shadeExpansionIntent,
activeNotificationsInteractor,
notificationRebindingTracker,
- Optional.of(notificationStackRebindingHider),
+ notificationStackRebindingHider,
configurationController,
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeOverlayContentViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeOverlayContentViewModelKosmos.kt
index 20e4523fda0f..55e35f2b2703 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeOverlayContentViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeOverlayContentViewModelKosmos.kt
@@ -18,9 +18,11 @@ package com.android.systemui.shade.ui.viewmodel
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.media.controls.domain.pipeline.interactor.mediaCarouselInteractor
import com.android.systemui.notifications.ui.viewmodel.NotificationsShadeOverlayContentViewModel
import com.android.systemui.scene.domain.interactor.sceneInteractor
import com.android.systemui.shade.domain.interactor.shadeInteractor
+import com.android.systemui.statusbar.disableflags.domain.interactor.disableFlagsInteractor
import com.android.systemui.statusbar.notification.domain.interactor.activeNotificationsInteractor
import com.android.systemui.statusbar.notification.stack.ui.viewmodel.notificationsPlaceholderViewModelFactory
@@ -31,6 +33,8 @@ val Kosmos.notificationsShadeOverlayContentViewModel:
notificationsPlaceholderViewModelFactory = notificationsPlaceholderViewModelFactory,
sceneInteractor = sceneInteractor,
shadeInteractor = shadeInteractor,
+ disableFlagsInteractor = disableFlagsInteractor,
+ mediaCarouselInteractor = mediaCarouselInteractor,
activeNotificationsInteractor = activeNotificationsInteractor,
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelKosmos.kt
index 878c2deb43b2..d8e0cfe4fbf8 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelKosmos.kt
@@ -21,6 +21,7 @@ import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.applicationCoroutineScope
import com.android.systemui.statusbar.chips.notification.domain.interactor.statusBarNotificationChipsInteractor
import com.android.systemui.statusbar.notification.stack.domain.interactor.headsUpNotificationInteractor
+import com.android.systemui.util.time.fakeSystemClock
val Kosmos.notifChipsViewModel: NotifChipsViewModel by
Kosmos.Fixture {
@@ -29,5 +30,6 @@ val Kosmos.notifChipsViewModel: NotifChipsViewModel by
applicationCoroutineScope,
statusBarNotificationChipsInteractor,
headsUpNotificationInteractor,
+ fakeSystemClock,
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/NotificationEntryBuilderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/NotificationEntryBuilderKosmos.kt
new file mode 100644
index 000000000000..59f5ecd2563f
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/NotificationEntryBuilderKosmos.kt
@@ -0,0 +1,90 @@
+/*
+ * 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
+
+import android.app.Notification
+import android.app.PendingIntent
+import android.app.Person
+import android.content.Intent
+import android.content.applicationContext
+import android.graphics.drawable.Icon
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
+import com.android.systemui.statusbar.notification.icon.IconPack
+import com.android.systemui.statusbar.notification.promoted.setPromotedContent
+import org.mockito.kotlin.mock
+
+fun Kosmos.setIconPackWithMockIconViews(entry: NotificationEntry) {
+ entry.icons =
+ IconPack.buildPack(
+ /* statusBarIcon = */ mock(),
+ /* statusBarChipIcon = */ mock(),
+ /* shelfIcon = */ mock(),
+ /* aodIcon = */ mock(),
+ /* source = */ null,
+ )
+}
+
+fun Kosmos.buildOngoingCallEntry(
+ promoted: Boolean = false,
+ block: NotificationEntryBuilder.() -> Unit = {},
+): NotificationEntry =
+ buildNotificationEntry(
+ tag = "call",
+ promoted = promoted,
+ style = makeOngoingCallStyle(),
+ block = block,
+ )
+
+fun Kosmos.buildPromotedOngoingEntry(
+ block: NotificationEntryBuilder.() -> Unit = {}
+): NotificationEntry =
+ buildNotificationEntry(tag = "ron", promoted = true, style = null, block = block)
+
+fun Kosmos.buildNotificationEntry(
+ tag: String? = null,
+ promoted: Boolean = false,
+ style: Notification.Style? = null,
+ block: NotificationEntryBuilder.() -> Unit = {},
+): NotificationEntry =
+ NotificationEntryBuilder()
+ .apply {
+ setTag(tag)
+ setFlag(applicationContext, Notification.FLAG_PROMOTED_ONGOING, promoted)
+ modifyNotification(applicationContext)
+ .setSmallIcon(Icon.createWithContentUri("content://null"))
+ .setStyle(style)
+ }
+ .apply(block)
+ .build()
+ .also {
+ setIconPackWithMockIconViews(it)
+ if (promoted) setPromotedContent(it)
+ }
+
+private fun Kosmos.makeOngoingCallStyle(): Notification.CallStyle {
+ val pendingIntent =
+ PendingIntent.getBroadcast(
+ applicationContext,
+ 0,
+ Intent("action"),
+ PendingIntent.FLAG_IMMUTABLE,
+ )
+ val person = Person.Builder().setName("person").build()
+ return Notification.CallStyle.forOngoingCall(person, pendingIntent)
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/NotifPipelineKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/NotifPipelineKosmos.kt
index a48b27015c02..fa3702cea5ee 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/NotifPipelineKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/NotifPipelineKosmos.kt
@@ -17,6 +17,7 @@
package com.android.systemui.statusbar.notification.collection
import com.android.systemui.kosmos.Kosmos
-import com.android.systemui.util.mockito.mock
+import org.mockito.kotlin.mock
-var Kosmos.notifPipeline by Kosmos.Fixture { mock<NotifPipeline>() }
+var Kosmos.notifPipeline by Kosmos.Fixture { mockNotifPipeline }
+var Kosmos.mockNotifPipeline by Kosmos.Fixture { mock<NotifPipeline>() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/icon/domain/interactor/NotificationIconsInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/icon/domain/interactor/NotificationIconsInteractorKosmos.kt
index dc7595f7f2e4..87e0a0f0dda3 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/icon/domain/interactor/NotificationIconsInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/icon/domain/interactor/NotificationIconsInteractorKosmos.kt
@@ -24,6 +24,7 @@ import com.android.systemui.statusbar.data.repository.notificationListenerSettin
import com.android.systemui.statusbar.notification.data.repository.notificationsKeyguardViewStateRepository
import com.android.systemui.statusbar.notification.domain.interactor.activeNotificationsInteractor
import com.android.systemui.statusbar.notification.domain.interactor.headsUpNotificationIconInteractor
+import com.android.systemui.statusbar.notification.promoted.domain.interactor.aodPromotedNotificationInteractor
import com.android.wm.shell.bubbles.bubblesOptional
val Kosmos.alwaysOnDisplayNotificationIconsInteractor by Fixture {
@@ -47,6 +48,7 @@ val Kosmos.notificationIconsInteractor by Fixture {
activeNotificationsInteractor = activeNotificationsInteractor,
bubbles = bubblesOptional,
headsUpNotificationIconInteractor = headsUpNotificationIconInteractor,
+ aodPromotedNotificationInteractor = aodPromotedNotificationInteractor,
keyguardViewStateRepository = notificationsKeyguardViewStateRepository,
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractorKosmos.kt
index 63521de096c9..e55cd0dc16f4 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractorKosmos.kt
@@ -16,8 +16,11 @@
package com.android.systemui.statusbar.notification.promoted
+import android.app.Notification
import android.content.applicationContext
import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import com.android.systemui.statusbar.notification.row.RowImageInflater
import com.android.systemui.statusbar.notification.row.shared.skeletonImageTransform
var Kosmos.promotedNotificationContentExtractor by
@@ -28,3 +31,14 @@ var Kosmos.promotedNotificationContentExtractor by
promotedNotificationLogger,
)
}
+
+fun Kosmos.setPromotedContent(entry: NotificationEntry) {
+ val extractedContent =
+ promotedNotificationContentExtractor.extractContent(
+ entry,
+ Notification.Builder.recoverBuilder(applicationContext, entry.sbn.notification),
+ RowImageInflater.newInstance(null).useForContentModel(),
+ )
+ entry.promotedNotificationContentModel =
+ requireNotNull(extractedContent) { "extractContent returned null" }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/AODPromotedNotificationInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/AODPromotedNotificationInteractorKosmos.kt
index df1c82278bc2..fcd484353011 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/AODPromotedNotificationInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/AODPromotedNotificationInteractorKosmos.kt
@@ -18,12 +18,11 @@ package com.android.systemui.statusbar.notification.promoted.domain.interactor
import com.android.systemui.dump.dumpManager
import com.android.systemui.kosmos.Kosmos
-import com.android.systemui.statusbar.notification.domain.interactor.activeNotificationsInteractor
val Kosmos.aodPromotedNotificationInteractor by
Kosmos.Fixture {
AODPromotedNotificationInteractor(
- activeNotificationsInteractor = activeNotificationsInteractor,
+ promotedNotificationsInteractor = promotedNotificationsInteractor,
dumpManager = dumpManager,
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/PromotedNotificationsInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/PromotedNotificationsInteractorKosmos.kt
new file mode 100644
index 000000000000..093ec10e2642
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/PromotedNotificationsInteractorKosmos.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.statusbar.notification.promoted.domain.interactor
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.statusbar.chips.call.domain.interactor.callChipInteractor
+import com.android.systemui.statusbar.chips.notification.domain.interactor.statusBarNotificationChipsInteractor
+import com.android.systemui.statusbar.notification.domain.interactor.activeNotificationsInteractor
+
+val Kosmos.promotedNotificationsInteractor by
+ Kosmos.Fixture {
+ PromotedNotificationsInteractor(
+ activeNotificationsInteractor = activeNotificationsInteractor,
+ callChipInteractor = callChipInteractor,
+ notifChipsInteractor = statusBarNotificationChipsInteractor,
+ backgroundDispatcher = testDispatcher,
+ )
+ }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowBuilder.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowBuilder.kt
index e445a73b06d0..8b19491bfdf8 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowBuilder.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowBuilder.kt
@@ -41,6 +41,7 @@ import com.android.systemui.media.controls.util.MediaFeatureFlag
import com.android.systemui.media.dialog.MediaOutputDialogManager
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.settings.UserTracker
import com.android.systemui.shared.system.ActivityManagerWrapper
import com.android.systemui.shared.system.DevicePolicyManagerWrapper
import com.android.systemui.shared.system.PackageManagerWrapper
@@ -346,10 +347,15 @@ class ExpandableNotificationRowBuilder(
// NOTE: This flag is read when the ExpandableNotificationRow is inflated, so it needs to be
// set, but we do not want to override an existing value that is needed by a specific test.
+ val userTracker = Mockito.mock(UserTracker::class.java, STUB_ONLY)
+ whenever(userTracker.userHandle).thenReturn(context.user)
+
val rowInflaterTask =
RowInflaterTask(
mFakeSystemClock,
Mockito.mock(RowInflaterTaskLogger::class.java, STUB_ONLY),
+ userTracker,
+ Mockito.mock(AsyncRowInflater::class.java, STUB_ONLY),
)
val row = rowInflaterTask.inflateSynchronously(context, null, entry)
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinderKosmos.kt
index bc1363ac3d5c..970b87cd368a 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinderKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinderKosmos.kt
@@ -33,7 +33,7 @@ import java.util.Optional
val Kosmos.notificationListViewBinder by Fixture {
NotificationListViewBinder(
- backgroundDispatcher = testDispatcher,
+ inflationDispatcher = testDispatcher,
hiderTracker = displaySwitchNotificationsHiderTracker,
configuration = configurationState,
falsingManager = falsingManager,
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 047bd13f0c27..7a2b7c24252b 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
@@ -29,7 +29,6 @@ import com.android.systemui.keyguard.ui.viewmodel.aodToGoneTransitionViewModel
import com.android.systemui.keyguard.ui.viewmodel.aodToLockscreenTransitionViewModel
import com.android.systemui.keyguard.ui.viewmodel.aodToOccludedTransitionViewModel
import com.android.systemui.keyguard.ui.viewmodel.aodToPrimaryBouncerTransitionViewModel
-import com.android.systemui.keyguard.ui.viewmodel.dozingToDreamingTransitionViewModel
import com.android.systemui.keyguard.ui.viewmodel.dozingToGlanceableHubTransitionViewModel
import com.android.systemui.keyguard.ui.viewmodel.dozingToLockscreenTransitionViewModel
import com.android.systemui.keyguard.ui.viewmodel.dozingToOccludedTransitionViewModel
@@ -82,7 +81,6 @@ val Kosmos.sharedNotificationContainerViewModel by Fixture {
aodToLockscreenTransitionViewModel = aodToLockscreenTransitionViewModel,
aodToOccludedTransitionViewModel = aodToOccludedTransitionViewModel,
aodToPrimaryBouncerTransitionViewModel = aodToPrimaryBouncerTransitionViewModel,
- dozingToDreamingTransitionViewModel = dozingToDreamingTransitionViewModel,
dozingToGlanceableHubTransitionViewModel = dozingToGlanceableHubTransitionViewModel,
dozingToLockscreenTransitionViewModel = dozingToLockscreenTransitionViewModel,
dozingToOccludedTransitionViewModel = dozingToOccludedTransitionViewModel,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/util/settings/data/repository/SecureSettingsForUserRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/util/settings/data/repository/SecureSettingsForUserRepositoryKosmos.kt
new file mode 100644
index 000000000000..81f71e9f7b2f
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/util/settings/data/repository/SecureSettingsForUserRepositoryKosmos.kt
@@ -0,0 +1,28 @@
+/*
+ * 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.util.settings.data.repository
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.backgroundCoroutineContext
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.util.settings.fakeSettings
+import com.android.systemui.util.settings.repository.SecureSettingsForUserRepository
+
+val Kosmos.secureSettingsForUserRepository by
+ Kosmos.Fixture {
+ SecureSettingsForUserRepository(fakeSettings, testDispatcher, backgroundCoroutineContext)
+ }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/util/time/FakeSystemClockKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/util/time/FakeSystemClockKosmos.kt
index 703d6ad83eac..a209ec9d0c9c 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/util/time/FakeSystemClockKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/util/time/FakeSystemClockKosmos.kt
@@ -31,3 +31,6 @@ val Kosmos.systemClock by
}
val Kosmos.fakeSystemClock by Kosmos.Fixture { FakeSystemClock() }
+
+val SystemClock.fake
+ get() = this as FakeSystemClock
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioSharingStreamSliderViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioSharingStreamSliderViewModelKosmos.kt
index 96bc9722635a..8c8d0240f572 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioSharingStreamSliderViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioSharingStreamSliderViewModelKosmos.kt
@@ -16,11 +16,11 @@
package com.android.systemui.volume.panel.component.volume.slider.ui.viewmodel
-import android.content.applicationContext
import com.android.internal.logging.uiEventLogger
import com.android.systemui.haptics.slider.sliderHapticsViewModelFactory
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.volume.domain.interactor.audioSharingInteractor
+import com.android.systemui.volume.shared.volumePanelLogger
import kotlinx.coroutines.CoroutineScope
val Kosmos.audioSharingStreamSliderViewModelFactory by
@@ -29,10 +29,10 @@ val Kosmos.audioSharingStreamSliderViewModelFactory by
override fun create(coroutineScope: CoroutineScope): AudioSharingStreamSliderViewModel {
return AudioSharingStreamSliderViewModel(
coroutineScope,
- applicationContext,
audioSharingInteractor,
uiEventLogger,
sliderHapticsViewModelFactory,
+ volumePanelLogger,
)
}
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModelKosmos.kt
index abd4235143f1..6875619d45fc 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModelKosmos.kt
@@ -21,6 +21,7 @@ import com.android.systemui.haptics.slider.sliderHapticsViewModelFactory
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.volume.mediaDeviceSessionInteractor
import com.android.systemui.volume.panel.component.mediaoutput.shared.model.MediaDeviceSession
+import com.android.systemui.volume.shared.volumePanelLogger
import kotlinx.coroutines.CoroutineScope
val Kosmos.castVolumeSliderViewModelFactory by
@@ -36,6 +37,7 @@ val Kosmos.castVolumeSliderViewModelFactory by
applicationContext,
mediaDeviceSessionInteractor,
sliderHapticsViewModelFactory,
+ volumePanelLogger,
)
}
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/wallpapers/data/repository/FakeWallpaperFocalAreaRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/wallpapers/data/repository/FakeWallpaperFocalAreaRepository.kt
index aeff86ed89bb..24d2f1f0d901 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/wallpapers/data/repository/FakeWallpaperFocalAreaRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/wallpapers/data/repository/FakeWallpaperFocalAreaRepository.kt
@@ -34,12 +34,15 @@ class FakeWallpaperFocalAreaRepository : WallpaperFocalAreaRepository {
_wallpaperFocalAreaBounds.asStateFlow()
private val _wallpaperFocalAreaTapPosition = MutableStateFlow(PointF(0F, 0F))
- override val wallpaperFocalAreaTapPosition: StateFlow<PointF> =
+ val wallpaperFocalAreaTapPosition: StateFlow<PointF> =
_wallpaperFocalAreaTapPosition.asStateFlow()
private val _notificationDefaultTop = MutableStateFlow(0F)
override val notificationDefaultTop: StateFlow<Float> = _notificationDefaultTop.asStateFlow()
+ private val _hasFocalArea = MutableStateFlow(false)
+ override val hasFocalArea: StateFlow<Boolean> = _hasFocalArea.asStateFlow()
+
override fun setShortcutAbsoluteTop(top: Float) {
_shortcutAbsoluteTop.value = top
}
@@ -56,7 +59,7 @@ class FakeWallpaperFocalAreaRepository : WallpaperFocalAreaRepository {
_wallpaperFocalAreaBounds.value = bounds
}
- override fun setTapPosition(point: PointF) {
- _wallpaperFocalAreaTapPosition.value = point
+ override fun setTapPosition(tapPosition: PointF) {
+ _wallpaperFocalAreaTapPosition.value = tapPosition
}
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/wallpapers/data/repository/FakeWallpaperRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/wallpapers/data/repository/FakeWallpaperRepository.kt
index 8689e04e62dd..66bb803c182d 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/wallpapers/data/repository/FakeWallpaperRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/wallpapers/data/repository/FakeWallpaperRepository.kt
@@ -17,6 +17,8 @@
package com.android.systemui.wallpapers.data.repository
import android.app.WallpaperInfo
+import android.graphics.PointF
+import android.graphics.RectF
import android.view.View
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
@@ -34,9 +36,9 @@ class FakeWallpaperRepository : WallpaperRepository {
private val _shouldSendFocalArea = MutableStateFlow(false)
override val shouldSendFocalArea: StateFlow<Boolean> = _shouldSendFocalArea.asStateFlow()
- fun setShouldSendFocalArea(shouldSendFocalArea: Boolean) {
- _shouldSendFocalArea.value = shouldSendFocalArea
- }
+ override fun sendLockScreenLayoutChangeCommand(wallpaperFocalAreaBounds: RectF) {}
+
+ override fun sendTapCommand(tapPosition: PointF) {}
fun setWallpaperInfo(wallpaperInfo: WallpaperInfo?) {
_wallpaperInfo.value = wallpaperInfo
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/wallpapers/data/repository/WallpaperRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/wallpapers/data/repository/WallpaperRepositoryKosmos.kt
index 7ebec6c3a7b9..1761503b2cc9 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/wallpapers/data/repository/WallpaperRepositoryKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/wallpapers/data/repository/WallpaperRepositoryKosmos.kt
@@ -19,7 +19,6 @@ package com.android.systemui.wallpapers.data.repository
import android.content.applicationContext
import com.android.app.wallpaperManager
import com.android.systemui.broadcast.broadcastDispatcher
-import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.Kosmos.Fixture
import com.android.systemui.kosmos.testDispatcher
@@ -34,8 +33,6 @@ val Kosmos.wallpaperRepository by Fixture {
bgDispatcher = testDispatcher,
broadcastDispatcher = broadcastDispatcher,
userRepository = userRepository,
- keyguardTransitionInteractor = keyguardTransitionInteractor,
- wallpaperFocalAreaRepository = wallpaperFocalAreaRepository,
wallpaperManager = wallpaperManager,
secureSettings = fakeSettings,
)
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/wallpapers/domain/interactor/WallpaperFocalAreaInteractor.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/wallpapers/domain/interactor/WallpaperFocalAreaInteractor.kt
index 88eb5511160b..eaf55a72be93 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/wallpapers/domain/interactor/WallpaperFocalAreaInteractor.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/wallpapers/domain/interactor/WallpaperFocalAreaInteractor.kt
@@ -18,20 +18,14 @@ package com.android.systemui.wallpapers.domain.interactor
import android.content.applicationContext
import com.android.systemui.kosmos.Kosmos
-import com.android.systemui.kosmos.applicationCoroutineScope
import com.android.systemui.shade.data.repository.shadeRepository
-import com.android.systemui.statusbar.notification.domain.interactor.activeNotificationsInteractor
import com.android.systemui.wallpapers.data.repository.wallpaperFocalAreaRepository
-import com.android.systemui.wallpapers.data.repository.wallpaperRepository
-val Kosmos.wallpaperFocalAreaInteractor by
+var Kosmos.wallpaperFocalAreaInteractor by
Kosmos.Fixture {
WallpaperFocalAreaInteractor(
- applicationScope = applicationCoroutineScope,
context = applicationContext,
wallpaperFocalAreaRepository = wallpaperFocalAreaRepository,
shadeRepository = shadeRepository,
- activeNotificationsInteractor = activeNotificationsInteractor,
- wallpaperRepository = wallpaperRepository,
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/wallpapers/ui/viewmodel/WallpaperFocalAreaViewModel.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/wallpapers/ui/viewmodel/WallpaperFocalAreaViewModel.kt
index 7e232c526732..4032503d04c1 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/wallpapers/ui/viewmodel/WallpaperFocalAreaViewModel.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/wallpapers/ui/viewmodel/WallpaperFocalAreaViewModel.kt
@@ -16,10 +16,14 @@
package com.android.systemui.wallpapers.ui.viewmodel
+import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.wallpapers.domain.interactor.wallpaperFocalAreaInteractor
var Kosmos.wallpaperFocalAreaViewModel by
Kosmos.Fixture {
- WallpaperFocalAreaViewModel(wallpaperFocalAreaInteractor = wallpaperFocalAreaInteractor)
+ WallpaperFocalAreaViewModel(
+ wallpaperFocalAreaInteractor = wallpaperFocalAreaInteractor,
+ keyguardTransitionInteractor = keyguardTransitionInteractor,
+ )
}
diff --git a/ravenwood/runtime-jni/ravenwood_initializer.cpp b/ravenwood/runtime-jni/ravenwood_initializer.cpp
index 391c5d56b212..8a35ade649b2 100644
--- a/ravenwood/runtime-jni/ravenwood_initializer.cpp
+++ b/ravenwood/runtime-jni/ravenwood_initializer.cpp
@@ -26,6 +26,10 @@
#include <fcntl.h>
#include <set>
+#include <fstream>
+#include <iostream>
+#include <string>
+#include <cstdlib>
#include "jni_helper.h"
@@ -182,17 +186,82 @@ static jboolean removeSystemProperty(JNIEnv* env, jclass, jstring javaKey) {
}
}
+// Find the PPID of child_pid using /proc/N/stat. The 4th field is the PPID.
+// Also returns child_pid's process name (2nd field).
+static pid_t getppid_of(pid_t child_pid, std::string& out_process_name) {
+ if (child_pid < 0) {
+ return -1;
+ }
+ std::string stat_file = "/proc/" + std::to_string(child_pid) + "/stat";
+ std::ifstream stat_stream(stat_file);
+ if (!stat_stream.is_open()) {
+ ALOGW("Unable to open '%s': %s", stat_file.c_str(), strerror(errno));
+ return -1;
+ }
+
+ std::string field;
+ int field_count = 0;
+ while (std::getline(stat_stream, field, ' ')) {
+ if (++field_count == 4) {
+ return atoi(field.c_str());
+ }
+ if (field_count == 2) {
+ out_process_name = field;
+ }
+ }
+ ALOGW("Unexpected format in '%s'", stat_file.c_str());
+ return -1;
+}
+
+// Find atest's PID. Climb up the process tree, and find "atest-py3".
+static pid_t find_atest_pid() {
+ auto ret = getpid(); // self (isolation runner process)
+
+ while (ret != -1) {
+ std::string proc;
+ ret = getppid_of(ret, proc);
+ if (proc == "(atest-py3)") {
+ return ret;
+ }
+ }
+
+ return ret;
+}
+
+// If $RAVENWOOD_LOG_OUT is set, redirect stdout/err to this file.
+// Originally it was added to allow to monitor log in realtime, with
+// RAVENWOOD_LOG_OUT=$(tty) atest...
+//
+// As a special case, if $RAVENWOOD_LOG_OUT is set to "-", we try to find
+// atest's process and send the output to its stdout. It's sort of hacky, but
+// this allows shell redirection to work on Ravenwood output too,
+// so e.g. `atest ... |tee atest.log` would work on Ravenwood's output.
+// (which wouldn't work with `RAVENWOOD_LOG_OUT=$(tty)`).
+//
+// Otherwise -- if $RAVENWOOD_LOG_OUT isn't set -- atest/tradefed just writes
+// the test's output to its own log file.
static void maybeRedirectLog() {
auto ravenwoodLogOut = getenv("RAVENWOOD_LOG_OUT");
- if (ravenwoodLogOut == NULL) {
+ if (ravenwoodLogOut == NULL || *ravenwoodLogOut == '\0') {
return;
}
- ALOGI("RAVENWOOD_LOG_OUT set. Redirecting output to %s", ravenwoodLogOut);
+ std::string path;
+ if (strcmp("-", ravenwoodLogOut) == 0) {
+ pid_t ppid = find_atest_pid();
+ if (ppid < 0) {
+ ALOGI("RAVENWOOD_LOG_OUT set to '-', but unable to find atest's PID");
+ return;
+ }
+ path = std::format("/proc/{}/fd/1", ppid);
+ } else {
+ path = ravenwoodLogOut;
+ }
+ ALOGI("RAVENWOOD_LOG_OUT set. Redirecting output to '%s'", path.c_str());
// Redirect stdin / stdout to /dev/tty.
- int ttyFd = open(ravenwoodLogOut, O_WRONLY | O_APPEND);
+ int ttyFd = open(path.c_str(), O_WRONLY | O_APPEND);
if (ttyFd == -1) {
- ALOGW("$RAVENWOOD_LOG_OUT is set to %s, but failed to open: %s ", ravenwoodLogOut,
+ ALOGW("$RAVENWOOD_LOG_OUT is set, but failed to open '%s': %s ", path.c_str(),
strerror(errno));
return;
}
diff --git a/services/accessibility/accessibility.aconfig b/services/accessibility/accessibility.aconfig
index 529a564ea607..bb0eacb5afa7 100644
--- a/services/accessibility/accessibility.aconfig
+++ b/services/accessibility/accessibility.aconfig
@@ -145,6 +145,13 @@ flag {
}
flag {
+ name: "enable_magnification_follows_mouse_with_pointer_motion_filter"
+ namespace: "accessibility"
+ description: "Whether to enable mouse following using pointer motion filter"
+ bug: "361817142"
+}
+
+flag {
name: "enable_magnification_keyboard_control"
namespace: "accessibility"
description: "Whether to enable keyboard control for magnification"
diff --git a/services/contextualsearch/java/com/android/server/contextualsearch/ContextualSearchManagerService.java b/services/contextualsearch/java/com/android/server/contextualsearch/ContextualSearchManagerService.java
index 89c9d690a82c..700a1624f7d4 100644
--- a/services/contextualsearch/java/com/android/server/contextualsearch/ContextualSearchManagerService.java
+++ b/services/contextualsearch/java/com/android/server/contextualsearch/ContextualSearchManagerService.java
@@ -34,6 +34,8 @@ import static com.android.server.wm.ActivityTaskManagerInternal.ASSIST_KEY_STRUC
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
+import android.app.ActivityManager;
+import android.app.ActivityManagerInternal;
import android.app.ActivityOptions;
import android.app.AppOpsManager;
import android.app.admin.DevicePolicyManagerInternal;
@@ -69,7 +71,6 @@ import android.provider.Settings;
import android.util.Log;
import android.util.Slog;
import android.view.IWindowManager;
-import android.window.ScreenCapture;
import android.window.ScreenCapture.ScreenshotHardwareBuffer;
import com.android.internal.R;
@@ -86,7 +87,6 @@ import java.io.FileDescriptor;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
-import java.util.Set;
public class ContextualSearchManagerService extends SystemService {
private static final String TAG = ContextualSearchManagerService.class.getSimpleName();
@@ -95,9 +95,20 @@ public class ContextualSearchManagerService extends SystemService {
private static final int MSG_INVALIDATE_TOKEN = 1;
private static final int MAX_TOKEN_VALID_DURATION_MS = 1_000 * 60 * 10; // 10 minutes
+ /**
+ * Below are internal entrypoints not supported by the
+ * {@link ContextualSearchManager#startContextualSearch(int entrypoint)} method.
+ *
+ * <p>These values should be negative to avoid conflicting with the system entrypoints.
+ */
+
+ /** Entrypoint to be used when a foreground app invokes Contextual Search. */
+ private static final int INTERNAL_ENTRYPOINT_APP = -1;
+
private static final boolean DEBUG = false;
private final Context mContext;
+ private final ActivityManagerInternal mActivityManagerInternal;
private final ActivityTaskManagerInternal mAtmInternal;
private final PackageManagerInternal mPackageManager;
private final WindowManagerInternal mWmInternal;
@@ -162,6 +173,8 @@ public class ContextualSearchManagerService extends SystemService {
super(context);
if (DEBUG) Log.d(TAG, "ContextualSearchManagerService created");
mContext = context;
+ mActivityManagerInternal = Objects.requireNonNull(
+ LocalServices.getService(ActivityManagerInternal.class));
mAtmInternal = Objects.requireNonNull(
LocalServices.getService(ActivityTaskManagerInternal.class));
mPackageManager = LocalServices.getService(PackageManagerInternal.class);
@@ -391,6 +404,20 @@ public class ContextualSearchManagerService extends SystemService {
}
}
+ private void enforceForegroundApp(@NonNull final String func) {
+ final int callingUid = Binder.getCallingUid();
+ final String callingPackage = mPackageManager.getNameForUid(Binder.getCallingUid());
+ if (mActivityManagerInternal.getUidProcessState(callingUid)
+ > ActivityManager.PROCESS_STATE_TOP) {
+ // The calling process must be displaying an activity in foreground to
+ // trigger contextual search.
+ String msg = "Permission Denial: Cannot call " + func + " from pid="
+ + Binder.getCallingPid() + ", uid=" + callingUid
+ + ", package=" + callingPackage + " without a foreground activity.";
+ throw new SecurityException(msg);
+ }
+ }
+
private void enforceOverridingPermission(@NonNull final String func) {
if (!(Binder.getCallingUid() == Process.SHELL_UID
|| Binder.getCallingUid() == Process.ROOT_UID
@@ -448,29 +475,43 @@ public class ContextualSearchManagerService extends SystemService {
}
@Override
+ public void startContextualSearchForForegroundApp() {
+ synchronized (this) {
+ if (DEBUG) {
+ Log.d(TAG, "Starting contextual search from: "
+ + mPackageManager.getNameForUid(Binder.getCallingUid()));
+ }
+ enforceForegroundApp("startContextualSearchForForegroundApp");
+ startContextualSearchInternal(INTERNAL_ENTRYPOINT_APP);
+ }
+ }
+
+ @Override
public void startContextualSearch(int entrypoint) {
synchronized (this) {
if (DEBUG) Log.d(TAG, "startContextualSearch entrypoint: " + entrypoint);
enforcePermission("startContextualSearch");
- final int callingUserId = Binder.getCallingUserHandle().getIdentifier();
-
- mAssistDataRequester.cancel();
- // Creates a new CallbackToken at mToken and an expiration handler.
- issueToken();
- // We get the launch intent with the system server's identity because the system
- // server has READ_FRAME_BUFFER permission to get the screenshot and because only
- // the system server can invoke non-exported activities.
- Binder.withCleanCallingIdentity(() -> {
- Intent launchIntent =
- getContextualSearchIntent(entrypoint, callingUserId, mToken);
- if (launchIntent != null) {
- int result = invokeContextualSearchIntent(launchIntent, callingUserId);
- if (DEBUG) Log.d(TAG, "Launch result: " + result);
- }
- });
+ startContextualSearchInternal(entrypoint);
}
}
+ private void startContextualSearchInternal(int entrypoint) {
+ final int callingUserId = Binder.getCallingUserHandle().getIdentifier();
+ mAssistDataRequester.cancel();
+ // Creates a new CallbackToken at mToken and an expiration handler.
+ issueToken();
+ // We get the launch intent with the system server's identity because the system
+ // server has READ_FRAME_BUFFER permission to get the screenshot and because only
+ // the system server can invoke non-exported activities.
+ Binder.withCleanCallingIdentity(() -> {
+ Intent launchIntent = getContextualSearchIntent(entrypoint, callingUserId, mToken);
+ if (launchIntent != null) {
+ int result = invokeContextualSearchIntent(launchIntent, callingUserId);
+ if (DEBUG) Log.d(TAG, "Launch result: " + result);
+ }
+ });
+ }
+
@Override
public void getContextualSearchState(
@NonNull IBinder token,
diff --git a/services/core/java/com/android/server/UiModeManagerService.java b/services/core/java/com/android/server/UiModeManagerService.java
index 4976a63b016b..8eda17698b9b 100644
--- a/services/core/java/com/android/server/UiModeManagerService.java
+++ b/services/core/java/com/android/server/UiModeManagerService.java
@@ -18,7 +18,6 @@ package com.android.server;
import static android.app.Flags.enableCurrentModeTypeBinderCache;
import static android.app.Flags.enableNightModeBinderCache;
-import static android.app.Flags.modesApi;
import static android.app.UiModeManager.ContrastUtils.CONTRAST_DEFAULT_VALUE;
import static android.app.UiModeManager.DEFAULT_PRIORITY;
import static android.app.UiModeManager.FORCE_INVERT_TYPE_DARK;
@@ -2208,14 +2207,12 @@ final class UiModeManagerService extends SystemService {
appliedOverrides = true;
}
- if (modesApi()) {
- // Computes final night mode values based on Attention Mode.
- mComputedNightMode = switch (mAttentionModeThemeOverlay) {
- case (UiModeManager.MODE_ATTENTION_THEME_OVERLAY_NIGHT) -> true;
- case (UiModeManager.MODE_ATTENTION_THEME_OVERLAY_DAY) -> false;
- default -> newComputedValue; // case OFF
- };
- }
+ // Computes final night mode values based on Attention Mode.
+ mComputedNightMode = switch (mAttentionModeThemeOverlay) {
+ case (UiModeManager.MODE_ATTENTION_THEME_OVERLAY_NIGHT) -> true;
+ case (UiModeManager.MODE_ATTENTION_THEME_OVERLAY_DAY) -> false;
+ default -> newComputedValue; // case OFF
+ };
if (appliedOverrides) {
return;
diff --git a/services/core/java/com/android/server/am/AppStartInfoTracker.java b/services/core/java/com/android/server/am/AppStartInfoTracker.java
index 961022b7231b..517279bd7527 100644
--- a/services/core/java/com/android/server/am/AppStartInfoTracker.java
+++ b/services/core/java/com/android/server/am/AppStartInfoTracker.java
@@ -54,15 +54,21 @@ import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.app.ProcessMap;
import com.android.internal.os.Clock;
import com.android.internal.os.MonotonicClock;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
import com.android.server.IoThread;
import com.android.server.ServiceThread;
import com.android.server.SystemServiceManager;
import com.android.server.wm.WindowProcessController;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Collections;
@@ -1006,6 +1012,12 @@ public final class AppStartInfoTracker {
throws IOException, WireTypeMismatchException, ClassNotFoundException {
long token = proto.start(fieldId);
String pkgName = "";
+
+ // Create objects for reuse.
+ ByteArrayInputStream byteArrayInputStream = null;
+ ObjectInputStream objectInputStream = null;
+ TypedXmlPullParser typedXmlPullParser = null;
+
for (int next = proto.nextField();
next != ProtoInputStream.NO_MORE_FIELDS;
next = proto.nextField()) {
@@ -1017,7 +1029,7 @@ public final class AppStartInfoTracker {
AppStartInfoContainer container =
new AppStartInfoContainer(mAppStartInfoHistoryListSize);
int uid = container.readFromProto(proto, AppsStartInfoProto.Package.USERS,
- pkgName);
+ pkgName, byteArrayInputStream, objectInputStream, typedXmlPullParser);
// If the isolated process flag is enabled and the uid is that of an isolated
// process, then break early so that the container will not be added to mData.
@@ -1052,6 +1064,12 @@ public final class AppStartInfoTracker {
out = af.startWrite();
ProtoOutputStream proto = new ProtoOutputStream(out);
proto.write(AppsStartInfoProto.LAST_UPDATE_TIMESTAMP, now);
+
+ // Create objects for reuse.
+ ByteArrayOutputStream byteArrayOutputStream = null;
+ ObjectOutputStream objectOutputStream = null;
+ TypedXmlSerializer typedXmlSerializer = null;
+
synchronized (mLock) {
succeeded = forEachPackageLocked(
(packageName, records) -> {
@@ -1060,8 +1078,9 @@ public final class AppStartInfoTracker {
int uidArraySize = records.size();
for (int j = 0; j < uidArraySize; j++) {
try {
- records.valueAt(j)
- .writeToProto(proto, AppsStartInfoProto.Package.USERS);
+ records.valueAt(j).writeToProto(proto,
+ AppsStartInfoProto.Package.USERS, byteArrayOutputStream,
+ objectOutputStream, typedXmlSerializer);
} catch (IOException e) {
Slog.w(TAG, "Unable to write app start info into persistent"
+ "storage: " + e);
@@ -1414,19 +1433,23 @@ public final class AppStartInfoTracker {
}
@GuardedBy("mLock")
- void writeToProto(ProtoOutputStream proto, long fieldId) throws IOException {
+ void writeToProto(ProtoOutputStream proto, long fieldId,
+ ByteArrayOutputStream byteArrayOutputStream, ObjectOutputStream objectOutputStream,
+ TypedXmlSerializer typedXmlSerializer) throws IOException {
long token = proto.start(fieldId);
proto.write(AppsStartInfoProto.Package.User.UID, mUid);
int size = mInfos.size();
for (int i = 0; i < size; i++) {
- mInfos.get(i)
- .writeToProto(proto, AppsStartInfoProto.Package.User.APP_START_INFO);
+ mInfos.get(i).writeToProto(proto, AppsStartInfoProto.Package.User.APP_START_INFO,
+ byteArrayOutputStream, objectOutputStream, typedXmlSerializer);
}
proto.write(AppsStartInfoProto.Package.User.MONITORING_ENABLED, mMonitoringModeEnabled);
proto.end(token);
}
- int readFromProto(ProtoInputStream proto, long fieldId, String packageName)
+ int readFromProto(ProtoInputStream proto, long fieldId, String packageName,
+ ByteArrayInputStream byteArrayInputStream, ObjectInputStream objectInputStream,
+ TypedXmlPullParser typedXmlPullParser)
throws IOException, WireTypeMismatchException, ClassNotFoundException {
long token = proto.start(fieldId);
for (int next = proto.nextField();
@@ -1440,7 +1463,8 @@ public final class AppStartInfoTracker {
// Create record with monotonic time 0 in case the persisted record does not
// have a create time.
ApplicationStartInfo info = new ApplicationStartInfo(0);
- info.readFromProto(proto, AppsStartInfoProto.Package.User.APP_START_INFO);
+ info.readFromProto(proto, AppsStartInfoProto.Package.User.APP_START_INFO,
+ byteArrayInputStream, objectInputStream, typedXmlPullParser);
info.setPackageName(packageName);
mInfos.add(info);
break;
diff --git a/services/core/java/com/android/server/appop/DiscreteOpsSqlRegistry.java b/services/core/java/com/android/server/appop/DiscreteOpsSqlRegistry.java
index 30c2a82296ca..604cb30294a9 100644
--- a/services/core/java/com/android/server/appop/DiscreteOpsSqlRegistry.java
+++ b/services/core/java/com/android/server/appop/DiscreteOpsSqlRegistry.java
@@ -418,7 +418,9 @@ public class DiscreteOpsSqlRegistry extends DiscreteOpsRegistry {
evictedEvents.addAll(mCache);
mCache.clear();
}
- mSqliteWriteHandler.obtainMessage(WRITE_CACHE_EVICTED_OP_EVENTS, evictedEvents);
+ Message msg = mSqliteWriteHandler.obtainMessage(
+ WRITE_CACHE_EVICTED_OP_EVENTS, evictedEvents);
+ mSqliteWriteHandler.sendMessage(msg);
}
}
}
diff --git a/services/core/java/com/android/server/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
index 6d6e1fb6bfb3..ef80d59993e9 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
@@ -18,6 +18,7 @@ package com.android.server.audio;
import static android.media.audio.Flags.scoManagedByAudio;
import static com.android.media.audio.Flags.equalScoLeaVcIndexRange;
+import static com.android.media.audio.Flags.optimizeBtDeviceSwitch;
import static com.android.server.audio.AudioService.BT_COMM_DEVICE_ACTIVE_BLE_HEADSET;
import static com.android.server.audio.AudioService.BT_COMM_DEVICE_ACTIVE_BLE_SPEAKER;
import static com.android.server.audio.AudioService.BT_COMM_DEVICE_ACTIVE_SCO;
@@ -290,8 +291,8 @@ public class AudioDeviceBroker {
}
@GuardedBy("mDeviceStateLock")
- /*package*/ void onSetBtScoActiveDevice(BluetoothDevice btDevice) {
- mBtHelper.onSetBtScoActiveDevice(btDevice);
+ /*package*/ void onSetBtScoActiveDevice(BluetoothDevice btDevice, boolean deviceSwitch) {
+ mBtHelper.onSetBtScoActiveDevice(btDevice, deviceSwitch);
}
/*package*/ void setBluetoothA2dpOn_Async(boolean on, String source) {
@@ -941,6 +942,7 @@ public class AudioDeviceBroker {
final @NonNull String mEventSource;
final int mAudioSystemDevice;
final int mMusicDevice;
+ final boolean mIsDeviceSwitch;
BtDeviceInfo(@NonNull BtDeviceChangedData d, @NonNull BluetoothDevice device, int state,
int audioDevice, @AudioSystem.AudioFormatNativeEnumForBtCodec int codec) {
@@ -953,6 +955,8 @@ public class AudioDeviceBroker {
mEventSource = d.mEventSource;
mAudioSystemDevice = audioDevice;
mMusicDevice = AudioSystem.DEVICE_NONE;
+ mIsDeviceSwitch = optimizeBtDeviceSwitch()
+ && d.mNewDevice != null && d.mPreviousDevice != null;
}
// constructor used by AudioDeviceBroker to search similar message
@@ -966,6 +970,7 @@ public class AudioDeviceBroker {
mSupprNoisy = false;
mVolume = -1;
mIsLeOutput = false;
+ mIsDeviceSwitch = false;
}
// constructor used by AudioDeviceInventory when config change failed
@@ -980,6 +985,7 @@ public class AudioDeviceBroker {
mSupprNoisy = false;
mVolume = -1;
mIsLeOutput = false;
+ mIsDeviceSwitch = false;
}
BtDeviceInfo(@NonNull BtDeviceInfo src, int state) {
@@ -992,6 +998,7 @@ public class AudioDeviceBroker {
mEventSource = src.mEventSource;
mAudioSystemDevice = src.mAudioSystemDevice;
mMusicDevice = src.mMusicDevice;
+ mIsDeviceSwitch = false;
}
// redefine equality op so we can match messages intended for this device
@@ -1026,7 +1033,8 @@ public class AudioDeviceBroker {
+ " isLeOutput=" + mIsLeOutput
+ " eventSource=" + mEventSource
+ " audioSystemDevice=" + mAudioSystemDevice
- + " musicDevice=" + mMusicDevice;
+ + " musicDevice=" + mMusicDevice
+ + " isDeviceSwitch=" + mIsDeviceSwitch;
}
}
@@ -1196,6 +1204,8 @@ public class AudioDeviceBroker {
AudioSystem.setParameters("A2dpSuspended=true");
AudioSystem.setParameters("LeAudioSuspended=true");
AudioSystem.setParameters("BT_SCO=on");
+ mBluetoothA2dpSuspendedApplied = true;
+ mBluetoothLeSuspendedApplied = true;
} else {
AudioSystem.setParameters("BT_SCO=off");
if (mBluetoothA2dpSuspendedApplied) {
@@ -1680,10 +1690,11 @@ public class AudioDeviceBroker {
}
/*package*/ boolean handleDeviceConnection(@NonNull AudioDeviceAttributes attributes,
- boolean connect, @Nullable BluetoothDevice btDevice) {
+ boolean connect, @Nullable BluetoothDevice btDevice,
+ boolean deviceSwitch) {
synchronized (mDeviceStateLock) {
return mDeviceInventory.handleDeviceConnection(
- attributes, connect, false /*for test*/, btDevice);
+ attributes, connect, false /*for test*/, btDevice, deviceSwitch);
}
}
@@ -1776,6 +1787,18 @@ public class AudioDeviceBroker {
pw.println("\n" + prefix + "mScoManagedByAudio: " + mScoManagedByAudio);
+ pw.println("\n" + prefix + "Bluetooth SCO on"
+ + ", requested: " + mBluetoothScoOn
+ + ", applied: " + mBluetoothScoOnApplied);
+ pw.println("\n" + prefix + "Bluetooth A2DP suspended"
+ + ", requested ext: " + mBluetoothA2dpSuspendedExt
+ + ", requested int: " + mBluetoothA2dpSuspendedInt
+ + ", applied " + mBluetoothA2dpSuspendedApplied);
+ pw.println("\n" + prefix + "Bluetooth LE Audio suspended"
+ + ", requested ext: " + mBluetoothLeSuspendedExt
+ + ", requested int: " + mBluetoothLeSuspendedInt
+ + ", applied " + mBluetoothLeSuspendedApplied);
+
mBtHelper.dump(pw, prefix);
}
@@ -1930,10 +1953,12 @@ public class AudioDeviceBroker {
|| btInfo.mIsLeOutput)
? mAudioService.getBluetoothContextualVolumeStream()
: AudioSystem.STREAM_DEFAULT);
- if (btInfo.mProfile == BluetoothProfile.LE_AUDIO
+ if ((btInfo.mProfile == BluetoothProfile.LE_AUDIO
|| btInfo.mProfile == BluetoothProfile.HEARING_AID
|| (mScoManagedByAudio
- && btInfo.mProfile == BluetoothProfile.HEADSET)) {
+ && btInfo.mProfile == BluetoothProfile.HEADSET))
+ && (btInfo.mState == BluetoothProfile.STATE_CONNECTED
+ || !btInfo.mIsDeviceSwitch)) {
onUpdateCommunicationRouteClient(
bluetoothScoRequestOwnerAttributionSource(),
"setBluetoothActiveDevice");
diff --git a/services/core/java/com/android/server/audio/AudioDeviceInventory.java b/services/core/java/com/android/server/audio/AudioDeviceInventory.java
index ef10793fd955..ae91934e7498 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceInventory.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceInventory.java
@@ -799,7 +799,7 @@ public class AudioDeviceInventory {
di.mDeviceAddress,
di.mDeviceName),
AudioSystem.DEVICE_STATE_AVAILABLE,
- di.mDeviceCodecFormat);
+ di.mDeviceCodecFormat, false /*deviceSwitch*/);
if (asDeviceConnectionFailure() && res != AudioSystem.AUDIO_STATUS_OK) {
failedReconnectionDeviceList.add(di);
}
@@ -811,7 +811,7 @@ public class AudioDeviceInventory {
EventLogger.Event.ALOGE, TAG);
mConnectedDevices.remove(di.getKey(), di);
if (AudioSystem.isBluetoothScoDevice(di.mDeviceType)) {
- mDeviceBroker.onSetBtScoActiveDevice(null);
+ mDeviceBroker.onSetBtScoActiveDevice(null, false /*deviceSwitch*/);
}
}
}
@@ -851,7 +851,8 @@ public class AudioDeviceInventory {
Log.d(TAG, "onSetBtActiveDevice"
+ " btDevice=" + btInfo.mDevice
+ " profile=" + BluetoothProfile.getProfileName(btInfo.mProfile)
- + " state=" + BluetoothProfile.getConnectionStateName(btInfo.mState));
+ + " state=" + BluetoothProfile.getConnectionStateName(btInfo.mState)
+ + " isDeviceSwitch=" + btInfo.mIsDeviceSwitch);
}
String address = btInfo.mDevice.getAddress();
if (!BluetoothAdapter.checkBluetoothAddress(address)) {
@@ -897,7 +898,8 @@ public class AudioDeviceInventory {
break;
case BluetoothProfile.A2DP:
if (switchToUnavailable) {
- makeA2dpDeviceUnavailableNow(address, di.mDeviceCodecFormat);
+ makeA2dpDeviceUnavailableNow(address, di.mDeviceCodecFormat,
+ btInfo.mIsDeviceSwitch);
} else if (switchToAvailable) {
// device is not already connected
if (btInfo.mVolume != -1) {
@@ -911,7 +913,7 @@ public class AudioDeviceInventory {
break;
case BluetoothProfile.HEARING_AID:
if (switchToUnavailable) {
- makeHearingAidDeviceUnavailable(address);
+ makeHearingAidDeviceUnavailable(address, btInfo.mIsDeviceSwitch);
} else if (switchToAvailable) {
makeHearingAidDeviceAvailable(address, BtHelper.getName(btInfo.mDevice),
streamType, "onSetBtActiveDevice");
@@ -921,7 +923,8 @@ public class AudioDeviceInventory {
case BluetoothProfile.LE_AUDIO_BROADCAST:
if (switchToUnavailable) {
makeLeAudioDeviceUnavailableNow(address,
- btInfo.mAudioSystemDevice, di.mDeviceCodecFormat);
+ btInfo.mAudioSystemDevice, di.mDeviceCodecFormat,
+ btInfo.mIsDeviceSwitch);
} else if (switchToAvailable) {
makeLeAudioDeviceAvailable(
btInfo, streamType, codec, "onSetBtActiveDevice");
@@ -930,9 +933,10 @@ public class AudioDeviceInventory {
case BluetoothProfile.HEADSET:
if (mDeviceBroker.isScoManagedByAudio()) {
if (switchToUnavailable) {
- mDeviceBroker.onSetBtScoActiveDevice(null);
+ mDeviceBroker.onSetBtScoActiveDevice(null, btInfo.mIsDeviceSwitch);
} else if (switchToAvailable) {
- mDeviceBroker.onSetBtScoActiveDevice(btInfo.mDevice);
+ mDeviceBroker.onSetBtScoActiveDevice(
+ btInfo.mDevice, false /*deviceSwitch*/);
}
}
break;
@@ -1053,19 +1057,19 @@ public class AudioDeviceInventory {
/*package*/ void onMakeA2dpDeviceUnavailableNow(String address, int a2dpCodec) {
synchronized (mDevicesLock) {
- makeA2dpDeviceUnavailableNow(address, a2dpCodec);
+ makeA2dpDeviceUnavailableNow(address, a2dpCodec, false /*deviceSwitch*/);
}
}
/*package*/ void onMakeLeAudioDeviceUnavailableNow(String address, int device, int codec) {
synchronized (mDevicesLock) {
- makeLeAudioDeviceUnavailableNow(address, device, codec);
+ makeLeAudioDeviceUnavailableNow(address, device, codec, false /*deviceSwitch*/);
}
}
/*package*/ void onMakeHearingAidDeviceUnavailableNow(String address) {
synchronized (mDevicesLock) {
- makeHearingAidDeviceUnavailable(address);
+ makeHearingAidDeviceUnavailable(address, false /*deviceSwitch*/);
}
}
@@ -1180,7 +1184,8 @@ public class AudioDeviceInventory {
}
if (!handleDeviceConnection(wdcs.mAttributes,
- wdcs.mState == AudioService.CONNECTION_STATE_CONNECTED, wdcs.mForTest, null)) {
+ wdcs.mState == AudioService.CONNECTION_STATE_CONNECTED, wdcs.mForTest,
+ null, false /*deviceSwitch*/)) {
// change of connection state failed, bailout
mmi.set(MediaMetrics.Property.EARLY_RETURN, "change of connection state failed")
.record();
@@ -1788,14 +1793,15 @@ public class AudioDeviceInventory {
*/
/*package*/ boolean handleDeviceConnection(@NonNull AudioDeviceAttributes attributes,
boolean connect, boolean isForTesting,
- @Nullable BluetoothDevice btDevice) {
+ @Nullable BluetoothDevice btDevice,
+ boolean deviceSwitch) {
int device = attributes.getInternalType();
String address = attributes.getAddress();
String deviceName = attributes.getName();
if (AudioService.DEBUG_DEVICES) {
Slog.i(TAG, "handleDeviceConnection(" + connect + " dev:"
+ Integer.toHexString(device) + " address:" + address
- + " name:" + deviceName + ")");
+ + " name:" + deviceName + ", deviceSwitch: " + deviceSwitch + ")");
}
MediaMetrics.Item mmi = new MediaMetrics.Item(mMetricsId + "handleDeviceConnection")
.set(MediaMetrics.Property.ADDRESS, address)
@@ -1829,7 +1835,8 @@ public class AudioDeviceInventory {
res = AudioSystem.AUDIO_STATUS_OK;
} else {
res = mAudioSystem.setDeviceConnectionState(attributes,
- AudioSystem.DEVICE_STATE_AVAILABLE, AudioSystem.AUDIO_FORMAT_DEFAULT);
+ AudioSystem.DEVICE_STATE_AVAILABLE, AudioSystem.AUDIO_FORMAT_DEFAULT,
+ false /*deviceSwitch*/);
}
if (res != AudioSystem.AUDIO_STATUS_OK) {
final String reason = "not connecting device 0x" + Integer.toHexString(device)
@@ -1856,7 +1863,8 @@ public class AudioDeviceInventory {
status = true;
} else if (!connect && isConnected) {
mAudioSystem.setDeviceConnectionState(attributes,
- AudioSystem.DEVICE_STATE_UNAVAILABLE, AudioSystem.AUDIO_FORMAT_DEFAULT);
+ AudioSystem.DEVICE_STATE_UNAVAILABLE, AudioSystem.AUDIO_FORMAT_DEFAULT,
+ deviceSwitch);
// always remove even if disconnection failed
mConnectedDevices.remove(deviceKey);
mDeviceBroker.postCheckCommunicationDeviceRemoval(attributes);
@@ -2030,7 +2038,7 @@ public class AudioDeviceInventory {
}
}
if (disconnect) {
- mDeviceBroker.onSetBtScoActiveDevice(null);
+ mDeviceBroker.onSetBtScoActiveDevice(null, false /*deviceSwitch*/);
}
}
@@ -2068,7 +2076,8 @@ public class AudioDeviceInventory {
|| info.mProfile == BluetoothProfile.LE_AUDIO_BROADCAST)
&& info.mIsLeOutput)
|| info.mProfile == BluetoothProfile.HEARING_AID
- || info.mProfile == BluetoothProfile.A2DP)) {
+ || info.mProfile == BluetoothProfile.A2DP)
+ && !info.mIsDeviceSwitch) {
@AudioService.ConnectionState int asState =
(info.mState == BluetoothProfile.STATE_CONNECTED)
? AudioService.CONNECTION_STATE_CONNECTED
@@ -2124,7 +2133,7 @@ public class AudioDeviceInventory {
AudioDeviceAttributes ada = new AudioDeviceAttributes(
AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, address, name);
final int res = mAudioSystem.setDeviceConnectionState(ada,
- AudioSystem.DEVICE_STATE_AVAILABLE, codec);
+ AudioSystem.DEVICE_STATE_AVAILABLE, codec, false);
// TODO: log in MediaMetrics once distinction between connection failure and
// double connection is made.
@@ -2362,7 +2371,7 @@ public class AudioDeviceInventory {
}
@GuardedBy("mDevicesLock")
- private void makeA2dpDeviceUnavailableNow(String address, int codec) {
+ private void makeA2dpDeviceUnavailableNow(String address, int codec, boolean deviceSwitch) {
MediaMetrics.Item mmi = new MediaMetrics.Item(mMetricsId + "a2dp." + address)
.set(MediaMetrics.Property.ENCODING, AudioSystem.audioFormatToString(codec))
.set(MediaMetrics.Property.EVENT, "makeA2dpDeviceUnavailableNow");
@@ -2393,7 +2402,7 @@ public class AudioDeviceInventory {
AudioDeviceAttributes ada = new AudioDeviceAttributes(
AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, address);
final int res = mAudioSystem.setDeviceConnectionState(ada,
- AudioSystem.DEVICE_STATE_UNAVAILABLE, codec);
+ AudioSystem.DEVICE_STATE_UNAVAILABLE, codec, deviceSwitch);
if (res != AudioSystem.AUDIO_STATUS_OK) {
AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent(
@@ -2404,7 +2413,8 @@ public class AudioDeviceInventory {
} else {
AudioService.sDeviceLogger.enqueue((new EventLogger.StringEvent(
"A2DP device addr=" + Utils.anonymizeBluetoothAddress(address)
- + " made unavailable")).printSlog(EventLogger.Event.ALOGI, TAG));
+ + " made unavailable, deviceSwitch" + deviceSwitch))
+ .printSlog(EventLogger.Event.ALOGI, TAG));
}
mApmConnectedDevices.remove(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP);
@@ -2440,7 +2450,7 @@ public class AudioDeviceInventory {
final int res = mAudioSystem.setDeviceConnectionState(new AudioDeviceAttributes(
AudioSystem.DEVICE_IN_BLUETOOTH_A2DP, address),
AudioSystem.DEVICE_STATE_AVAILABLE,
- AudioSystem.AUDIO_FORMAT_DEFAULT);
+ AudioSystem.AUDIO_FORMAT_DEFAULT, false);
if (res != AudioSystem.AUDIO_STATUS_OK) {
AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent(
"APM failed to make available A2DP source device addr="
@@ -2465,7 +2475,7 @@ public class AudioDeviceInventory {
AudioSystem.DEVICE_IN_BLUETOOTH_A2DP, address);
mAudioSystem.setDeviceConnectionState(ada,
AudioSystem.DEVICE_STATE_UNAVAILABLE,
- AudioSystem.AUDIO_FORMAT_DEFAULT);
+ AudioSystem.AUDIO_FORMAT_DEFAULT, false);
// always remove regardless of the result
mConnectedDevices.remove(
DeviceInfo.makeDeviceListKey(AudioSystem.DEVICE_IN_BLUETOOTH_A2DP, address));
@@ -2485,7 +2495,7 @@ public class AudioDeviceInventory {
DEVICE_OUT_HEARING_AID, address, name);
final int res = mAudioSystem.setDeviceConnectionState(ada,
AudioSystem.DEVICE_STATE_AVAILABLE,
- AudioSystem.AUDIO_FORMAT_DEFAULT);
+ AudioSystem.AUDIO_FORMAT_DEFAULT, false);
if (asDeviceConnectionFailure() && res != AudioSystem.AUDIO_STATUS_OK) {
AudioService.sDeviceLogger.enqueueAndSlog(
"APM failed to make available HearingAid addr=" + address
@@ -2515,12 +2525,12 @@ public class AudioDeviceInventory {
}
@GuardedBy("mDevicesLock")
- private void makeHearingAidDeviceUnavailable(String address) {
+ private void makeHearingAidDeviceUnavailable(String address, boolean deviceSwitch) {
AudioDeviceAttributes ada = new AudioDeviceAttributes(
DEVICE_OUT_HEARING_AID, address);
mAudioSystem.setDeviceConnectionState(ada,
AudioSystem.DEVICE_STATE_UNAVAILABLE,
- AudioSystem.AUDIO_FORMAT_DEFAULT);
+ AudioSystem.AUDIO_FORMAT_DEFAULT, deviceSwitch);
// always remove regardless of return code
mConnectedDevices.remove(
DeviceInfo.makeDeviceListKey(DEVICE_OUT_HEARING_AID, address));
@@ -2622,7 +2632,7 @@ public class AudioDeviceInventory {
AudioDeviceAttributes ada = new AudioDeviceAttributes(device, address, name);
final int res = mAudioSystem.setDeviceConnectionState(ada,
- AudioSystem.DEVICE_STATE_AVAILABLE, codec);
+ AudioSystem.DEVICE_STATE_AVAILABLE, codec, false /*deviceSwitch*/);
if (res != AudioSystem.AUDIO_STATUS_OK) {
AudioService.sDeviceLogger.enqueueAndSlog(
"APM failed to make available LE Audio device addr=" + address
@@ -2669,13 +2679,13 @@ public class AudioDeviceInventory {
@GuardedBy("mDevicesLock")
private void makeLeAudioDeviceUnavailableNow(String address, int device,
- @AudioSystem.AudioFormatNativeEnumForBtCodec int codec) {
+ @AudioSystem.AudioFormatNativeEnumForBtCodec int codec, boolean deviceSwitch) {
AudioDeviceAttributes ada = null;
if (device != AudioSystem.DEVICE_NONE) {
ada = new AudioDeviceAttributes(device, address);
final int res = mAudioSystem.setDeviceConnectionState(ada,
AudioSystem.DEVICE_STATE_UNAVAILABLE,
- codec);
+ codec, deviceSwitch);
if (res != AudioSystem.AUDIO_STATUS_OK) {
AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent(
@@ -2685,7 +2695,8 @@ public class AudioDeviceInventory {
} else {
AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent(
"LE Audio device addr=" + Utils.anonymizeBluetoothAddress(address)
- + " made unavailable").printSlog(EventLogger.Event.ALOGI, TAG));
+ + " made unavailable, deviceSwitch" + deviceSwitch)
+ .printSlog(EventLogger.Event.ALOGI, TAG));
}
mConnectedDevices.remove(DeviceInfo.makeDeviceListKey(device, address));
}
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index 86871ea45d13..057d1274d47d 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -63,6 +63,7 @@ import static com.android.media.audio.Flags.audioserverPermissions;
import static com.android.media.audio.Flags.disablePrescaleAbsoluteVolume;
import static com.android.media.audio.Flags.deferWearPermissionUpdates;
import static com.android.media.audio.Flags.equalScoLeaVcIndexRange;
+import static com.android.media.audio.Flags.optimizeBtDeviceSwitch;
import static com.android.media.audio.Flags.replaceStreamBtSco;
import static com.android.media.audio.Flags.ringMyCar;
import static com.android.media.audio.Flags.ringerModeAffectsAlarm;
@@ -4990,6 +4991,8 @@ public class AudioService extends IAudioService.Stub
+ cacheGetStreamMinMaxVolume());
pw.println("\tandroid.media.audio.Flags.cacheGetStreamVolume:"
+ cacheGetStreamVolume());
+ pw.println("\tcom.android.media.audio.optimizeBtDeviceSwitch:"
+ + optimizeBtDeviceSwitch());
}
private void dumpAudioMode(PrintWriter pw) {
diff --git a/services/core/java/com/android/server/audio/AudioSystemAdapter.java b/services/core/java/com/android/server/audio/AudioSystemAdapter.java
index e86c34cab88a..a6267c156fb3 100644
--- a/services/core/java/com/android/server/audio/AudioSystemAdapter.java
+++ b/services/core/java/com/android/server/audio/AudioSystemAdapter.java
@@ -367,9 +367,9 @@ public class AudioSystemAdapter implements AudioSystem.RoutingUpdateCallback,
* @return
*/
public int setDeviceConnectionState(AudioDeviceAttributes attributes, int state,
- int codecFormat) {
+ int codecFormat, boolean deviceSwitch) {
invalidateRoutingCache();
- return AudioSystem.setDeviceConnectionState(attributes, state, codecFormat);
+ return AudioSystem.setDeviceConnectionState(attributes, state, codecFormat, deviceSwitch);
}
/**
diff --git a/services/core/java/com/android/server/audio/BtHelper.java b/services/core/java/com/android/server/audio/BtHelper.java
index 922116999bc7..844e3524384d 100644
--- a/services/core/java/com/android/server/audio/BtHelper.java
+++ b/services/core/java/com/android/server/audio/BtHelper.java
@@ -26,6 +26,8 @@ import static android.media.AudioManager.AUDIO_DEVICE_CATEGORY_SPEAKER;
import static android.media.AudioManager.AUDIO_DEVICE_CATEGORY_UNKNOWN;
import static android.media.AudioManager.AUDIO_DEVICE_CATEGORY_WATCH;
+import static com.android.media.audio.Flags.optimizeBtDeviceSwitch;
+
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.bluetooth.BluetoothA2dp;
@@ -393,8 +395,11 @@ public class BtHelper {
+ "received with null profile proxy for device: "
+ btDevice)).printLog(TAG));
return;
+
}
- onSetBtScoActiveDevice(btDevice);
+ boolean deviceSwitch = optimizeBtDeviceSwitch()
+ && btDevice != null && mBluetoothHeadsetDevice != null;
+ onSetBtScoActiveDevice(btDevice, deviceSwitch);
} else if (action.equals(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED)) {
int btState = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1);
onScoAudioStateChanged(btState);
@@ -814,7 +819,7 @@ public class BtHelper {
if (device == null) {
continue;
}
- onSetBtScoActiveDevice(device);
+ onSetBtScoActiveDevice(device, false /*deviceSwitch*/);
}
} else {
Log.e(TAG, "onHeadsetProfileConnected: Null BluetoothAdapter");
@@ -907,7 +912,8 @@ public class BtHelper {
}
@GuardedBy("mDeviceBroker.mDeviceStateLock")
- private boolean handleBtScoActiveDeviceChange(BluetoothDevice btDevice, boolean isActive) {
+ private boolean handleBtScoActiveDeviceChange(BluetoothDevice btDevice, boolean isActive,
+ boolean deviceSwitch) {
if (btDevice == null) {
return true;
}
@@ -919,12 +925,12 @@ public class BtHelper {
if (isActive) {
audioDevice = btHeadsetDeviceToAudioDevice(btDevice);
result = mDeviceBroker.handleDeviceConnection(
- audioDevice, true /*connect*/, btDevice);
+ audioDevice, true /*connect*/, btDevice, false /*deviceSwitch*/);
} else {
AudioDeviceAttributes ada = mResolvedScoAudioDevices.get(btDevice);
if (ada != null) {
result = mDeviceBroker.handleDeviceConnection(
- ada, false /*connect*/, btDevice);
+ ada, false /*connect*/, btDevice, deviceSwitch);
} else {
// Disconnect all possible audio device types if the disconnected device type is
// unknown
@@ -935,7 +941,8 @@ public class BtHelper {
};
for (int outDeviceType : outDeviceTypes) {
result |= mDeviceBroker.handleDeviceConnection(new AudioDeviceAttributes(
- outDeviceType, address, name), false /*connect*/, btDevice);
+ outDeviceType, address, name), false /*connect*/, btDevice,
+ deviceSwitch);
}
}
}
@@ -944,7 +951,7 @@ public class BtHelper {
// handleDeviceConnection() && result to make sure the method get executed
result = mDeviceBroker.handleDeviceConnection(new AudioDeviceAttributes(
inDevice, address, name),
- isActive, btDevice) && result;
+ isActive, btDevice, deviceSwitch) && result;
if (result) {
if (isActive) {
mResolvedScoAudioDevices.put(btDevice, audioDevice);
@@ -961,18 +968,18 @@ public class BtHelper {
}
@GuardedBy("mDeviceBroker.mDeviceStateLock")
- /*package */ void onSetBtScoActiveDevice(BluetoothDevice btDevice) {
+ /*package */ void onSetBtScoActiveDevice(BluetoothDevice btDevice, boolean deviceSwitch) {
Log.i(TAG, "onSetBtScoActiveDevice: " + getAnonymizedAddress(mBluetoothHeadsetDevice)
- + " -> " + getAnonymizedAddress(btDevice));
+ + " -> " + getAnonymizedAddress(btDevice) + ", deviceSwitch: " + deviceSwitch);
final BluetoothDevice previousActiveDevice = mBluetoothHeadsetDevice;
if (Objects.equals(btDevice, previousActiveDevice)) {
return;
}
- if (!handleBtScoActiveDeviceChange(previousActiveDevice, false)) {
+ if (!handleBtScoActiveDeviceChange(previousActiveDevice, false, deviceSwitch)) {
Log.w(TAG, "onSetBtScoActiveDevice() failed to remove previous device "
+ getAnonymizedAddress(previousActiveDevice));
}
- if (!handleBtScoActiveDeviceChange(btDevice, true)) {
+ if (!handleBtScoActiveDeviceChange(btDevice, true, false /*deviceSwitch*/)) {
Log.e(TAG, "onSetBtScoActiveDevice() failed to add new device "
+ getAnonymizedAddress(btDevice));
// set mBluetoothHeadsetDevice to null when failing to add new device
diff --git a/services/core/java/com/android/server/backup/SystemBackupAgent.java b/services/core/java/com/android/server/backup/SystemBackupAgent.java
index b11267ef8634..79523bd02404 100644
--- a/services/core/java/com/android/server/backup/SystemBackupAgent.java
+++ b/services/core/java/com/android/server/backup/SystemBackupAgent.java
@@ -69,6 +69,7 @@ public class SystemBackupAgent extends BackupAgentHelper {
private static final String SYSTEM_GENDER_HELPER = "system_gender";
private static final String DISPLAY_HELPER = "display";
private static final String INPUT_HELPER = "input";
+ private static final String WEAR_BACKUP_HELPER = "wear";
// These paths must match what the WallpaperManagerService uses. The leaf *_FILENAME
// are also used in the full-backup file format, so must not change unless steps are
@@ -113,7 +114,7 @@ public class SystemBackupAgent extends BackupAgentHelper {
private static final Set<String> sEligibleHelpersForNonSystemUser =
SetUtils.union(sEligibleHelpersForProfileUser,
Sets.newArraySet(ACCOUNT_MANAGER_HELPER, USAGE_STATS_HELPER, PREFERRED_HELPER,
- SHORTCUT_MANAGER_HELPER, INPUT_HELPER));
+ SHORTCUT_MANAGER_HELPER, INPUT_HELPER, WEAR_BACKUP_HELPER));
private int mUserId = UserHandle.USER_SYSTEM;
private boolean mIsProfileUser = false;
@@ -153,6 +154,11 @@ public class SystemBackupAgent extends BackupAgentHelper {
if (com.android.hardware.input.Flags.enableBackupAndRestoreForInputGestures()) {
addHelperIfEligibleForUser(INPUT_HELPER, new InputBackupHelper(mUserId));
}
+
+ // Add Wear helper only if the device is a watch
+ if (getPackageManager().hasSystemFeature(PackageManager.FEATURE_WATCH)) {
+ addHelperIfEligibleForUser(WEAR_BACKUP_HELPER, new WearBackupHelper());
+ }
}
@Override
diff --git a/services/core/java/com/android/server/backup/WearBackupHelper.java b/services/core/java/com/android/server/backup/WearBackupHelper.java
new file mode 100644
index 000000000000..27416b3eb2a6
--- /dev/null
+++ b/services/core/java/com/android/server/backup/WearBackupHelper.java
@@ -0,0 +1,49 @@
+/*
+ * 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
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.backup;
+
+import android.annotation.Nullable;
+import android.app.backup.BlobBackupHelper;
+
+import com.android.server.LocalServices;
+
+/** A {@link android.app.backup.BlobBackupHelper} for Wear */
+public class WearBackupHelper extends BlobBackupHelper {
+
+ private static final int BLOB_VERSION = 1;
+ private static final String KEY_WEAR_BACKUP = "wear";
+ @Nullable private final WearBackupInternal mWearBackupInternal;
+
+ public WearBackupHelper() {
+ super(BLOB_VERSION, KEY_WEAR_BACKUP);
+ mWearBackupInternal = LocalServices.getService(WearBackupInternal.class);
+ }
+
+ @Override
+ protected byte[] getBackupPayload(String key) {
+ return KEY_WEAR_BACKUP.equals(key) && mWearBackupInternal != null
+ ? mWearBackupInternal.getBackupPayload(getLogger())
+ : null;
+ }
+
+ @Override
+ protected void applyRestoredPayload(String key, byte[] payload) {
+ if (KEY_WEAR_BACKUP.equals(key) && mWearBackupInternal != null) {
+ mWearBackupInternal.applyRestoredPayload(payload);
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/backup/WearBackupInternal.java b/services/core/java/com/android/server/backup/WearBackupInternal.java
new file mode 100644
index 000000000000..7b4847b51df6
--- /dev/null
+++ b/services/core/java/com/android/server/backup/WearBackupInternal.java
@@ -0,0 +1,32 @@
+/*
+ * 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
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.backup;
+
+import android.app.backup.BackupRestoreEventLogger;
+
+import com.android.internal.annotations.Keep;
+
+/** A local service internal for Wear OS handle backup/restore */
+@Keep
+public interface WearBackupInternal {
+
+ /** Gets the backup payload */
+ byte[] getBackupPayload(BackupRestoreEventLogger logger);
+
+ /** Applies the restored payload */
+ void applyRestoredPayload(byte[] payload);
+}
diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java
index b6a3f4041b13..d4bb1d52c111 100644
--- a/services/core/java/com/android/server/display/DisplayManagerService.java
+++ b/services/core/java/com/android/server/display/DisplayManagerService.java
@@ -2587,6 +2587,11 @@ public final class DisplayManagerService extends SystemService {
sendDisplayEventIfEnabledLocked(display, DisplayManagerGlobal.EVENT_DISPLAY_STATE_CHANGED);
}
+ private void handleLogicalDisplayCommittedStateChangedLocked(@NonNull LogicalDisplay display) {
+ sendDisplayEventIfEnabledLocked(display,
+ DisplayManagerGlobal.EVENT_DISPLAY_COMMITTED_STATE_CHANGED);
+ }
+
private void notifyDefaultDisplayDeviceUpdated(LogicalDisplay display) {
mDisplayModeDirector.defaultDisplayDeviceUpdated(display.getPrimaryDisplayDeviceLocked()
.mDisplayDeviceConfig);
@@ -2609,7 +2614,8 @@ public final class DisplayManagerService extends SystemService {
// Blank or unblank the display immediately to match the state requested
// by the display power controller (if known).
DisplayDeviceInfo info = device.getDisplayDeviceInfoLocked();
- if ((info.flags & DisplayDeviceInfo.FLAG_NEVER_BLANK) == 0) {
+ if ((info.flags & DisplayDeviceInfo.FLAG_NEVER_BLANK) == 0
+ || android.companion.virtualdevice.flags.Flags.correctVirtualDisplayPowerState()) {
final LogicalDisplay display = mLogicalDisplayMapper.getDisplayLocked(device);
if (display == null) {
return null;
@@ -4165,6 +4171,9 @@ public final class DisplayManagerService extends SystemService {
case LogicalDisplayMapper.LOGICAL_DISPLAY_EVENT_STATE_CHANGED:
handleLogicalDisplayStateChangedLocked(display);
break;
+ case LogicalDisplayMapper.LOGICAL_DISPLAY_EVENT_COMMITTED_STATE_CHANGED:
+ handleLogicalDisplayCommittedStateChangedLocked(display);
+ break;
}
}
@@ -4419,6 +4428,9 @@ public final class DisplayManagerService extends SystemService {
case DisplayManagerGlobal.EVENT_DISPLAY_STATE_CHANGED:
return (mask & DisplayManagerGlobal
.INTERNAL_EVENT_FLAG_DISPLAY_STATE) != 0;
+ case DisplayManagerGlobal.EVENT_DISPLAY_COMMITTED_STATE_CHANGED:
+ return (mask & DisplayManagerGlobal
+ .INTERNAL_EVENT_FLAG_DISPLAY_COMMITTED_STATE_CHANGED) != 0;
default:
// This should never happen.
Slog.e(TAG, "Unknown display event " + event);
@@ -5563,7 +5575,9 @@ public final class DisplayManagerService extends SystemService {
final DisplayDevice displayDevice = mLogicalDisplayMapper.getDisplayLocked(
id).getPrimaryDisplayDeviceLocked();
final int flags = displayDevice.getDisplayDeviceInfoLocked().flags;
- if ((flags & DisplayDeviceInfo.FLAG_NEVER_BLANK) == 0) {
+ if ((flags & DisplayDeviceInfo.FLAG_NEVER_BLANK) == 0
+ || android.companion.virtualdevice.flags.Flags
+ .correctVirtualDisplayPowerState()) {
final DisplayPowerController displayPowerController =
mDisplayPowerControllers.get(id);
if (displayPowerController != null) {
diff --git a/services/core/java/com/android/server/display/LogicalDisplayMapper.java b/services/core/java/com/android/server/display/LogicalDisplayMapper.java
index 02db051dff57..872f33484951 100644
--- a/services/core/java/com/android/server/display/LogicalDisplayMapper.java
+++ b/services/core/java/com/android/server/display/LogicalDisplayMapper.java
@@ -91,6 +91,8 @@ class LogicalDisplayMapper implements DisplayDeviceRepository.Listener {
public static final int LOGICAL_DISPLAY_EVENT_DISCONNECTED = 1 << 8;
public static final int LOGICAL_DISPLAY_EVENT_REFRESH_RATE_CHANGED = 1 << 9;
public static final int LOGICAL_DISPLAY_EVENT_STATE_CHANGED = 1 << 10;
+ public static final int LOGICAL_DISPLAY_EVENT_COMMITTED_STATE_CHANGED = 1 << 11;
+
public static final int DISPLAY_GROUP_EVENT_ADDED = 1;
public static final int DISPLAY_GROUP_EVENT_CHANGED = 2;
@@ -810,7 +812,7 @@ class LogicalDisplayMapper implements DisplayDeviceRepository.Listener {
int logicalDisplayEventMask = mLogicalDisplaysToUpdate
.get(displayId, LOGICAL_DISPLAY_EVENT_BASE);
boolean hasBasicInfoChanged =
- !mTempDisplayInfo.equals(newDisplayInfo, /* compareRefreshRate */ false);
+ !mTempDisplayInfo.equals(newDisplayInfo, /* compareOnlyBasicChanges */ true);
// The display is no longer valid and needs to be removed.
if (!display.isValidLocked()) {
// Remove from group
@@ -930,6 +932,7 @@ class LogicalDisplayMapper implements DisplayDeviceRepository.Listener {
sendUpdatesForDisplaysLocked(LOGICAL_DISPLAY_EVENT_BASIC_CHANGED);
sendUpdatesForDisplaysLocked(LOGICAL_DISPLAY_EVENT_REFRESH_RATE_CHANGED);
sendUpdatesForDisplaysLocked(LOGICAL_DISPLAY_EVENT_STATE_CHANGED);
+ sendUpdatesForDisplaysLocked(LOGICAL_DISPLAY_EVENT_COMMITTED_STATE_CHANGED);
sendUpdatesForDisplaysLocked(LOGICAL_DISPLAY_EVENT_FRAME_RATE_OVERRIDES_CHANGED);
sendUpdatesForDisplaysLocked(LOGICAL_DISPLAY_EVENT_SWAPPED);
sendUpdatesForDisplaysLocked(LOGICAL_DISPLAY_EVENT_CONNECTED);
@@ -961,6 +964,11 @@ class LogicalDisplayMapper implements DisplayDeviceRepository.Listener {
&& mTempDisplayInfo.state != newDisplayInfo.state) {
mask |= LOGICAL_DISPLAY_EVENT_STATE_CHANGED;
}
+
+ if (mFlags.isCommittedStateSeparateEventEnabled()
+ && mTempDisplayInfo.committedState != newDisplayInfo.committedState) {
+ mask |= LOGICAL_DISPLAY_EVENT_COMMITTED_STATE_CHANGED;
+ }
return mask;
}
/**
@@ -1360,6 +1368,8 @@ class LogicalDisplayMapper implements DisplayDeviceRepository.Listener {
return "disconnected";
case LOGICAL_DISPLAY_EVENT_STATE_CHANGED:
return "state_changed";
+ case LOGICAL_DISPLAY_EVENT_COMMITTED_STATE_CHANGED:
+ return "committed_state_changed";
case LOGICAL_DISPLAY_EVENT_REFRESH_RATE_CHANGED:
return "refresh_rate_changed";
case LOGICAL_DISPLAY_EVENT_BASIC_CHANGED:
diff --git a/services/core/java/com/android/server/display/VirtualDisplayAdapter.java b/services/core/java/com/android/server/display/VirtualDisplayAdapter.java
index 4779b690adfb..e7939bb50ece 100644
--- a/services/core/java/com/android/server/display/VirtualDisplayAdapter.java
+++ b/services/core/java/com/android/server/display/VirtualDisplayAdapter.java
@@ -371,7 +371,15 @@ public class VirtualDisplayAdapter extends DisplayAdapter {
mCallback = callback;
mProjection = projection;
mMediaProjectionCallback = mediaProjectionCallback;
- mDisplayState = Display.STATE_ON;
+ if (android.companion.virtualdevice.flags.Flags.correctVirtualDisplayPowerState()) {
+ // The display's power state depends on the power state of the state of its
+ // display / power group, which we don't know here. Initializing to UNKNOWN allows
+ // the first call to requestDisplayStateLocked() to set the correct state.
+ // This also triggers VirtualDisplay.Callback to tell the owner the initial state.
+ mDisplayState = Display.STATE_UNKNOWN;
+ } else {
+ mDisplayState = Display.STATE_ON;
+ }
mPendingChanges |= PENDING_SURFACE_CHANGE;
mDisplayIdToMirror = virtualDisplayConfig.getDisplayIdToMirror();
mIsWindowManagerMirroring = virtualDisplayConfig.isWindowManagerMirroringEnabled();
@@ -564,14 +572,23 @@ public class VirtualDisplayAdapter extends DisplayAdapter {
mInfo.yDpi = mDensityDpi;
mInfo.presentationDeadlineNanos = 1000000000L / (int) getRefreshRate(); // 1 frame
mInfo.flags = 0;
- if ((mFlags & VIRTUAL_DISPLAY_FLAG_PUBLIC) == 0) {
- mInfo.flags |= DisplayDeviceInfo.FLAG_PRIVATE
- | DisplayDeviceInfo.FLAG_NEVER_BLANK;
- }
- if ((mFlags & VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR) != 0) {
- mInfo.flags &= ~DisplayDeviceInfo.FLAG_NEVER_BLANK;
+ if (android.companion.virtualdevice.flags.Flags.correctVirtualDisplayPowerState()) {
+ if ((mFlags & VIRTUAL_DISPLAY_FLAG_PUBLIC) == 0) {
+ mInfo.flags |= DisplayDeviceInfo.FLAG_PRIVATE;
+ }
+ if ((mFlags & VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR) == 0) {
+ mInfo.flags |= DisplayDeviceInfo.FLAG_OWN_CONTENT_ONLY;
+ }
} else {
- mInfo.flags |= DisplayDeviceInfo.FLAG_OWN_CONTENT_ONLY;
+ if ((mFlags & VIRTUAL_DISPLAY_FLAG_PUBLIC) == 0) {
+ mInfo.flags |= DisplayDeviceInfo.FLAG_PRIVATE
+ | DisplayDeviceInfo.FLAG_NEVER_BLANK;
+ }
+ if ((mFlags & VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR) != 0) {
+ mInfo.flags &= ~DisplayDeviceInfo.FLAG_NEVER_BLANK;
+ } else {
+ mInfo.flags |= DisplayDeviceInfo.FLAG_OWN_CONTENT_ONLY;
+ }
}
if ((mFlags & VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP) != 0) {
mInfo.flags |= DisplayDeviceInfo.FLAG_OWN_DISPLAY_GROUP;
diff --git a/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java b/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java
index bc5d90599b41..e4b595ab7c55 100644
--- a/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java
+++ b/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java
@@ -280,6 +280,11 @@ public class DisplayManagerFlags {
Flags::refreshRateEventForForegroundApps
);
+ private final FlagState mCommittedStateSeparateEvent = new FlagState(
+ Flags.FLAG_COMMITTED_STATE_SEPARATE_EVENT,
+ Flags::committedStateSeparateEvent
+ );
+
/**
* @return {@code true} if 'port' is allowed in display layout configuration file.
*/
@@ -603,6 +608,14 @@ public class DisplayManagerFlags {
}
/**
+ * @return {@code true} if the flag for having a separate event for display's committed state
+ * is enabled
+ */
+ public boolean isCommittedStateSeparateEventEnabled() {
+ return mCommittedStateSeparateEvent.isEnabled();
+ }
+
+ /**
* dumps all flagstates
* @param pw printWriter
*/
@@ -659,6 +672,7 @@ public class DisplayManagerFlags {
pw.println(" " + mBaseDensityForExternalDisplays);
pw.println(" " + mFramerateOverrideTriggersRrCallbacks);
pw.println(" " + mRefreshRateEventForForegroundApps);
+ pw.println(" " + mCommittedStateSeparateEvent);
}
private static class FlagState {
diff --git a/services/core/java/com/android/server/display/feature/display_flags.aconfig b/services/core/java/com/android/server/display/feature/display_flags.aconfig
index 8211febade60..acdc0e0cf891 100644
--- a/services/core/java/com/android/server/display/feature/display_flags.aconfig
+++ b/services/core/java/com/android/server/display/feature/display_flags.aconfig
@@ -456,9 +456,8 @@ flag {
flag {
name: "enable_display_content_mode_management"
namespace: "lse_desktop_experience"
- description: "Enable switching the content mode of connected displays between mirroring and extened. Also change the default content mode to extended mode."
+ description: "Enable switching the content mode of connected displays between mirroring and extended. Also change the default content mode to extended mode."
bug: "378385869"
- is_fixed_read_only: true
}
flag {
@@ -509,3 +508,14 @@ flag {
bug: "293651324"
is_fixed_read_only: false
}
+
+flag {
+ name: "committed_state_separate_event"
+ namespace: "display_manager"
+ description: "Move Display committed state into a separate event"
+ bug: "342192387"
+ is_fixed_read_only: true
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
diff --git a/services/core/java/com/android/server/dreams/DreamManagerService.java b/services/core/java/com/android/server/dreams/DreamManagerService.java
index 2af74f620c95..7e8bb28b6a37 100644
--- a/services/core/java/com/android/server/dreams/DreamManagerService.java
+++ b/services/core/java/com/android/server/dreams/DreamManagerService.java
@@ -569,8 +569,7 @@ public final class DreamManagerService extends SystemService {
}
private void requestDreamInternal() {
- if (isDreamingInternal() && !dreamIsFrontmost() && mController.bringDreamToFront()
- && !isDozingInternal()) {
+ if (isDreamingInternal() && !dreamIsFrontmost() && mController.bringDreamToFront()) {
return;
}
diff --git a/services/core/java/com/android/server/hdmi/AudioDeviceVolumeManagerWrapper.java b/services/core/java/com/android/server/hdmi/AudioDeviceVolumeManagerWrapper.java
index 94842041af82..ab86433ca50d 100644
--- a/services/core/java/com/android/server/hdmi/AudioDeviceVolumeManagerWrapper.java
+++ b/services/core/java/com/android/server/hdmi/AudioDeviceVolumeManagerWrapper.java
@@ -58,9 +58,9 @@ public interface AudioDeviceVolumeManagerWrapper {
void setDeviceAbsoluteVolumeBehavior(
@NonNull AudioDeviceAttributes device,
@NonNull VolumeInfo volume,
+ boolean handlesVolumeAdjustment,
@NonNull @CallbackExecutor Executor executor,
- @NonNull AudioDeviceVolumeManager.OnAudioDeviceVolumeChangedListener vclistener,
- boolean handlesVolumeAdjustment);
+ @NonNull AudioDeviceVolumeManager.OnAudioDeviceVolumeChangedListener vclistener);
/**
* Wrapper for {@link AudioDeviceVolumeManager#setDeviceAbsoluteVolumeAdjustOnlyBehavior(
@@ -69,7 +69,7 @@ public interface AudioDeviceVolumeManagerWrapper {
void setDeviceAbsoluteVolumeAdjustOnlyBehavior(
@NonNull AudioDeviceAttributes device,
@NonNull VolumeInfo volume,
+ boolean handlesVolumeAdjustment,
@NonNull @CallbackExecutor Executor executor,
- @NonNull AudioDeviceVolumeManager.OnAudioDeviceVolumeChangedListener vclistener,
- boolean handlesVolumeAdjustment);
+ @NonNull AudioDeviceVolumeManager.OnAudioDeviceVolumeChangedListener vclistener);
}
diff --git a/services/core/java/com/android/server/hdmi/DefaultAudioDeviceVolumeManagerWrapper.java b/services/core/java/com/android/server/hdmi/DefaultAudioDeviceVolumeManagerWrapper.java
index ff99ace38ef0..10cbb00d2398 100644
--- a/services/core/java/com/android/server/hdmi/DefaultAudioDeviceVolumeManagerWrapper.java
+++ b/services/core/java/com/android/server/hdmi/DefaultAudioDeviceVolumeManagerWrapper.java
@@ -61,21 +61,21 @@ public class DefaultAudioDeviceVolumeManagerWrapper
public void setDeviceAbsoluteVolumeBehavior(
@NonNull AudioDeviceAttributes device,
@NonNull VolumeInfo volume,
+ boolean handlesVolumeAdjustment,
@NonNull @CallbackExecutor Executor executor,
- @NonNull AudioDeviceVolumeManager.OnAudioDeviceVolumeChangedListener vclistener,
- boolean handlesVolumeAdjustment) {
- mAudioDeviceVolumeManager.setDeviceAbsoluteVolumeBehavior(device, volume, executor,
- vclistener, handlesVolumeAdjustment);
+ @NonNull AudioDeviceVolumeManager.OnAudioDeviceVolumeChangedListener vclistener) {
+ mAudioDeviceVolumeManager.setDeviceAbsoluteVolumeBehavior(device, volume,
+ handlesVolumeAdjustment, executor, vclistener);
}
@Override
public void setDeviceAbsoluteVolumeAdjustOnlyBehavior(
@NonNull AudioDeviceAttributes device,
@NonNull VolumeInfo volume,
+ boolean handlesVolumeAdjustment,
@NonNull @CallbackExecutor Executor executor,
- @NonNull AudioDeviceVolumeManager.OnAudioDeviceVolumeChangedListener vclistener,
- boolean handlesVolumeAdjustment) {
+ @NonNull AudioDeviceVolumeManager.OnAudioDeviceVolumeChangedListener vclistener) {
mAudioDeviceVolumeManager.setDeviceAbsoluteVolumeAdjustOnlyBehavior(device, volume,
- executor, vclistener, handlesVolumeAdjustment);
+ handlesVolumeAdjustment, executor, vclistener);
}
}
diff --git a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
index 3d6d34bf9911..3cb21c3e2697 100644
--- a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
+++ b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
@@ -1404,6 +1404,9 @@ public class HdmiCecLocalDeviceTv extends HdmiCecLocalDevice {
if (connected) {
if (mArcEstablished) {
enableAudioReturnChannel(true);
+ } else {
+ HdmiLogger.debug("Restart ARC again");
+ onNewAvrAdded(getAvrDeviceInfo());
}
} else {
enableAudioReturnChannel(false);
diff --git a/services/core/java/com/android/server/hdmi/HdmiControlService.java b/services/core/java/com/android/server/hdmi/HdmiControlService.java
index 89f0d0edbf2b..6d973ac8d1b5 100644
--- a/services/core/java/com/android/server/hdmi/HdmiControlService.java
+++ b/services/core/java/com/android/server/hdmi/HdmiControlService.java
@@ -4798,15 +4798,15 @@ public class HdmiControlService extends SystemService {
Slog.d(TAG, "Enabling absolute volume behavior");
for (AudioDeviceAttributes device : getAvbCapableAudioOutputDevices()) {
getAudioDeviceVolumeManager().setDeviceAbsoluteVolumeBehavior(
- device, volumeInfo, mServiceThreadExecutor,
- mAbsoluteVolumeChangedListener, true);
+ device, volumeInfo, true, mServiceThreadExecutor,
+ mAbsoluteVolumeChangedListener);
}
} else if (tv() != null) {
Slog.d(TAG, "Enabling adjust-only absolute volume behavior");
for (AudioDeviceAttributes device : getAvbCapableAudioOutputDevices()) {
getAudioDeviceVolumeManager().setDeviceAbsoluteVolumeAdjustOnlyBehavior(
- device, volumeInfo, mServiceThreadExecutor,
- mAbsoluteVolumeChangedListener, true);
+ device, volumeInfo, true, mServiceThreadExecutor,
+ mAbsoluteVolumeChangedListener);
}
}
diff --git a/services/core/java/com/android/server/input/InputManagerInternal.java b/services/core/java/com/android/server/input/InputManagerInternal.java
index 87f693cc7291..1ace41cba364 100644
--- a/services/core/java/com/android/server/input/InputManagerInternal.java
+++ b/services/core/java/com/android/server/input/InputManagerInternal.java
@@ -344,4 +344,42 @@ public abstract class InputManagerInternal {
*/
public abstract void applyBackupPayload(Map<Integer, byte[]> payload, int userId)
throws XmlPullParserException, IOException;
+
+ /**
+ * An interface for filtering pointer motion event before cursor position is determined.
+ * <p>
+ * Different from {@code android.view.InputFilter}, this filter can filter motion events at
+ * an early stage of the input pipeline, but only called for pointer's relative motion events.
+ * Unless the user really needs to filter events before the cursor position in the display is
+ * determined, use {@code android.view.InputFilter} instead.
+ */
+ public interface AccessibilityPointerMotionFilter {
+ /**
+ * Called everytime pointer's relative motion event happens.
+ * The returned dx and dy will be used to move the cursor in the display.
+ * <p>
+ * This call happens on the input hot path and it is extremely performance sensitive. It
+ * also must not call back into native code.
+ *
+ * @param dx delta x of the event in pixels.
+ * @param dy delta y of the event in pixels.
+ * @param currentX the cursor x coordinate on the screen before the motion event.
+ * @param currentY the cursor y coordinate on the screen before the motion event.
+ * @param displayId the display ID of the current cursor.
+ * @return an array of length 2, delta x and delta y after filtering the motion. The delta
+ * values are in pixels and must be between 0 and original delta.
+ */
+ @NonNull
+ float[] filterPointerMotionEvent(float dx, float dy, float currentX, float currentY,
+ int displayId);
+ }
+
+ /**
+ * Registers an {@code AccessibilityCursorFilter}.
+ *
+ * @param filter The filter to register. If a filter is already registered, the old filter is
+ * unregistered. {@code null} unregisters the filter that is already registered.
+ */
+ public abstract void registerAccessibilityPointerMotionFilter(
+ @Nullable AccessibilityPointerMotionFilter filter);
}
diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java
index 8624f4230e9c..0e37238bcb84 100644
--- a/services/core/java/com/android/server/input/InputManagerService.java
+++ b/services/core/java/com/android/server/input/InputManagerService.java
@@ -25,8 +25,8 @@ import static android.view.KeyEvent.KEYCODE_UNKNOWN;
import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
import static com.android.hardware.input.Flags.enableCustomizableInputGestures;
-import static com.android.hardware.input.Flags.touchpadVisualizer;
import static com.android.hardware.input.Flags.keyEventActivityDetection;
+import static com.android.hardware.input.Flags.touchpadVisualizer;
import static com.android.hardware.input.Flags.useKeyGestureEventHandler;
import static com.android.server.policy.WindowManagerPolicy.ACTION_PASS_TO_USER;
@@ -193,15 +193,11 @@ public class InputManagerService extends IInputManager.Stub
private static final int MSG_SYSTEM_READY = 5;
private static final int DEFAULT_VIBRATION_MAGNITUDE = 192;
- private static final AdditionalDisplayInputProperties
- DEFAULT_ADDITIONAL_DISPLAY_INPUT_PROPERTIES = new AdditionalDisplayInputProperties();
private final NativeInputManagerService mNative;
private final Context mContext;
private final InputManagerHandler mHandler;
- @UserIdInt
- private int mCurrentUserId = UserHandle.USER_SYSTEM;
private DisplayManagerInternal mDisplayManagerInternal;
private WindowManagerInternal mWindowManagerInternal;
@@ -289,7 +285,7 @@ public class InputManagerService extends IInputManager.Stub
final Object mKeyEventActivityLock = new Object();
@GuardedBy("mKeyEventActivityLock")
- private List<IKeyEventActivityListener> mKeyEventActivityListenersToNotify =
+ private final List<IKeyEventActivityListener> mKeyEventActivityListenersToNotify =
new ArrayList<>();
// Rate limit for key event activity detection. Prevent the listener from being notified
@@ -460,6 +456,14 @@ public class InputManagerService extends IInputManager.Stub
private boolean mShowKeyPresses = false;
private boolean mShowRotaryInput = false;
+ /**
+ * A lock for the accessibility pointer motion filter. Don't call native methods while holding
+ * this lock.
+ */
+ private final Object mAccessibilityPointerMotionFilterLock = new Object();
+ private InputManagerInternal.AccessibilityPointerMotionFilter
+ mAccessibilityPointerMotionFilter = null;
+
/** Point of injection for test dependencies. */
@VisibleForTesting
static class Injector {
@@ -2593,6 +2597,23 @@ public class InputManagerService extends IInputManager.Stub
// Native callback.
@SuppressWarnings("unused")
+ final float[] filterPointerMotion(float dx, float dy, float currentX, float currentY,
+ int displayId) {
+ // This call happens on the input hot path and it is extremely performance sensitive.
+ // This must not call back into native code. This is called while the
+ // PointerChoreographer's lock is held.
+ synchronized (mAccessibilityPointerMotionFilterLock) {
+ if (mAccessibilityPointerMotionFilter == null) {
+ throw new IllegalStateException(
+ "filterCursor is invoked but no callback is registered.");
+ }
+ return mAccessibilityPointerMotionFilter.filterPointerMotionEvent(dx, dy, currentX,
+ currentY, displayId);
+ }
+ }
+
+ // Native callback.
+ @SuppressWarnings("unused")
@VisibleForTesting
public int interceptKeyBeforeQueueing(KeyEvent event, int policyFlags) {
notifyKeyActivityListeners(event);
@@ -3215,7 +3236,6 @@ public class InputManagerService extends IInputManager.Stub
}
private void handleCurrentUserChanged(@UserIdInt int userId) {
- mCurrentUserId = userId;
mKeyGestureController.setCurrentUserId(userId);
}
@@ -3828,6 +3848,12 @@ public class InputManagerService extends IInputManager.Stub
payload.get(BACKUP_CATEGORY_INPUT_GESTURES), userId);
}
}
+
+ @Override
+ public void registerAccessibilityPointerMotionFilter(
+ AccessibilityPointerMotionFilter filter) {
+ InputManagerService.this.registerAccessibilityPointerMotionFilter(filter);
+ }
}
@Override
@@ -4014,6 +4040,26 @@ public class InputManagerService extends IInputManager.Stub
mPointerIconCache.setAccessibilityScaleFactor(displayId, scaleFactor);
}
+ void registerAccessibilityPointerMotionFilter(
+ InputManagerInternal.AccessibilityPointerMotionFilter filter) {
+ // `#filterPointerMotion` expects that when it's called, `mAccessibilityPointerMotionFilter`
+ // is not null.
+ // Also, to avoid potential lock contention, we shouldn't call native method while holding
+ // the lock here. Native code calls `#filterPointerMotion` while PointerChoreographer's
+ // lock is held.
+ // Thus, we must set filter before we enable the filter in native, and reset the filter
+ // after we disable the filter.
+ // This also ensures the previously installed filter isn't called after the filter is
+ // updated.
+ mNative.setAccessibilityPointerMotionFilterEnabled(false);
+ synchronized (mAccessibilityPointerMotionFilterLock) {
+ mAccessibilityPointerMotionFilter = filter;
+ }
+ if (filter != null) {
+ mNative.setAccessibilityPointerMotionFilterEnabled(true);
+ }
+ }
+
interface KeyboardBacklightControllerInterface {
default void incrementKeyboardBacklight(int deviceId) {}
default void decrementKeyboardBacklight(int deviceId) {}
diff --git a/services/core/java/com/android/server/input/NativeInputManagerService.java b/services/core/java/com/android/server/input/NativeInputManagerService.java
index f34338a397db..32409d39db3b 100644
--- a/services/core/java/com/android/server/input/NativeInputManagerService.java
+++ b/services/core/java/com/android/server/input/NativeInputManagerService.java
@@ -315,6 +315,16 @@ interface NativeInputManagerService {
*/
boolean setKernelWakeEnabled(int deviceId, boolean enabled);
+ /**
+ * Set whether the accessibility pointer motion filter is enabled.
+ * <p>
+ * Once enabled, {@link InputManagerService#filterPointerMotion} is called for evety motion
+ * event from pointer devices.
+ *
+ * @param enabled {@code true} if the filter is enabled, {@code false} otherwise.
+ */
+ void setAccessibilityPointerMotionFilterEnabled(boolean enabled);
+
/** The native implementation of InputManagerService methods. */
class NativeImpl implements NativeInputManagerService {
/** Pointer to native input manager service object, used by native code. */
@@ -628,5 +638,8 @@ interface NativeInputManagerService {
@Override
public native boolean setKernelWakeEnabled(int deviceId, boolean enabled);
+
+ @Override
+ public native void setAccessibilityPointerMotionFilterEnabled(boolean enabled);
}
}
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index 484b47022f04..508bc2f811e0 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -365,7 +365,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl.
return mCurrentImeUserId;
}
- /**
+ /**
* Figures out the target IME user ID associated with the given {@code displayId}.
*
* @param displayId the display ID to be queried about
@@ -649,12 +649,25 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl.
visibilityStateComputer.getImePolicy().setA11yRequestNoSoftKeyboard(
accessibilitySoftKeyboardSetting);
if (visibilityStateComputer.getImePolicy().isA11yRequestNoSoftKeyboard()) {
- hideCurrentInputLocked(userData.mImeBindingState.mFocusedWindow,
- 0 /* flags */, SoftInputShowHideReason.HIDE_SETTINGS_ON_CHANGE, userId);
+ if (Flags.refactorInsetsController()) {
+ final var statsToken = createStatsTokenForFocusedClient(false /* show */,
+ SoftInputShowHideReason.HIDE_SETTINGS_ON_CHANGE, userId);
+ setImeVisibilityOnFocusedWindowClient(false, userData, statsToken);
+ } else {
+ hideCurrentInputLocked(userData.mImeBindingState.mFocusedWindow,
+ 0 /* flags */, SoftInputShowHideReason.HIDE_SETTINGS_ON_CHANGE,
+ userId);
+ }
} else if (isShowRequestedForCurrentWindow(userId)) {
- showCurrentInputLocked(userData.mImeBindingState.mFocusedWindow,
- InputMethodManager.SHOW_IMPLICIT,
- SoftInputShowHideReason.SHOW_SETTINGS_ON_CHANGE, userId);
+ if (Flags.refactorInsetsController()) {
+ final var statsToken = createStatsTokenForFocusedClient(true /* show */,
+ SoftInputShowHideReason.SHOW_SETTINGS_ON_CHANGE, userId);
+ setImeVisibilityOnFocusedWindowClient(true, userData, statsToken);
+ } else {
+ showCurrentInputLocked(userData.mImeBindingState.mFocusedWindow,
+ InputMethodManager.SHOW_IMPLICIT,
+ SoftInputShowHideReason.SHOW_SETTINGS_ON_CHANGE, userId);
+ }
}
break;
}
@@ -1319,8 +1332,8 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl.
// Do not reset the default (current) IME when it is a 3rd-party IME
String selectedMethodId = bindingController.getSelectedMethodId();
final InputMethodSettings settings = InputMethodSettingsRepository.get(userId);
- if (selectedMethodId != null && settings.getMethodMap().get(selectedMethodId) != null
- && !settings.getMethodMap().get(selectedMethodId).isSystem()) {
+ final InputMethodInfo selectedImi = settings.getMethodMap().get(selectedMethodId);
+ if (selectedImi != null && !selectedImi.isSystem()) {
return;
}
final List<InputMethodInfo> suitableImes = InputMethodInfoUtils.getDefaultEnabledImes(
diff --git a/services/core/java/com/android/server/media/MediaRoute2ProviderWatcher.java b/services/core/java/com/android/server/media/MediaRoute2ProviderWatcher.java
index 42303e042561..b735b2447486 100644
--- a/services/core/java/com/android/server/media/MediaRoute2ProviderWatcher.java
+++ b/services/core/java/com/android/server/media/MediaRoute2ProviderWatcher.java
@@ -141,8 +141,7 @@ final class MediaRoute2ProviderWatcher {
isSelfScanOnlyProvider |=
MediaRoute2ProviderService.CATEGORY_SELF_SCAN_ONLY.equals(category);
supportsSystemMediaRouting |=
- MediaRoute2ProviderService.SERVICE_INTERFACE_SYSTEM_MEDIA.equals(
- category);
+ MediaRoute2ProviderService.CATEGORY_SYSTEM_MEDIA.equals(category);
}
}
int sourceIndex = findProvider(serviceInfo.packageName, serviceInfo.name);
diff --git a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
index 6c0d8ad7264d..debac9436bb3 100644
--- a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
+++ b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
@@ -1635,11 +1635,11 @@ class MediaRouter2ServiceImpl {
manager));
}
- List<MediaRoute2Info> routes =
- userRecord.mHandler.mLastNotifiedRoutesToPrivilegedRouters.values().stream()
- .toList();
userRecord.mHandler.sendMessage(
- obtainMessage(ManagerRecord::notifyRoutesUpdated, managerRecord, routes));
+ obtainMessage(
+ UserHandler::dispatchRoutesToManagerOnHandler,
+ userRecord.mHandler,
+ managerRecord));
}
@GuardedBy("mLock")
@@ -2119,6 +2119,9 @@ class MediaRouter2ServiceImpl {
mHasBluetoothRoutingPermission.set(checkCallerHasBluetoothPermissions(mPid, mUid));
boolean newSystemRoutingPermissionValue = hasSystemRoutingPermission();
if (oldSystemRoutingPermissionValue != newSystemRoutingPermissionValue) {
+ // TODO: b/379788233 - Ensure access to fields like
+ // mLastNotifiedRoutesToPrivilegedRouters happens on the right thread. We might need
+ // to run this on the handler.
Map<String, MediaRoute2Info> routesToReport =
newSystemRoutingPermissionValue
? mUserRecord.mHandler.mLastNotifiedRoutesToPrivilegedRouters
@@ -2543,6 +2546,8 @@ class MediaRouter2ServiceImpl {
* both system route providers and user route providers.
*
* <p>See {@link #getRouterRecords(boolean hasModifyAudioRoutingPermission)}.
+ *
+ * <p>Must be accessed on this handler's thread.
*/
private final Map<String, MediaRoute2Info> mLastNotifiedRoutesToPrivilegedRouters =
new ArrayMap<>();
@@ -2558,6 +2563,8 @@ class MediaRouter2ServiceImpl {
* (e.g. volume changes) to non-privileged routers.
*
* <p>See {@link SystemMediaRoute2Provider#mDefaultRoute}.
+ *
+ * <p>Must be accessed on this handler's thread.
*/
private final Map<String, MediaRoute2Info> mLastNotifiedRoutesToNonPrivilegedRouters =
new ArrayMap<>();
@@ -2800,7 +2807,7 @@ class MediaRouter2ServiceImpl {
removedRoutes));
}
- dispatchUpdates(
+ dispatchUpdatesOnHandler(
hasAddedOrModifiedRoutes,
hasRemovedRoutes,
provider.mIsSystemRouteProvider,
@@ -2822,6 +2829,13 @@ class MediaRouter2ServiceImpl {
source, providerId, routesString);
}
+ /** Notifies the given manager of the current routes. */
+ public void dispatchRoutesToManagerOnHandler(ManagerRecord managerRecord) {
+ List<MediaRoute2Info> routes =
+ mLastNotifiedRoutesToPrivilegedRouters.values().stream().toList();
+ managerRecord.notifyRoutesUpdated(routes);
+ }
+
/**
* Dispatches the latest route updates in {@link #mLastNotifiedRoutesToPrivilegedRouters}
* and {@link #mLastNotifiedRoutesToNonPrivilegedRouters} to registered {@link
@@ -2834,7 +2848,7 @@ class MediaRouter2ServiceImpl {
* @param isSystemProvider whether the latest update was caused by a system provider.
* @param defaultRoute the current default route in {@link #mSystemProvider}.
*/
- private void dispatchUpdates(
+ private void dispatchUpdatesOnHandler(
boolean hasAddedOrModifiedRoutes,
boolean hasRemovedRoutes,
boolean isSystemProvider,
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 0b8b115e65d0..91a2843ccaf7 100644
--- a/services/core/java/com/android/server/media/quality/MediaQualityService.java
+++ b/services/core/java/com/android/server/media/quality/MediaQualityService.java
@@ -120,6 +120,8 @@ public class MediaQualityService extends SystemService {
private final Object mPictureProfileLock = new Object();
// A global lock for sound profile objects.
private final Object mSoundProfileLock = new Object();
+ // A global lock for user state objects.
+ private final Object mUserStateLock = new Object();
// A global lock for ambient backlight objects.
private final Object mAmbientBacklightLock = new Object();
@@ -127,17 +129,17 @@ public class MediaQualityService extends SystemService {
super(context);
mContext = context;
mHalAmbientBacklightCallback = new HalAmbientBacklightCallback();
- mPictureProfileAdjListener = new PictureProfileAdjustmentListenerImpl(mContext);
- mSoundProfileAdjListener = new SoundProfileAdjustmentListenerImpl(mContext);
mPackageManager = mContext.getPackageManager();
mPictureProfileTempIdMap = new BiMap<>();
mSoundProfileTempIdMap = new BiMap<>();
mMediaQualityDbHelper = new MediaQualityDbHelper(mContext);
- mMqDatabaseUtils = new MqDatabaseUtils(mContext);
mMediaQualityDbHelper.setWriteAheadLoggingEnabled(true);
mMediaQualityDbHelper.setIdleConnectionTimeout(30);
- mHalNotifier = new HalNotifier();
mMqManagerNotifier = new MqManagerNotifier();
+ mMqDatabaseUtils = new MqDatabaseUtils();
+ mHalNotifier = new HalNotifier();
+ mPictureProfileAdjListener = new PictureProfileAdjustmentListenerImpl();
+ mSoundProfileAdjListener = new SoundProfileAdjustmentListenerImpl();
// The package info in the context isn't initialized in the way it is for normal apps,
// so the standard, name-based context.getSharedPreferences doesn't work. Instead, we
@@ -166,20 +168,21 @@ public class MediaQualityService extends SystemService {
if (mMediaQuality != null) {
try {
mMediaQuality.setAmbientBacklightCallback(mHalAmbientBacklightCallback);
+
+ mPpChangedListener = mMediaQuality.getPictureProfileListener();
+ mSpChangedListener = mMediaQuality.getSoundProfileListener();
+
mMediaQuality.setPictureProfileAdjustmentListener(mPictureProfileAdjListener);
mMediaQuality.setSoundProfileAdjustmentListener(mSoundProfileAdjListener);
+
} catch (RemoteException e) {
Slog.e(TAG, "Failed to set ambient backlight detector callback", e);
}
}
- mPpChangedListener = IPictureProfileChangedListener.Stub.asInterface(binder);
- mSpChangedListener = ISoundProfileChangedListener.Stub.asInterface(binder);
-
publishBinderService(Context.MEDIA_QUALITY_SERVICE, new BinderService());
}
- // TODO: Add additional APIs. b/373951081
private final class BinderService extends IMediaQualityManager.Stub {
@GuardedBy("mPictureProfileLock")
@@ -225,7 +228,6 @@ public class MediaQualityService extends SystemService {
PictureProfile.ERROR_NO_PERMISSION,
Binder.getCallingUid(), Binder.getCallingPid());
}
-
synchronized (mPictureProfileLock) {
ContentValues values = MediaQualityUtils.getContentValues(dbId,
pp.getProfileType(),
@@ -233,7 +235,6 @@ public class MediaQualityService extends SystemService {
pp.getPackageName(),
pp.getInputId(),
pp.getParameters());
-
updateDatabaseOnPictureProfileAndNotifyManagerAndHal(values, pp.getParameters());
}
}
@@ -269,12 +270,13 @@ public class MediaQualityService extends SystemService {
mMqManagerNotifier.notifyOnPictureProfileError(id,
PictureProfile.ERROR_INVALID_ARGUMENT,
Binder.getCallingUid(), Binder.getCallingPid());
+ } else {
+ mMqManagerNotifier.notifyOnPictureProfileRemoved(
+ mPictureProfileTempIdMap.getValue(dbId), toDelete,
+ Binder.getCallingUid(), Binder.getCallingPid());
+ mPictureProfileTempIdMap.remove(dbId);
+ mHalNotifier.notifyHalOnPictureProfileChange(dbId, null);
}
- mMqManagerNotifier.notifyOnPictureProfileRemoved(
- mPictureProfileTempIdMap.getValue(dbId), toDelete,
- Binder.getCallingUid(), Binder.getCallingPid());
- mPictureProfileTempIdMap.remove(dbId);
- mHalNotifier.notifyHalOnPictureProfileChange(dbId, null);
}
}
}
@@ -520,12 +522,13 @@ public class MediaQualityService extends SystemService {
mMqManagerNotifier.notifyOnSoundProfileError(id,
SoundProfile.ERROR_INVALID_ARGUMENT,
Binder.getCallingUid(), Binder.getCallingPid());
+ } else {
+ mMqManagerNotifier.notifyOnSoundProfileRemoved(
+ mSoundProfileTempIdMap.getValue(dbId), toDelete,
+ Binder.getCallingUid(), Binder.getCallingPid());
+ mSoundProfileTempIdMap.remove(dbId);
+ mHalNotifier.notifyHalOnSoundProfileChange(dbId, null);
}
- mMqManagerNotifier.notifyOnSoundProfileRemoved(
- mSoundProfileTempIdMap.getValue(dbId), toDelete,
- Binder.getCallingUid(), Binder.getCallingPid());
- mSoundProfileTempIdMap.remove(dbId);
- mHalNotifier.notifyHalOnSoundProfileChange(dbId, null);
}
}
}
@@ -684,24 +687,22 @@ public class MediaQualityService extends SystemService {
mContext.getPackageName()) == mPackageManager.PERMISSION_GRANTED;
}
- //TODO: need lock here?
@Override
public void registerPictureProfileCallback(final IPictureProfileCallback callback) {
int callingPid = Binder.getCallingPid();
int callingUid = Binder.getCallingUid();
- UserState userState = getOrCreateUserStateLocked(Binder.getCallingUid());
+ UserState userState = getOrCreateUserState(Binder.getCallingUid());
userState.mPictureProfileCallbackPidUidMap.put(callback,
Pair.create(callingPid, callingUid));
}
- //TODO: need lock here?
@Override
public void registerSoundProfileCallback(final ISoundProfileCallback callback) {
int callingPid = Binder.getCallingPid();
int callingUid = Binder.getCallingUid();
- UserState userState = getOrCreateUserStateLocked(Binder.getCallingUid());
+ UserState userState = getOrCreateUserState(Binder.getCallingUid());
userState.mSoundProfileCallbackPidUidMap.put(callback,
Pair.create(callingPid, callingUid));
}
@@ -792,7 +793,6 @@ public class MediaQualityService extends SystemService {
}
}
- //TODO: do I need a lock here?
@Override
public List<ParameterCapability> getParameterCapabilities(
List<String> names, UserHandle user) {
@@ -809,14 +809,20 @@ public class MediaQualityService extends SystemService {
private List<ParameterCapability> getListParameterCapability(ParamCapability[] caps) {
List<ParameterCapability> pcList = new ArrayList<>();
- for (ParamCapability pcHal : caps) {
- String name = MediaQualityUtils.getParameterName(pcHal.name);
- boolean isSupported = pcHal.isSupported;
- int type = pcHal.defaultValue == null ? 0 : pcHal.defaultValue.getTag() + 1;
- Bundle bundle = MediaQualityUtils.convertToCaps(type, pcHal.range);
- pcList.add(new ParameterCapability(name, isSupported, type, bundle));
+ if (caps != null) {
+ for (ParamCapability pcHal : caps) {
+ if (pcHal != null) {
+ String name = MediaQualityUtils.getParameterName(pcHal.name);
+ boolean isSupported = pcHal.isSupported;
+ int type = pcHal.defaultValue == null ? 0 : pcHal.defaultValue.getTag() + 1;
+ Bundle bundle = MediaQualityUtils.convertToCaps(type, pcHal.range);
+
+ pcList.add(new ParameterCapability(name, isSupported, type, bundle));
+ }
+ }
}
+
return pcList;
}
@@ -1055,7 +1061,7 @@ public class MediaQualityService extends SystemService {
synchronized (mPictureProfileLock) {
for (int i = 0; i < mUserStates.size(); i++) {
int userId = mUserStates.keyAt(i);
- UserState userState = getOrCreateUserStateLocked(userId);
+ UserState userState = getOrCreateUserState(userId);
userState.mPictureProfileCallbackPidUidMap.remove(callback);
}
}
@@ -1069,7 +1075,7 @@ public class MediaQualityService extends SystemService {
synchronized (mSoundProfileLock) {
for (int i = 0; i < mUserStates.size(); i++) {
int userId = mUserStates.keyAt(i);
- UserState userState = getOrCreateUserStateLocked(userId);
+ UserState userState = getOrCreateUserState(userId);
userState.mSoundProfileCallbackPidUidMap.remove(callback);
}
}
@@ -1095,25 +1101,27 @@ public class MediaQualityService extends SystemService {
}
}
- //TODO: used by both picture and sound. can i add both locks?
- private UserState getOrCreateUserStateLocked(int userId) {
- UserState userState = getUserStateLocked(userId);
+ @GuardedBy("mUserStateLock")
+ private UserState getOrCreateUserState(int userId) {
+ UserState userState = getUserState(userId);
if (userState == null) {
userState = new UserState(mContext, userId);
- mUserStates.put(userId, userState);
+ synchronized (mUserStateLock) {
+ mUserStates.put(userId, userState);
+ }
}
return userState;
}
- //TODO: used by both picture and sound. can i add both locks?
- private UserState getUserStateLocked(int userId) {
- return mUserStates.get(userId);
+ @GuardedBy("mUserStateLock")
+ private UserState getUserState(int userId) {
+ synchronized (mUserStateLock) {
+ return mUserStates.get(userId);
+ }
}
private final class MqDatabaseUtils {
- MediaQualityDbHelper mMediaQualityDbHelper;
-
private PictureProfile getPictureProfile(Long dbId) {
String selection = BaseParameters.PARAMETER_ID + " = ?";
String[] selectionArguments = {Long.toString(dbId)};
@@ -1205,8 +1213,7 @@ public class MediaQualityService extends SystemService {
/*groupBy=*/ null, /*having=*/ null, /*orderBy=*/ null);
}
- private MqDatabaseUtils(Context context) {
- mMediaQualityDbHelper = new MediaQualityDbHelper(context);
+ private MqDatabaseUtils() {
}
}
@@ -1264,7 +1271,7 @@ public class MediaQualityService extends SystemService {
private void notifyPictureProfileHelper(int mode, String profileId,
PictureProfile profile, Integer errorCode,
List<ParameterCapability> paramCaps, int uid, int pid) {
- UserState userState = getOrCreateUserStateLocked(UserHandle.USER_SYSTEM);
+ UserState userState = getOrCreateUserState(UserHandle.USER_SYSTEM);
int n = userState.mPictureProfileCallbacks.beginBroadcast();
for (int i = 0; i < n; ++i) {
@@ -1349,7 +1356,7 @@ public class MediaQualityService extends SystemService {
private void notifySoundProfileHelper(int mode, String profileId,
SoundProfile profile, Integer errorCode,
List<ParameterCapability> paramCaps, int uid, int pid) {
- UserState userState = getOrCreateUserStateLocked(UserHandle.USER_SYSTEM);
+ UserState userState = getOrCreateUserState(UserHandle.USER_SYSTEM);
int n = userState.mSoundProfileCallbacks.beginBroadcast();
for (int i = 0; i < n; ++i) {
@@ -1404,11 +1411,13 @@ public class MediaQualityService extends SystemService {
private void notifyHalOnPictureProfileChange(Long dbId, PersistableBundle params) {
// TODO: only notify HAL when the profile is active / being used
- try {
- mPpChangedListener.onPictureProfileChanged(convertToHalPictureProfile(dbId,
- params));
- } catch (RemoteException e) {
- Slog.e(TAG, "Failed to notify HAL on picture profile change.", e);
+ if (mPpChangedListener != null) {
+ try {
+ mPpChangedListener.onPictureProfileChanged(convertToHalPictureProfile(dbId,
+ params));
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Failed to notify HAL on picture profile change.", e);
+ }
}
}
@@ -1429,10 +1438,13 @@ public class MediaQualityService extends SystemService {
private void notifyHalOnSoundProfileChange(Long dbId, PersistableBundle params) {
// TODO: only notify HAL when the profile is active / being used
- try {
- mSpChangedListener.onSoundProfileChanged(convertToHalSoundProfile(dbId, params));
- } catch (RemoteException e) {
- Slog.e(TAG, "Failed to notify HAL on sound profile change.", e);
+ if (mSpChangedListener != null) {
+ try {
+ mSpChangedListener
+ .onSoundProfileChanged(convertToHalSoundProfile(dbId, params));
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Failed to notify HAL on sound profile change.", e);
+ }
}
}
@@ -1488,9 +1500,6 @@ public class MediaQualityService extends SystemService {
private final class PictureProfileAdjustmentListenerImpl extends
IPictureProfileAdjustmentListener.Stub {
- MqDatabaseUtils mMqDatabaseUtils;
- MqManagerNotifier mMqManagerNotifier;
- HalNotifier mHalNotifier;
@Override
public void onPictureProfileAdjusted(
@@ -1542,18 +1551,13 @@ public class MediaQualityService extends SystemService {
return null;
}
- private PictureProfileAdjustmentListenerImpl(Context context) {
- mMqDatabaseUtils = new MqDatabaseUtils(context);
- mMqManagerNotifier = new MqManagerNotifier();
- mHalNotifier = new HalNotifier();
+ private PictureProfileAdjustmentListenerImpl() {
+
}
}
private final class SoundProfileAdjustmentListenerImpl extends
ISoundProfileAdjustmentListener.Stub {
- MqDatabaseUtils mMqDatabaseUtils;
- MqManagerNotifier mMqManagerNotifier;
- HalNotifier mHalNotifier;
@Override
public void onSoundProfileAdjusted(
@@ -1598,10 +1602,8 @@ public class MediaQualityService extends SystemService {
return null;
}
- private SoundProfileAdjustmentListenerImpl(Context context) {
- mMqDatabaseUtils = new MqDatabaseUtils(context);
- mMqManagerNotifier = new MqManagerNotifier();
- mHalNotifier = new HalNotifier();
+ private SoundProfileAdjustmentListenerImpl() {
+
}
}
diff --git a/services/core/java/com/android/server/notification/ManagedServices.java b/services/core/java/com/android/server/notification/ManagedServices.java
index b0ef80793cd7..62e26e189a35 100644
--- a/services/core/java/com/android/server/notification/ManagedServices.java
+++ b/services/core/java/com/android/server/notification/ManagedServices.java
@@ -25,6 +25,8 @@ import static android.os.UserHandle.USER_ALL;
import static android.os.UserHandle.USER_SYSTEM;
import static android.service.notification.NotificationListenerService.META_DATA_DEFAULT_AUTOBIND;
+import static com.android.server.notification.Flags.FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER;
+import static com.android.server.notification.Flags.managedServicesConcurrentMultiuser;
import static com.android.server.notification.NotificationManagerService.privateSpaceFlagsEnabled;
import android.annotation.FlaggedApi;
@@ -75,7 +77,9 @@ import com.android.internal.util.XmlUtils;
import com.android.internal.util.function.TriPredicate;
import com.android.modules.utils.TypedXmlPullParser;
import com.android.modules.utils.TypedXmlSerializer;
+import com.android.server.LocalServices;
import com.android.server.notification.NotificationManagerService.DumpFilter;
+import com.android.server.pm.UserManagerInternal;
import com.android.server.utils.TimingsTraceAndSlog;
import org.xmlpull.v1.XmlPullParser;
@@ -134,6 +138,7 @@ abstract public class ManagedServices {
private final UserProfiles mUserProfiles;
protected final IPackageManager mPm;
protected final UserManager mUm;
+ protected final UserManagerInternal mUmInternal;
private final Config mConfig;
private final Handler mHandler = new Handler(Looper.getMainLooper());
@@ -157,12 +162,17 @@ abstract public class ManagedServices {
protected final ArraySet<String> mDefaultPackages = new ArraySet<>();
// lists the component names of all enabled (and therefore potentially connected)
- // app services for current profiles.
+ // app services for each user. This is intended to support a concurrent multi-user environment.
+ // key value is the resolved userId.
@GuardedBy("mMutex")
- private final ArraySet<ComponentName> mEnabledServicesForCurrentProfiles = new ArraySet<>();
- // Just the packages from mEnabledServicesForCurrentProfiles
+ private final SparseArray<ArraySet<ComponentName>> mEnabledServicesByUser =
+ new SparseArray<>();
+ // Just the packages from mEnabledServicesByUser
+ // This is intended to support a concurrent multi-user environment.
+ // key value is the resolved userId.
@GuardedBy("mMutex")
- private final ArraySet<String> mEnabledServicesPackageNames = new ArraySet<>();
+ private final SparseArray<ArraySet<String>> mEnabledServicesPackageNamesByUser =
+ new SparseArray<>();
// Per user id, list of enabled packages that have nevertheless asked not to be run
@GuardedBy("mSnoozing")
private final SparseSetArray<ComponentName> mSnoozing = new SparseSetArray<>();
@@ -195,6 +205,10 @@ abstract public class ManagedServices {
mConfig = getConfig();
mApprovalLevel = APPROVAL_BY_COMPONENT;
mUm = (UserManager) mContext.getSystemService(Context.USER_SERVICE);
+ mUmInternal = LocalServices.getService(UserManagerInternal.class);
+ // Initialize for the current user.
+ mEnabledServicesByUser.put(UserHandle.USER_CURRENT, new ArraySet<>());
+ mEnabledServicesPackageNamesByUser.put(UserHandle.USER_CURRENT, new ArraySet<>());
}
abstract protected Config getConfig();
@@ -383,11 +397,30 @@ abstract public class ManagedServices {
}
synchronized (mMutex) {
- pw.println(" All " + getCaption() + "s (" + mEnabledServicesForCurrentProfiles.size()
- + ") enabled for current profiles:");
- for (ComponentName cmpt : mEnabledServicesForCurrentProfiles) {
- if (filter != null && !filter.matches(cmpt)) continue;
- pw.println(" " + cmpt);
+ if (managedServicesConcurrentMultiuser()) {
+ for (int i = 0; i < mEnabledServicesByUser.size(); i++) {
+ final int userId = mEnabledServicesByUser.keyAt(i);
+ final ArraySet<ComponentName> componentNames =
+ mEnabledServicesByUser.get(userId);
+ String userString = userId == UserHandle.USER_CURRENT
+ ? "current profiles" : "user " + Integer.toString(userId);
+ pw.println(" All " + getCaption() + "s (" + componentNames.size()
+ + ") enabled for " + userString + ":");
+ for (ComponentName cmpt : componentNames) {
+ if (filter != null && !filter.matches(cmpt)) continue;
+ pw.println(" " + cmpt);
+ }
+ }
+ } else {
+ final ArraySet<ComponentName> enabledServicesForCurrentProfiles =
+ mEnabledServicesByUser.get(UserHandle.USER_CURRENT);
+ pw.println(" All " + getCaption() + "s ("
+ + enabledServicesForCurrentProfiles.size()
+ + ") enabled for current profiles:");
+ for (ComponentName cmpt : enabledServicesForCurrentProfiles) {
+ if (filter != null && !filter.matches(cmpt)) continue;
+ pw.println(" " + cmpt);
+ }
}
pw.println(" Live " + getCaption() + "s (" + mServices.size() + "):");
@@ -442,11 +475,24 @@ abstract public class ManagedServices {
}
}
-
synchronized (mMutex) {
- for (ComponentName cmpt : mEnabledServicesForCurrentProfiles) {
- if (filter != null && !filter.matches(cmpt)) continue;
- cmpt.dumpDebug(proto, ManagedServicesProto.ENABLED);
+ if (managedServicesConcurrentMultiuser()) {
+ for (int i = 0; i < mEnabledServicesByUser.size(); i++) {
+ final int userId = mEnabledServicesByUser.keyAt(i);
+ final ArraySet<ComponentName> componentNames =
+ mEnabledServicesByUser.get(userId);
+ for (ComponentName cmpt : componentNames) {
+ if (filter != null && !filter.matches(cmpt)) continue;
+ cmpt.dumpDebug(proto, ManagedServicesProto.ENABLED);
+ }
+ }
+ } else {
+ final ArraySet<ComponentName> enabledServicesForCurrentProfiles =
+ mEnabledServicesByUser.get(UserHandle.USER_CURRENT);
+ for (ComponentName cmpt : enabledServicesForCurrentProfiles) {
+ if (filter != null && !filter.matches(cmpt)) continue;
+ cmpt.dumpDebug(proto, ManagedServicesProto.ENABLED);
+ }
}
for (ManagedServiceInfo info : mServices) {
if (filter != null && !filter.matches(info.component)) continue;
@@ -841,9 +887,31 @@ abstract public class ManagedServices {
}
}
+ /** convenience method for looking in mEnabledServicesPackageNamesByUser
+ * for UserHandle.USER_CURRENT.
+ * This is a legacy API. When FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER becomes
+ * trunk stable, this API should be deprecated. Additionally, when this method
+ * is deprecated, the unit tests written using this method should also be revised.
+ *
+ * @param pkg target package name
+ * @return boolean value that indicates whether it is enabled for the current profiles
+ */
protected boolean isComponentEnabledForPackage(String pkg) {
+ return isComponentEnabledForPackage(pkg, UserHandle.USER_CURRENT);
+ }
+
+ /** convenience method for looking in mEnabledServicesPackageNamesByUser
+ *
+ * @param pkg target package name
+ * @param userId the id of the target user
+ * @return boolean value that indicates whether it is enabled for the target user
+ */
+ @FlaggedApi(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER)
+ protected boolean isComponentEnabledForPackage(String pkg, int userId) {
synchronized (mMutex) {
- return mEnabledServicesPackageNames.contains(pkg);
+ ArraySet<String> enabledServicesPackageNames =
+ mEnabledServicesPackageNamesByUser.get(resolveUserId(userId));
+ return enabledServicesPackageNames != null && enabledServicesPackageNames.contains(pkg);
}
}
@@ -1016,9 +1084,14 @@ abstract public class ManagedServices {
public void onPackagesChanged(boolean removingPackage, String[] pkgList, int[] uidList) {
if (DEBUG) {
synchronized (mMutex) {
+ int resolvedUserId = (managedServicesConcurrentMultiuser()
+ && (uidList != null && uidList.length > 0))
+ ? resolveUserId(UserHandle.getUserId(uidList[0]))
+ : UserHandle.USER_CURRENT;
Slog.d(TAG, "onPackagesChanged removingPackage=" + removingPackage
+ " pkgList=" + (pkgList == null ? null : Arrays.asList(pkgList))
- + " mEnabledServicesPackageNames=" + mEnabledServicesPackageNames);
+ + " mEnabledServicesPackageNames="
+ + mEnabledServicesPackageNamesByUser.get(resolvedUserId));
}
}
@@ -1034,11 +1107,18 @@ abstract public class ManagedServices {
}
}
for (String pkgName : pkgList) {
- if (isComponentEnabledForPackage(pkgName)) {
- anyServicesInvolved = true;
+ if (!managedServicesConcurrentMultiuser()) {
+ if (isComponentEnabledForPackage(pkgName)) {
+ anyServicesInvolved = true;
+ }
}
if (uidList != null && uidList.length > 0) {
for (int uid : uidList) {
+ if (managedServicesConcurrentMultiuser()) {
+ if (isComponentEnabledForPackage(pkgName, UserHandle.getUserId(uid))) {
+ anyServicesInvolved = true;
+ }
+ }
if (isPackageAllowed(pkgName, UserHandle.getUserId(uid))) {
anyServicesInvolved = true;
trimApprovedListsForInvalidServices(pkgName, UserHandle.getUserId(uid));
@@ -1065,6 +1145,36 @@ abstract public class ManagedServices {
unbindUserServices(user);
}
+ /**
+ * Call this method when a user is stopped
+ *
+ * @param user the id of the stopped user
+ */
+ public void onUserStopped(int user) {
+ if (!managedServicesConcurrentMultiuser()) {
+ return;
+ }
+ boolean hasAny = false;
+ synchronized (mMutex) {
+ if (mEnabledServicesByUser.contains(user)
+ && mEnabledServicesPackageNamesByUser.contains(user)) {
+ // Through the ManagedServices.resolveUserId,
+ // we resolve UserHandle.USER_CURRENT as the key for users
+ // other than the visible background user.
+ // Therefore, the user IDs that exist as keys for each member variable
+ // correspond to the visible background user.
+ // We need to unbind services of the stopped visible background user.
+ mEnabledServicesByUser.remove(user);
+ mEnabledServicesPackageNamesByUser.remove(user);
+ hasAny = true;
+ }
+ }
+ if (hasAny) {
+ Slog.i(TAG, "Removing approved services for stopped user " + user);
+ unbindUserServices(user);
+ }
+ }
+
public void onUserSwitched(int user) {
if (DEBUG) Slog.d(TAG, "onUserSwitched u=" + user);
unbindOtherUserServices(user);
@@ -1386,19 +1496,42 @@ abstract public class ManagedServices {
protected void populateComponentsToBind(SparseArray<Set<ComponentName>> componentsToBind,
final IntArray activeUsers,
SparseArray<ArraySet<ComponentName>> approvedComponentsByUser) {
- mEnabledServicesForCurrentProfiles.clear();
- mEnabledServicesPackageNames.clear();
final int nUserIds = activeUsers.size();
-
+ if (managedServicesConcurrentMultiuser()) {
+ for (int i = 0; i < nUserIds; ++i) {
+ final int resolvedUserId = resolveUserId(activeUsers.get(i));
+ if (mEnabledServicesByUser.get(resolvedUserId) != null) {
+ mEnabledServicesByUser.get(resolvedUserId).clear();
+ }
+ if (mEnabledServicesPackageNamesByUser.get(resolvedUserId) != null) {
+ mEnabledServicesPackageNamesByUser.get(resolvedUserId).clear();
+ }
+ }
+ } else {
+ mEnabledServicesByUser.get(UserHandle.USER_CURRENT).clear();
+ mEnabledServicesPackageNamesByUser.get(UserHandle.USER_CURRENT).clear();
+ }
for (int i = 0; i < nUserIds; ++i) {
- // decode the list of components
final int userId = activeUsers.get(i);
+ // decode the list of components
final ArraySet<ComponentName> userComponents = approvedComponentsByUser.get(userId);
if (null == userComponents) {
componentsToBind.put(userId, new ArraySet<>());
continue;
}
+ final int resolvedUserId = managedServicesConcurrentMultiuser()
+ ? resolveUserId(userId)
+ : UserHandle.USER_CURRENT;
+ ArraySet<ComponentName> enabledServices =
+ mEnabledServicesByUser.contains(resolvedUserId)
+ ? mEnabledServicesByUser.get(resolvedUserId)
+ : new ArraySet<>();
+ ArraySet<String> enabledServicesPackageName =
+ mEnabledServicesPackageNamesByUser.contains(resolvedUserId)
+ ? mEnabledServicesPackageNamesByUser.get(resolvedUserId)
+ : new ArraySet<>();
+
final Set<ComponentName> add = new HashSet<>(userComponents);
synchronized (mSnoozing) {
ArraySet<ComponentName> snoozed = mSnoozing.get(userId);
@@ -1409,12 +1542,12 @@ abstract public class ManagedServices {
componentsToBind.put(userId, add);
- mEnabledServicesForCurrentProfiles.addAll(userComponents);
-
+ enabledServices.addAll(userComponents);
for (int j = 0; j < userComponents.size(); j++) {
- final ComponentName component = userComponents.valueAt(j);
- mEnabledServicesPackageNames.add(component.getPackageName());
+ enabledServicesPackageName.add(userComponents.valueAt(j).getPackageName());
}
+ mEnabledServicesByUser.put(resolvedUserId, enabledServices);
+ mEnabledServicesPackageNamesByUser.put(resolvedUserId, enabledServicesPackageName);
}
}
@@ -1453,13 +1586,9 @@ abstract public class ManagedServices {
*/
protected void rebindServices(boolean forceRebind, int userToRebind) {
if (DEBUG) Slog.d(TAG, "rebindServices " + forceRebind + " " + userToRebind);
- IntArray userIds = mUserProfiles.getCurrentProfileIds();
boolean rebindAllCurrentUsers = mUserProfiles.isProfileUser(userToRebind, mContext)
&& allowRebindForParentUser();
- if (userToRebind != USER_ALL && !rebindAllCurrentUsers) {
- userIds = new IntArray(1);
- userIds.add(userToRebind);
- }
+ IntArray userIds = getUserIdsForRebindServices(userToRebind, rebindAllCurrentUsers);
final SparseArray<Set<ComponentName>> componentsToBind = new SparseArray<>();
final SparseArray<Set<ComponentName>> componentsToUnbind = new SparseArray<>();
@@ -1483,6 +1612,23 @@ abstract public class ManagedServices {
bindToServices(componentsToBind);
}
+ private IntArray getUserIdsForRebindServices(int userToRebind, boolean rebindAllCurrentUsers) {
+ IntArray userIds = mUserProfiles.getCurrentProfileIds();
+ if (userToRebind != USER_ALL && !rebindAllCurrentUsers) {
+ userIds = new IntArray(1);
+ userIds.add(userToRebind);
+ } else if (managedServicesConcurrentMultiuser()
+ && userToRebind == USER_ALL) {
+ for (UserInfo user : mUm.getUsers()) {
+ if (mUmInternal.isVisibleBackgroundFullUser(user.id)
+ && !userIds.contains(user.id)) {
+ userIds.add(user.id);
+ }
+ }
+ }
+ return userIds;
+ }
+
/**
* Called when user switched to unbind all services from other users.
*/
@@ -1506,7 +1652,11 @@ abstract public class ManagedServices {
synchronized (mMutex) {
final Set<ManagedServiceInfo> removableBoundServices = getRemovableConnectedServices();
for (ManagedServiceInfo info : removableBoundServices) {
- if ((allExceptUser && (info.userid != user))
+ // User switching is the event for the forground user.
+ // It should not affect the service of the visible background user.
+ if ((allExceptUser && (info.userid != user)
+ && !(managedServicesConcurrentMultiuser()
+ && info.isVisibleBackgroundUserService))
|| (!allExceptUser && (info.userid == user))) {
Set<ComponentName> toUnbind =
componentsToUnbind.get(info.userid, new ArraySet<>());
@@ -1861,6 +2011,29 @@ abstract public class ManagedServices {
}
/**
+ * This method returns the mapped id for the incoming user id
+ * If the incoming id was not the id of the visible background user, it returns USER_CURRENT.
+ * In the other cases, it returns the same value as the input.
+ *
+ * @param userId the id of the user
+ * @return the user id if it is a visible background user, otherwise
+ * {@link UserHandle#USER_CURRENT}
+ */
+ @FlaggedApi(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER)
+ @VisibleForTesting
+ public int resolveUserId(int userId) {
+ if (managedServicesConcurrentMultiuser()) {
+ if (mUmInternal.isVisibleBackgroundFullUser(userId)) {
+ // The dataset of the visible background user should be managed independently.
+ return userId;
+ }
+ }
+ // The data of current user and its profile users need to be managed
+ // in a dataset as before.
+ return UserHandle.USER_CURRENT;
+ }
+
+ /**
* Returns true if services in the parent user should be rebound
* when rebindServices is called with a profile userId.
* Must be false for NotificationAssistants.
@@ -1878,6 +2051,8 @@ abstract public class ManagedServices {
public int targetSdkVersion;
public Pair<ComponentName, Integer> mKey;
public int uid;
+ @FlaggedApi(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER)
+ public boolean isVisibleBackgroundUserService;
public ManagedServiceInfo(IInterface service, ComponentName component,
int userid, boolean isSystem, ServiceConnection connection, int targetSdkVersion,
@@ -1889,6 +2064,10 @@ abstract public class ManagedServices {
this.connection = connection;
this.targetSdkVersion = targetSdkVersion;
this.uid = uid;
+ if (managedServicesConcurrentMultiuser()) {
+ this.isVisibleBackgroundUserService = LocalServices
+ .getService(UserManagerInternal.class).isVisibleBackgroundFullUser(userid);
+ }
mKey = Pair.create(component, userid);
}
@@ -1937,19 +2116,28 @@ abstract public class ManagedServices {
}
public boolean isSameUser(int userId) {
- if (!isEnabledForCurrentProfiles()) {
+ if (!isEnabledForUser()) {
return false;
}
return userId == USER_ALL || userId == this.userid;
}
public boolean enabledAndUserMatches(int nid) {
- if (!isEnabledForCurrentProfiles()) {
+ if (!isEnabledForUser()) {
return false;
}
if (this.userid == USER_ALL) return true;
if (this.isSystem) return true;
if (nid == USER_ALL || nid == this.userid) return true;
+ if (managedServicesConcurrentMultiuser()
+ && mUmInternal.getProfileParentId(nid)
+ != mUmInternal.getProfileParentId(this.userid)) {
+ // If the profile parent IDs do not match each other,
+ // it is determined that the users do not match.
+ // This situation may occur when comparing the current user's ID
+ // with the visible background user's ID.
+ return false;
+ }
return supportsProfiles()
&& mUserProfiles.isCurrentProfile(nid)
&& isPermittedForProfile(nid);
@@ -1969,12 +2157,21 @@ abstract public class ManagedServices {
removeServiceImpl(this.service, this.userid);
}
- /** convenience method for looking in mEnabledServicesForCurrentProfiles */
- public boolean isEnabledForCurrentProfiles() {
+ /**
+ * convenience method for looking in mEnabledServicesByUser.
+ * If FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER is disabled, this manages the data using
+ * only UserHandle.USER_CURRENT as the key, in order to behave the same as the legacy logic.
+ */
+ public boolean isEnabledForUser() {
if (this.isSystem) return true;
if (this.connection == null) return false;
synchronized (mMutex) {
- return mEnabledServicesForCurrentProfiles.contains(this.component);
+ int resolvedUserId = managedServicesConcurrentMultiuser()
+ ? resolveUserId(this.userid)
+ : UserHandle.USER_CURRENT;
+ ArraySet<ComponentName> enabledServices =
+ mEnabledServicesByUser.get(resolvedUserId);
+ return enabledServices != null && enabledServices.contains(this.component);
}
}
@@ -2017,10 +2214,30 @@ abstract public class ManagedServices {
}
}
- /** convenience method for looking in mEnabledServicesForCurrentProfiles */
+ /** convenience method for looking in mEnabledServicesByUser for UserHandle.USER_CURRENT.
+ * This is a legacy API. When FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER becomes
+ * trunk stable, this API should be deprecated. Additionally, when this method
+ * is deprecated, the unit tests written using this method should also be revised.
+ *
+ * @param component target component name
+ * @return boolean value that indicates whether it is enabled for the current profiles
+ */
public boolean isComponentEnabledForCurrentProfiles(ComponentName component) {
+ return isComponentEnabledForUser(component, UserHandle.USER_CURRENT);
+ }
+
+ /** convenience method for looking in mEnabledServicesForUser
+ *
+ * @param component target component name
+ * @param userId the id of the target user
+ * @return boolean value that indicates whether it is enabled for the target user
+ */
+ @FlaggedApi(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER)
+ public boolean isComponentEnabledForUser(ComponentName component, int userId) {
synchronized (mMutex) {
- return mEnabledServicesForCurrentProfiles.contains(component);
+ ArraySet<ComponentName> enabledServicesForUser =
+ mEnabledServicesByUser.get(resolveUserId(userId));
+ return enabledServicesForUser != null && enabledServicesForUser.contains(component);
}
}
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index 340afb776405..6fddfb5f90a7 100644
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -173,6 +173,7 @@ import static com.android.server.am.PendingIntentRecord.FLAG_ACTIVITY_SENDER;
import static com.android.server.am.PendingIntentRecord.FLAG_BROADCAST_SENDER;
import static com.android.server.am.PendingIntentRecord.FLAG_SERVICE_SENDER;
import static com.android.server.notification.Flags.expireBitmaps;
+import static com.android.server.notification.Flags.managedServicesConcurrentMultiuser;
import static com.android.server.policy.PhoneWindowManager.TOAST_WINDOW_ANIM_BUFFER;
import static com.android.server.policy.PhoneWindowManager.TOAST_WINDOW_TIMEOUT;
import static com.android.server.utils.PriorityDump.PRIORITY_ARG;
@@ -1207,7 +1208,7 @@ public class NotificationManagerService extends SystemService {
}
mAssistants.resetDefaultAssistantsIfNecessary();
- mPreferencesHelper.syncChannelsBypassingDnd();
+ mPreferencesHelper.syncHasPriorityChannels();
}
@VisibleForTesting
@@ -2323,6 +2324,9 @@ public class NotificationManagerService extends SystemService {
if (userHandle >= 0) {
cancelAllNotificationsInt(MY_UID, MY_PID, null, null, 0, 0, userHandle,
REASON_USER_STOPPED);
+ mConditionProviders.onUserStopped(userHandle);
+ mListeners.onUserStopped(userHandle);
+ mAssistants.onUserStopped(userHandle);
}
} else if (
isProfileUnavailable(action)) {
@@ -2343,7 +2347,7 @@ public class NotificationManagerService extends SystemService {
mConditionProviders.onUserSwitched(userId);
mListeners.onUserSwitched(userId);
mZenModeHelper.onUserSwitched(userId);
- mPreferencesHelper.syncChannelsBypassingDnd();
+ mPreferencesHelper.syncHasPriorityChannels();
}
// assistant is the only thing that cares about managed profiles specifically
mAssistants.onUserSwitched(userId);
@@ -2367,7 +2371,7 @@ public class NotificationManagerService extends SystemService {
mConditionProviders.onUserRemoved(userId);
mAssistants.onUserRemoved(userId);
mHistoryManager.onUserRemoved(userId);
- mPreferencesHelper.syncChannelsBypassingDnd();
+ mPreferencesHelper.syncHasPriorityChannels();
handleSavePolicyFile();
} else if (action.equals(Intent.ACTION_USER_UNLOCKED)) {
final int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, USER_NULL);
@@ -2376,9 +2380,6 @@ public class NotificationManagerService extends SystemService {
if (!mUserProfiles.isProfileUser(userId, context)) {
mConditionProviders.onUserUnlocked(userId);
mListeners.onUserUnlocked(userId);
- if (!android.app.Flags.modesApi()) {
- mZenModeHelper.onUserUnlocked(userId);
- }
}
}
}
@@ -2767,9 +2768,7 @@ public class NotificationManagerService extends SystemService {
void onPolicyChanged(Policy newPolicy) {
Binder.withCleanCallingIdentity(() -> {
Intent intent = new Intent(ACTION_NOTIFICATION_POLICY_CHANGED);
- if (android.app.Flags.modesApi()) {
- intent.putExtra(EXTRA_NOTIFICATION_POLICY, newPolicy);
- }
+ intent.putExtra(EXTRA_NOTIFICATION_POLICY, newPolicy);
sendRegisteredOnlyBroadcast(intent);
mRankingHandler.requestSort();
});
@@ -2778,11 +2777,10 @@ public class NotificationManagerService extends SystemService {
@Override
void onConsolidatedPolicyChanged(Policy newConsolidatedPolicy) {
Binder.withCleanCallingIdentity(() -> {
- if (android.app.Flags.modesApi()) {
- Intent intent = new Intent(ACTION_CONSOLIDATED_NOTIFICATION_POLICY_CHANGED);
- intent.putExtra(EXTRA_NOTIFICATION_POLICY, newConsolidatedPolicy);
- sendRegisteredOnlyBroadcast(intent);
- }
+ Intent intent = new Intent(ACTION_CONSOLIDATED_NOTIFICATION_POLICY_CHANGED);
+ intent.putExtra(EXTRA_NOTIFICATION_POLICY, newConsolidatedPolicy);
+ sendRegisteredOnlyBroadcast(intent);
+
mRankingHandler.requestSort();
});
}
@@ -3368,7 +3366,7 @@ public class NotificationManagerService extends SystemService {
migrateDefaultNAS();
maybeShowInitialReviewPermissionsNotification();
- if (android.app.Flags.modesApi() && !mZenModeHelper.hasDeviceEffectsApplier()) {
+ if (!mZenModeHelper.hasDeviceEffectsApplier()) {
// Cannot be done earlier, as some services aren't ready until this point.
mZenModeHelper.setDeviceEffectsApplier(
new DefaultDeviceEffectsApplier(getContext()));
@@ -3446,7 +3444,7 @@ public class NotificationManagerService extends SystemService {
mConditionProviders.onUserSwitched(userId);
mListeners.onUserSwitched(userId);
mZenModeHelper.onUserSwitched(userId);
- mPreferencesHelper.syncChannelsBypassingDnd();
+ mPreferencesHelper.syncHasPriorityChannels();
}
// assistant is the only thing that cares about managed profiles specifically
mAssistants.onUserSwitched(userId);
@@ -5236,11 +5234,8 @@ public class NotificationManagerService extends SystemService {
@Override
public boolean areChannelsBypassingDnd() {
- if (android.app.Flags.modesApi()) {
- return mZenModeHelper.getConsolidatedNotificationPolicy().allowPriorityChannels()
- && mPreferencesHelper.areChannelsBypassingDnd();
- }
- return mPreferencesHelper.areChannelsBypassingDnd();
+ return mZenModeHelper.getConsolidatedNotificationPolicy().allowPriorityChannels()
+ && mPreferencesHelper.hasPriorityChannels();
}
@Override
@@ -5730,12 +5725,13 @@ public class NotificationManagerService extends SystemService {
public void requestBindListener(ComponentName component) {
checkCallerIsSystemOrSameApp(component.getPackageName());
int uid = Binder.getCallingUid();
+ int userId = UserHandle.getUserId(uid);
final long identity = Binder.clearCallingIdentity();
try {
- ManagedServices manager =
- mAssistants.isComponentEnabledForCurrentProfiles(component)
- ? mAssistants
- : mListeners;
+ boolean isAssistantEnabled = managedServicesConcurrentMultiuser()
+ ? mAssistants.isComponentEnabledForUser(component, userId)
+ : mAssistants.isComponentEnabledForCurrentProfiles(component);
+ ManagedServices manager = isAssistantEnabled ? mAssistants : mListeners;
manager.setComponentState(component, UserHandle.getUserId(uid), true);
} finally {
Binder.restoreCallingIdentity(identity);
@@ -5762,16 +5758,16 @@ public class NotificationManagerService extends SystemService {
public void requestUnbindListenerComponent(ComponentName component) {
checkCallerIsSameApp(component.getPackageName());
int uid = Binder.getCallingUid();
+ int userId = UserHandle.getUserId(uid);
final long identity = Binder.clearCallingIdentity();
try {
synchronized (mNotificationLock) {
- ManagedServices manager =
- mAssistants.isComponentEnabledForCurrentProfiles(component)
- ? mAssistants
- : mListeners;
- if (manager.isPackageOrComponentAllowed(component.flattenToString(),
- UserHandle.getUserId(uid))) {
- manager.setComponentState(component, UserHandle.getUserId(uid), false);
+ boolean isAssistantEnabled = managedServicesConcurrentMultiuser()
+ ? mAssistants.isComponentEnabledForUser(component, userId)
+ : mAssistants.isComponentEnabledForCurrentProfiles(component);
+ ManagedServices manager = isAssistantEnabled ? mAssistants : mListeners;
+ if (manager.isPackageOrComponentAllowed(component.flattenToString(), userId)) {
+ manager.setComponentState(component, userId, false);
}
}
} finally {
@@ -6092,43 +6088,27 @@ public class NotificationManagerService extends SystemService {
@Override
public void requestInterruptionFilterFromListener(INotificationListener token,
int interruptionFilter) throws RemoteException {
- if (android.app.Flags.modesApi()) {
- final int callingUid = Binder.getCallingUid();
- ManagedServiceInfo info;
- synchronized (mNotificationLock) {
- info = mListeners.checkServiceTokenLocked(token);
- }
+ final int callingUid = Binder.getCallingUid();
+ ManagedServiceInfo info;
+ synchronized (mNotificationLock) {
+ info = mListeners.checkServiceTokenLocked(token);
+ }
- final int zenMode = zenModeFromInterruptionFilter(interruptionFilter, -1);
- if (zenMode == -1) return;
+ final int zenMode = zenModeFromInterruptionFilter(interruptionFilter, -1);
+ if (zenMode == -1) return;
- UserHandle zenUser = getCallingZenUser();
- if (!canManageGlobalZenPolicy(info.component.getPackageName(), callingUid)) {
- mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(
- zenUser, info.component.getPackageName(), callingUid, zenMode);
- } else {
- int origin = computeZenOrigin(/* fromUser= */ false);
- Binder.withCleanCallingIdentity(() -> {
- mZenModeHelper.setManualZenMode(zenUser, zenMode, /* conditionId= */ null,
- origin, "listener:" + info.component.flattenToShortString(),
- /* caller= */ info.component.getPackageName(),
- callingUid);
- });
- }
+ UserHandle zenUser = getCallingZenUser();
+ if (!canManageGlobalZenPolicy(info.component.getPackageName(), callingUid)) {
+ mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(
+ zenUser, info.component.getPackageName(), callingUid, zenMode);
} else {
- final int callingUid = Binder.getCallingUid();
- final boolean isSystemOrSystemUi = isCallerSystemOrSystemUi();
- final long identity = Binder.clearCallingIdentity();
- try {
- synchronized (mNotificationLock) {
- final ManagedServiceInfo info = mListeners.checkServiceTokenLocked(token);
- mZenModeHelper.requestFromListener(info.component, interruptionFilter,
- callingUid, isSystemOrSystemUi);
- updateInterruptionFilterLocked();
- }
- } finally {
- Binder.restoreCallingIdentity(identity);
- }
+ int origin = computeZenOrigin(/* fromUser= */ false);
+ Binder.withCleanCallingIdentity(() -> {
+ mZenModeHelper.setManualZenMode(zenUser, zenMode, /* conditionId= */ null,
+ origin, "listener:" + info.component.flattenToShortString(),
+ /* caller= */ info.component.getPackageName(),
+ callingUid);
+ });
}
}
@@ -6177,19 +6157,8 @@ public class NotificationManagerService extends SystemService {
}
}
- // TODO: b/310620812 - Remove getZenRules() when MODES_API is inlined.
- @Override
- public List<ZenModeConfig.ZenRule> getZenRules() throws RemoteException {
- int callingUid = Binder.getCallingUid();
- enforcePolicyAccess(callingUid, "getZenRules");
- return mZenModeHelper.getZenRules(getCallingZenUser(), callingUid);
- }
-
@Override
public Map<String, AutomaticZenRule> getAutomaticZenRules() {
- if (!android.app.Flags.modesApi()) {
- throw new IllegalStateException("getAutomaticZenRules called with flag off!");
- }
int callingUid = Binder.getCallingUid();
enforcePolicyAccess(callingUid, "getAutomaticZenRules");
return mZenModeHelper.getAutomaticZenRules(getCallingZenUser(), callingUid);
@@ -6260,50 +6229,40 @@ public class NotificationManagerService extends SystemService {
// Implicit rules have no ConditionProvider or Activity. We allow the user to customize
// them (via Settings), but not the owner app. Should the app want to start using it as
// a "normal" rule, it must provide a CP/ConfigActivity too.
- if (android.app.Flags.modesApi()) {
- boolean isImplicitRuleUpdateFromSystem = updateId != null
- && ZenModeConfig.isImplicitRuleId(updateId)
- && isCallerSystemOrSystemUi();
- if (!isImplicitRuleUpdateFromSystem
- && rule.getOwner() == null
- && rule.getConfigurationActivity() == null) {
- throw new NullPointerException(
- "Rule must have a ConditionProviderService and/or configuration "
- + "activity");
- }
- } else {
- if (rule.getOwner() == null && rule.getConfigurationActivity() == null) {
- throw new NullPointerException(
- "Rule must have a ConditionProviderService and/or configuration "
- + "activity");
- }
+ boolean isImplicitRuleUpdateFromSystem = updateId != null
+ && ZenModeConfig.isImplicitRuleId(updateId)
+ && isCallerSystemOrSystemUi();
+ if (!isImplicitRuleUpdateFromSystem
+ && rule.getOwner() == null
+ && rule.getConfigurationActivity() == null) {
+ throw new NullPointerException(
+ "Rule must have a ConditionProviderService and/or configuration "
+ + "activity");
}
Objects.requireNonNull(rule.getConditionId(), "ConditionId is null");
- if (android.app.Flags.modesApi()) {
- if (isCallerSystemOrSystemUi()) {
- return; // System callers can use any type.
- }
- int uid = Binder.getCallingUid();
- int userId = UserHandle.getUserId(uid);
+ if (isCallerSystemOrSystemUi()) {
+ return; // System callers can use any type.
+ }
+ int uid = Binder.getCallingUid();
+ int userId = UserHandle.getUserId(uid);
- if (rule.getType() == AutomaticZenRule.TYPE_MANAGED) {
- boolean isDeviceOwner = Binder.withCleanCallingIdentity(
- () -> mDpm.isActiveDeviceOwner(uid));
- if (!isDeviceOwner) {
- throw new IllegalArgumentException(
- "Only Device Owners can use AutomaticZenRules with TYPE_MANAGED");
- }
- } else if (rule.getType() == AutomaticZenRule.TYPE_BEDTIME) {
- String wellbeingPackage = getContext().getResources().getString(
- com.android.internal.R.string.config_systemWellbeing);
- boolean isCallerWellbeing = !TextUtils.isEmpty(wellbeingPackage)
- && mPackageManagerInternal.isSameApp(wellbeingPackage, uid, userId);
- if (!isCallerWellbeing) {
- throw new IllegalArgumentException(
- "Only the 'Wellbeing' package can use AutomaticZenRules with "
- + "TYPE_BEDTIME");
- }
+ if (rule.getType() == AutomaticZenRule.TYPE_MANAGED) {
+ boolean isDeviceOwner = Binder.withCleanCallingIdentity(
+ () -> mDpm.isActiveDeviceOwner(uid));
+ if (!isDeviceOwner) {
+ throw new IllegalArgumentException(
+ "Only Device Owners can use AutomaticZenRules with TYPE_MANAGED");
+ }
+ } else if (rule.getType() == AutomaticZenRule.TYPE_BEDTIME) {
+ String wellbeingPackage = getContext().getResources().getString(
+ com.android.internal.R.string.config_systemWellbeing);
+ boolean isCallerWellbeing = !TextUtils.isEmpty(wellbeingPackage)
+ && mPackageManagerInternal.isSameApp(wellbeingPackage, uid, userId);
+ if (!isCallerWellbeing) {
+ throw new IllegalArgumentException(
+ "Only the 'Wellbeing' package can use AutomaticZenRules with "
+ + "TYPE_BEDTIME");
}
}
}
@@ -6386,9 +6345,7 @@ public class NotificationManagerService extends SystemService {
@ZenModeConfig.ConfigOrigin
private int computeZenOrigin(boolean fromUser) {
- // "fromUser" is introduced with MODES_API, so only consider it in that case.
- // (Non-MODES_API behavior should also not depend at all on ORIGIN_USER_IN_X).
- if (android.app.Flags.modesApi() && fromUser) {
+ if (fromUser) {
if (isCallerSystemOrSystemUi()) {
return ZenModeConfig.ORIGIN_USER_IN_SYSTEMUI;
} else {
@@ -6402,9 +6359,7 @@ public class NotificationManagerService extends SystemService {
}
private void enforceUserOriginOnlyFromSystem(boolean fromUser, String method) {
- if (android.app.Flags.modesApi()
- && fromUser
- && !isCallerSystemOrSystemUiOrShell()) {
+ if (fromUser && !isCallerSystemOrSystemUiOrShell()) {
throw new SecurityException(TextUtils.formatSimple(
"Calling %s with fromUser == true is only allowed for system", method));
}
@@ -6419,7 +6374,7 @@ public class NotificationManagerService extends SystemService {
enforceUserOriginOnlyFromSystem(fromUser, "setInterruptionFilter");
UserHandle zenUser = getCallingZenUser();
- if (android.app.Flags.modesApi() && !canManageGlobalZenPolicy(pkg, callingUid)) {
+ if (!canManageGlobalZenPolicy(pkg, callingUid)) {
mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(zenUser, pkg, callingUid, zen);
return;
}
@@ -6549,6 +6504,13 @@ public class NotificationManagerService extends SystemService {
} catch (NameNotFoundException e) {
return false;
}
+ if (managedServicesConcurrentMultiuser()) {
+ return checkPackagePolicyAccess(pkg)
+ || mListeners.isComponentEnabledForPackage(pkg,
+ UserHandle.getCallingUserId())
+ || (mDpm != null
+ && (mDpm.isActiveProfileOwner(uid) || mDpm.isActiveDeviceOwner(uid)));
+ }
//TODO(b/169395065) Figure out if this flow makes sense in Device Owner mode.
return checkPackagePolicyAccess(pkg)
|| mListeners.isComponentEnabledForPackage(pkg)
@@ -6723,7 +6685,7 @@ public class NotificationManagerService extends SystemService {
public Policy getNotificationPolicy(String pkg) {
final int callingUid = Binder.getCallingUid();
UserHandle zenUser = getCallingZenUser();
- if (android.app.Flags.modesApi() && !canManageGlobalZenPolicy(pkg, callingUid)) {
+ if (!canManageGlobalZenPolicy(pkg, callingUid)) {
return mZenModeHelper.getNotificationPolicyFromImplicitZenRule(zenUser, pkg);
}
final long identity = Binder.clearCallingIdentity();
@@ -6760,8 +6722,7 @@ public class NotificationManagerService extends SystemService {
UserHandle zenUser = getCallingZenUser();
boolean isSystemCaller = isCallerSystemOrSystemUiOrShell();
- boolean shouldApplyAsImplicitRule = android.app.Flags.modesApi()
- && !canManageGlobalZenPolicy(pkg, callingUid);
+ boolean shouldApplyAsImplicitRule = !canManageGlobalZenPolicy(pkg, callingUid);
final long identity = Binder.clearCallingIdentity();
try {
@@ -6953,7 +6914,8 @@ public class NotificationManagerService extends SystemService {
android.Manifest.permission.INTERACT_ACROSS_USERS,
"setNotificationListenerAccessGrantedForUser for user " + userId);
}
- if (mUmInternal.isVisibleBackgroundFullUser(userId)) {
+ if (!managedServicesConcurrentMultiuser()
+ && mUmInternal.isVisibleBackgroundFullUser(userId)) {
// The main use case for visible background users is the Automotive multi-display
// configuration where a passenger can use a secondary display while the driver is
// using the main display. NotificationListeners is designed only for the current
@@ -8219,9 +8181,6 @@ public class NotificationManagerService extends SystemService {
@Override
public void setDeviceEffectsApplier(DeviceEffectsApplier applier) {
- if (!android.app.Flags.modesApi()) {
- return;
- }
if (mZenModeHelper == null) {
throw new IllegalStateException("ZenModeHelper is not yet ready!");
}
@@ -13165,7 +13124,8 @@ public class NotificationManagerService extends SystemService {
@Override
public void onUserUnlocked(int user) {
- if (mUmInternal.isVisibleBackgroundFullUser(user)) {
+ if (!managedServicesConcurrentMultiuser()
+ && mUmInternal.isVisibleBackgroundFullUser(user)) {
// The main use case for visible background users is the Automotive
// multi-display configuration where a passenger can use a secondary
// display while the driver is using the main display.
@@ -13805,7 +13765,7 @@ public class NotificationManagerService extends SystemService {
// TODO (b/73052211): if the ranking update changed the notification type,
// cancel notifications for NLSes that can't see them anymore
for (final ManagedServiceInfo serviceInfo : getServices()) {
- if (!serviceInfo.isEnabledForCurrentProfiles() || !isInteractionVisibleToListener(
+ if (!serviceInfo.isEnabledForUser() || !isInteractionVisibleToListener(
serviceInfo, ActivityManager.getCurrentUser())) {
continue;
}
@@ -13833,7 +13793,7 @@ public class NotificationManagerService extends SystemService {
@GuardedBy("mNotificationLock")
public void notifyListenerHintsChangedLocked(final int hints) {
for (final ManagedServiceInfo serviceInfo : getServices()) {
- if (!serviceInfo.isEnabledForCurrentProfiles() || !isInteractionVisibleToListener(
+ if (!serviceInfo.isEnabledForUser() || !isInteractionVisibleToListener(
serviceInfo, ActivityManager.getCurrentUser())) {
continue;
}
@@ -13889,7 +13849,7 @@ public class NotificationManagerService extends SystemService {
public void notifyInterruptionFilterChanged(final int interruptionFilter) {
for (final ManagedServiceInfo serviceInfo : getServices()) {
- if (!serviceInfo.isEnabledForCurrentProfiles() || !isInteractionVisibleToListener(
+ if (!serviceInfo.isEnabledForUser() || !isInteractionVisibleToListener(
serviceInfo, ActivityManager.getCurrentUser())) {
continue;
}
diff --git a/services/core/java/com/android/server/notification/NotificationShellCmd.java b/services/core/java/com/android/server/notification/NotificationShellCmd.java
index c305d66c24c1..bc987ed21251 100644
--- a/services/core/java/com/android/server/notification/NotificationShellCmd.java
+++ b/services/core/java/com/android/server/notification/NotificationShellCmd.java
@@ -183,13 +183,8 @@ public class NotificationShellCmd extends ShellCommand {
interruptionFilter = INTERRUPTION_FILTER_ALL;
}
final int filter = interruptionFilter;
- if (android.app.Flags.modesApi()) {
- mBinderService.setInterruptionFilter(callingPackage, filter,
- /* fromUser= */ true);
- } else {
- mBinderService.setInterruptionFilter(callingPackage, filter,
- /* fromUser= */ false);
- }
+ mBinderService.setInterruptionFilter(callingPackage, filter,
+ /* fromUser= */ true);
}
break;
case "allow_dnd": {
diff --git a/services/core/java/com/android/server/notification/PreferencesHelper.java b/services/core/java/com/android/server/notification/PreferencesHelper.java
index 3974c839fd38..0fc182f3f1bb 100644
--- a/services/core/java/com/android/server/notification/PreferencesHelper.java
+++ b/services/core/java/com/android/server/notification/PreferencesHelper.java
@@ -233,11 +233,9 @@ public class PreferencesHelper implements RankingConfig {
private SparseBooleanArray mLockScreenShowNotifications;
private SparseBooleanArray mLockScreenPrivateNotifications;
private boolean mIsMediaNotificationFilteringEnabled;
- // When modes_api flag is enabled, this value only tracks whether the current user has any
- // channels marked as "priority channels", but not necessarily whether they are permitted
- // to bypass DND by current zen policy.
- // TODO: b/310620812 - Rename to be more accurate when modes_api flag is inlined.
- private boolean mCurrentUserHasChannelsBypassingDnd;
+ // Whether the current user has any channels marked as "priority channels" -- but not
+ // necessarily whether they are permitted to bypass DND by current zen policy.
+ private boolean mCurrentUserHasPriorityChannels;
private boolean mHideSilentStatusBarIcons = DEFAULT_HIDE_SILENT_STATUS_BAR_ICONS;
private final boolean mShowReviewPermissionsNotification;
@@ -1063,7 +1061,7 @@ public class PreferencesHelper implements RankingConfig {
r.groups.put(group.getId(), group);
}
if (needsDndChange) {
- updateCurrentUserHasChannelsBypassingDnd(callingUid, fromSystemOrSystemUi);
+ updateCurrentUserHasPriorityChannels(callingUid, fromSystemOrSystemUi);
}
if (android.app.Flags.nmBinderPerfCacheChannels() && changed) {
invalidateNotificationChannelGroupCache();
@@ -1150,7 +1148,7 @@ public class PreferencesHelper implements RankingConfig {
existing.setBypassDnd(bypassDnd);
needsPolicyFileChange = true;
- if (bypassDnd != mCurrentUserHasChannelsBypassingDnd
+ if (bypassDnd != mCurrentUserHasPriorityChannels
|| previousExistingImportance != existing.getImportance()) {
needsDndChange = true;
}
@@ -1214,7 +1212,7 @@ public class PreferencesHelper implements RankingConfig {
}
r.channels.put(channel.getId(), channel);
- if (channel.canBypassDnd() != mCurrentUserHasChannelsBypassingDnd) {
+ if (channel.canBypassDnd() != mCurrentUserHasPriorityChannels) {
needsDndChange = true;
}
MetricsLogger.action(getChannelLog(channel, pkg).setType(
@@ -1224,7 +1222,7 @@ public class PreferencesHelper implements RankingConfig {
}
if (needsDndChange) {
- updateCurrentUserHasChannelsBypassingDnd(callingUid, fromSystemOrSystemUi);
+ updateCurrentUserHasPriorityChannels(callingUid, fromSystemOrSystemUi);
}
if (android.app.Flags.nmBinderPerfCacheChannels() && needsPolicyFileChange) {
@@ -1317,14 +1315,14 @@ public class PreferencesHelper implements RankingConfig {
// relevantly affected without the parent channel already having been.
}
- if (updatedChannel.canBypassDnd() != mCurrentUserHasChannelsBypassingDnd
+ if (updatedChannel.canBypassDnd() != mCurrentUserHasPriorityChannels
|| channel.getImportance() != updatedChannel.getImportance()) {
needsDndChange = true;
changed = true;
}
}
if (needsDndChange) {
- updateCurrentUserHasChannelsBypassingDnd(callingUid, fromSystemOrSystemUi);
+ updateCurrentUserHasPriorityChannels(callingUid, fromSystemOrSystemUi);
}
if (changed) {
if (android.app.Flags.nmBinderPerfCacheChannels()) {
@@ -1550,7 +1548,7 @@ public class PreferencesHelper implements RankingConfig {
}
}
if (channelBypassedDnd) {
- updateCurrentUserHasChannelsBypassingDnd(callingUid, fromSystemOrSystemUi);
+ updateCurrentUserHasPriorityChannels(callingUid, fromSystemOrSystemUi);
}
if (android.app.Flags.nmBinderPerfCacheChannels() && deletedChannel) {
@@ -1745,7 +1743,7 @@ public class PreferencesHelper implements RankingConfig {
}
}
if (groupBypassedDnd) {
- updateCurrentUserHasChannelsBypassingDnd(callingUid, fromSystemOrSystemUi);
+ updateCurrentUserHasPriorityChannels(callingUid, fromSystemOrSystemUi);
}
if (android.app.Flags.nmBinderPerfCacheChannels()) {
if (deletedChannels.size() > 0) {
@@ -1906,8 +1904,8 @@ public class PreferencesHelper implements RankingConfig {
}
}
if (!deletedChannelIds.isEmpty()) {
- if (mCurrentUserHasChannelsBypassingDnd) {
- updateCurrentUserHasChannelsBypassingDnd(callingUid, fromSystemOrSystemUi);
+ if (mCurrentUserHasPriorityChannels) {
+ updateCurrentUserHasPriorityChannels(callingUid, fromSystemOrSystemUi);
}
if (android.app.Flags.nmBinderPerfCacheChannels()) {
invalidateNotificationChannelCache();
@@ -2098,7 +2096,7 @@ public class PreferencesHelper implements RankingConfig {
}
/**
- * Syncs {@link #mCurrentUserHasChannelsBypassingDnd} with the current user's notification
+ * Syncs {@link #mCurrentUserHasPriorityChannels} with the current user's notification
* policy before updating. Must be called:
* <ul>
* <li>On system init, after channels and DND configurations are loaded.
@@ -2106,22 +2104,23 @@ public class PreferencesHelper implements RankingConfig {
* <li>If users are removed (the removed user could've been a profile of the current one).
* </ul>
*/
- void syncChannelsBypassingDnd() {
- mCurrentUserHasChannelsBypassingDnd =
+ void syncHasPriorityChannels() {
+ mCurrentUserHasPriorityChannels =
(mZenModeHelper.getNotificationPolicy(UserHandle.CURRENT).state
- & NotificationManager.Policy.STATE_CHANNELS_BYPASSING_DND) != 0;
+ & NotificationManager.Policy.STATE_HAS_PRIORITY_CHANNELS) != 0;
- updateCurrentUserHasChannelsBypassingDnd(/* callingUid= */ Process.SYSTEM_UID,
+ updateCurrentUserHasPriorityChannels(/* callingUid= */ Process.SYSTEM_UID,
/* fromSystemOrSystemUi= */ true);
}
/**
* Updates the user's NotificationPolicy based on whether the current userId has channels
- * bypassing DND. It should be called whenever a channel is created, updated, or deleted, or
- * when the current user (or its profiles) change.
+ * marked as "priority" (which might bypass DND, depending on the zen rule details). It should
+ * be called whenever a channel is created, updated, or deleted, or when the current user (or
+ * its profiles) change.
*/
// TODO: b/368247671 - remove fromSystemOrSystemUi argument when modes_ui is inlined.
- private void updateCurrentUserHasChannelsBypassingDnd(int callingUid,
+ private void updateCurrentUserHasPriorityChannels(int callingUid,
boolean fromSystemOrSystemUi) {
ArraySet<Pair<String, Integer>> candidatePkgs = new ArraySet<>();
@@ -2149,13 +2148,13 @@ public class PreferencesHelper implements RankingConfig {
}
}
boolean haveBypassingApps = candidatePkgs.size() > 0;
- if (mCurrentUserHasChannelsBypassingDnd != haveBypassingApps) {
- mCurrentUserHasChannelsBypassingDnd = haveBypassingApps;
+ if (mCurrentUserHasPriorityChannels != haveBypassingApps) {
+ mCurrentUserHasPriorityChannels = haveBypassingApps;
if (android.app.Flags.modesUi()) {
mZenModeHelper.updateHasPriorityChannels(UserHandle.CURRENT,
- mCurrentUserHasChannelsBypassingDnd);
+ mCurrentUserHasPriorityChannels);
} else {
- updateZenPolicy(mCurrentUserHasChannelsBypassingDnd, callingUid,
+ updateZenPolicy(mCurrentUserHasPriorityChannels, callingUid,
fromSystemOrSystemUi);
}
}
@@ -2188,16 +2187,20 @@ public class PreferencesHelper implements RankingConfig {
policy.priorityCategories, policy.priorityCallSenders,
policy.priorityMessageSenders, policy.suppressedVisualEffects,
(areChannelsBypassingDnd
- ? NotificationManager.Policy.STATE_CHANNELS_BYPASSING_DND : 0),
+ ? NotificationManager.Policy.STATE_HAS_PRIORITY_CHANNELS : 0),
policy.priorityConversationSenders),
fromSystemOrSystemUi ? ZenModeConfig.ORIGIN_SYSTEM
: ZenModeConfig.ORIGIN_APP,
callingUid);
}
- // TODO: b/310620812 - rename to hasPriorityChannels() when modes_api is inlined.
- public boolean areChannelsBypassingDnd() {
- return mCurrentUserHasChannelsBypassingDnd;
+ /**
+ * Whether the current user has any channels marked as "priority channels"
+ * ({@link NotificationChannel#canBypassDnd}), but not necessarily whether they are permitted
+ * to bypass the filters set by the current zen policy.
+ */
+ public boolean hasPriorityChannels() {
+ return mCurrentUserHasPriorityChannels;
}
/**
diff --git a/services/core/java/com/android/server/notification/ZenModeEventLogger.java b/services/core/java/com/android/server/notification/ZenModeEventLogger.java
index fcc5e9771f94..ec9a2db52de9 100644
--- a/services/core/java/com/android/server/notification/ZenModeEventLogger.java
+++ b/services/core/java/com/android/server/notification/ZenModeEventLogger.java
@@ -16,7 +16,7 @@
package com.android.server.notification;
-import static android.app.NotificationManager.Policy.STATE_CHANNELS_BYPASSING_DND;
+import static android.app.NotificationManager.Policy.STATE_HAS_PRIORITY_CHANNELS;
import static android.provider.Settings.Global.ZEN_MODE_OFF;
import static android.service.notification.NotificationServiceProto.CHANNEL_POLICY_NONE;
import static android.service.notification.NotificationServiceProto.CHANNEL_POLICY_PRIORITY;
@@ -285,11 +285,10 @@ class ZenModeEventLogger {
return true;
}
- if (Flags.modesApi() && hasActiveRuleCountDiff()) {
- // Rules with INTERRUPTION_FILTER_ALL were always possible but before MODES_API
- // they were completely useless; now they can apply effects, so we want to log
- // when they become active/inactive, even though DND itself (as in "notification
- // blocking") is off.
+ if (hasActiveRuleCountDiff()) {
+ // Rules with INTERRUPTION_FILTER_ALL can apply effects, so we want to log when they
+ // become active/inactive, even though DND itself (as in "notification blocking")
+ // is off.
return true;
}
@@ -331,7 +330,7 @@ class ZenModeEventLogger {
}
}
- if (Flags.modesApi() && mNewZenMode == ZEN_MODE_OFF) {
+ if (mNewZenMode == ZEN_MODE_OFF) {
// If the mode is OFF -> OFF then there cannot be any *effective* change to policy.
// (Note that, in theory, a policy diff is impossible since we don't merge the
// policies of INTERRUPTION_FILTER_ALL rules; this is a "just in case" check).
@@ -439,24 +438,14 @@ class ZenModeEventLogger {
// Determine the number of (automatic & manual) rules active after the change takes place.
int getNumRulesActive() {
- if (!Flags.modesApi()) {
- // If the zen mode has turned off, that means nothing can be active.
- if (mNewZenMode == ZEN_MODE_OFF) {
- return 0;
- }
- }
return numActiveRulesInConfig(mNewConfig);
}
/**
- * Return a list of the types of each of the active rules in the configuration.
- * Only available when {@code MODES_API} is active; otherwise returns an empty list.
+ * Return a list of the types of each of the active rules in the configuration (sorted by
+ * the numerical value of the type, and including duplicates).
*/
int[] getActiveRuleTypes() {
- if (!Flags.modesApi()) {
- return new int[0];
- }
-
ArrayList<Integer> activeTypes = new ArrayList<>();
List<ZenRule> activeRules = activeRulesList(mNewConfig);
if (activeRules.size() == 0) {
@@ -476,77 +465,10 @@ class ZenModeEventLogger {
return out;
}
- /**
- * Return our best guess as to whether the changes observed are due to a user action.
- * Note that this (before {@code MODES_API}) won't be 100% accurate as we can't necessarily
- * distinguish between a system uid call indicating "user interacted with Settings" vs "a
- * system app changed something automatically".
- */
+ /** Return whether the changes observed are due to a user action. */
boolean getIsUserAction() {
- if (Flags.modesApi()) {
- return mOrigin == ZenModeConfig.ORIGIN_USER_IN_SYSTEMUI
- || mOrigin == ZenModeConfig.ORIGIN_USER_IN_APP;
- }
-
- // Approach for pre-MODES_API:
- // - if manual rule turned on or off, the calling UID is system, and the new manual
- // rule does not have an enabler set, guess that this is likely to be a user action.
- // This may represent a system app turning on DND automatically, but we guess "user"
- // in this case.
- // - note that this has a known failure mode of "manual rule turning off
- // automatically after the default time runs out". We currently have no way
- // of distinguishing this case from a user manually turning off the rule.
- // - the reason for checking the enabler field is that a call may look like it's
- // coming from a system UID, but if an enabler is set then the request came
- // from an external source. "enabler" will be blank when manual rule is turned
- // on from Quick Settings or Settings.
- // - if an automatic rule's state changes in whether it is "enabled", then
- // that is probably a user action.
- // - if an automatic rule goes from "not snoozing" to "snoozing", that is probably
- // a user action; that means that the user temporarily turned off DND associated
- // with that rule.
- // - if an automatic rule becomes active but does *not* change in its enabled state
- // (covered by a previous case anyway), we guess that this is an automatic change.
- // - if a rule is added or removed and the call comes from the system, we guess that
- // this is a user action (as system rules can't be added or removed without a user
- // action).
- switch (getChangedRuleType()) {
- case RULE_TYPE_MANUAL:
- // TODO(b/278888961): Distinguish the automatically-turned-off state
- return isFromSystemOrSystemUi() && (getNewManualRuleEnabler() == null);
- case RULE_TYPE_AUTOMATIC:
- for (ZenModeDiff.RuleDiff d : getChangedAutomaticRules().values()) {
- if (d.wasAdded() || d.wasRemoved()) {
- // If the change comes from system, a rule being added/removed indicates
- // a likely user action. From an app, it's harder to know for sure.
- return isFromSystemOrSystemUi();
- }
- ZenModeDiff.FieldDiff enabled = d.getDiffForField(
- ZenModeDiff.RuleDiff.FIELD_ENABLED);
- if (enabled != null && enabled.hasDiff()) {
- return true;
- }
- ZenModeDiff.FieldDiff snoozing = d.getDiffForField(
- ZenModeDiff.RuleDiff.FIELD_SNOOZING);
- if (snoozing != null && snoozing.hasDiff() && (boolean) snoozing.to()) {
- return true;
- }
- }
- // If the change was in an automatic rule and none of the "probably triggered
- // by a user" cases apply, then it's probably an automatic change.
- return false;
- case RULE_TYPE_UNKNOWN:
- default:
- }
-
- // If the change wasn't in a rule, but was in the zen policy: consider to be user action
- // if the calling uid is system
- if (hasPolicyDiff() || hasChannelsBypassingDiff()) {
- return mCallingUid == Process.SYSTEM_UID;
- }
-
- // don't know, or none of the other things triggered; assume not a user action
- return false;
+ return mOrigin == ZenModeConfig.ORIGIN_USER_IN_SYSTEMUI
+ || mOrigin == ZenModeConfig.ORIGIN_USER_IN_APP;
}
boolean isFromSystemOrSystemUi() {
@@ -587,7 +509,7 @@ class ZenModeEventLogger {
*/
@Nullable
byte[] getDNDPolicyProto() {
- if (Flags.modesApi() && mNewZenMode == ZEN_MODE_OFF) {
+ if (mNewZenMode == ZEN_MODE_OFF) {
return null;
}
@@ -628,13 +550,10 @@ class ZenModeEventLogger {
mNewPolicy.allowMessagesFrom()));
proto.write(DNDPolicyProto.ALLOW_CONVERSATIONS_FROM,
mNewPolicy.allowConversationsFrom());
-
- if (Flags.modesApi()) {
- proto.write(DNDPolicyProto.ALLOW_CHANNELS,
- mNewPolicy.allowPriorityChannels()
- ? CHANNEL_POLICY_PRIORITY
- : CHANNEL_POLICY_NONE);
- }
+ proto.write(DNDPolicyProto.ALLOW_CHANNELS,
+ mNewPolicy.allowPriorityChannels()
+ ? CHANNEL_POLICY_PRIORITY
+ : CHANNEL_POLICY_NONE);
} else {
Log.wtf(TAG, "attempted to write zen mode log event with null policy");
}
@@ -648,14 +567,14 @@ class ZenModeEventLogger {
*/
boolean getAreChannelsBypassing() {
if (mNewPolicy != null) {
- return (mNewPolicy.state & STATE_CHANNELS_BYPASSING_DND) != 0;
+ return (mNewPolicy.state & STATE_HAS_PRIORITY_CHANNELS) != 0;
}
return false;
}
private boolean hasChannelsBypassingDiff() {
boolean prevChannelsBypassing = mPrevPolicy != null
- ? (mPrevPolicy.state & STATE_CHANNELS_BYPASSING_DND) != 0 : false;
+ ? (mPrevPolicy.state & STATE_HAS_PRIORITY_CHANNELS) != 0 : false;
return prevChannelsBypassing != getAreChannelsBypassing();
}
diff --git a/services/core/java/com/android/server/notification/ZenModeFiltering.java b/services/core/java/com/android/server/notification/ZenModeFiltering.java
index bdca555707e3..87ae78195ff5 100644
--- a/services/core/java/com/android/server/notification/ZenModeFiltering.java
+++ b/services/core/java/com/android/server/notification/ZenModeFiltering.java
@@ -19,7 +19,6 @@ package com.android.server.notification;
import static android.provider.Settings.Global.ZEN_MODE_OFF;
import static android.service.notification.ZenPolicy.CONVERSATION_SENDERS_ANYONE;
-import android.app.Flags;
import android.app.Notification;
import android.app.NotificationManager;
import android.content.ComponentName;
@@ -146,16 +145,12 @@ public class ZenModeFiltering {
// Returns whether the record is permitted to bypass DND when the zen mode is
// ZEN_MODE_IMPORTANT_INTERRUPTIONS. This depends on whether the record's package priority is
- // marked as PRIORITY_MAX (an indication of it belonging to a priority channel), and, if
- // the modes_api flag is on, whether the given policy permits priority channels to bypass.
- // TODO: b/310620812 - simplify when modes_api is inlined.
+ // marked as PRIORITY_MAX (an indication of it belonging to a priority channel), and whether the
+ // given policy permits priority channels to bypass.
private boolean canRecordBypassDnd(NotificationRecord record,
NotificationManager.Policy policy) {
boolean inPriorityChannel = record.getPackagePriority() == Notification.PRIORITY_MAX;
- if (Flags.modesApi()) {
- return inPriorityChannel && policy.allowPriorityChannels();
- }
- return inPriorityChannel;
+ return inPriorityChannel && policy.allowPriorityChannels();
}
/**
diff --git a/services/core/java/com/android/server/notification/ZenModeHelper.java b/services/core/java/com/android/server/notification/ZenModeHelper.java
index b39b6fde6258..889df512dd60 100644
--- a/services/core/java/com/android/server/notification/ZenModeHelper.java
+++ b/services/core/java/com/android/server/notification/ZenModeHelper.java
@@ -157,6 +157,12 @@ public class ZenModeHelper {
static final int RULE_LIMIT_PER_PACKAGE = 100;
private static final Duration DELETED_RULE_KEPT_FOR = Duration.ofDays(30);
+ /**
+ * Amount of time since last activation after which implicit rules that have never been
+ * customized by the user are automatically cleaned up.
+ */
+ private static final Duration IMPLICIT_RULE_KEPT_FOR = Duration.ofDays(30);
+
private static final int MAX_ICON_RESOURCE_NAME_LENGTH = 1000;
/**
@@ -326,9 +332,6 @@ public class ZenModeHelper {
* applied immediately.
*/
void setDeviceEffectsApplier(@NonNull DeviceEffectsApplier deviceEffectsApplier) {
- if (!Flags.modesApi()) {
- return;
- }
synchronized (mConfigLock) {
if (mDeviceEffectsApplier != null) {
throw new IllegalStateException("Already set up a DeviceEffectsApplier!");
@@ -350,11 +353,6 @@ public class ZenModeHelper {
}
}
- // TODO: b/310620812 - Remove when MODES_API is inlined (no more callers).
- public void onUserUnlocked(int user) {
- loadConfigForUser(user, "onUserUnlocked");
- }
-
void setPriorityOnlyDndExemptPackages(String[] packages) {
mPriorityOnlyDndExemptPackages = packages;
}
@@ -385,21 +383,6 @@ public class ZenModeHelper {
return NotificationManager.zenModeToInterruptionFilter(mZenMode);
}
- // TODO: b/310620812 - Remove when MODES_API is inlined (no more callers).
- public void requestFromListener(ComponentName name, int filter, int callingUid,
- boolean fromSystemOrSystemUi) {
- final int newZen = NotificationManager.zenModeFromInterruptionFilter(filter, -1);
- if (newZen != -1) {
- // This change is known to be for UserHandle.CURRENT because NLSes for
- // background users are unbound.
- setManualZenMode(UserHandle.CURRENT, newZen, null,
- fromSystemOrSystemUi ? ORIGIN_SYSTEM : ORIGIN_APP,
- /* reason= */ "listener:" + (name != null ? name.flattenToShortString() : null),
- /* caller= */ name != null ? name.getPackageName() : null,
- callingUid);
- }
- }
-
public void setSuppressedEffects(long suppressedEffects) {
if (mSuppressedEffects == suppressedEffects) return;
mSuppressedEffects = suppressedEffects;
@@ -414,33 +397,24 @@ public class ZenModeHelper {
return mZenMode;
}
- // TODO: b/310620812 - Make private (or inline) when MODES_API is inlined.
- public List<ZenRule> getZenRules(UserHandle user, int callingUid) {
- List<ZenRule> rules = new ArrayList<>();
+ /**
+ * Get the list of {@link AutomaticZenRule} instances that the calling package can manage
+ * (which means the owned rules for a regular app, and every rule for system callers) together
+ * with their ids.
+ */
+ Map<String, AutomaticZenRule> getAutomaticZenRules(UserHandle user, int callingUid) {
+ HashMap<String, AutomaticZenRule> rules = new HashMap<>();
synchronized (mConfigLock) {
ZenModeConfig config = getConfigLocked(user);
if (config == null) return rules;
+
for (ZenRule rule : config.automaticRules.values()) {
if (canManageAutomaticZenRule(rule, callingUid)) {
- rules.add(rule);
+ rules.put(rule.id, zenRuleToAutomaticZenRule(rule));
}
}
+ return rules;
}
- return rules;
- }
-
- /**
- * Get the list of {@link AutomaticZenRule} instances that the calling package can manage
- * (which means the owned rules for a regular app, and every rule for system callers) together
- * with their ids.
- */
- Map<String, AutomaticZenRule> getAutomaticZenRules(UserHandle user, int callingUid) {
- List<ZenRule> ruleList = getZenRules(user, callingUid);
- HashMap<String, AutomaticZenRule> rules = new HashMap<>(ruleList.size());
- for (ZenRule rule : ruleList) {
- rules.put(rule.id, zenRuleToAutomaticZenRule(rule));
- }
- return rules;
}
public AutomaticZenRule getAutomaticZenRule(UserHandle user, String id, int callingUid) {
@@ -511,9 +485,6 @@ public class ZenModeHelper {
@GuardedBy("mConfigLock")
private ZenRule maybeRestoreRemovedRule(ZenModeConfig config, String pkg, ZenRule ruleToAdd,
AutomaticZenRule azrToAdd, @ConfigOrigin int origin) {
- if (!Flags.modesApi()) {
- return ruleToAdd;
- }
String deletedKey = ZenModeConfig.deletedRuleKey(ruleToAdd);
if (deletedKey == null) {
// Couldn't calculate the deletedRuleKey (condition or pkg null?). This should
@@ -561,9 +532,6 @@ public class ZenModeHelper {
*/
private static void maybeReplaceDefaultRule(ZenModeConfig config, @Nullable ZenRule oldRule,
AutomaticZenRule rule) {
- if (!Flags.modesApi()) {
- return;
- }
if (rule.getType() == AutomaticZenRule.TYPE_BEDTIME
&& (oldRule == null || oldRule.type != rule.getType())) {
// Note: we must not verify canManageAutomaticZenRule here, since most likely they
@@ -572,7 +540,7 @@ public class ZenModeHelper {
ZenModeConfig.EVERY_NIGHT_DEFAULT_RULE_ID);
if (sleepingRule != null
&& !sleepingRule.enabled
- && sleepingRule.canBeUpdatedByApp() /* meaning it's not user-customized */) {
+ && !sleepingRule.isUserModified()) {
config.automaticRules.remove(ZenModeConfig.EVERY_NIGHT_DEFAULT_RULE_ID);
}
}
@@ -599,18 +567,10 @@ public class ZenModeHelper {
}
ZenModeConfig newConfig = config.copy();
ZenModeConfig.ZenRule newRule = requireNonNull(newConfig.automaticRules.get(ruleId));
- if (!Flags.modesApi()) {
- if (newRule.enabled != automaticZenRule.isEnabled()) {
- dispatchOnAutomaticRuleStatusChanged(config.user, newRule.getPkg(), ruleId,
- automaticZenRule.isEnabled()
- ? AUTOMATIC_RULE_STATUS_ENABLED
- : AUTOMATIC_RULE_STATUS_DISABLED);
- }
- }
boolean updated = populateZenRule(newRule.pkg, automaticZenRule, newConfig, newRule,
origin, /* isNew= */ false);
- if (Flags.modesApi() && !updated) {
+ if (!updated) {
// Bail out so we don't have the side effects of updating a rule (i.e. dropping
// condition) when no changes happen.
return true;
@@ -643,10 +603,6 @@ public class ZenModeHelper {
*/
void applyGlobalZenModeAsImplicitZenRule(UserHandle user, String callingPkg, int callingUid,
int zenMode) {
- if (!android.app.Flags.modesApi()) {
- Log.wtf(TAG, "applyGlobalZenModeAsImplicitZenRule called with flag off!");
- return;
- }
synchronized (mConfigLock) {
ZenModeConfig config = getConfigLocked(user);
if (config == null) {
@@ -712,10 +668,6 @@ public class ZenModeHelper {
*/
void applyGlobalPolicyAsImplicitZenRule(UserHandle user, String callingPkg, int callingUid,
NotificationManager.Policy policy) {
- if (!android.app.Flags.modesApi()) {
- Log.wtf(TAG, "applyGlobalPolicyAsImplicitZenRule called with flag off!");
- return;
- }
synchronized (mConfigLock) {
ZenModeConfig config = getConfigLocked(user);
if (config == null) {
@@ -772,10 +724,6 @@ public class ZenModeHelper {
*/
@Nullable
Policy getNotificationPolicyFromImplicitZenRule(UserHandle user, String callingPkg) {
- if (!android.app.Flags.modesApi()) {
- Log.wtf(TAG, "getNotificationPolicyFromImplicitZenRule called with flag off!");
- return getNotificationPolicy(user);
- }
synchronized (mConfigLock) {
ZenModeConfig config = getConfigLocked(user);
if (config == null) {
@@ -814,7 +762,6 @@ public class ZenModeHelper {
.appendPath(pkg)
.build();
rule.enabled = true;
- rule.modified = false;
rule.component = null;
rule.configurationActivity = null;
return rule;
@@ -918,15 +865,12 @@ public class ZenModeHelper {
private void maybePreserveRemovedRule(ZenModeConfig config, ZenRule ruleToRemove,
@ConfigOrigin int origin) {
- if (!Flags.modesApi()) {
- return;
- }
// If an app deletes a previously customized rule, keep it around to preserve
// the user's customization when/if it's recreated later.
// We don't try to preserve system-owned rules because their conditionIds (used as
// deletedRuleKey) are not stable. This is almost moot anyway because an app cannot
// delete a system-owned rule.
- if (origin == ORIGIN_APP && !ruleToRemove.canBeUpdatedByApp()
+ if (origin == ORIGIN_APP && ruleToRemove.isUserModified()
&& !PACKAGE_ANDROID.equals(ruleToRemove.pkg)) {
String deletedKey = ZenModeConfig.deletedRuleKey(ruleToRemove);
if (deletedKey != null) {
@@ -952,7 +896,7 @@ public class ZenModeHelper {
if (rule == null || !canManageAutomaticZenRule(rule, callingUid)) {
return Condition.STATE_UNKNOWN;
}
- if (Flags.modesApi() && Flags.modesUi()) {
+ if (Flags.modesUi()) {
return rule.isActive() ? STATE_TRUE : STATE_FALSE;
} else {
// Buggy, does not consider snoozing!
@@ -971,16 +915,9 @@ public class ZenModeHelper {
newConfig = config.copy();
ZenRule rule = newConfig.automaticRules.get(id);
- if (Flags.modesApi()) {
- if (rule != null && canManageAutomaticZenRule(rule, callingUid)) {
- setAutomaticZenRuleStateLocked(newConfig, Collections.singletonList(rule),
- condition, origin, "setAzrState: " + rule.id, callingUid);
- }
- } else {
- ArrayList<ZenRule> rules = new ArrayList<>();
- rules.add(rule); // rule may be null and throw NPE in the next method.
- setAutomaticZenRuleStateLocked(newConfig, rules, condition, origin,
- "setAzrState: " + (rule != null ? rule.id : "null!"), callingUid);
+ if (rule != null && canManageAutomaticZenRule(rule, callingUid)) {
+ setAutomaticZenRuleStateLocked(newConfig, Collections.singletonList(rule),
+ condition, origin, "setAzrState: " + rule.id, callingUid);
}
}
}
@@ -995,13 +932,12 @@ public class ZenModeHelper {
newConfig = config.copy();
List<ZenRule> matchingRules = findMatchingRules(newConfig, ruleConditionId, condition);
- if (Flags.modesApi()) {
- for (int i = matchingRules.size() - 1; i >= 0; i--) {
- if (!canManageAutomaticZenRule(matchingRules.get(i), callingUid)) {
- matchingRules.remove(i);
- }
+ for (int i = matchingRules.size() - 1; i >= 0; i--) {
+ if (!canManageAutomaticZenRule(matchingRules.get(i), callingUid)) {
+ matchingRules.remove(i);
}
}
+
setAutomaticZenRuleStateLocked(newConfig, matchingRules, condition, origin,
"setAzrStateFromCps: " + ruleConditionId, callingUid);
}
@@ -1013,7 +949,7 @@ public class ZenModeHelper {
if (rules == null || rules.isEmpty()) return;
if (!Flags.modesUi()) {
- if (Flags.modesApi() && condition.source == SOURCE_USER_ACTION) {
+ if (condition.source == SOURCE_USER_ACTION) {
origin = ORIGIN_USER_IN_APP; // Although coming from app, it's actually from user.
}
}
@@ -1026,7 +962,7 @@ public class ZenModeHelper {
private static void applyConditionAndReconsiderOverride(ZenRule rule, Condition condition,
int origin) {
- if (Flags.modesApi() && Flags.modesUi()) {
+ if (Flags.modesUi()) {
if (isImplicitRuleId(rule.id)) {
// Implicit rules do not use overrides, and always apply conditions directly.
// This is compatible with the previous behavior (where the package set the
@@ -1173,8 +1109,7 @@ public class ZenModeHelper {
// if default rule wasn't user-modified use localized name
// instead of previous system name
if (currRule != null
- && !currRule.modified
- && (currRule.zenPolicyUserModifiedFields & AutomaticZenRule.FIELD_NAME) == 0
+ && (currRule.userModifiedFields & AutomaticZenRule.FIELD_NAME) == 0
&& !defaultRule.name.equals(currRule.name)) {
if (DEBUG) {
Slog.d(TAG, "Locale change - updating default zen rule name "
@@ -1184,7 +1119,7 @@ public class ZenModeHelper {
updated = true;
}
}
- if (Flags.modesApi() && Flags.modesUi()) {
+ if (Flags.modesUi()) {
for (ZenRule rule : newConfig.automaticRules.values()) {
if (SystemZenRules.isSystemOwnedRule(rule)) {
updated |= SystemZenRules.updateTriggerDescription(mContext, rule);
@@ -1256,172 +1191,145 @@ public class ZenModeHelper {
@GuardedBy("mConfigLock")
private boolean populateZenRule(String pkg, AutomaticZenRule azr, ZenModeConfig config,
ZenRule rule, @ConfigOrigin int origin, boolean isNew) {
- if (Flags.modesApi()) {
- boolean modified = false;
- // These values can always be edited by the app, so we apply changes immediately.
- if (isNew) {
- rule.id = ZenModeConfig.newRuleId();
- rule.creationTime = mClock.millis();
- rule.component = azr.getOwner();
- rule.pkg = pkg;
- modified = true;
- }
- // Allow updating the CPS backing system rules (e.g. for custom manual -> schedule)
- if (Flags.modesUi()
- && (origin == ORIGIN_SYSTEM || origin == ORIGIN_USER_IN_SYSTEMUI)
- && Objects.equals(rule.pkg, SystemZenRules.PACKAGE_ANDROID)
- && !Objects.equals(rule.component, azr.getOwner())) {
- rule.component = azr.getOwner();
- modified = true;
- }
+ boolean modified = false;
+ // These values can always be edited by the app, so we apply changes immediately.
+ if (isNew) {
+ rule.id = ZenModeConfig.newRuleId();
+ rule.creationTime = mClock.millis();
+ rule.component = azr.getOwner();
+ rule.pkg = pkg;
+ modified = true;
+ }
- if (Flags.modesUi()) {
- if (!azr.isEnabled() && (isNew || rule.enabled)) {
- // Creating a rule as disabled, or disabling a previously enabled rule.
- // Record whodunit.
- rule.disabledOrigin = origin;
- } else if (azr.isEnabled()) {
- // Enabling or previously enabled. Clear disabler.
- rule.disabledOrigin = ORIGIN_UNKNOWN;
- }
- }
+ // Allow updating the CPS backing system rules (e.g. for custom manual -> schedule)
+ if (Flags.modesUi()
+ && (origin == ORIGIN_SYSTEM || origin == ORIGIN_USER_IN_SYSTEMUI)
+ && Objects.equals(rule.pkg, SystemZenRules.PACKAGE_ANDROID)
+ && !Objects.equals(rule.component, azr.getOwner())) {
+ rule.component = azr.getOwner();
+ modified = true;
+ }
- if (!Objects.equals(rule.conditionId, azr.getConditionId())) {
- rule.conditionId = azr.getConditionId();
- modified = true;
- }
- // This can be removed when {@link Flags#modesUi} is fully ramped up
- final boolean isWatch =
- mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WATCH);
- boolean shouldPreserveCondition =
- Flags.modesApi()
- && (Flags.modesUi() || isWatch)
- && !isNew
- && origin == ORIGIN_USER_IN_SYSTEMUI
- && rule.enabled == azr.isEnabled()
- && rule.conditionId != null
- && rule.condition != null
- && rule.conditionId.equals(rule.condition.id);
- if (!shouldPreserveCondition) {
- // Do not update 'modified'. If only this changes we treat it as a no-op updateAZR.
- rule.condition = null;
- }
-
- if (rule.enabled != azr.isEnabled()) {
- rule.enabled = azr.isEnabled();
- rule.resetConditionOverride();
- modified = true;
- }
- if (!Objects.equals(rule.configurationActivity, azr.getConfigurationActivity())) {
- rule.configurationActivity = azr.getConfigurationActivity();
- modified = true;
- }
- if (rule.allowManualInvocation != azr.isManualInvocationAllowed()) {
- rule.allowManualInvocation = azr.isManualInvocationAllowed();
- modified = true;
- }
- if (!Flags.modesUi()) {
- String iconResName = drawableResIdToResName(rule.pkg, azr.getIconResId());
- if (!Objects.equals(rule.iconResName, iconResName)) {
- rule.iconResName = iconResName;
- modified = true;
- }
- }
- if (!Objects.equals(rule.triggerDescription, azr.getTriggerDescription())) {
- rule.triggerDescription = azr.getTriggerDescription();
- modified = true;
+ if (Flags.modesUi()) {
+ if (!azr.isEnabled() && (isNew || rule.enabled)) {
+ // Creating a rule as disabled, or disabling a previously enabled rule.
+ // Record whodunit.
+ rule.disabledOrigin = origin;
+ } else if (azr.isEnabled()) {
+ // Enabling or previously enabled. Clear disabler.
+ rule.disabledOrigin = ORIGIN_UNKNOWN;
}
- if (rule.type != azr.getType()) {
- rule.type = azr.getType();
+ }
+
+ if (!Objects.equals(rule.conditionId, azr.getConditionId())) {
+ rule.conditionId = azr.getConditionId();
+ modified = true;
+ }
+ // This can be removed when {@link Flags#modesUi} is fully ramped up
+ final boolean isWatch =
+ mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WATCH);
+ boolean shouldPreserveCondition =
+ (Flags.modesUi() || isWatch)
+ && !isNew
+ && origin == ORIGIN_USER_IN_SYSTEMUI
+ && rule.enabled == azr.isEnabled()
+ && rule.conditionId != null
+ && rule.condition != null
+ && rule.conditionId.equals(rule.condition.id);
+ if (!shouldPreserveCondition) {
+ // Do not update 'modified'. If only this changes we treat it as a no-op updateAZR.
+ rule.condition = null;
+ }
+
+ if (rule.enabled != azr.isEnabled()) {
+ rule.enabled = azr.isEnabled();
+ rule.resetConditionOverride();
+ modified = true;
+ }
+ if (!Objects.equals(rule.configurationActivity, azr.getConfigurationActivity())) {
+ rule.configurationActivity = azr.getConfigurationActivity();
+ modified = true;
+ }
+ if (rule.allowManualInvocation != azr.isManualInvocationAllowed()) {
+ rule.allowManualInvocation = azr.isManualInvocationAllowed();
+ modified = true;
+ }
+ if (!Flags.modesUi()) {
+ String iconResName = drawableResIdToResName(rule.pkg, azr.getIconResId());
+ if (!Objects.equals(rule.iconResName, iconResName)) {
+ rule.iconResName = iconResName;
modified = true;
}
- // TODO: b/310620812 - Remove this once FLAG_MODES_API is inlined.
- rule.modified = azr.isModified();
+ }
+ if (!Objects.equals(rule.triggerDescription, azr.getTriggerDescription())) {
+ rule.triggerDescription = azr.getTriggerDescription();
+ modified = true;
+ }
+ if (rule.type != azr.getType()) {
+ rule.type = azr.getType();
+ modified = true;
+ }
- // Name is treated differently than other values:
- // App is allowed to update name if the name was not modified by the user (even if
- // other values have been modified). In this way, if the locale of an app changes,
- // i18n of the rule name can still occur even if the user has customized the rule
- // contents.
- String previousName = rule.name;
- if (isNew || doesOriginAlwaysUpdateValues(origin)
- || (rule.userModifiedFields & AutomaticZenRule.FIELD_NAME) == 0) {
- rule.name = azr.getName();
- modified |= !Objects.equals(rule.name, previousName);
- }
+ // Name is treated differently than other values:
+ // App is allowed to update name if the name was not modified by the user (even if
+ // other values have been modified). In this way, if the locale of an app changes,
+ // i18n of the rule name can still occur even if the user has customized the rule
+ // contents.
+ String previousName = rule.name;
+ if (isNew || doesOriginAlwaysUpdateValues(origin)
+ || (rule.userModifiedFields & AutomaticZenRule.FIELD_NAME) == 0) {
+ rule.name = azr.getName();
+ modified |= !Objects.equals(rule.name, previousName);
+ }
- // For the remaining values, rules can always have all values updated if:
- // * the rule is newly added, or
- // * the request comes from an origin that can always update values, like the user, or
- // * the rule has not yet been user modified, and thus can be updated by the app.
- boolean updateValues = isNew || doesOriginAlwaysUpdateValues(origin)
- || rule.canBeUpdatedByApp();
+ // For the remaining values, rules can always have all values updated if:
+ // * the rule is newly added, or
+ // * the request comes from an origin that can always update values, like the user, or
+ // * the rule has not yet been user modified, and thus can be updated by the app.
+ boolean updateValues = isNew || doesOriginAlwaysUpdateValues(origin)
+ || !rule.isUserModified();
- // For all other values, if updates are not allowed, we discard the update.
- if (!updateValues) {
- return modified;
- }
+ // For all other values, if updates are not allowed, we discard the update.
+ if (!updateValues) {
+ return modified;
+ }
- // Updates the bitmasks if the origin of the change is the user.
- boolean updateBitmask = (origin == ORIGIN_USER_IN_SYSTEMUI);
+ // Updates the bitmasks if the origin of the change is the user.
+ boolean updateBitmask = (origin == ORIGIN_USER_IN_SYSTEMUI);
- if (updateBitmask && !TextUtils.equals(previousName, azr.getName())) {
- rule.userModifiedFields |= AutomaticZenRule.FIELD_NAME;
+ if (updateBitmask && !TextUtils.equals(previousName, azr.getName())) {
+ rule.userModifiedFields |= AutomaticZenRule.FIELD_NAME;
+ }
+ int newZenMode = NotificationManager.zenModeFromInterruptionFilter(
+ azr.getInterruptionFilter(), Global.ZEN_MODE_OFF);
+ if (rule.zenMode != newZenMode) {
+ rule.zenMode = newZenMode;
+ if (updateBitmask) {
+ rule.userModifiedFields |= AutomaticZenRule.FIELD_INTERRUPTION_FILTER;
}
- int newZenMode = NotificationManager.zenModeFromInterruptionFilter(
- azr.getInterruptionFilter(), Global.ZEN_MODE_OFF);
- if (rule.zenMode != newZenMode) {
- rule.zenMode = newZenMode;
+ modified = true;
+ }
+
+ if (Flags.modesUi()) {
+ String iconResName = drawableResIdToResName(rule.pkg, azr.getIconResId());
+ if (!Objects.equals(rule.iconResName, iconResName)) {
+ rule.iconResName = iconResName;
if (updateBitmask) {
- rule.userModifiedFields |= AutomaticZenRule.FIELD_INTERRUPTION_FILTER;
+ rule.userModifiedFields |= AutomaticZenRule.FIELD_ICON;
}
modified = true;
}
+ }
- if (Flags.modesUi()) {
- String iconResName = drawableResIdToResName(rule.pkg, azr.getIconResId());
- if (!Objects.equals(rule.iconResName, iconResName)) {
- rule.iconResName = iconResName;
- if (updateBitmask) {
- rule.userModifiedFields |= AutomaticZenRule.FIELD_ICON;
- }
- modified = true;
- }
- }
-
- // Updates the bitmask and values for all policy fields, based on the origin.
- modified |= updatePolicy(config, rule, azr.getZenPolicy(), updateBitmask, isNew);
+ // Updates the bitmask and values for all policy fields, based on the origin.
+ modified |= updatePolicy(config, rule, azr.getZenPolicy(), updateBitmask, isNew);
- // Updates the bitmask and values for all device effect fields, based on the origin.
- modified |= updateZenDeviceEffects(rule, azr.getDeviceEffects(),
- origin == ORIGIN_APP, updateBitmask);
+ // Updates the bitmask and values for all device effect fields, based on the origin.
+ modified |= updateZenDeviceEffects(rule, azr.getDeviceEffects(),
+ origin == ORIGIN_APP, updateBitmask);
- return modified;
- } else {
- if (rule.enabled != azr.isEnabled()) {
- rule.resetConditionOverride();
- }
- rule.name = azr.getName();
- rule.condition = null;
- rule.conditionId = azr.getConditionId();
- rule.enabled = azr.isEnabled();
- rule.modified = azr.isModified();
- rule.zenPolicy = azr.getZenPolicy();
- rule.zenMode = NotificationManager.zenModeFromInterruptionFilter(
- azr.getInterruptionFilter(), Global.ZEN_MODE_OFF);
- rule.configurationActivity = azr.getConfigurationActivity();
-
- if (isNew) {
- rule.id = ZenModeConfig.newRuleId();
- rule.creationTime = System.currentTimeMillis();
- rule.component = azr.getOwner();
- rule.pkg = pkg;
- }
-
- // Only the MODES_API path cares about the result, so just return whatever here.
- return true;
- }
+ return modified;
}
/**
@@ -1629,32 +1537,21 @@ public class ZenModeHelper {
}
private AutomaticZenRule zenRuleToAutomaticZenRule(ZenRule rule) {
- AutomaticZenRule azr;
- if (Flags.modesApi()) {
- azr = new AutomaticZenRule.Builder(rule.name, rule.conditionId)
- .setManualInvocationAllowed(rule.allowManualInvocation)
- .setPackage(rule.pkg)
- .setCreationTime(rule.creationTime)
- .setIconResId(drawableResNameToResId(rule.pkg, rule.iconResName))
- .setType(rule.type)
- .setZenPolicy(rule.zenPolicy)
- .setDeviceEffects(rule.zenDeviceEffects)
- .setEnabled(rule.enabled)
- .setInterruptionFilter(
- NotificationManager.zenModeToInterruptionFilter(rule.zenMode))
- .setOwner(rule.component)
- .setConfigurationActivity(rule.configurationActivity)
- .setTriggerDescription(rule.triggerDescription)
- .build();
- } else {
- azr = new AutomaticZenRule(rule.name, rule.component,
- rule.configurationActivity,
- rule.conditionId, rule.zenPolicy,
- NotificationManager.zenModeToInterruptionFilter(rule.zenMode),
- rule.enabled, rule.creationTime);
- azr.setPackageName(rule.pkg);
- }
- return azr;
+ return new AutomaticZenRule.Builder(rule.name, rule.conditionId)
+ .setManualInvocationAllowed(rule.allowManualInvocation)
+ .setPackage(rule.pkg)
+ .setCreationTime(rule.creationTime)
+ .setIconResId(drawableResNameToResId(rule.pkg, rule.iconResName))
+ .setType(rule.type)
+ .setZenPolicy(rule.zenPolicy)
+ .setDeviceEffects(rule.zenDeviceEffects)
+ .setEnabled(rule.enabled)
+ .setInterruptionFilter(
+ NotificationManager.zenModeToInterruptionFilter(rule.zenMode))
+ .setOwner(rule.component)
+ .setConfigurationActivity(rule.configurationActivity)
+ .setTriggerDescription(rule.triggerDescription)
+ .build();
}
// Update only the hasPriorityChannels state (aka areChannelsBypassingDnd) without modifying
@@ -1669,12 +1566,12 @@ public class ZenModeHelper {
if (config == null) return;
// If it already matches, do nothing
- if (config.areChannelsBypassingDnd == hasPriorityChannels) {
+ if (config.hasPriorityChannels == hasPriorityChannels) {
return;
}
ZenModeConfig newConfig = config.copy();
- newConfig.areChannelsBypassingDnd = hasPriorityChannels;
+ newConfig.hasPriorityChannels = hasPriorityChannels;
// The updated calculation of whether there are priority channels is always done by
// the system, even if the event causing the calculation had a different origin.
setConfigLocked(newConfig, null, ORIGIN_SYSTEM, "updateHasPriorityChannels",
@@ -1754,9 +1651,7 @@ public class ZenModeHelper {
newRule.zenMode = zenMode;
newRule.conditionId = conditionId;
newRule.enabler = caller;
- if (Flags.modesApi()) {
- newRule.allowManualInvocation = true;
- }
+ newRule.allowManualInvocation = true;
newConfig.manualRule = newRule;
}
}
@@ -1849,7 +1744,7 @@ public class ZenModeHelper {
boolean hasDefaultRules = config.automaticRules.containsAll(
ZenModeConfig.getDefaultRuleIds());
- long time = Flags.modesApi() ? mClock.millis() : System.currentTimeMillis();
+ long time = mClock.millis();
if (config.automaticRules != null && config.automaticRules.size() > 0) {
for (ZenRule automaticRule : config.automaticRules.values()) {
if (forRestore) {
@@ -1863,7 +1758,7 @@ public class ZenModeHelper {
// Upon upgrading to a version with modes_api enabled, keep all behaviors of
// rules with null ZenPolicies explicitly as a copy of the global policy.
- if (Flags.modesApi() && config.version < ZenModeConfig.XML_VERSION_MODES_API) {
+ if (config.version < ZenModeConfig.XML_VERSION_MODES_API) {
// Keep the manual ("global") policy that from config.
ZenPolicy manualRulePolicy = config.getZenPolicy();
if (automaticRule.zenPolicy == null) {
@@ -1877,8 +1772,7 @@ public class ZenModeHelper {
}
}
- if (Flags.modesApi() && Flags.modesUi()
- && config.version < ZenModeConfig.XML_VERSION_MODES_UI) {
+ if (Flags.modesUi() && config.version < ZenModeConfig.XML_VERSION_MODES_UI) {
// Clear icons from implicit rules. App icons are not suitable for some
// surfaces, so juse use a default (the user can select a different one).
if (ZenModeConfig.isImplicitRuleId(automaticRule.id)) {
@@ -1904,11 +1798,11 @@ public class ZenModeHelper {
reason += ", reset to default rules";
}
- if (Flags.modesApi() && Flags.modesUi()) {
+ if (Flags.modesUi()) {
SystemZenRules.maybeUpgradeRules(mContext, config);
}
- if (Flags.modesApi() && forRestore) {
+ if (forRestore) {
// Note: forBackup doesn't write deletedRules, but just in case.
config.deletedRules.clear();
}
@@ -1995,7 +1889,7 @@ public class ZenModeHelper {
if (config == null) return;
final ZenModeConfig newConfig = config.copy();
- if (Flags.modesApi() && !Flags.modesUi()) {
+ if (!Flags.modesUi()) {
// Fix for b/337193321 -- propagate changes to notificationPolicy to rules where
// the user cannot edit zen policy to emulate the previous "inheritance".
ZenPolicy previousPolicy = ZenAdapters.notificationPolicyToZenPolicy(
@@ -2026,6 +1920,7 @@ public class ZenModeHelper {
* <ul>
* <li>Rule instances whose owner is not installed.
* <li>Deleted rules that were deleted more than 30 days ago.
+ * <li>Implicit rules that haven't been used in 30 days (and have not been customized).
* </ul>
*/
private void cleanUpZenRules() {
@@ -2034,17 +1929,20 @@ public class ZenModeHelper {
final ZenModeConfig newConfig = mConfig.copy();
deleteRulesWithoutOwner(newConfig.automaticRules);
- if (Flags.modesApi()) {
- deleteRulesWithoutOwner(newConfig.deletedRules);
- for (int i = newConfig.deletedRules.size() - 1; i >= 0; i--) {
- ZenRule deletedRule = newConfig.deletedRules.valueAt(i);
- if (deletedRule.deletionInstant == null
- || deletedRule.deletionInstant.isBefore(keptRuleThreshold)) {
- newConfig.deletedRules.removeAt(i);
- }
+ deleteRulesWithoutOwner(newConfig.deletedRules);
+
+ for (int i = newConfig.deletedRules.size() - 1; i >= 0; i--) {
+ ZenRule deletedRule = newConfig.deletedRules.valueAt(i);
+ if (deletedRule.deletionInstant == null
+ || deletedRule.deletionInstant.isBefore(keptRuleThreshold)) {
+ newConfig.deletedRules.removeAt(i);
}
}
+ if (Flags.modesUi() && Flags.modesCleanupImplicit()) {
+ deleteUnusedImplicitRules(newConfig.automaticRules);
+ }
+
if (!newConfig.equals(mConfig)) {
setConfigLocked(newConfig, null, ORIGIN_SYSTEM,
"cleanUpZenRules", Process.SYSTEM_UID);
@@ -2053,7 +1951,7 @@ public class ZenModeHelper {
}
private void deleteRulesWithoutOwner(ArrayMap<String, ZenRule> ruleList) {
- long currentTime = Flags.modesApi() ? mClock.millis() : System.currentTimeMillis();
+ long currentTime = mClock.millis();
if (ruleList != null) {
for (int i = ruleList.size() - 1; i >= 0; i--) {
ZenRule rule = ruleList.valueAt(i);
@@ -2070,6 +1968,29 @@ public class ZenModeHelper {
}
}
+ private void deleteUnusedImplicitRules(ArrayMap<String, ZenRule> ruleList) {
+ if (ruleList == null) {
+ return;
+ }
+ Instant deleteIfUnusedSince = mClock.instant().minus(IMPLICIT_RULE_KEPT_FOR);
+
+ for (int i = ruleList.size() - 1; i >= 0; i--) {
+ ZenRule rule = ruleList.valueAt(i);
+ if (isImplicitRuleId(rule.id) && !rule.isUserModified()) {
+ if (rule.lastActivation == null) {
+ // This rule existed before we started tracking activation time. It *might* be
+ // in use. Set lastActivation=now so it has some time (IMPLICIT_RULE_KEPT_FOR)
+ // before being removed if truly unused.
+ rule.lastActivation = mClock.instant();
+ }
+
+ if (rule.lastActivation.isBefore(deleteIfUnusedSince)) {
+ ruleList.removeAt(i);
+ }
+ }
+ }
+ }
+
/**
* @return a copy of the zen mode configuration
*/
@@ -2188,7 +2109,7 @@ public class ZenModeHelper {
mZenMode, mConfig, mConsolidatedPolicy);
if (!config.equals(mConfig)) {
// Schedule broadcasts. Cannot be sent during boot, though.
- if (Flags.modesApi() && origin != ORIGIN_INIT) {
+ if (origin != ORIGIN_INIT) {
for (ZenRule rule : config.automaticRules.values()) {
ZenRule original = mConfig.automaticRules.get(rule.id);
if (original != null) {
@@ -2204,6 +2125,20 @@ public class ZenModeHelper {
}
}
+ // Update last activation for rules that are being activated.
+ if (Flags.modesUi() && Flags.modesCleanupImplicit()) {
+ Instant now = mClock.instant();
+ if (!mConfig.isManualActive() && config.isManualActive()) {
+ config.manualRule.lastActivation = now;
+ }
+ for (ZenRule rule : config.automaticRules.values()) {
+ ZenRule previousRule = mConfig.automaticRules.get(rule.id);
+ if (rule.isActive() && (previousRule == null || !previousRule.isActive())) {
+ rule.lastActivation = now;
+ }
+ }
+ }
+
mConfig = config;
dispatchOnConfigChanged();
updateAndApplyConsolidatedPolicyAndDeviceEffects(origin, reason);
@@ -2295,7 +2230,7 @@ public class ZenModeHelper {
private void applyCustomPolicy(ZenModeConfig config, ZenPolicy policy, ZenRule rule,
boolean useManualConfig) {
if (rule.zenMode == Global.ZEN_MODE_NO_INTERRUPTIONS) {
- if (Flags.modesApi() && Flags.modesUi()) {
+ if (Flags.modesUi()) {
policy.apply(ZenPolicy.getBasePolicyInterruptionFilterNone());
} else {
policy.apply(new ZenPolicy.Builder()
@@ -2304,7 +2239,7 @@ public class ZenModeHelper {
.build());
}
} else if (rule.zenMode == Global.ZEN_MODE_ALARMS) {
- if (Flags.modesApi() && Flags.modesUi()) {
+ if (Flags.modesUi()) {
policy.apply(ZenPolicy.getBasePolicyInterruptionFilterAlarms());
} else {
policy.apply(new ZenPolicy.Builder()
@@ -2317,22 +2252,17 @@ public class ZenModeHelper {
} else if (rule.zenPolicy != null) {
policy.apply(rule.zenPolicy);
} else {
- if (Flags.modesApi()) {
- if (useManualConfig) {
- // manual rule is configured using the settings stored directly in ZenModeConfig
- policy.apply(config.getZenPolicy());
- } else {
- // under modes_api flag, an active automatic rule with no specified policy
- // inherits the device default settings as stored in mDefaultConfig. While the
- // rule's policy fields should be set upon creation, this is a fallback to
- // catch any that may have fallen through the cracks.
- Log.wtf(TAG, "active automatic rule found with no specified policy: " + rule);
- policy.apply(Flags.modesUi()
- ? mDefaultConfig.getZenPolicy() : config.getZenPolicy());
- }
- } else {
- // active rule with no specified policy inherits the manual rule config settings
+ if (useManualConfig) {
+ // manual rule is configured using the settings stored directly in ZenModeConfig
policy.apply(config.getZenPolicy());
+ } else {
+ // An active automatic rule with no specified policy inherits the device default
+ // settings as stored in mDefaultConfig. While the rule's policy fields should be
+ // set upon creation, this is a fallback to catch any that may have fallen through
+ // the cracks.
+ Log.wtf(TAG, "active automatic rule found with no specified policy: " + rule);
+ policy.apply(Flags.modesUi()
+ ? mDefaultConfig.getZenPolicy() : config.getZenPolicy());
}
}
}
@@ -2346,9 +2276,7 @@ public class ZenModeHelper {
ZenDeviceEffects.Builder deviceEffectsBuilder = new ZenDeviceEffects.Builder();
if (mConfig.isManualActive()) {
applyCustomPolicy(mConfig, policy, mConfig.manualRule, true);
- if (Flags.modesApi()) {
- deviceEffectsBuilder.add(mConfig.manualRule.zenDeviceEffects);
- }
+ deviceEffectsBuilder.add(mConfig.manualRule.zenDeviceEffects);
}
for (ZenRule automaticRule : mConfig.automaticRules.values()) {
@@ -2356,12 +2284,10 @@ public class ZenModeHelper {
// Active rules with INTERRUPTION_FILTER_ALL are not included in consolidated
// policy. This is relevant in case some other active rule has a more
// restrictive INTERRUPTION_FILTER but a more lenient ZenPolicy!
- if (!Flags.modesApi() || automaticRule.zenMode != Global.ZEN_MODE_OFF) {
+ if (automaticRule.zenMode != Global.ZEN_MODE_OFF) {
applyCustomPolicy(mConfig, policy, automaticRule, false);
}
- if (Flags.modesApi()) {
- deviceEffectsBuilder.add(automaticRule.zenDeviceEffects);
- }
+ deviceEffectsBuilder.add(automaticRule.zenDeviceEffects);
}
}
@@ -2380,40 +2306,35 @@ public class ZenModeHelper {
ZenLog.traceSetConsolidatedZenPolicy(mConsolidatedPolicy, reason);
}
- if (Flags.modesApi()) {
- // Prevent other rules from applying grayscale if Driving is active (but allow it
- // if _Driving itself_ wants grayscale).
- if (Flags.modesUi() && preventZenDeviceEffectsWhileDriving()) {
- boolean hasActiveDriving = false;
- boolean hasActiveDrivingWithGrayscale = false;
- for (ZenRule rule : mConfig.automaticRules.values()) {
- if (rule.isActive() && rule.type == TYPE_DRIVING) {
- hasActiveDriving = true;
- if (rule.zenDeviceEffects != null
- && rule.zenDeviceEffects.shouldDisplayGrayscale()) {
- hasActiveDrivingWithGrayscale = true;
- break; // Further rules won't affect decision.
- }
+ // Prevent other rules from applying grayscale if Driving is active (but allow it
+ // if _Driving itself_ wants grayscale).
+ if (Flags.modesUi() && preventZenDeviceEffectsWhileDriving()) {
+ boolean hasActiveDriving = false;
+ boolean hasActiveDrivingWithGrayscale = false;
+ for (ZenRule rule : mConfig.automaticRules.values()) {
+ if (rule.isActive() && rule.type == TYPE_DRIVING) {
+ hasActiveDriving = true;
+ if (rule.zenDeviceEffects != null
+ && rule.zenDeviceEffects.shouldDisplayGrayscale()) {
+ hasActiveDrivingWithGrayscale = true;
+ break; // Further rules won't affect decision.
}
}
- if (hasActiveDriving && !hasActiveDrivingWithGrayscale) {
- deviceEffectsBuilder.setShouldDisplayGrayscale(false);
- }
}
-
- ZenDeviceEffects deviceEffects = deviceEffectsBuilder.build();
- if (!deviceEffects.equals(mConsolidatedDeviceEffects)) {
- mConsolidatedDeviceEffects = deviceEffects;
- mHandler.postApplyDeviceEffects(origin);
+ if (hasActiveDriving && !hasActiveDrivingWithGrayscale) {
+ deviceEffectsBuilder.setShouldDisplayGrayscale(false);
}
}
+
+ ZenDeviceEffects deviceEffects = deviceEffectsBuilder.build();
+ if (!deviceEffects.equals(mConsolidatedDeviceEffects)) {
+ mConsolidatedDeviceEffects = deviceEffects;
+ mHandler.postApplyDeviceEffects(origin);
+ }
}
}
private void applyConsolidatedDeviceEffects(@ConfigOrigin int source) {
- if (!Flags.modesApi()) {
- return;
- }
DeviceEffectsApplier applier;
ZenDeviceEffects effects;
synchronized (mConfigLock) {
@@ -2434,10 +2355,8 @@ public class ZenModeHelper {
* to the current locale.
*/
private static void updateDefaultConfig(Context context, ZenModeConfig defaultConfig) {
- if (Flags.modesApi()) {
- updateDefaultAutomaticRulePolicies(defaultConfig);
- }
- if (Flags.modesApi() && Flags.modesUi()) {
+ updateDefaultAutomaticRulePolicies(defaultConfig);
+ if (Flags.modesUi()) {
SystemZenRules.maybeUpgradeRules(context, defaultConfig);
}
updateRuleStringsForCurrentLocale(context, defaultConfig);
@@ -2453,7 +2372,7 @@ public class ZenModeHelper {
rule.name = context.getResources()
.getString(R.string.zen_mode_default_every_night_name);
}
- if (Flags.modesApi() && Flags.modesUi()) {
+ if (Flags.modesUi()) {
SystemZenRules.updateTriggerDescription(context, rule);
}
}
@@ -2462,10 +2381,6 @@ public class ZenModeHelper {
// Updates the policies in the default automatic rules (provided via default XML config) to
// be fully filled in default values.
private static void updateDefaultAutomaticRulePolicies(ZenModeConfig defaultConfig) {
- if (!Flags.modesApi()) {
- // Should be checked before calling, but just in case.
- return;
- }
ZenPolicy defaultPolicy = defaultConfig.getZenPolicy();
for (ZenRule rule : defaultConfig.automaticRules.values()) {
if (ZenModeConfig.getDefaultRuleIds().contains(rule.id) && rule.zenPolicy == null) {
@@ -2611,6 +2526,7 @@ public class ZenModeHelper {
}
}
+ // TODO: b/368247671 - Delete this method AND default_zen_mode_config.xml when inlining modes_ui
private ZenModeConfig readDefaultConfig(Resources resources) {
XmlResourceParser parser = null;
try {
@@ -2649,7 +2565,7 @@ public class ZenModeHelper {
events.add(FrameworkStatsLog.buildStatsEvent(DND_MODE_RULE,
/* optional int32 user = 1 */ user,
/* optional bool enabled = 2 */ config.isManualActive(),
- /* optional bool channels_bypassing = 3 */ config.areChannelsBypassingDnd,
+ /* optional bool channels_bypassing = 3 */ config.hasPriorityChannels,
/* optional LoggedZenMode zen_mode = 4 */ ROOT_CONFIG,
/* optional string id = 5 */ "", // empty for root config
/* optional int32 uid = 6 */ Process.SYSTEM_UID, // system owns root config
@@ -2924,9 +2840,6 @@ public class ZenModeHelper {
* ({@link #addAutomaticZenRule}, {@link #removeAutomaticZenRule}, etc, makes sense.
*/
private static void checkManageRuleOrigin(String method, @ConfigOrigin int origin) {
- if (!Flags.modesApi()) {
- return;
- }
checkArgument(origin == ORIGIN_APP || origin == ORIGIN_SYSTEM
|| origin == ORIGIN_USER_IN_SYSTEMUI,
"Expected one of ORIGIN_APP, ORIGIN_SYSTEM, or "
@@ -2939,9 +2852,6 @@ public class ZenModeHelper {
* {@link #setAutomaticZenRuleStateFromConditionProvider} makes sense.
*/
private static void checkSetRuleStateOrigin(String method, @ConfigOrigin int origin) {
- if (!Flags.modesApi()) {
- return;
- }
checkArgument(origin == ORIGIN_APP || origin == ORIGIN_USER_IN_APP
|| origin == ORIGIN_SYSTEM || origin == ORIGIN_USER_IN_SYSTEMUI,
"Expected one of ORIGIN_APP, ORIGIN_USER_IN_APP, ORIGIN_SYSTEM, or "
diff --git a/services/core/java/com/android/server/notification/flags.aconfig b/services/core/java/com/android/server/notification/flags.aconfig
index 048f2b6b0cbc..76cd5c88b388 100644
--- a/services/core/java/com/android/server/notification/flags.aconfig
+++ b/services/core/java/com/android/server/notification/flags.aconfig
@@ -210,3 +210,10 @@ flag {
purpose: PURPOSE_BUGFIX
}
}
+
+flag {
+ name: "managed_services_concurrent_multiuser"
+ namespace: "systemui"
+ description: "Enables ManagedServices to support Concurrent multi user environment"
+ bug: "380297485"
+}
diff --git a/services/core/java/com/android/server/pm/BackgroundInstallControlService.java b/services/core/java/com/android/server/pm/BackgroundInstallControlService.java
index d538bb876b64..c3af578de369 100644
--- a/services/core/java/com/android/server/pm/BackgroundInstallControlService.java
+++ b/services/core/java/com/android/server/pm/BackgroundInstallControlService.java
@@ -176,16 +176,13 @@ public class BackgroundInstallControlService extends SystemService {
if (Flags.bicClient()) {
mService.enforceCallerPermissions();
}
- if (!Build.IS_DEBUGGABLE) {
- return mService.getBackgroundInstalledPackages(flags, userId);
- }
// The debug.transparency.bg-install-apps (only works for debuggable builds)
// is used to set mock list of background installed apps for testing.
// The list of apps' names is delimited by ",".
// TODO: Remove after migrating test to new background install method using
// {@link BackgroundInstallControlCallbackHelperTest}.installPackage b/310983905
String propertyString = SystemProperties.get("debug.transparency.bg-install-apps");
- if (TextUtils.isEmpty(propertyString)) {
+ if (TextUtils.isEmpty(propertyString) || !Build.IS_DEBUGGABLE) {
return mService.getBackgroundInstalledPackages(flags, userId);
} else {
return mService.getMockBackgroundInstalledPackages(propertyString);
@@ -219,10 +216,27 @@ public class BackgroundInstallControlService extends SystemService {
PackageManager.PackageInfoFlags.of(flags), userId);
initBackgroundInstalledPackages();
+ if(Build.IS_DEBUGGABLE) {
+ StringBuilder sb = new StringBuilder();
+ sb.append("Tracked background installed package size: ")
+ .append(mBackgroundInstalledPackages.size())
+ .append("\n");
+ for (int i = 0; i < mBackgroundInstalledPackages.size(); ++i) {
+ int installingUserId = mBackgroundInstalledPackages.keyAt(i);
+ mBackgroundInstalledPackages.get(installingUserId).forEach(pkgName ->
+ sb.append("userId: ").append(installingUserId)
+ .append(", name: ").append(pkgName).append("\n"));
+ }
+ Slog.d(TAG, "Tracked background installed package: " + sb.toString());
+ }
+
ListIterator<PackageInfo> iter = packages.listIterator();
while (iter.hasNext()) {
String packageName = iter.next().packageName;
if (!mBackgroundInstalledPackages.contains(userId, packageName)) {
+ if(Build.IS_DEBUGGABLE) {
+ Slog.d(TAG, packageName + " is not tracked, removing");
+ }
iter.remove();
}
}
@@ -284,6 +298,9 @@ public class BackgroundInstallControlService extends SystemService {
}
void handlePackageAdd(String packageName, int userId) {
+ if(Build.IS_DEBUGGABLE) {
+ Slog.d(TAG, "handlePackageAdd: checking " + packageName);
+ }
ApplicationInfo appInfo = null;
try {
appInfo =
@@ -302,7 +319,7 @@ public class BackgroundInstallControlService extends SystemService {
installerPackageName = installInfo.getInstallingPackageName();
initiatingPackageName = installInfo.getInitiatingPackageName();
} catch (PackageManager.NameNotFoundException e) {
- Slog.w(TAG, "Package's installer not found " + packageName);
+ Slog.w(TAG, "Package's installer not found: " + packageName);
return;
}
@@ -314,6 +331,10 @@ public class BackgroundInstallControlService extends SystemService {
VirtualDeviceManager.PERSISTENT_DEVICE_ID_DEFAULT,
userId)
!= PERMISSION_GRANTED) {
+ if(Build.IS_DEBUGGABLE) {
+ Slog.d(TAG, "handlePackageAdd " + packageName + ": installer doesn't "
+ + "have INSTALL_PACKAGES permission, skipping");
+ }
return;
}
@@ -324,6 +345,10 @@ public class BackgroundInstallControlService extends SystemService {
if (installedByAdb(initiatingPackageName)
|| wasForegroundInstallation(installerPackageName, userId, installTimestamp)) {
+ if(Build.IS_DEBUGGABLE) {
+ Slog.d(TAG, "handlePackageAdd " + packageName + ": is installed by ADB or was "
+ + "foreground installation, skipping");
+ }
return;
}
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index 4153cd1be0a6..76c5240ab623 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -1163,15 +1163,6 @@ public class PhoneWindowManager implements WindowManagerPolicy {
}
}
- private boolean shouldShowHub() {
- final boolean hubEnabled = Settings.Secure.getIntForUser(
- mContext.getContentResolver(), Settings.Secure.GLANCEABLE_HUB_ENABLED,
- 1, mCurrentUserId) == 1;
-
- return mUserManagerInternal.isUserUnlocked(mCurrentUserId) && hubEnabled
- && mDreamManagerInternal.dreamConditionActive();
- }
-
@VisibleForTesting
void powerPress(long eventTime, int count, int displayId) {
// SideFPS still needs to know about suppressed power buttons, in case it needs to block
@@ -1270,10 +1261,9 @@ public class PhoneWindowManager implements WindowManagerPolicy {
// show hub.
boolean keyguardAvailable = !mLockPatternUtils.isLockScreenDisabled(
mCurrentUserId);
- if (shouldShowHub() && keyguardAvailable) {
- // If the hub can be launched, send a message to keyguard. We do not know if
- // the hub is already running or not, keyguard handles turning screen off if
- // it is.
+ if (mUserManagerInternal.isUserUnlocked(mCurrentUserId) && hubEnabled
+ && keyguardAvailable && mDreamManagerInternal.dreamConditionActive()) {
+ // If the hub can be launched, send a message to keyguard.
Bundle options = new Bundle();
options.putBoolean(EXTRA_TRIGGER_HUB, true);
lockNow(options);
@@ -1334,14 +1324,14 @@ public class PhoneWindowManager implements WindowManagerPolicy {
* @param isScreenOn Whether the screen is currently on.
* @param noDreamAction The action to perform if dreaming is not possible.
*/
- private boolean attemptToDreamFromShortPowerButtonPress(
+ private void attemptToDreamFromShortPowerButtonPress(
boolean isScreenOn, Runnable noDreamAction) {
if (mShortPressOnPowerBehavior != SHORT_PRESS_POWER_DREAM_OR_SLEEP
&& mShortPressOnPowerBehavior != SHORT_PRESS_POWER_HUB_OR_DREAM_OR_SLEEP) {
// If the power button behavior isn't one that should be able to trigger the dream, give
// up.
noDreamAction.run();
- return false;
+ return;
}
final DreamManagerInternal dreamManagerInternal = getDreamManagerInternal();
@@ -1349,7 +1339,7 @@ public class PhoneWindowManager implements WindowManagerPolicy {
Slog.d(TAG, "Can't start dreaming when attempting to dream from short power"
+ " press (isScreenOn=" + isScreenOn + ")");
noDreamAction.run();
- return false;
+ return;
}
synchronized (mLock) {
@@ -1360,8 +1350,6 @@ public class PhoneWindowManager implements WindowManagerPolicy {
}
dreamManagerInternal.requestDream();
-
- return true;
}
/**
@@ -6410,17 +6398,6 @@ public class PhoneWindowManager implements WindowManagerPolicy {
event.getDisplayId(), event.getKeyCode(), "wakeUpFromWakeKey")) {
return;
}
-
- if (!shouldShowHub()
- && mShortPressOnPowerBehavior == SHORT_PRESS_POWER_HUB_OR_DREAM_OR_SLEEP
- && event.getKeyCode() == KEYCODE_POWER
- && attemptToDreamFromShortPowerButtonPress(false, () -> {})) {
- // In the case that we should wake to dream and successfully initiate dreaming, do not
- // continue waking up. Doing so will exit the dream state and cause UI to react
- // accordingly.
- return;
- }
-
wakeUpFromWakeKey(
event.getEventTime(),
event.getKeyCode(),
diff --git a/services/core/java/com/android/server/power/PowerManagerService.java b/services/core/java/com/android/server/power/PowerManagerService.java
index 8fae875eb29b..e3eced252d1f 100644
--- a/services/core/java/com/android/server/power/PowerManagerService.java
+++ b/services/core/java/com/android/server/power/PowerManagerService.java
@@ -3379,7 +3379,7 @@ public final class PowerManagerService extends SystemService
}
changed = sleepPowerGroupLocked(powerGroup, time,
PowerManager.GO_TO_SLEEP_REASON_INATTENTIVE, Process.SYSTEM_UID);
- } else if (shouldNapAtBedTimeLocked()) {
+ } else if (shouldNapAtBedTimeLocked(powerGroup)) {
changed = dreamPowerGroupLocked(powerGroup, time,
Process.SYSTEM_UID, /* allowWake= */ false);
} else {
@@ -3395,7 +3395,10 @@ public final class PowerManagerService extends SystemService
* activity timeout has expired and it's bedtime.
*/
@GuardedBy("mLock")
- private boolean shouldNapAtBedTimeLocked() {
+ private boolean shouldNapAtBedTimeLocked(PowerGroup powerGroup) {
+ if (!powerGroup.supportsSandmanLocked()) {
+ return false;
+ }
return mDreamsActivateOnSleepSetting
|| (mDreamsActivateOnDockSetting
&& mDockState != Intent.EXTRA_DOCK_STATE_UNDOCKED)
@@ -3617,9 +3620,10 @@ public final class PowerManagerService extends SystemService
if (!mDreamsDisabledByAmbientModeSuppressionConfig) {
return;
}
+ final PowerGroup defaultPowerGroup = mPowerGroups.get(Display.DEFAULT_DISPLAY_GROUP);
if (!isSuppressed && mIsPowered && mDreamsSupportedConfig && mDreamsEnabledSetting
- && shouldNapAtBedTimeLocked() && isItBedTimeYetLocked(
- mPowerGroups.get(Display.DEFAULT_DISPLAY_GROUP))) {
+ && shouldNapAtBedTimeLocked(defaultPowerGroup)
+ && isItBedTimeYetLocked(defaultPowerGroup)) {
napInternal(SystemClock.uptimeMillis(), Process.SYSTEM_UID, /* allowWake= */ true);
} else if (isSuppressed) {
mDirty |= DIRTY_SETTINGS;
diff --git a/services/core/java/com/android/server/resources/ResourcesManagerShellCommand.java b/services/core/java/com/android/server/resources/ResourcesManagerShellCommand.java
index a75d110e3cd1..17739712d65a 100644
--- a/services/core/java/com/android/server/resources/ResourcesManagerShellCommand.java
+++ b/services/core/java/com/android/server/resources/ResourcesManagerShellCommand.java
@@ -88,6 +88,5 @@ 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/security/AttestationVerificationPeerDeviceVerifier.java b/services/core/java/com/android/server/security/AttestationVerificationPeerDeviceVerifier.java
index f060e4d11e82..82df310db9a4 100644
--- a/services/core/java/com/android/server/security/AttestationVerificationPeerDeviceVerifier.java
+++ b/services/core/java/com/android/server/security/AttestationVerificationPeerDeviceVerifier.java
@@ -303,7 +303,11 @@ class AttestationVerificationPeerDeviceVerifier {
if (mRevocationEnabled) {
// Checks Revocation Status List based on
// https://developer.android.com/training/articles/security-key-attestation#certificate_status
- mCertificateRevocationStatusManager.checkRevocationStatus(certificates);
+ // The first certificate is the leaf, which is generated at runtime with the attestation
+ // attributes such as the challenge. It is specific to this attestation instance and
+ // does not need to be checked for revocation.
+ mCertificateRevocationStatusManager.checkRevocationStatus(
+ new ArrayList<>(certificates.subList(1, certificates.size())));
}
}
diff --git a/services/core/java/com/android/server/security/CertificateRevocationStatusManager.java b/services/core/java/com/android/server/security/CertificateRevocationStatusManager.java
index d36d9f5f6636..4cd4b3b84910 100644
--- a/services/core/java/com/android/server/security/CertificateRevocationStatusManager.java
+++ b/services/core/java/com/android/server/security/CertificateRevocationStatusManager.java
@@ -42,6 +42,7 @@ import java.net.MalformedURLException;
import java.net.URL;
import java.security.cert.CertPathValidatorException;
import java.security.cert.X509Certificate;
+import java.time.Duration;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneId;
@@ -67,6 +68,8 @@ class CertificateRevocationStatusManager {
*/
@VisibleForTesting static final int MAX_DAYS_SINCE_LAST_CHECK = 30;
+ @VisibleForTesting static final int NUM_HOURS_BEFORE_NEXT_CHECK = 24;
+
/**
* The number of days since issue date for an intermediary certificate to be considered fresh
* and not require a revocation list check.
@@ -127,6 +130,17 @@ class CertificateRevocationStatusManager {
serialNumbers.add(serialNumber);
}
try {
+ if (isLastCheckedWithin(Duration.ofHours(NUM_HOURS_BEFORE_NEXT_CHECK), serialNumbers)) {
+ Slog.d(
+ TAG,
+ "All certificates have been checked for revocation recently. No need to"
+ + " check this time.");
+ return;
+ }
+ } catch (IOException ignored) {
+ // Proceed to check the revocation status
+ }
+ try {
JSONObject revocationList = fetchRemoteRevocationList();
Map<String, Boolean> areCertificatesRevoked = new HashMap<>();
for (String serialNumber : serialNumbers) {
@@ -151,25 +165,32 @@ class CertificateRevocationStatusManager {
serialNumbers.remove(serialNumber);
}
}
- Map<String, LocalDateTime> lastRevocationCheckData;
try {
- lastRevocationCheckData = getLastRevocationCheckData();
+ if (!isLastCheckedWithin(
+ Duration.ofDays(MAX_DAYS_SINCE_LAST_CHECK), serialNumbers)) {
+ throw new CertPathValidatorException(
+ "Unable to verify the revocation status of one of the certificates "
+ + serialNumbers);
+ }
} catch (IOException ex2) {
throw new CertPathValidatorException(
"Unable to load stored revocation status", ex2);
}
- for (String serialNumber : serialNumbers) {
- if (!lastRevocationCheckData.containsKey(serialNumber)
- || lastRevocationCheckData
- .get(serialNumber)
- .isBefore(
- LocalDateTime.now().minusDays(MAX_DAYS_SINCE_LAST_CHECK))) {
- throw new CertPathValidatorException(
- "Unable to verify the revocation status of certificate "
- + serialNumber);
- }
+ }
+ }
+
+ private boolean isLastCheckedWithin(Duration lastCheckedWithin, List<String> serialNumbers)
+ throws IOException {
+ Map<String, LocalDateTime> lastRevocationCheckData = getLastRevocationCheckData();
+ for (String serialNumber : serialNumbers) {
+ if (!lastRevocationCheckData.containsKey(serialNumber)
+ || lastRevocationCheckData
+ .get(serialNumber)
+ .isBefore(LocalDateTime.now().minus(lastCheckedWithin))) {
+ return false;
}
}
+ return true;
}
private static boolean needToCheckRevocationStatus(
diff --git a/services/core/java/com/android/server/security/FileIntegrityService.java b/services/core/java/com/android/server/security/FileIntegrityService.java
index bfd86d724583..9f9a9807d973 100644
--- a/services/core/java/com/android/server/security/FileIntegrityService.java
+++ b/services/core/java/com/android/server/security/FileIntegrityService.java
@@ -54,11 +54,6 @@ public class FileIntegrityService extends SystemService {
super(PermissionEnforcer.fromContext(context));
}
- @Override
- public boolean isApkVeritySupported() {
- return VerityUtils.isFsVeritySupported();
- }
-
private void checkCallerPackageName(String packageName) {
final int callingUid = Binder.getCallingUid();
final int callingUserId = UserHandle.getUserId(callingUid);
diff --git a/services/core/java/com/android/server/security/intrusiondetection/DataAggregator.java b/services/core/java/com/android/server/security/intrusiondetection/DataAggregator.java
index 687442b47fb3..cdeacaa2e43a 100644
--- a/services/core/java/com/android/server/security/intrusiondetection/DataAggregator.java
+++ b/services/core/java/com/android/server/security/intrusiondetection/DataAggregator.java
@@ -62,7 +62,7 @@ public class DataAggregator {
/** Initialize DataSources */
private void initialize() {
mDataSources.add(new SecurityLogSource(mContext, this));
- mDataSources.add(new NetworkLogSource(mContext, this));
+ mDataSources.add(new NetworkLogSource(this));
}
/**
diff --git a/services/core/java/com/android/server/security/intrusiondetection/NetworkLogSource.java b/services/core/java/com/android/server/security/intrusiondetection/NetworkLogSource.java
index f303a588d30c..fe0cf80a48f2 100644
--- a/services/core/java/com/android/server/security/intrusiondetection/NetworkLogSource.java
+++ b/services/core/java/com/android/server/security/intrusiondetection/NetworkLogSource.java
@@ -18,7 +18,6 @@ package com.android.server.security.intrusiondetection;
import android.app.admin.ConnectEvent;
import android.app.admin.DnsEvent;
-import android.content.Context;
import android.content.pm.PackageManagerInternal;
import android.net.IIpConnectivityMetrics;
import android.net.INetdEventCallback;
@@ -44,8 +43,7 @@ public class NetworkLogSource implements DataSource {
private IIpConnectivityMetrics mIpConnectivityMetrics;
private long mId;
- public NetworkLogSource(Context context, DataAggregator dataAggregator)
- throws SecurityException {
+ public NetworkLogSource(DataAggregator dataAggregator) throws SecurityException {
mDataAggregator = dataAggregator;
mPm = LocalServices.getService(PackageManagerInternal.class);
mId = 0;
diff --git a/services/core/java/com/android/server/security/intrusiondetection/SecurityLogSource.java b/services/core/java/com/android/server/security/intrusiondetection/SecurityLogSource.java
index 142094c9d9f4..7501799198e8 100644
--- a/services/core/java/com/android/server/security/intrusiondetection/SecurityLogSource.java
+++ b/services/core/java/com/android/server/security/intrusiondetection/SecurityLogSource.java
@@ -19,14 +19,15 @@ package com.android.server.security.intrusiondetection;
import android.Manifest.permission;
import android.annotation.RequiresPermission;
import android.app.admin.DevicePolicyManager;
+import android.app.admin.DevicePolicyManagerInternal;
import android.app.admin.SecurityLog.SecurityEvent;
import android.content.Context;
import android.security.intrusiondetection.IntrusionDetectionEvent;
import android.util.Slog;
+import com.android.server.LocalServices;
+
import java.util.List;
-import java.util.concurrent.Executor;
-import java.util.concurrent.Executors;
import java.util.function.Consumer;
import java.util.stream.Collectors;
@@ -36,13 +37,13 @@ public class SecurityLogSource implements DataSource {
private SecurityEventCallback mEventCallback;
private DevicePolicyManager mDpm;
- private Executor mExecutor;
+ private DevicePolicyManagerInternal mDpmInternal;
private DataAggregator mDataAggregator;
public SecurityLogSource(Context context, DataAggregator dataAggregator) {
mDataAggregator = dataAggregator;
mDpm = context.getSystemService(DevicePolicyManager.class);
- mExecutor = Executors.newSingleThreadExecutor();
+ mDpmInternal = LocalServices.getService(DevicePolicyManagerInternal.class);
mEventCallback = new SecurityEventCallback();
}
@@ -50,12 +51,13 @@ public class SecurityLogSource implements DataSource {
@RequiresPermission(permission.MANAGE_DEVICE_POLICY_AUDIT_LOGGING)
public void enable() {
enableAuditLog();
- mDpm.setAuditLogEventCallback(mExecutor, mEventCallback);
+ mDpmInternal.setInternalEventsCallback(mEventCallback);
}
@Override
@RequiresPermission(permission.MANAGE_DEVICE_POLICY_AUDIT_LOGGING)
public void disable() {
+ mDpmInternal.setInternalEventsCallback(null);
disableAuditLog();
}
@@ -82,10 +84,11 @@ public class SecurityLogSource implements DataSource {
@Override
public void accept(List<SecurityEvent> events) {
- if (events.size() == 0) {
+ if (events == null || events.size() == 0) {
Slog.w(TAG, "No events received; caller may not be authorized");
return;
}
+
List<IntrusionDetectionEvent> intrusionDetectionEvents =
events.stream()
.filter(event -> event != null)
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index 03fe7775edb0..c37b5a055140 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -8051,6 +8051,7 @@ final class ActivityRecord extends WindowToken {
mConfigurationSeq = Math.max(++mConfigurationSeq, 1);
getResolvedOverrideConfiguration().seq = mConfigurationSeq;
+ // TODO(b/392069771): Move to AppCompatSandboxingPolicy.
// Sandbox max bounds by setting it to the activity bounds, if activity is letterboxed, or
// has or will have mAppCompatDisplayInsets for size compat. Also forces an activity to be
// sandboxed or not depending upon the configuration settings.
@@ -8079,6 +8080,9 @@ final class ActivityRecord extends WindowToken {
resolvedConfig.windowConfiguration.setMaxBounds(mTmpBounds);
}
+ mAppCompatController.getSandboxingPolicy().sandboxBoundsIfNeeded(resolvedConfig,
+ parentWindowingMode);
+
applySizeOverrideIfNeeded(
mDisplayContent,
info.applicationInfo,
diff --git a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
index 6a5adca91e39..b607b0fce9ab 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
@@ -145,6 +145,7 @@ import android.util.SparseIntArray;
import android.view.Display;
import android.webkit.URLUtil;
import android.window.ActivityWindowInfo;
+import android.window.DesktopExperienceFlags;
import android.window.DesktopModeFlags;
import com.android.internal.R;
@@ -2916,6 +2917,8 @@ public class ActivityTaskSupervisor implements RecentTasks.Callbacks {
/** The helper to calculate whether a container is opaque. */
static class OpaqueContainerHelper implements Predicate<ActivityRecord> {
+ private final boolean mEnableMultipleDesktopsBackend =
+ DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue();
private ActivityRecord mStarting;
private boolean mIgnoringInvisibleActivity;
private boolean mIgnoringKeyguard;
@@ -2938,7 +2941,7 @@ public class ActivityTaskSupervisor implements RecentTasks.Callbacks {
mIgnoringKeyguard = ignoringKeyguard;
final boolean isOpaque;
- if (!Flags.enableMultipleDesktopsBackend()) {
+ if (!mEnableMultipleDesktopsBackend) {
isOpaque = container.getActivity(this,
true /* traverseTopToBottom */, null /* boundary */) != null;
} else {
@@ -2949,13 +2952,16 @@ public class ActivityTaskSupervisor implements RecentTasks.Callbacks {
}
private boolean isOpaqueInner(@NonNull WindowContainer<?> container) {
- // If it's a leaf task fragment, then opacity is calculated based on its activities.
- if (container.asTaskFragment() != null
- && ((TaskFragment) container).isLeafTaskFragment()) {
+ final boolean isActivity = container.asActivityRecord() != null;
+ final boolean isLeafTaskFragment = container.asTaskFragment() != null
+ && ((TaskFragment) container).isLeafTaskFragment();
+ if (isActivity || isLeafTaskFragment) {
+ // When it is an activity or leaf task fragment, then opacity is calculated based
+ // on itself or its activities.
return container.getActivity(this,
true /* traverseTopToBottom */, null /* boundary */) != null;
}
- // When not a leaf, it's considered opaque if any of its opaque children fill this
+ // Otherwise, it's considered opaque if any of its opaque children fill this
// container, unless the children are adjacent fragments, in which case as long as they
// are all opaque then |container| is also considered opaque, even if the adjacent
// task fragment aren't filling.
diff --git a/services/core/java/com/android/server/wm/AppCompatController.java b/services/core/java/com/android/server/wm/AppCompatController.java
index bed95face1c9..fc504796b0ac 100644
--- a/services/core/java/com/android/server/wm/AppCompatController.java
+++ b/services/core/java/com/android/server/wm/AppCompatController.java
@@ -44,6 +44,8 @@ class AppCompatController {
private final AppCompatLetterboxPolicy mLetterboxPolicy;
@NonNull
private final AppCompatSizeCompatModePolicy mSizeCompatModePolicy;
+ @NonNull
+ private final AppCompatSandboxingPolicy mSandboxingPolicy;
AppCompatController(@NonNull WindowManagerService wmService,
@NonNull ActivityRecord activityRecord) {
@@ -66,6 +68,7 @@ class AppCompatController {
mAppCompatOverrides, mTransparentPolicy, wmService.mAppCompatConfiguration);
mSizeCompatModePolicy = new AppCompatSizeCompatModePolicy(activityRecord,
mAppCompatOverrides);
+ mSandboxingPolicy = new AppCompatSandboxingPolicy(activityRecord);
}
@NonNull
@@ -143,6 +146,11 @@ class AppCompatController {
return mSizeCompatModePolicy;
}
+ @NonNull
+ AppCompatSandboxingPolicy getSandboxingPolicy() {
+ return mSandboxingPolicy;
+ }
+
void dump(@NonNull PrintWriter pw, @NonNull String prefix) {
getTransparentPolicy().dump(pw, prefix);
getLetterboxPolicy().dump(pw, prefix);
diff --git a/services/core/java/com/android/server/wm/AppCompatSandboxingPolicy.java b/services/core/java/com/android/server/wm/AppCompatSandboxingPolicy.java
new file mode 100644
index 000000000000..26cf32b12d4f
--- /dev/null
+++ b/services/core/java/com/android/server/wm/AppCompatSandboxingPolicy.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.server.wm;
+
+import static com.android.server.wm.AppCompatUtils.isInDesktopMode;
+
+import android.annotation.NonNull;
+import android.app.WindowConfiguration.WindowingMode;
+import android.content.res.Configuration;
+import android.graphics.Rect;
+
+import com.android.window.flags.Flags;
+
+/**
+ * Encapsulate logic related to sandboxing for app compatibility.
+ */
+class AppCompatSandboxingPolicy {
+
+ @NonNull
+ private final ActivityRecord mActivityRecord;
+
+ AppCompatSandboxingPolicy(@NonNull ActivityRecord activityRecord) {
+ mActivityRecord = activityRecord;
+ }
+
+ /**
+ * In freeform, the container bounds are scaled with app bounds. Activity bounds can be
+ * outside of its container bounds if insets are coupled with configuration outside of
+ * freeform and maintained in freeform for size compat mode.
+ *
+ * <p>Sandbox activity bounds in freeform to app bounds to force app to display within the
+ * container. This prevents UI cropping when activities can draw below insets which are
+ * normally excluded from appBounds before targetSDK < 35
+ * (see ConfigurationContainer#applySizeOverrideIfNeeded).
+ */
+ void sandboxBoundsIfNeeded(@NonNull Configuration resolvedConfig,
+ @WindowingMode int windowingMode) {
+ if (!Flags.excludeCaptionFromAppBounds()) {
+ return;
+ }
+
+ if (isInDesktopMode(mActivityRecord.mAtmService.mContext, windowingMode)) {
+ Rect appBounds = resolvedConfig.windowConfiguration.getAppBounds();
+ if (appBounds == null || appBounds.isEmpty()) {
+ // When there is no override bounds, the activity will inherit the bounds from
+ // parent.
+ appBounds = mActivityRecord.mResolveConfigHint.mParentAppBoundsOverride;
+ }
+ resolvedConfig.windowConfiguration.setBounds(appBounds);
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/wm/AppCompatSizeCompatModePolicy.java b/services/core/java/com/android/server/wm/AppCompatSizeCompatModePolicy.java
index bbc33004ee54..2cfa242bc5fe 100644
--- a/services/core/java/com/android/server/wm/AppCompatSizeCompatModePolicy.java
+++ b/services/core/java/com/android/server/wm/AppCompatSizeCompatModePolicy.java
@@ -17,14 +17,13 @@
package com.android.server.wm;
import static android.app.WindowConfiguration.ROTATION_UNDEFINED;
-import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
import static android.content.pm.ActivityInfo.SIZE_CHANGES_SUPPORTED_METADATA;
import static android.content.pm.ActivityInfo.SIZE_CHANGES_SUPPORTED_OVERRIDE;
import static android.content.pm.ActivityInfo.SIZE_CHANGES_UNSUPPORTED_METADATA;
import static android.content.pm.ActivityInfo.SIZE_CHANGES_UNSUPPORTED_OVERRIDE;
import static android.content.res.Configuration.ORIENTATION_UNDEFINED;
-import static com.android.server.wm.DesktopModeHelper.canEnterDesktopMode;
+import static com.android.server.wm.AppCompatUtils.isInDesktopMode;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -545,9 +544,8 @@ class AppCompatSizeCompatModePolicy {
// Allow an application to be up-scaled if its window is smaller than its
// original container or if it's a freeform window in desktop mode.
boolean shouldAllowUpscaling = !(contentW <= viewportW && contentH <= viewportH)
- || (canEnterDesktopMode(mActivityRecord.mAtmService.mContext)
- && newParentConfig.windowConfiguration.getWindowingMode()
- == WINDOWING_MODE_FREEFORM);
+ || isInDesktopMode(mActivityRecord.mAtmService.mContext,
+ newParentConfig.windowConfiguration.getWindowingMode());
return shouldAllowUpscaling ? Math.min(
(float) viewportW / contentW, (float) viewportH / contentH) : 1f;
}
diff --git a/services/core/java/com/android/server/wm/AppCompatUtils.java b/services/core/java/com/android/server/wm/AppCompatUtils.java
index 3e054fc40540..146044008b3f 100644
--- a/services/core/java/com/android/server/wm/AppCompatUtils.java
+++ b/services/core/java/com/android/server/wm/AppCompatUtils.java
@@ -16,16 +16,20 @@
package com.android.server.wm;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
import static android.content.res.Configuration.UI_MODE_TYPE_MASK;
import static android.content.res.Configuration.UI_MODE_TYPE_VR_HEADSET;
import static com.android.server.wm.ActivityRecord.State.RESUMED;
+import static com.android.server.wm.DesktopModeHelper.canEnterDesktopMode;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.AppCompatTaskInfo;
import android.app.CameraCompatTaskInfo;
import android.app.TaskInfo;
+import android.app.WindowConfiguration.WindowingMode;
+import android.content.Context;
import android.content.res.Configuration;
import android.graphics.Rect;
import android.view.InsetsSource;
@@ -276,6 +280,14 @@ final class AppCompatUtils {
inOutConfig.windowConfiguration.getAppBounds().offset(offsetX, offsetY);
}
+ /**
+ * Return {@code true} if window is currently in desktop mode.
+ */
+ static boolean isInDesktopMode(@NonNull Context context,
+ @WindowingMode int parentWindowingMode) {
+ return parentWindowingMode == WINDOWING_MODE_FREEFORM && canEnterDesktopMode(context);
+ }
+
private static void clearAppCompatTaskInfo(@NonNull AppCompatTaskInfo info) {
info.topActivityLetterboxVerticalPosition = TaskInfo.PROPERTY_VALUE_UNSET;
info.topActivityLetterboxHorizontalPosition = TaskInfo.PROPERTY_VALUE_UNSET;
diff --git a/services/core/java/com/android/server/wm/BackNavigationController.java b/services/core/java/com/android/server/wm/BackNavigationController.java
index e76a83453a9d..094ad187686c 100644
--- a/services/core/java/com/android/server/wm/BackNavigationController.java
+++ b/services/core/java/com/android/server/wm/BackNavigationController.java
@@ -190,7 +190,9 @@ class BackNavigationController {
currentActivity = window.mActivityRecord;
currentTask = window.getTask();
if ((currentTask != null && !currentTask.isVisibleRequested())
- || (currentActivity != null && !currentActivity.isVisibleRequested())) {
+ || (currentActivity != null && !currentActivity.isVisibleRequested())
+ || (currentActivity != null && currentTask != null
+ && currentTask.getTopNonFinishingActivity() != currentActivity)) {
// Closing transition is happening on focus window and should be update soon,
// don't drive back navigation with it.
ProtoLog.d(WM_DEBUG_BACK_PREVIEW, "Focus window is closing.");
diff --git a/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java b/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java
index 4eaa11bac016..f473b7b7e4fb 100644
--- a/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java
+++ b/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java
@@ -60,10 +60,11 @@ class DeferredDisplayUpdater {
*/
@VisibleForTesting
static final DisplayInfoFieldsUpdater DEFERRABLE_FIELDS = (out, override) -> {
- // Treat unique id and address change as WM-specific display change as we re-query display
- // settings and parameters based on it which could cause window changes
+ // Treat unique id, address, and canHostTasks change as WM-specific display change as we
+ // re-query display settings and parameters based on it which could cause window changes.
out.uniqueId = override.uniqueId;
out.address = override.address;
+ out.canHostTasks = override.canHostTasks;
// Also apply WM-override fields, since they might produce differences in window hierarchy
WM_OVERRIDE_FIELDS.setFields(out, override);
@@ -433,7 +434,7 @@ class DeferredDisplayUpdater {
second.thermalRefreshRateThrottling)
|| !Objects.equals(first.thermalBrightnessThrottlingDataId,
second.thermalBrightnessThrottlingDataId)
- || first.canHostTasks != second.canHostTasks) {
+ ) {
diff |= DIFF_NOT_WM_DEFERRABLE;
}
@@ -454,6 +455,7 @@ class DeferredDisplayUpdater {
|| !Objects.equals(first.displayShape, second.displayShape)
|| !Objects.equals(first.uniqueId, second.uniqueId)
|| !Objects.equals(first.address, second.address)
+ || first.canHostTasks != second.canHostTasks
) {
diff |= DIFF_WM_DEFERRABLE;
}
diff --git a/services/core/java/com/android/server/wm/DesktopModeHelper.java b/services/core/java/com/android/server/wm/DesktopModeHelper.java
index f35930700653..c2255d8d011a 100644
--- a/services/core/java/com/android/server/wm/DesktopModeHelper.java
+++ b/services/core/java/com/android/server/wm/DesktopModeHelper.java
@@ -51,13 +51,8 @@ public final class DesktopModeHelper {
}
/**
- * Return {@code true} if the current device can hosts desktop sessions on its internal display.
+ * Return {@code true} if the current device supports desktop mode.
*/
- @VisibleForTesting
- static boolean canInternalDisplayHostDesktops(@NonNull Context context) {
- return context.getResources().getBoolean(R.bool.config_canInternalDisplayHostDesktops);
- }
-
// TODO(b/337819319): use a companion object instead.
private static boolean isDesktopModeSupported(@NonNull Context context) {
return context.getResources().getBoolean(R.bool.config_isDesktopModeSupported);
@@ -68,32 +63,45 @@ public final class DesktopModeHelper {
}
/**
+ * Return {@code true} if the current device can hosts desktop sessions on its internal display.
+ */
+ @VisibleForTesting
+ static boolean canInternalDisplayHostDesktops(@NonNull Context context) {
+ return context.getResources().getBoolean(R.bool.config_canInternalDisplayHostDesktops);
+ }
+
+ /**
* Check if Desktop mode should be enabled because the dev option is shown and enabled.
*/
private static boolean isDesktopModeEnabledByDevOption(@NonNull Context context) {
return DesktopModeFlags.isDesktopModeForcedEnabled() && (isDesktopModeDevOptionsSupported(
- context) || isInternalDisplayEligibleToHostDesktops(context));
+ context) || isDeviceEligibleForDesktopMode(context));
}
@VisibleForTesting
- static boolean isInternalDisplayEligibleToHostDesktops(@NonNull Context context) {
- return !shouldEnforceDeviceRestrictions() || canInternalDisplayHostDesktops(context) || (
- Flags.enableDesktopModeThroughDevOption() && isDesktopModeDevOptionsSupported(
- context));
+ static boolean isDeviceEligibleForDesktopMode(@NonNull Context context) {
+ if (!shouldEnforceDeviceRestrictions()) {
+ return true;
+ }
+ final boolean desktopModeSupported = isDesktopModeSupported(context)
+ && canInternalDisplayHostDesktops(context);
+ final boolean desktopModeSupportedByDevOptions =
+ Flags.enableDesktopModeThroughDevOption()
+ && isDesktopModeDevOptionsSupported(context);
+ return desktopModeSupported || desktopModeSupportedByDevOptions;
}
/**
* Return {@code true} if desktop mode can be entered on the current device.
*/
static boolean canEnterDesktopMode(@NonNull Context context) {
- return (isInternalDisplayEligibleToHostDesktops(context)
- && DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_MODE.isTrue()
- && (isDesktopModeSupported(context) || !shouldEnforceDeviceRestrictions()))
+ return (isDeviceEligibleForDesktopMode(context)
+ && DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_MODE.isTrue())
|| isDesktopModeEnabledByDevOption(context);
}
/** Returns {@code true} if desktop experience wallpaper is supported on this device. */
public static boolean isDeviceEligibleForDesktopExperienceWallpaper(@NonNull Context context) {
- return enableConnectedDisplaysWallpaper() && canEnterDesktopMode(context);
+ return enableConnectedDisplaysWallpaper() && isDeviceEligibleForDesktopMode(context);
}
}
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index 682f3d8cf1e5..703ce7d24468 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -3239,25 +3239,43 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp
Slog.e(TAG, "ShouldShowSystemDecors shouldn't be updated when the flag is off.");
}
- final boolean shouldShow;
- if (isDefaultDisplay) {
- shouldShow = true;
- } else if (isPrivate()) {
- shouldShow = false;
- } else {
- shouldShow = mDisplay.canHostTasks();
+ final boolean shouldShowContent;
+ if (!allowContentModeSwitch()) {
+ return;
}
+ shouldShowContent = mDisplay.canHostTasks();
- if (shouldShow == mWmService.mDisplayWindowSettings.shouldShowSystemDecorsLocked(this)) {
+ if (shouldShowContent == mWmService.mDisplayWindowSettings
+ .shouldShowSystemDecorsLocked(this)) {
return;
}
- mWmService.mDisplayWindowSettings.setShouldShowSystemDecorsLocked(this, shouldShow);
+ mWmService.mDisplayWindowSettings.setShouldShowSystemDecorsLocked(this, shouldShowContent);
- if (!shouldShow) {
+ if (!shouldShowContent) {
clearAllTasksOnDisplay(null /* clearTasksCallback */, false /* isRemovingDisplay */);
}
}
+ private boolean allowContentModeSwitch() {
+ // The default display should always show system decorations.
+ if (isDefaultDisplay) {
+ return false;
+ }
+
+ // Private display should never show system decorations.
+ if (isPrivate()) {
+ return false;
+ }
+
+ // TODO(b/391965805): Remove this after introducing FLAG_ALLOW_SYSTEM_DECORATIONS_CHANGE.
+ // Virtual displays cannot add or remove system decorations during their lifecycle.
+ if (mDisplay.getType() == Display.TYPE_VIRTUAL) {
+ return false;
+ }
+
+ return true;
+ }
+
DisplayCutout loadDisplayCutout(int displayWidth, int displayHeight) {
if (mDisplayPolicy == null || mInitialDisplayCutout == null) {
return null;
@@ -6578,22 +6596,6 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp
.getKeyguardController().isKeyguardLocked(mDisplayId);
}
- boolean isKeyguardLockedOrAodShowing() {
- return isKeyguardLocked() || isAodShowing();
- }
-
- /**
- * @return whether aod is showing for this display
- */
- boolean isAodShowing() {
- final boolean isAodShowing = mRootWindowContainer.mTaskSupervisor
- .getKeyguardController().isAodShowing(mDisplayId);
- if (mDisplayId == DEFAULT_DISPLAY && isAodShowing) {
- return !isKeyguardGoingAway();
- }
- return isAodShowing;
- }
-
/**
* @return whether keyguard is going away on this display
*/
diff --git a/services/core/java/com/android/server/wm/DragState.java b/services/core/java/com/android/server/wm/DragState.java
index 69f32cb7b8ea..84281b8fbecf 100644
--- a/services/core/java/com/android/server/wm/DragState.java
+++ b/services/core/java/com/android/server/wm/DragState.java
@@ -122,7 +122,7 @@ class DragState {
float mThumbOffsetX, mThumbOffsetY;
InputInterceptor mInputInterceptor;
ArrayList<WindowState> mNotifiedWindows;
- boolean mDragInProgress;
+ private boolean mDragInProgress;
// Set to non -1 value if a valid app requests DRAG_FLAG_HIDE_CALLING_TASK_ON_DRAG_START
int mCallingTaskIdToHide;
/**
@@ -161,7 +161,7 @@ class DragState {
private boolean mIsClosing;
// Stores the last drop event which was reported to a valid drop target window, or null
- // otherwise. This drop event will contain private info and should only be consumed by the
+ // otherwise. This drop event will contain private info and should only be consumed by the
// unhandled drag listener.
DragEvent mUnhandledDropEvent;
@@ -243,7 +243,7 @@ class DragState {
for (WindowState ws : mNotifiedWindows) {
float inWindowX = 0;
float inWindowY = 0;
- SurfaceControl dragSurface = null;
+ boolean includeDragSurface = false;
if (!mDragResult && (ws.mSession.mPid == mPid)) {
// Report unconsumed drop location back to the app that started the drag.
inWindowX = ws.translateToWindowX(mCurrentDisplayX);
@@ -251,13 +251,10 @@ class DragState {
if (relinquishDragSurfaceToDragSource()) {
// If requested (and allowed), report the drag surface back to the app
// starting the drag to handle the return animation
- dragSurface = mSurfaceControl;
+ includeDragSurface = true;
}
}
- DragEvent event = DragEvent.obtain(DragEvent.ACTION_DRAG_ENDED, inWindowX,
- inWindowY, mThumbOffsetX, mThumbOffsetY,
- mCurrentDisplayContent.getDisplayId(), mFlags, null, null, null,
- dragSurface, null, mDragResult);
+ DragEvent event = obtainDragEndedEvent(inWindowX, inWindowY, includeDragSurface);
try {
if (DEBUG_DRAG) Slog.d(TAG_WM, "Sending DRAG_ENDED to " + ws);
ws.mClient.dispatchDragEvent(event);
@@ -310,10 +307,10 @@ class DragState {
/**
* Creates the drop event for dispatching to the unhandled drag.
- * TODO(b/384841906): Update `inWindowX` and `inWindowY` to be display-coordinate.
*/
- private DragEvent createUnhandledDropEvent(float inWindowX, float inWindowY) {
- return obtainDragEvent(DragEvent.ACTION_DROP, inWindowX, inWindowY, mDataDescription, mData,
+ private DragEvent createUnhandledDropEvent(float inDisplayX, float inDisplayY) {
+ return obtainDragEvent(DragEvent.ACTION_DROP, inDisplayX, inDisplayY, mDataDescription,
+ mData,
/* includeDragSurface= */ true,
/* includeDragFlags= */ true, null /* dragAndDropPermissions */);
}
@@ -370,11 +367,8 @@ class DragState {
}
final WindowState touchedWin = mService.mInputToWindowMap.get(token);
- // TODO(b/384841906): The x, y here when sent to a window and unhandled, will still be
- // relative to the window it was originally sent to. Need to update this to actually be
- // display-coordinate.
- final DragEvent unhandledDropEvent = createUnhandledDropEvent(inWindowX, inWindowY);
if (!isWindowNotified(touchedWin)) {
+ final DragEvent unhandledDropEvent = createUnhandledDropEvent(inWindowX, inWindowY);
// Delegate to the unhandled drag listener as a first pass
if (mDragDropController.notifyUnhandledDrop(unhandledDropEvent, "unhandled-drop")) {
// The unhandled drag listener will call back to notify whether it has consumed
@@ -392,6 +386,8 @@ class DragState {
}
if (DEBUG_DRAG) Slog.d(TAG_WM, "Sending DROP to " + touchedWin);
+ final DragEvent unhandledDropEvent = createUnhandledDropEvent(
+ touchedWin.getBounds().left + inWindowX, touchedWin.getBounds().top + inWindowY);
final IBinder clientToken = touchedWin.mClient.asBinder();
final DragEvent event = createDropEvent(inWindowX, inWindowY, touchedWin);
@@ -776,28 +772,37 @@ class DragState {
displayId, (int) (displayX - mThumbOffsetX), (int) (displayY - mThumbOffsetY));
}
- /**
- * Returns true if it has sent DRAG_STARTED broadcast out but has not been sent DRAG_END
- * broadcast.
- */
- boolean isInProgress() {
- return mDragInProgress;
+ private DragEvent obtainDragEndedEvent(float x, float y, boolean includeDragSurface) {
+ return obtainDragEvent(DragEvent.ACTION_DRAG_ENDED, x, y, /* description= */
+ null, /* data= */ null, includeDragSurface, /* includeDragFlags= */
+ true, /* dragAndDropPermissions= */ null, mDragResult);
+ }
+
+ private DragEvent obtainDragEvent(int action, float x, float y, ClipDescription description,
+ ClipData data, boolean includeDragSurface, boolean includeDragFlags,
+ IDragAndDropPermissions dragAndDropPermissions) {
+ return obtainDragEvent(action, x, y, description, data, includeDragSurface,
+ includeDragFlags, dragAndDropPermissions, /* dragResult= */ false);
}
/**
* `x` and `y` here varies between local window coordinate, relative coordinate to another
* window and local display coordinate, all depending on the `action`. Please take a look
* at the callers to determine the type.
- * TODO(b/384845022): Properly document the events sent based on the event type.
+ * - ACTION_DRAG_STARTED: (x, y) is relative coordinate to the target window's origin
+ * (possible to have negative values).
+ * - ACTION_DROP:
+ * --- UnhandledDropEvent: (x, y) is in display space coordinate.
+ * --- DropEvent: (x, y) is in local window coordinate where event is targeted to.
+ * - ACTION_DRAG_ENDED: (x, y) is in local window coordinate where event is targeted to.
*/
private DragEvent obtainDragEvent(int action, float x, float y, ClipDescription description,
ClipData data, boolean includeDragSurface, boolean includeDragFlags,
- IDragAndDropPermissions dragAndDropPermissions) {
+ IDragAndDropPermissions dragAndDropPermissions, boolean dragResult) {
return DragEvent.obtain(action, x, y, mThumbOffsetX, mThumbOffsetY,
mCurrentDisplayContent.getDisplayId(), includeDragFlags ? mFlags : 0,
null /* localState */, description, data,
- includeDragSurface ? mSurfaceControl : null, dragAndDropPermissions,
- false /* result */);
+ includeDragSurface ? mSurfaceControl : null, dragAndDropPermissions, dragResult);
}
private ValueAnimator createReturnAnimationLocked() {
diff --git a/services/core/java/com/android/server/wm/KeyguardController.java b/services/core/java/com/android/server/wm/KeyguardController.java
index dd2f49e171a8..6091b8334438 100644
--- a/services/core/java/com/android/server/wm/KeyguardController.java
+++ b/services/core/java/com/android/server/wm/KeyguardController.java
@@ -18,7 +18,6 @@ package com.android.server.wm;
import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER;
import static android.view.Display.DEFAULT_DISPLAY;
-import static android.view.WindowManager.TRANSIT_FLAG_AOD_APPEARING;
import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_APPEARING;
import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY;
import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY_NO_ANIMATION;
@@ -217,9 +216,6 @@ class KeyguardController {
} else if (keyguardShowing && !state.mKeyguardShowing) {
transition.addFlag(TRANSIT_FLAG_KEYGUARD_APPEARING);
}
- if (mWindowManager.mFlags.mAodTransition && aodShowing && !state.mAodShowing) {
- transition.addFlag(TRANSIT_FLAG_AOD_APPEARING);
- }
}
}
// Update the task snapshot if the screen will not be turned off. To make sure that the
@@ -242,27 +238,19 @@ class KeyguardController {
state.mAodShowing = aodShowing;
state.writeEventLog("setKeyguardShown");
- if (keyguardChanged || aodChanged) {
- if (keyguardChanged) {
- // Irrelevant to AOD.
- state.mKeyguardGoingAway = false;
- if (keyguardShowing) {
- state.mDismissalRequested = false;
- }
+ if (keyguardChanged) {
+ // Irrelevant to AOD.
+ state.mKeyguardGoingAway = false;
+ if (keyguardShowing) {
+ state.mDismissalRequested = false;
}
if (goingAwayRemoved
- || (keyguardShowing && !Display.isOffState(dc.getDisplayInfo().state))
- || (mWindowManager.mFlags.mAodTransition && aodShowing)) {
+ || (keyguardShowing && !Display.isOffState(dc.getDisplayInfo().state))) {
// Keyguard decided to show or stopped going away. Send a transition to animate back
// to the locked state before holding the sleep token again
if (!ENABLE_NEW_KEYGUARD_SHELL_TRANSITIONS) {
dc.requestTransitionAndLegacyPrepare(
TRANSIT_TO_FRONT, TRANSIT_FLAG_KEYGUARD_APPEARING);
- if (mWindowManager.mFlags.mAodTransition && aodShowing
- && dc.mTransitionController.isCollecting()) {
- dc.mTransitionController.getCollectingTransition().addFlag(
- TRANSIT_FLAG_AOD_APPEARING);
- }
}
dc.mWallpaperController.adjustWallpaperWindows();
dc.executeAppTransition();
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index 3abab8bf62c2..bf9883c76a06 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -166,6 +166,7 @@ import android.view.InsetsState;
import android.view.SurfaceControl;
import android.view.WindowInsets;
import android.view.WindowManager;
+import android.window.DesktopExperienceFlags;
import android.window.DesktopModeFlags;
import android.window.ITaskOrganizer;
import android.window.PictureInPictureSurfaceTransaction;
@@ -2378,7 +2379,7 @@ class Task extends TaskFragment {
// configurations and let its parent (organized task) to control it;
final Task rootTask = getRootTask();
boolean shouldInheritBounds = rootTask != this && rootTask.isOrganized();
- if (Flags.enableMultipleDesktopsBackend()) {
+ if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue()) {
// Only inherit from organized parent when this task is not organized.
shouldInheritBounds &= !isOrganized();
}
diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java
index fe653e454d6c..5217a759c6ae 100644
--- a/services/core/java/com/android/server/wm/Transition.java
+++ b/services/core/java/com/android/server/wm/Transition.java
@@ -36,7 +36,6 @@ import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_STARTING;
import static android.view.WindowManager.LayoutParams.TYPE_INPUT_METHOD;
import static android.view.WindowManager.TRANSIT_CHANGE;
import static android.view.WindowManager.TRANSIT_CLOSE;
-import static android.view.WindowManager.TRANSIT_FLAG_AOD_APPEARING;
import static android.view.WindowManager.TRANSIT_FLAG_IS_RECENTS;
import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_LOCKED;
import static android.view.WindowManager.TRANSIT_OPEN;
@@ -974,10 +973,6 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener {
return false;
}
- boolean isInAodAppearTransition() {
- return (mFlags & TRANSIT_FLAG_AOD_APPEARING) != 0;
- }
-
/**
* Specifies configuration change explicitly for the window container, so it can be chosen as
* transition target. This is usually used with transition mode
diff --git a/services/core/java/com/android/server/wm/TransitionController.java b/services/core/java/com/android/server/wm/TransitionController.java
index 25b513d85384..ba7f36419ac5 100644
--- a/services/core/java/com/android/server/wm/TransitionController.java
+++ b/services/core/java/com/android/server/wm/TransitionController.java
@@ -525,19 +525,6 @@ class TransitionController {
return false;
}
- boolean isInAodAppearTransition() {
- if (mCollectingTransition != null && mCollectingTransition.isInAodAppearTransition()) {
- return true;
- }
- for (int i = mWaitingTransitions.size() - 1; i >= 0; --i) {
- if (mWaitingTransitions.get(i).isInAodAppearTransition()) return true;
- }
- for (int i = mPlayingTransitions.size() - 1; i >= 0; --i) {
- if (mPlayingTransitions.get(i).isInAodAppearTransition()) return true;
- }
- return false;
- }
-
/**
* @return A pair of the transition and restore-behind target for the given {@param container}.
* @param container An ancestor of a transient-launch activity
diff --git a/services/core/java/com/android/server/wm/WallpaperController.java b/services/core/java/com/android/server/wm/WallpaperController.java
index 70948e1264c4..c1ef208d1d4d 100644
--- a/services/core/java/com/android/server/wm/WallpaperController.java
+++ b/services/core/java/com/android/server/wm/WallpaperController.java
@@ -166,14 +166,6 @@ class WallpaperController {
mFindResults.setWallpaperTarget(w);
return false;
}
- } else if (mService.mFlags.mAodTransition
- && mDisplayContent.isKeyguardLockedOrAodShowing()) {
- if (mService.mPolicy.isKeyguardHostWindow(w.mAttrs)
- && w.mTransitionController.isInAodAppearTransition()) {
- if (DEBUG_WALLPAPER) Slog.v(TAG, "Found aod transition wallpaper target: " + w);
- mFindResults.setWallpaperTarget(w);
- return true;
- }
}
final boolean animationWallpaper = animatingContainer != null
@@ -692,8 +684,7 @@ class WallpaperController {
private WallpaperWindowToken getTokenForTarget(WindowState target) {
if (target == null) return null;
WindowState window = mFindResults.getTopWallpaper(
- (target.canShowWhenLocked() && mService.isKeyguardLocked())
- || (mService.mFlags.mAodTransition && mDisplayContent.isAodShowing()));
+ target.canShowWhenLocked() && mService.isKeyguardLocked());
return window == null ? null : window.mToken.asWallpaperToken();
}
@@ -736,9 +727,7 @@ class WallpaperController {
if (mFindResults.wallpaperTarget == null && mFindResults.useTopWallpaperAsTarget) {
mFindResults.setWallpaperTarget(
- mFindResults.getTopWallpaper(mService.mFlags.mAodTransition
- ? mDisplayContent.isKeyguardLockedOrAodShowing()
- : mDisplayContent.isKeyguardLocked()));
+ mFindResults.getTopWallpaper(mDisplayContent.isKeyguardLocked()));
}
}
@@ -910,17 +899,11 @@ class WallpaperController {
if (mDisplayContent.mWmService.mFlags.mEnsureWallpaperInTransitions) {
visibleRequested = mWallpaperTarget != null && mWallpaperTarget.isVisibleRequested();
}
- updateWallpaperTokens(visibleRequested,
- mService.mFlags.mAodTransition
- ? mDisplayContent.isKeyguardLockedOrAodShowing()
- : mDisplayContent.isKeyguardLocked());
+ updateWallpaperTokens(visibleRequested, mDisplayContent.isKeyguardLocked());
ProtoLog.v(WM_DEBUG_WALLPAPER,
"Wallpaper at display %d - visibility: %b, keyguardLocked: %b",
- mDisplayContent.getDisplayId(), visible,
- mService.mFlags.mAodTransition
- ? mDisplayContent.isKeyguardLockedOrAodShowing()
- : mDisplayContent.isKeyguardLocked());
+ mDisplayContent.getDisplayId(), visible, mDisplayContent.isKeyguardLocked());
if (visible && mLastFrozen != mFindResults.isWallpaperTargetForLetterbox) {
mLastFrozen = mFindResults.isWallpaperTargetForLetterbox;
diff --git a/services/core/jni/com_android_server_input_InputManagerService.cpp b/services/core/jni/com_android_server_input_InputManagerService.cpp
index f07e6722d836..0d0c0bad24fa 100644
--- a/services/core/jni/com_android_server_input_InputManagerService.cpp
+++ b/services/core/jni/com_android_server_input_InputManagerService.cpp
@@ -124,6 +124,7 @@ static struct {
jmethodID notifyStylusGestureStarted;
jmethodID notifyVibratorState;
jmethodID filterInputEvent;
+ jmethodID filterPointerMotion;
jmethodID interceptKeyBeforeQueueing;
jmethodID interceptMotionBeforeQueueingNonInteractive;
jmethodID interceptKeyBeforeDispatching;
@@ -451,6 +452,8 @@ public:
void notifyPointerDisplayIdChanged(ui::LogicalDisplayId displayId,
const vec2& position) override;
void notifyMouseCursorFadedOnTyping() override;
+ std::optional<vec2> filterPointerMotionForAccessibility(
+ const vec2& current, const vec2& delta, const ui::LogicalDisplayId& displayId) override;
/* --- InputFilterPolicyInterface implementation --- */
void notifyStickyModifierStateChanged(uint32_t modifierState,
@@ -938,6 +941,27 @@ void NativeInputManager::notifyStickyModifierStateChanged(uint32_t modifierState
checkAndClearExceptionFromCallback(env, "notifyStickyModifierStateChanged");
}
+std::optional<vec2> NativeInputManager::filterPointerMotionForAccessibility(
+ const vec2& current, const vec2& delta, const ui::LogicalDisplayId& displayId) {
+ JNIEnv* env = jniEnv();
+ ScopedFloatArrayRO filtered(env,
+ jfloatArray(
+ env->CallObjectMethod(mServiceObj,
+ gServiceClassInfo.filterPointerMotion,
+ delta.x, delta.y, current.x,
+ current.y, displayId.val())));
+ if (checkAndClearExceptionFromCallback(env, "filterPointerMotionForAccessibilityLocked")) {
+ ALOGE("Disabling accessibility pointer motion filter due to an error. "
+ "The filter state in Java and PointerChoreographer would no longer be in sync.");
+ return std::nullopt;
+ }
+ LOG_ALWAYS_FATAL_IF(filtered.size() != 2,
+ "Accessibility pointer motion filter is misbehaving. Returned array size "
+ "%zu should be 2.",
+ filtered.size());
+ return vec2{filtered[0], filtered[1]};
+}
+
sp<SurfaceControl> NativeInputManager::getParentSurfaceForPointers(ui::LogicalDisplayId displayId) {
JNIEnv* env = jniEnv();
jlong nativeSurfaceControlPtr =
@@ -3271,6 +3295,12 @@ static jboolean nativeSetKernelWakeEnabled(JNIEnv* env, jobject nativeImplObj, j
return im->getInputManager()->getReader().setKernelWakeEnabled(deviceId, enabled);
}
+static void nativeSetAccessibilityPointerMotionFilterEnabled(JNIEnv* env, jobject nativeImplObj,
+ jboolean enabled) {
+ NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
+ im->getInputManager()->getChoreographer().setAccessibilityPointerMotionFilterEnabled(enabled);
+}
+
// ----------------------------------------------------------------------------
static const JNINativeMethod gInputManagerMethods[] = {
@@ -3398,6 +3428,8 @@ static const JNINativeMethod gInputManagerMethods[] = {
{"setInputMethodConnectionIsActive", "(Z)V", (void*)nativeSetInputMethodConnectionIsActive},
{"getLastUsedInputDeviceId", "()I", (void*)nativeGetLastUsedInputDeviceId},
{"setKernelWakeEnabled", "(IZ)Z", (void*)nativeSetKernelWakeEnabled},
+ {"setAccessibilityPointerMotionFilterEnabled", "(Z)V",
+ (void*)nativeSetAccessibilityPointerMotionFilterEnabled},
};
#define FIND_CLASS(var, className) \
@@ -3482,6 +3514,8 @@ int register_android_server_InputManager(JNIEnv* env) {
GET_METHOD_ID(gServiceClassInfo.filterInputEvent, clazz,
"filterInputEvent", "(Landroid/view/InputEvent;I)Z");
+ GET_METHOD_ID(gServiceClassInfo.filterPointerMotion, clazz, "filterPointerMotion", "(FFFFI)[F");
+
GET_METHOD_ID(gServiceClassInfo.interceptKeyBeforeQueueing, clazz,
"interceptKeyBeforeQueueing", "(Landroid/view/KeyEvent;I)I");
diff --git a/services/supervision/java/com/android/server/supervision/SupervisionService.java b/services/supervision/java/com/android/server/supervision/SupervisionService.java
index a96c477c78d2..f731b50d81b4 100644
--- a/services/supervision/java/com/android/server/supervision/SupervisionService.java
+++ b/services/supervision/java/com/android/server/supervision/SupervisionService.java
@@ -17,6 +17,8 @@
package com.android.server.supervision;
import static android.Manifest.permission.INTERACT_ACROSS_USERS;
+import static android.Manifest.permission.MANAGE_USERS;
+import static android.Manifest.permission.QUERY_USERS;
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
import static com.android.internal.util.Preconditions.checkCallAuthorization;
@@ -79,6 +81,25 @@ public class SupervisionService extends ISupervisionManager.Stub {
}
/**
+ * Creates an {@link Intent} that can be used with {@link Context#startActivity(Intent)} to
+ * launch the activity to verify supervision credentials.
+ *
+ * <p>A valid {@link Intent} is always returned if supervision is enabled at the time this
+ * method is called, the launched activity still need to perform validity checks as the
+ * supervision state can change when it's launched. A null intent is returned if supervision is
+ * disabled at the time of this method call.
+ *
+ * <p>A result code of {@link android.app.Activity#RESULT_OK} indicates successful verification
+ * of the supervision credentials.
+ */
+ @Override
+ @Nullable
+ public Intent createConfirmSupervisionCredentialsIntent() {
+ // TODO(b/392961554): Implement createAuthenticationIntent API
+ throw new UnsupportedOperationException();
+ }
+
+ /**
* Returns whether supervision is enabled for the given user.
*
* <p>Supervision is automatically enabled when the supervision app becomes the profile owner or
@@ -86,6 +107,7 @@ public class SupervisionService extends ISupervisionManager.Stub {
*/
@Override
public boolean isSupervisionEnabledForUser(@UserIdInt int userId) {
+ enforceAnyPermission(QUERY_USERS, MANAGE_USERS);
if (UserHandle.getUserId(Binder.getCallingUid()) != userId) {
enforcePermission(INTERACT_ACROSS_USERS);
}
@@ -96,6 +118,7 @@ public class SupervisionService extends ISupervisionManager.Stub {
@Override
public void setSupervisionEnabledForUser(@UserIdInt int userId, boolean enabled) {
+ // TODO(b/395630828): Ensure that this method can only be called by the system.
if (UserHandle.getUserId(Binder.getCallingUid()) != userId) {
enforcePermission(INTERACT_ACROSS_USERS);
}
@@ -181,8 +204,8 @@ public class SupervisionService extends ISupervisionManager.Stub {
* Ensures that supervision is enabled when the supervision app is the profile owner.
*
* <p>The state syncing with the DevicePolicyManager can only enable supervision and never
- * disable. Supervision can only be disabled explicitly via calls to the
- * {@link #setSupervisionEnabledForUser} method.
+ * disable. Supervision can only be disabled explicitly via calls to the {@link
+ * #setSupervisionEnabledForUser} method.
*/
private void syncStateWithDevicePolicyManager(@UserIdInt int userId) {
final DevicePolicyManagerInternal dpmInternal = mInjector.getDpmInternal();
@@ -221,6 +244,17 @@ public class SupervisionService extends ISupervisionManager.Stub {
mContext.checkCallingOrSelfPermission(permission) == PERMISSION_GRANTED);
}
+ /** Enforces that the caller has at least one of the given permission. */
+ private void enforceAnyPermission(String... permissions) {
+ boolean authorized = false;
+ for (String permission : permissions) {
+ if (mContext.checkCallingOrSelfPermission(permission) == PERMISSION_GRANTED) {
+ authorized = true;
+ }
+ }
+ checkCallAuthorization(authorized);
+ }
+
/** Provides local services in a lazy manner. */
static class Injector {
private final Context mContext;
@@ -280,7 +314,7 @@ public class SupervisionService extends ISupervisionManager.Stub {
}
@VisibleForTesting
- @SuppressLint("MissingPermission") // not needed for a system service
+ @SuppressLint("MissingPermission")
void registerProfileOwnerListener() {
IntentFilter poIntentFilter = new IntentFilter();
poIntentFilter.addAction(DevicePolicyManager.ACTION_PROFILE_OWNER_CHANGED);
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java
index 5d64cb638702..2d3f7231cc5c 100644
--- a/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java
@@ -34,12 +34,12 @@ import static org.junit.Assert.fail;
import static org.junit.Assume.assumeFalse;
import static org.junit.Assume.assumeTrue;
+import android.app.ActivityManager;
import android.app.Instrumentation;
import android.content.res.Configuration;
import android.graphics.Insets;
+import android.os.Build;
import android.os.RemoteException;
-import android.platform.test.annotations.RequiresFlagsDisabled;
-import android.platform.test.flag.junit.CheckFlagsRule;
import android.platform.test.flag.junit.DeviceFlagsValueProvider;
import android.provider.Settings;
import android.server.wm.WindowManagerStateHelper;
@@ -86,25 +86,36 @@ public class InputMethodServiceTest {
"android:id/input_method_nav_back";
private static final String INPUT_METHOD_NAV_IME_SWITCHER_ID =
"android:id/input_method_nav_ime_switcher";
- private static final long TIMEOUT_IN_SECONDS = 3;
- private static final String ENABLE_SHOW_IME_WITH_HARD_KEYBOARD_CMD =
- "settings put secure " + Settings.Secure.SHOW_IME_WITH_HARD_KEYBOARD + " 1";
- private static final String DISABLE_SHOW_IME_WITH_HARD_KEYBOARD_CMD =
- "settings put secure " + Settings.Secure.SHOW_IME_WITH_HARD_KEYBOARD + " 0";
+
+ /** Timeout until the uiObject should be found. */
+ private static final long TIMEOUT_MS = 5000L * Build.HW_TIMEOUT_MULTIPLIER;
+
+ /** Timeout until the event is expected. */
+ private static final long EXPECT_TIMEOUT_MS = 3000L * Build.HW_TIMEOUT_MULTIPLIER;
+
+ /** Timeout during which the event is not expected. */
+ private static final long NOT_EXCEPT_TIMEOUT_MS = 2000L * Build.HW_TIMEOUT_MULTIPLIER;
+
+ /** Command to set showing the IME when a hardware keyboard is connected. */
+ private static final String SET_SHOW_IME_WITH_HARD_KEYBOARD_CMD =
+ "settings put secure " + Settings.Secure.SHOW_IME_WITH_HARD_KEYBOARD;
+ /** Command to get verbose ImeTracker logging state. */
+ private static final String GET_VERBOSE_IME_TRACKER_LOGGING_CMD =
+ "getprop persist.debug.imetracker";
+ /** Command to set verbose ImeTracker logging state. */
+ private static final String SET_VERBOSE_IME_TRACKER_LOGGING_CMD =
+ "setprop persist.debug.imetracker";
/** The ids of the subtypes of SimpleIme. */
private static final int[] SUBTYPE_IDS = new int[]{1, 2};
- private final WindowManagerStateHelper mWmState = new WindowManagerStateHelper();
+ private final WindowManagerStateHelper mWmState = new WindowManagerStateHelper();
private final GestureNavSwitchHelper mGestureNavSwitchHelper = new GestureNavSwitchHelper();
private final DeviceFlagsValueProvider mFlagsValueProvider = new DeviceFlagsValueProvider();
@Rule
- public final CheckFlagsRule mCheckFlagsRule = new CheckFlagsRule(mFlagsValueProvider);
-
- @Rule
public final TestName mName = new TestName();
private Instrumentation mInstrumentation;
@@ -114,7 +125,8 @@ public class InputMethodServiceTest {
private String mInputMethodId;
private TestActivity mActivity;
private InputMethodServiceWrapper mInputMethodService;
- private boolean mShowImeWithHardKeyboardEnabled;
+ private boolean mOriginalVerboseImeTrackerLoggingEnabled;
+ private boolean mOriginalShowImeWithHardKeyboardEnabled;
@Before
public void setUp() throws Exception {
@@ -123,9 +135,12 @@ public class InputMethodServiceTest {
mImm = mInstrumentation.getContext().getSystemService(InputMethodManager.class);
mTargetPackageName = mInstrumentation.getTargetContext().getPackageName();
mInputMethodId = getInputMethodId();
+ mOriginalVerboseImeTrackerLoggingEnabled = getVerboseImeTrackerLogging();
+ if (!mOriginalVerboseImeTrackerLoggingEnabled) {
+ setVerboseImeTrackerLogging(true);
+ }
prepareIme();
prepareActivity();
- mInstrumentation.waitForIdleSync();
mUiDevice.freezeRotation();
mUiDevice.setOrientationNatural();
// Waits for input binding ready.
@@ -148,17 +163,18 @@ public class InputMethodServiceTest {
.that(mInputMethodService.getCurrentInputViewStarted()).isFalse();
});
// Save the original value of show_ime_with_hard_keyboard from Settings.
- mShowImeWithHardKeyboardEnabled =
+ mOriginalShowImeWithHardKeyboardEnabled =
mInputMethodService.getShouldShowImeWithHardKeyboardForTesting();
}
@After
public void tearDown() throws Exception {
mUiDevice.unfreezeRotation();
+ if (!mOriginalVerboseImeTrackerLoggingEnabled) {
+ setVerboseImeTrackerLogging(false);
+ }
// Change back the original value of show_ime_with_hard_keyboard in Settings.
- executeShellCommand(mShowImeWithHardKeyboardEnabled
- ? ENABLE_SHOW_IME_WITH_HARD_KEYBOARD_CMD
- : DISABLE_SHOW_IME_WITH_HARD_KEYBOARD_CMD);
+ setShowImeWithHardKeyboard(mOriginalShowImeWithHardKeyboardEnabled);
executeShellCommand("ime disable " + mInputMethodId);
}
@@ -170,7 +186,7 @@ public class InputMethodServiceTest {
public void testShowHideKeyboard_byUserAction() {
waitUntilActivityReadyForInputInjection(mActivity);
- setShowImeWithHardKeyboard(true /* enabled */);
+ setShowImeWithHardKeyboard(true /* enable */);
// Performs click on EditText to bring up the IME.
Log.i(TAG, "Click on EditText");
@@ -201,14 +217,12 @@ public class InputMethodServiceTest {
*/
@Test
public void testShowHideKeyboard_byInputMethodManager() {
- setShowImeWithHardKeyboard(true /* enabled */);
+ setShowImeWithHardKeyboard(true /* enable */);
- // Triggers to show IME via public API.
verifyInputViewStatusOnMainSync(
() -> assertThat(mActivity.showImeWithInputMethodManager(0 /* flags */)).isTrue(),
EVENT_SHOW, true /* eventExpected */, true /* shown */, "IME is shown");
- // Triggers to hide IME via public API.
verifyInputViewStatusOnMainSync(
() -> assertThat(mActivity.hideImeWithInputMethodManager(0 /* flags */)).isTrue(),
EVENT_HIDE, true /* eventExpected */, false /* shown */, "IME is not shown");
@@ -219,14 +233,12 @@ public class InputMethodServiceTest {
*/
@Test
public void testShowHideKeyboard_byInsetsController() {
- setShowImeWithHardKeyboard(true /* enabled */);
+ setShowImeWithHardKeyboard(true /* enable */);
- // Triggers to show IME via public API.
verifyInputViewStatusOnMainSync(
() -> mActivity.showImeWithWindowInsetsController(),
EVENT_SHOW, true /* eventExpected */, true /* shown */, "IME is shown");
- // Triggers to hide IME via public API.
verifyInputViewStatusOnMainSync(
() -> mActivity.hideImeWithWindowInsetsController(),
EVENT_HIDE, true /* eventExpected */, false /* shown */, "IME is not shown");
@@ -234,53 +246,18 @@ public class InputMethodServiceTest {
/**
* This checks the result of calling IMS#requestShowSelf and IMS#requestHideSelf.
- *
- * <p>With the refactor in b/298172246, all calls to IMMS#{show,hide}MySoftInputLocked
- * will be just apply the requested visibility (by using the callback). Therefore, we will
- * lose flags like HIDE_IMPLICIT_ONLY.
*/
@Test
public void testShowHideSelf() {
- setShowImeWithHardKeyboard(true /* enabled */);
+ setShowImeWithHardKeyboard(true /* enable */);
- // IME request to show itself without any flags, expect shown.
- Log.i(TAG, "Call IMS#requestShowSelf(0)");
verifyInputViewStatusOnMainSync(
() -> mInputMethodService.requestShowSelf(0 /* flags */),
EVENT_SHOW, true /* eventExpected */, true /* shown */, "IME is shown");
- if (!mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) {
- // IME request to hide itself with flag HIDE_IMPLICIT_ONLY, expect not hide (shown).
- Log.i(TAG, "Call IMS#requestHideSelf(InputMethodManager.HIDE_IMPLICIT_ONLY)");
- verifyInputViewStatusOnMainSync(
- () -> mInputMethodService.requestHideSelf(
- InputMethodManager.HIDE_IMPLICIT_ONLY),
- EVENT_HIDE, false /* eventExpected */, true /* shown */,
- "IME is still shown after HIDE_IMPLICIT_ONLY");
- }
-
- // IME request to hide itself without any flags, expect hidden.
- Log.i(TAG, "Call IMS#requestHideSelf(0)");
verifyInputViewStatusOnMainSync(
() -> mInputMethodService.requestHideSelf(0 /* flags */),
EVENT_HIDE, true /* eventExpected */, false /* shown */, "IME is not shown");
-
- if (!mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) {
- // IME request to show itself with flag SHOW_IMPLICIT, expect shown.
- Log.i(TAG, "Call IMS#requestShowSelf(InputMethodManager.SHOW_IMPLICIT)");
- verifyInputViewStatusOnMainSync(
- () -> mInputMethodService.requestShowSelf(InputMethodManager.SHOW_IMPLICIT),
- EVENT_SHOW, true /* eventExpected */, true /* shown */,
- "IME is shown with SHOW_IMPLICIT");
-
- // IME request to hide itself with flag HIDE_IMPLICIT_ONLY, expect hidden.
- Log.i(TAG, "Call IMS#requestHideSelf(InputMethodManager.HIDE_IMPLICIT_ONLY)");
- verifyInputViewStatusOnMainSync(
- () -> mInputMethodService.requestHideSelf(
- InputMethodManager.HIDE_IMPLICIT_ONLY),
- EVENT_HIDE, true /* eventExpected */, false /* shown */,
- "IME is not shown after HIDE_IMPLICIT_ONLY");
- }
}
/**
@@ -289,28 +266,25 @@ public class InputMethodServiceTest {
*/
@Test
public void testOnEvaluateInputViewShown_showImeWithHardKeyboard() {
- setShowImeWithHardKeyboard(true /* enabled */);
+ setShowImeWithHardKeyboard(true /* enable */);
final var config = mInputMethodService.getResources().getConfiguration();
final var initialConfig = new Configuration(config);
try {
config.keyboard = Configuration.KEYBOARD_QWERTY;
config.hardKeyboardHidden = Configuration.HARDKEYBOARDHIDDEN_NO;
- eventually(() ->
- assertWithMessage("InputView should show with visible hardware keyboard")
- .that(mInputMethodService.onEvaluateInputViewShown()).isTrue());
+ assertWithMessage("InputView should show with visible hardware keyboard")
+ .that(mInputMethodService.onEvaluateInputViewShown()).isTrue();
config.keyboard = Configuration.KEYBOARD_NOKEYS;
config.hardKeyboardHidden = Configuration.HARDKEYBOARDHIDDEN_NO;
- eventually(() ->
- assertWithMessage("InputView should show without hardware keyboard")
- .that(mInputMethodService.onEvaluateInputViewShown()).isTrue());
+ assertWithMessage("InputView should show without hardware keyboard")
+ .that(mInputMethodService.onEvaluateInputViewShown()).isTrue();
config.keyboard = Configuration.KEYBOARD_QWERTY;
config.hardKeyboardHidden = Configuration.HARDKEYBOARDHIDDEN_YES;
- eventually(() ->
- assertWithMessage("InputView should show with hidden hardware keyboard")
- .that(mInputMethodService.onEvaluateInputViewShown()).isTrue());
+ assertWithMessage("InputView should show with hidden hardware keyboard")
+ .that(mInputMethodService.onEvaluateInputViewShown()).isTrue();
} finally {
mInputMethodService.getResources()
.updateConfiguration(initialConfig, null /* metrics */, null /* compat */);
@@ -323,28 +297,25 @@ public class InputMethodServiceTest {
*/
@Test
public void testOnEvaluateInputViewShown_disableShowImeWithHardKeyboard() {
- setShowImeWithHardKeyboard(false /* enabled */);
+ setShowImeWithHardKeyboard(false /* enable */);
final var config = mInputMethodService.getResources().getConfiguration();
final var initialConfig = new Configuration(config);
try {
config.keyboard = Configuration.KEYBOARD_QWERTY;
config.hardKeyboardHidden = Configuration.HARDKEYBOARDHIDDEN_NO;
- eventually(() ->
- assertWithMessage("InputView should not show with visible hardware keyboard")
- .that(mInputMethodService.onEvaluateInputViewShown()).isFalse());
+ assertWithMessage("InputView should not show with visible hardware keyboard")
+ .that(mInputMethodService.onEvaluateInputViewShown()).isFalse();
config.keyboard = Configuration.KEYBOARD_NOKEYS;
config.hardKeyboardHidden = Configuration.HARDKEYBOARDHIDDEN_NO;
- eventually(() ->
- assertWithMessage("InputView should show without hardware keyboard")
- .that(mInputMethodService.onEvaluateInputViewShown()).isTrue());
+ assertWithMessage("InputView should show without hardware keyboard")
+ .that(mInputMethodService.onEvaluateInputViewShown()).isTrue();
config.keyboard = Configuration.KEYBOARD_QWERTY;
config.hardKeyboardHidden = Configuration.HARDKEYBOARDHIDDEN_YES;
- eventually(() ->
- assertWithMessage("InputView should show with hidden hardware keyboard")
- .that(mInputMethodService.onEvaluateInputViewShown()).isTrue());
+ assertWithMessage("InputView should show with hidden hardware keyboard")
+ .that(mInputMethodService.onEvaluateInputViewShown()).isTrue();
} finally {
mInputMethodService.getResources()
.updateConfiguration(initialConfig, null /* metrics */, null /* compat */);
@@ -357,7 +328,7 @@ public class InputMethodServiceTest {
*/
@Test
public void testShowSoftInput_disableShowImeWithHardKeyboard() {
- setShowImeWithHardKeyboard(false /* enabled */);
+ setShowImeWithHardKeyboard(false /* enable */);
final var config = mInputMethodService.getResources().getConfiguration();
final var initialConfig = new Configuration(config);
@@ -386,49 +357,17 @@ public class InputMethodServiceTest {
}
/**
- * This checks that an explicit show request results in the IME being shown.
- */
- @Test
- public void testShowSoftInputExplicitly() {
- setShowImeWithHardKeyboard(true /* enabled */);
-
- // When InputMethodService#onEvaluateInputViewShown() returns true and flag is EXPLICIT, the
- // IME should be shown.
- verifyInputViewStatusOnMainSync(
- () -> assertThat(mActivity.showImeWithInputMethodManager(0 /* flags */)).isTrue(),
- EVENT_SHOW, true /* eventExpected */, true /* shown */, "IME is shown");
- }
-
- /**
- * This checks that an implicit show request results in the IME being shown.
- */
- @Test
- public void testShowSoftInputImplicitly() {
- setShowImeWithHardKeyboard(true /* enabled */);
-
- // When InputMethodService#onEvaluateInputViewShown() returns true and flag is IMPLICIT,
- // the IME should be shown.
- verifyInputViewStatusOnMainSync(() -> assertThat(
- mActivity.showImeWithInputMethodManager(InputMethodManager.SHOW_IMPLICIT)).isTrue(),
- EVENT_SHOW, true /* eventExpected */, true /* shown */, "IME is shown");
- }
-
- /**
* This checks that an explicit show request when the IME is not previously shown,
* and it should be shown in fullscreen mode, results in the IME being shown.
*/
@Test
public void testShowSoftInputExplicitly_fullScreenMode() {
- setShowImeWithHardKeyboard(true /* enabled */);
+ setShowImeWithHardKeyboard(true /* enable */);
// Set orientation landscape to enable fullscreen mode.
setOrientation(2);
- eventually(() -> assertWithMessage("No longer in natural orientation")
- .that(mUiDevice.isNaturalOrientation()).isFalse());
- // Wait for the TestActivity to be recreated.
eventually(() -> assertWithMessage("Activity was re-created after rotation")
.that(TestActivity.getInstance()).isNotEqualTo(mActivity));
- // Get the new TestActivity.
mActivity = TestActivity.getInstance();
assertWithMessage("Re-created activity is not null").that(mActivity).isNotNull();
// Wait for the new EditText to be served by InputMethodManager.
@@ -442,34 +381,40 @@ public class InputMethodServiceTest {
/**
* This checks that an implicit show request when the IME is not previously shown,
- * and it should be shown in fullscreen mode, results in the IME not being shown.
+ * and it should be shown in fullscreen mode behaves like an explicit show request, resulting
+ * in the IME being shown. This is due to the refactor in b/298172246, causing us to lose flag
+ * information like {@link InputMethodManager#SHOW_IMPLICIT}.
*
- * <p>With the refactor in b/298172246, all calls from InputMethodManager#{show,hide}SoftInput
- * will be redirected to InsetsController#{show,hide}. Therefore, we will lose flags like
- * SHOW_IMPLICIT.
+ * <p>Previously, an implicit show request when the IME is not previously shown,
+ * and it should be shown in fullscreen mode, would result in the IME not being shown.
*/
@Test
- @RequiresFlagsDisabled(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)
public void testShowSoftInputImplicitly_fullScreenMode() {
- setShowImeWithHardKeyboard(true /* enabled */);
+ setShowImeWithHardKeyboard(true /* enable */);
// Set orientation landscape to enable fullscreen mode.
setOrientation(2);
- eventually(() -> assertWithMessage("No longer in natural orientation")
- .that(mUiDevice.isNaturalOrientation()).isFalse());
- // Wait for the TestActivity to be recreated.
eventually(() -> assertWithMessage("Activity was re-created after rotation")
.that(TestActivity.getInstance()).isNotEqualTo(mActivity));
- // Get the new TestActivity.
mActivity = TestActivity.getInstance();
assertWithMessage("Re-created activity is not null").that(mActivity).isNotNull();
// Wait for the new EditText to be served by InputMethodManager.
eventually(() -> assertWithMessage("Has an input connection to the re-created Activity")
.that(mImm.hasActiveInputConnection(mActivity.getEditText())).isTrue());
- verifyInputViewStatusOnMainSync(() -> assertThat(
- mActivity.showImeWithInputMethodManager(InputMethodManager.SHOW_IMPLICIT)).isTrue(),
- EVENT_SHOW, false /* eventExpected */, false /* shown */, "IME is not shown");
+ if (mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) {
+ verifyInputViewStatusOnMainSync(() -> assertThat(
+ mActivity.showImeWithInputMethodManager(
+ InputMethodManager.SHOW_IMPLICIT))
+ .isTrue(),
+ EVENT_SHOW, true /* eventExpected */, true /* shown */, "IME is shown");
+ } else {
+ verifyInputViewStatusOnMainSync(() -> assertThat(
+ mActivity.showImeWithInputMethodManager(
+ InputMethodManager.SHOW_IMPLICIT))
+ .isTrue(),
+ EVENT_SHOW, false /* eventExpected */, false /* shown */, "IME is not shown");
+ }
}
/**
@@ -478,7 +423,7 @@ public class InputMethodServiceTest {
*/
@Test
public void testShowSoftInputExplicitly_withHardKeyboard() {
- setShowImeWithHardKeyboard(false /* enabled */);
+ setShowImeWithHardKeyboard(false /* enable */);
final var config = mInputMethodService.getResources().getConfiguration();
final var initialConfig = new Configuration(config);
@@ -497,17 +442,17 @@ public class InputMethodServiceTest {
}
/**
- * This checks that an implicit show request when a hardware keyboard is connected,
- * results in the IME not being shown.
+ * This checks that an implicit show request when a hardware keyboard is connected behaves
+ * like an explicit show request, resulting in the IME being shown. This is due to the
+ * refactor in b/298172246, causing us to lose flag information like
+ * {@link InputMethodManager#SHOW_IMPLICIT}.
*
- * <p>With the refactor in b/298172246, all calls from InputMethodManager#{show,hide}SoftInput
- * will be redirected to InsetsController#{show,hide}. Therefore, we will lose flags like
- * SHOW_IMPLICIT.
+ * <p>Previously, an implicit show request when a hardware keyboard is connected would
+ * result in the IME not being shown.
*/
@Test
- @RequiresFlagsDisabled(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)
public void testShowSoftInputImplicitly_withHardKeyboard() {
- setShowImeWithHardKeyboard(false /* enabled */);
+ setShowImeWithHardKeyboard(false /* enable */);
final var config = mInputMethodService.getResources().getConfiguration();
final var initialConfig = new Configuration(config);
@@ -516,10 +461,20 @@ public class InputMethodServiceTest {
config.keyboard = Configuration.KEYBOARD_QWERTY;
config.hardKeyboardHidden = Configuration.HARDKEYBOARDHIDDEN_YES;
- verifyInputViewStatusOnMainSync(() ->assertThat(
- mActivity.showImeWithInputMethodManager(InputMethodManager.SHOW_IMPLICIT))
- .isTrue(),
- EVENT_SHOW, false /* eventExpected */, false /* shown */, "IME is not shown");
+ if (mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) {
+ verifyInputViewStatusOnMainSync(() -> assertThat(
+ mActivity.showImeWithInputMethodManager(
+ InputMethodManager.SHOW_IMPLICIT))
+ .isTrue(),
+ EVENT_SHOW, true /* eventExpected */, true /* shown */, "IME is shown");
+ } else {
+ verifyInputViewStatusOnMainSync(() -> assertThat(
+ mActivity.showImeWithInputMethodManager(
+ InputMethodManager.SHOW_IMPLICIT))
+ .isTrue(),
+ EVENT_SHOW, false /* eventExpected */, false /* shown */,
+ "IME is not shown");
+ }
} finally {
mInputMethodService.getResources()
.updateConfiguration(initialConfig, null /* metrics */, null /* compat */);
@@ -532,7 +487,7 @@ public class InputMethodServiceTest {
*/
@Test
public void testShowSoftInputExplicitly_thenConfigurationChanged() {
- setShowImeWithHardKeyboard(false /* enabled */);
+ setShowImeWithHardKeyboard(false /* enable */);
final var config = mInputMethodService.getResources().getConfiguration();
final var initialConfig = new Configuration(config);
@@ -565,17 +520,17 @@ public class InputMethodServiceTest {
/**
* This checks that an implicit show request followed by connecting a hardware keyboard
- * and a configuration change, does not trigger IMS#onFinishInputView,
- * but results in the IME being hidden.
+ * and a configuration change behaves like an explicit show request, resulting in the IME
+ * still being shown. This is due to the refactor in b/298172246, causing us to lose flag
+ * information like {@link InputMethodManager#SHOW_IMPLICIT}.
*
- * <p>With the refactor in b/298172246, all calls from InputMethodManager#{show,hide}SoftInput
- * will be redirected to InsetsController#{show,hide}. Therefore, we will lose flags like
- * SHOW_IMPLICIT.
+ * <p>Previously, an implicit show request followed by connecting a hardware keyboard
+ * and a configuration change, would not trigger IMS#onFinishInputView, but resulted in the
+ * IME being hidden.
*/
@Test
- @RequiresFlagsDisabled(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)
public void testShowSoftInputImplicitly_thenConfigurationChanged() {
- setShowImeWithHardKeyboard(false /* enabled */);
+ setShowImeWithHardKeyboard(false /* enable */);
final var config = mInputMethodService.getResources().getConfiguration();
final var initialConfig = new Configuration(config);
@@ -596,16 +551,23 @@ public class InputMethodServiceTest {
// Simulate a fake configuration change to avoid the recreation of TestActivity.
config.orientation = Configuration.ORIENTATION_LANDSCAPE;
- // Normally, IMS#onFinishInputView will be called when finishing the input view by
- // the user. But if IMS#hideWindow is called when receiving a new configuration change,
- // we don't expect that it's user-driven to finish the lifecycle of input view with
- // IMS#onFinishInputView, because the input view will be re-initialized according
- // to the last #mShowInputRequested state. So in this case we treat the input view as
- // still alive.
- verifyInputViewStatusOnMainSync(
- () -> mInputMethodService.onConfigurationChanged(config),
- EVENT_CONFIG, true /* eventExpected */, true /* inputViewStarted */,
- false /* shown */, "IME is not shown after a configuration change");
+ if (mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) {
+ verifyInputViewStatusOnMainSync(
+ () -> mInputMethodService.onConfigurationChanged(config),
+ EVENT_CONFIG, true /* eventExpected */, true /* shown */,
+ "IME is still shown after a configuration change");
+ } else {
+ // Normally, IMS#onFinishInputView will be called when finishing the input view by
+ // the user. But if IMS#hideWindow is called when receiving a new configuration
+ // change, we don't expect that it's user-driven to finish the lifecycle of input
+ // view with IMS#onFinishInputView, because the input view will be re-initialized
+ // according to the last #mShowInputRequested state. So in this case we treat the
+ // input view as still alive.
+ verifyInputViewStatusOnMainSync(
+ () -> mInputMethodService.onConfigurationChanged(config),
+ EVENT_CONFIG, true /* eventExpected */, true /* inputViewStarted */,
+ false /* shown */, "IME is not shown after a configuration change");
+ }
} finally {
mInputMethodService.getResources()
.updateConfiguration(initialConfig, null /* metrics */, null /* compat */);
@@ -619,7 +581,7 @@ public class InputMethodServiceTest {
*/
@Test
public void testShowSoftInputExplicitly_thenShowSoftInputImplicitly_withHardKeyboard() {
- setShowImeWithHardKeyboard(false /* enabled */);
+ setShowImeWithHardKeyboard(false /* enable */);
final var config = mInputMethodService.getResources().getConfiguration();
final var initialConfig = new Configuration(config);
@@ -628,12 +590,10 @@ public class InputMethodServiceTest {
config.keyboard = Configuration.KEYBOARD_QWERTY;
config.hardKeyboardHidden = Configuration.HARDKEYBOARDHIDDEN_YES;
- // Explicit show request.
verifyInputViewStatusOnMainSync(() -> assertThat(
mActivity.showImeWithInputMethodManager(0 /* flags */)).isTrue(),
EVENT_SHOW, true /* eventExpected */, true /* shown */, "IME is shown");
- // Implicit show request.
verifyInputViewStatusOnMainSync(() -> assertThat(
mActivity.showImeWithInputMethodManager(
InputMethodManager.SHOW_IMPLICIT)).isTrue(),
@@ -654,17 +614,18 @@ public class InputMethodServiceTest {
/**
* This checks that a forced show request directly followed by an explicit show request,
- * and then a hide not always request, still results in the IME being shown
- * (i.e. the explicit show request retains the forced state).
+ * and then a not always hide request behaves like a normal hide request, resulting in the
+ * IME being hidden (i.e. the explicit show request does not retain the forced state). This is
+ * due to the refactor in b/298172246, causing us to lose flag information like
+ * {@link InputMethodManager#SHOW_FORCED}.
*
- * <p>With the refactor in b/298172246, all calls from InputMethodManager#{show,hide}SoftInput
- * will be redirected to InsetsController#{show,hide}. Therefore, we will lose flags like
- * HIDE_NOT_ALWAYS.
+ * <p>Previously, a forced show request directly followed by an explicit show request,
+ * and then a not always hide request, would result in the IME still being shown
+ * (i.e. the explicit show request would retain the forced state).
*/
@Test
- @RequiresFlagsDisabled(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)
public void testShowSoftInputForced_testShowSoftInputExplicitly_thenHideSoftInputNotAlways() {
- setShowImeWithHardKeyboard(true /* enabled */);
+ setShowImeWithHardKeyboard(true /* enable */);
verifyInputViewStatusOnMainSync(() -> assertThat(
mActivity.showImeWithInputMethodManager(InputMethodManager.SHOW_FORCED)).isTrue(),
@@ -674,11 +635,123 @@ public class InputMethodServiceTest {
mActivity.showImeWithInputMethodManager(0 /* flags */)).isTrue(),
EVENT_SHOW, false /* eventExpected */, true /* shown */, "IME is still shown");
- verifyInputViewStatusOnMainSync(() -> assertThat(
- mActivity.hideImeWithInputMethodManager(InputMethodManager.HIDE_NOT_ALWAYS))
- .isTrue(),
- EVENT_HIDE, false /* eventExpected */, true /* shown */,
- "IME is still shown after HIDE_NOT_ALWAYS");
+ if (mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) {
+ verifyInputViewStatusOnMainSync(() -> assertThat(mActivity
+ .hideImeWithInputMethodManager(InputMethodManager.HIDE_NOT_ALWAYS))
+ .isTrue(),
+ EVENT_HIDE, true /* eventExpected */, false /* shown */,
+ "IME is not shown after HIDE_NOT_ALWAYS");
+ } else {
+ verifyInputViewStatusOnMainSync(() -> assertThat(mActivity
+ .hideImeWithInputMethodManager(InputMethodManager.HIDE_NOT_ALWAYS))
+ .isTrue(),
+ EVENT_HIDE, false /* eventExpected */, true /* shown */,
+ "IME is still shown after HIDE_NOT_ALWAYS");
+ }
+ }
+
+ /**
+ * This checks that an explicit show request followed by an implicit only hide request
+ * behaves like a normal hide request, resulting in the IME being hidden. This is due to
+ * the refactor in b/298172246, causing us to lose flag information like
+ * {@link InputMethodManager#SHOW_IMPLICIT} and {@link InputMethodManager#HIDE_IMPLICIT_ONLY}.
+ *
+ * <p>Previously, an explicit show request followed by an implicit only hide request
+ * would result in the IME still being shown.
+ */
+ @Test
+ public void testShowSoftInputExplicitly_thenHideSoftInputImplicitOnly() {
+ setShowImeWithHardKeyboard(true /* enable */);
+
+ verifyInputViewStatusOnMainSync(
+ () -> mActivity.showImeWithInputMethodManager(0 /* flags */),
+ EVENT_SHOW, true /* eventExpected */, true /* shown */, "IME is shown");
+
+ if (mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) {
+ verifyInputViewStatusOnMainSync(
+ () -> mActivity.hideImeWithInputMethodManager(
+ InputMethodManager.HIDE_IMPLICIT_ONLY),
+ EVENT_HIDE, true /* eventExpected */, false /* shown */,
+ "IME is not shown after HIDE_IMPLICIT_ONLY");
+ } else {
+ verifyInputViewStatusOnMainSync(
+ () -> mActivity.hideImeWithInputMethodManager(
+ InputMethodManager.HIDE_IMPLICIT_ONLY),
+ EVENT_HIDE, false /* eventExpected */, true /* shown */,
+ "IME is still shown after HIDE_IMPLICIT_ONLY");
+ }
+ }
+
+ /**
+ * This checks that an implicit show request followed by an implicit only hide request
+ * results in the IME being hidden.
+ */
+ @Test
+ public void testShowSoftInputImplicitly_thenHideSoftInputImplicitOnly() {
+ setShowImeWithHardKeyboard(true /* enable */);
+
+ verifyInputViewStatusOnMainSync(
+ () -> mActivity.showImeWithInputMethodManager(InputMethodManager.SHOW_IMPLICIT),
+ EVENT_SHOW, true /* eventExpected */, true /* shown */,
+ "IME is shown with SHOW_IMPLICIT");
+
+ verifyInputViewStatusOnMainSync(
+ () -> mActivity.hideImeWithInputMethodManager(
+ InputMethodManager.HIDE_IMPLICIT_ONLY),
+ EVENT_HIDE, true /* eventExpected */, false /* shown */,
+ "IME is not shown after HIDE_IMPLICIT_ONLY");
+ }
+
+ /**
+ * This checks that an explicit show self request followed by an implicit only hide self request
+ * behaves like a normal hide self request, resulting in the IME being hidden. This is due to
+ * the refactor in b/298172246, causing us to lose flag information like
+ * {@link InputMethodManager#SHOW_IMPLICIT} and {@link InputMethodManager#HIDE_IMPLICIT_ONLY}.
+ *
+ * <p>Previously, an explicit show self request followed by an implicit only hide self request
+ * would result in the IME still being shown.
+ */
+ @Test
+ public void testShowSelfExplicitly_thenHideSelfImplicitOnly() {
+ setShowImeWithHardKeyboard(true /* enable */);
+
+ verifyInputViewStatusOnMainSync(
+ () -> mInputMethodService.requestShowSelf(0 /* flags */),
+ EVENT_SHOW, true /* eventExpected */, true /* shown */, "IME is shown");
+
+ if (mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) {
+ verifyInputViewStatusOnMainSync(
+ () -> mInputMethodService.requestHideSelf(
+ InputMethodManager.HIDE_IMPLICIT_ONLY),
+ EVENT_HIDE, true /* eventExpected */, false /* shown */,
+ "IME is not shown after HIDE_IMPLICIT_ONLY");
+ } else {
+ verifyInputViewStatusOnMainSync(
+ () -> mInputMethodService.requestHideSelf(
+ InputMethodManager.HIDE_IMPLICIT_ONLY),
+ EVENT_HIDE, false /* eventExpected */, true /* shown */,
+ "IME is still shown after HIDE_IMPLICIT_ONLY");
+ }
+ }
+
+ /**
+ * This checks that an implicit show self request followed by an implicit only hide self request
+ * results in the IME being hidden.
+ */
+ @Test
+ public void testShowSelfImplicitly_thenHideSelfImplicitOnly() {
+ setShowImeWithHardKeyboard(true /* enable */);
+
+ verifyInputViewStatusOnMainSync(
+ () -> mInputMethodService.requestShowSelf(InputMethodManager.SHOW_IMPLICIT),
+ EVENT_SHOW, true /* eventExpected */, true /* shown */,
+ "IME is shown with SHOW_IMPLICIT");
+
+ verifyInputViewStatusOnMainSync(
+ () -> mInputMethodService.requestHideSelf(
+ InputMethodManager.HIDE_IMPLICIT_ONLY),
+ EVENT_HIDE, true /* eventExpected */, false /* shown */,
+ "IME is not shown after HIDE_IMPLICIT_ONLY");
}
/**
@@ -686,7 +759,7 @@ public class InputMethodServiceTest {
*/
@Test
public void testFullScreenMode() {
- setShowImeWithHardKeyboard(true /* enabled */);
+ setShowImeWithHardKeyboard(true /* enable */);
Log.i(TAG, "Set orientation natural");
verifyFullscreenMode(() -> setOrientation(0), false /* eventExpected */,
@@ -723,25 +796,22 @@ public class InputMethodServiceTest {
public void testShowHideImeNavigationBar_doesDrawImeNavBar() {
assumeTrue("Must have a navigation bar", hasNavigationBar());
- setShowImeWithHardKeyboard(true /* enabled */);
+ setShowImeWithHardKeyboard(true /* enable */);
- // Show IME
verifyInputViewStatusOnMainSync(
() -> {
- setDrawsImeNavBarAndSwitcherButton(true /* enabled */);
+ setDrawsImeNavBarAndSwitcherButton(true /* enable */);
mActivity.showImeWithWindowInsetsController();
},
EVENT_SHOW, true /* eventExpected */, true /* shown */, "IME is shown");
assertWithMessage("IME navigation bar is initially shown")
.that(mInputMethodService.isImeNavigationBarShownForTesting()).isTrue();
- // Try to hide IME nav bar
mInstrumentation.runOnMainSync(() -> setShowImeNavigationBar(false /* show */));
mInstrumentation.waitForIdleSync();
assertWithMessage("IME navigation bar is not shown after hide request")
.that(mInputMethodService.isImeNavigationBarShownForTesting()).isFalse();
- // Try to show IME nav bar
mInstrumentation.runOnMainSync(() -> setShowImeNavigationBar(true /* show */));
mInstrumentation.waitForIdleSync();
assertWithMessage("IME navigation bar is shown after show request")
@@ -758,25 +828,22 @@ public class InputMethodServiceTest {
public void testShowHideImeNavigationBar_doesNotDrawImeNavBar() {
assumeTrue("Must have a navigation bar", hasNavigationBar());
- setShowImeWithHardKeyboard(true /* enabled */);
+ setShowImeWithHardKeyboard(true /* enable */);
- // Show IME
verifyInputViewStatusOnMainSync(
() -> {
- setDrawsImeNavBarAndSwitcherButton(false /* enabled */);
+ setDrawsImeNavBarAndSwitcherButton(false /* enable */);
mActivity.showImeWithWindowInsetsController();
},
EVENT_SHOW, true /* eventExpected */, true /* shown */, "IME is shown");
assertWithMessage("IME navigation bar is initially not shown")
.that(mInputMethodService.isImeNavigationBarShownForTesting()).isFalse();
- // Try to hide IME nav bar
mInstrumentation.runOnMainSync(() -> setShowImeNavigationBar(false /* show */));
mInstrumentation.waitForIdleSync();
assertWithMessage("IME navigation bar is not shown after hide request")
.that(mInputMethodService.isImeNavigationBarShownForTesting()).isFalse();
- // Try to show IME nav bar
mInstrumentation.runOnMainSync(() -> setShowImeNavigationBar(true /* show */));
mInstrumentation.waitForIdleSync();
assertWithMessage("IME navigation bar is not shown after show request")
@@ -792,7 +859,7 @@ public class InputMethodServiceTest {
waitUntilActivityReadyForInputInjection(mActivity);
- setShowImeWithHardKeyboard(true /* enabled */);
+ setShowImeWithHardKeyboard(true /* enable */);
try (var ignored = mGestureNavSwitchHelper.withGestureNavigationMode()) {
verifyInputViewStatusOnMainSync(
@@ -818,7 +885,7 @@ public class InputMethodServiceTest {
waitUntilActivityReadyForInputInjection(mActivity);
- setShowImeWithHardKeyboard(true /* enabled */);
+ setShowImeWithHardKeyboard(true /* enable */);
try (var ignored = mGestureNavSwitchHelper.withGestureNavigationMode()) {
verifyInputViewStatusOnMainSync(
@@ -844,7 +911,7 @@ public class InputMethodServiceTest {
waitUntilActivityReadyForInputInjection(mActivity);
- setShowImeWithHardKeyboard(true /* enabled */);
+ setShowImeWithHardKeyboard(true /* enable */);
final var info = mImm.getCurrentInputMethodInfo();
assertWithMessage("InputMethodInfo is not null").that(info).isNotNull();
@@ -855,7 +922,7 @@ public class InputMethodServiceTest {
try (var ignored = mGestureNavSwitchHelper.withGestureNavigationMode()) {
verifyInputViewStatusOnMainSync(
() -> {
- setDrawsImeNavBarAndSwitcherButton(true /* enabled */);
+ setDrawsImeNavBarAndSwitcherButton(true /* enable */);
mActivity.showImeWithWindowInsetsController();
},
EVENT_SHOW, true /* eventExpected */, true /* shown */, "IME is shown");
@@ -884,7 +951,7 @@ public class InputMethodServiceTest {
waitUntilActivityReadyForInputInjection(mActivity);
- setShowImeWithHardKeyboard(true /* enabled */);
+ setShowImeWithHardKeyboard(true /* enable */);
final var info = mImm.getCurrentInputMethodInfo();
assertWithMessage("InputMethodInfo is not null").that(info).isNotNull();
@@ -893,7 +960,7 @@ public class InputMethodServiceTest {
try (var ignored = mGestureNavSwitchHelper.withGestureNavigationMode()) {
verifyInputViewStatusOnMainSync(
() -> {
- setDrawsImeNavBarAndSwitcherButton(true /* enabled */);
+ setDrawsImeNavBarAndSwitcherButton(true /* enable */);
mActivity.showImeWithWindowInsetsController();
},
EVENT_SHOW, true /* eventExpected */, true /* shown */, "IME is shown");
@@ -956,7 +1023,8 @@ public class InputMethodServiceTest {
runnable.run();
}
mInstrumentation.waitForIdleSync();
- eventCalled = latch.await(TIMEOUT_IN_SECONDS, TimeUnit.SECONDS);
+ eventCalled = latch.await(eventExpected ? EXPECT_TIMEOUT_MS : NOT_EXCEPT_TIMEOUT_MS,
+ TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
fail("Interrupted while waiting for latch: " + e.getMessage());
return;
@@ -1016,10 +1084,8 @@ public class InputMethodServiceTest {
verifyInputViewStatus(runnable, EVENT_CONFIG, eventExpected, false /* shown */,
"IME is not shown");
if (eventExpected) {
- // Wait for the TestActivity to be recreated.
eventually(() -> assertWithMessage("Activity was re-created after rotation")
.that(TestActivity.getInstance()).isNotEqualTo(mActivity));
- // Get the new TestActivity.
mActivity = TestActivity.getInstance();
assertWithMessage("Re-created activity is not null").that(mActivity).isNotNull();
// Wait for the new EditText to be served by InputMethodManager.
@@ -1062,6 +1128,7 @@ public class InputMethodServiceTest {
private void prepareActivity() {
mActivity = TestActivity.startSync(mInstrumentation);
+ mInstrumentation.waitForIdleSync();
Log.i(TAG, "Finish preparing activity with editor.");
}
@@ -1086,21 +1153,51 @@ public class InputMethodServiceTest {
* @param enable the value to be set.
*/
private void setShowImeWithHardKeyboard(boolean enable) {
+ if (mInputMethodService == null) {
+ // If the IME is no longer around, reset the setting unconditionally.
+ executeShellCommand(SET_SHOW_IME_WITH_HARD_KEYBOARD_CMD + " " + (enable ? "1" : "0"));
+ return;
+ }
+
final boolean currentEnabled =
mInputMethodService.getShouldShowImeWithHardKeyboardForTesting();
if (currentEnabled != enable) {
- executeShellCommand(enable
- ? ENABLE_SHOW_IME_WITH_HARD_KEYBOARD_CMD
- : DISABLE_SHOW_IME_WITH_HARD_KEYBOARD_CMD);
+ executeShellCommand(SET_SHOW_IME_WITH_HARD_KEYBOARD_CMD + " " + (enable ? "1" : "0"));
eventually(() -> assertWithMessage("showImeWithHardKeyboard updated")
.that(mInputMethodService.getShouldShowImeWithHardKeyboardForTesting())
.isEqualTo(enable));
}
}
- private static void executeShellCommand(@NonNull String cmd) {
+ /**
+ * Gets the verbose logging state in {@link android.view.inputmethod.ImeTracker}.
+ *
+ * @return {@code true} iff verbose logging is enabled.
+ */
+ private static boolean getVerboseImeTrackerLogging() {
+ return executeShellCommand(GET_VERBOSE_IME_TRACKER_LOGGING_CMD).trim().equals("1");
+ }
+
+ /**
+ * Sets verbose logging in {@link android.view.inputmethod.ImeTracker}.
+ *
+ * @param enabled whether to enable or disable verbose logging.
+ *
+ * @implNote This must use {@link ActivityManager#notifySystemPropertiesChanged()} to listen
+ * for changes to the system property for the verbose ImeTracker logging.
+ */
+ private void setVerboseImeTrackerLogging(boolean enabled) {
+ final var context = mInstrumentation.getContext();
+ final var am = context.getSystemService(ActivityManager.class);
+
+ executeShellCommand(SET_VERBOSE_IME_TRACKER_LOGGING_CMD + " " + (enabled ? "1" : "0"));
+ am.notifySystemPropertiesChanged();
+ }
+
+ @NonNull
+ private static String executeShellCommand(@NonNull String cmd) {
Log.i(TAG, "Run command: " + cmd);
- SystemUtil.runShellCommandOrThrow(cmd);
+ return SystemUtil.runShellCommandOrThrow(cmd);
}
/**
@@ -1113,8 +1210,7 @@ public class InputMethodServiceTest {
@NonNull
private UiObject2 getUiObject(@NonNull BySelector bySelector) {
- final var uiObject = mUiDevice.wait(Until.findObject(bySelector),
- TimeUnit.SECONDS.toMillis(TIMEOUT_IN_SECONDS));
+ final var uiObject = mUiDevice.wait(Until.findObject(bySelector), TIMEOUT_MS);
assertWithMessage("UiObject with " + bySelector + " was found").that(uiObject).isNotNull();
return uiObject;
}
@@ -1137,10 +1233,10 @@ public class InputMethodServiceTest {
*
* <p>Note, neither of these are normally drawn when in three button navigation mode.
*
- * @param enabled whether the IME nav bar and IME Switcher button are drawn.
+ * @param enable whether the IME nav bar and IME Switcher button are drawn.
*/
- private void setDrawsImeNavBarAndSwitcherButton(boolean enabled) {
- final int flags = enabled ? IME_DRAWS_IME_NAV_BAR | SHOW_IME_SWITCHER_WHEN_IME_IS_SHOWN : 0;
+ private void setDrawsImeNavBarAndSwitcherButton(boolean enable) {
+ final int flags = enable ? IME_DRAWS_IME_NAV_BAR | SHOW_IME_SWITCHER_WHEN_IME_IS_SHOWN : 0;
mInputMethodService.getInputMethodInternal().onNavButtonFlagsChanged(flags);
}
diff --git a/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/src/com/android/apps/inputmethod/simpleime/ims/InputMethodServiceWrapper.java b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/src/com/android/apps/inputmethod/simpleime/ims/InputMethodServiceWrapper.java
index 558d1a7c4e8b..d4d4dcaa4f48 100644
--- a/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/src/com/android/apps/inputmethod/simpleime/ims/InputMethodServiceWrapper.java
+++ b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/src/com/android/apps/inputmethod/simpleime/ims/InputMethodServiceWrapper.java
@@ -111,12 +111,6 @@ public class InputMethodServiceWrapper extends InputMethodService {
}
@Override
- public void requestHideSelf(int flags) {
- Log.i(TAG, "requestHideSelf() " + flags);
- super.requestHideSelf(flags);
- }
-
- @Override
public void onConfigurationChanged(Configuration newConfig) {
Log.i(TAG, "onConfigurationChanged() " + newConfig);
super.onConfigurationChanged(newConfig);
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageParserTest.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageParserTest.java
index f5c0de034483..e1c65d27459e 100644
--- a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageParserTest.java
+++ b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageParserTest.java
@@ -65,10 +65,13 @@ import android.content.pm.SigningDetails;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
+import android.platform.test.annotations.DisableFlags;
+import android.platform.test.annotations.EnableFlags;
import android.platform.test.annotations.Presubmit;
import android.platform.test.annotations.RequiresFlagsEnabled;
import android.platform.test.flag.junit.CheckFlagsRule;
import android.platform.test.flag.junit.DeviceFlagsValueProvider;
+import android.platform.test.flag.junit.SetFlagsRule;
import android.util.ArraySet;
import androidx.annotation.Nullable;
@@ -150,6 +153,9 @@ public class PackageParserTest {
@Rule
public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+ @Rule
+ public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
private File mTmpDir;
private static final File FRAMEWORK = new File("/system/framework/framework-res.apk");
private static final String TEST_APP1_APK = "PackageParserTestApp1.apk";
@@ -846,7 +852,42 @@ public class PackageParserTest {
@Test
@RequiresFlagsEnabled(android.content.res.Flags.FLAG_MANIFEST_FLAGGING)
- public void testParseWithFeatureFlagAttributes() throws Exception {
+ @DisableFlags(android.content.res.Flags.FLAG_USE_NEW_ACONFIG_STORAGE)
+ public void testParseWithFeatureFlagAttributes_oldStorage() throws Exception {
+ final File testFile = extractFile(TEST_APP8_APK);
+ try (PackageParser2 parser = new TestPackageParser2()) {
+ Map<String, Boolean> flagValues = new HashMap<>();
+ flagValues.put("my.flag1", true);
+ flagValues.put("my.flag2", false);
+ flagValues.put("my.flag3", false);
+ flagValues.put("my.flag4", true);
+ ParsingPackageUtils.getAconfigFlags().addFlagValuesForTesting(flagValues);
+
+ // The manifest has:
+ // <permission android:name="PERM1" android:featureFlag="my.flag1 " />
+ // <permission android:name="PERM2" android:featureFlag=" !my.flag2" />
+ // <permission android:name="PERM3" android:featureFlag="my.flag3" />
+ // <permission android:name="PERM4" android:featureFlag="!my.flag4" />
+ // <permission android:name="PERM5" android:featureFlag="unknown.flag" />
+ // Therefore with the above flag values, only PERM1 and PERM2 should be present.
+
+ final ParsedPackage pkg = parser.parsePackage(testFile, 0, false);
+ List<String> permissionNames =
+ pkg.getPermissions().stream().map(ParsedComponent::getName).toList();
+ assertThat(permissionNames).contains(PACKAGE_NAME + ".PERM1");
+ assertThat(permissionNames).contains(PACKAGE_NAME + ".PERM2");
+ assertThat(permissionNames).doesNotContain(PACKAGE_NAME + ".PERM3");
+ assertThat(permissionNames).doesNotContain(PACKAGE_NAME + ".PERM4");
+ assertThat(permissionNames).doesNotContain(PACKAGE_NAME + ".PERM5");
+ } finally {
+ testFile.delete();
+ }
+ }
+
+ @Test
+ @RequiresFlagsEnabled(android.content.res.Flags.FLAG_MANIFEST_FLAGGING)
+ @EnableFlags(android.content.res.Flags.FLAG_USE_NEW_ACONFIG_STORAGE)
+ public void testParseWithFeatureFlagAttributes_newStorage() throws Exception {
final File testFile = extractFile(TEST_APP8_APK);
try (PackageParser2 parser = new TestPackageParser2()) {
Map<String, Boolean> flagValues = new HashMap<>();
diff --git a/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayMapperTest.java b/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayMapperTest.java
index 7d3cd8a8a9ae..38de7ce013c2 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayMapperTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayMapperTest.java
@@ -34,6 +34,7 @@ import static com.android.server.display.DisplayAdapter.DISPLAY_DEVICE_EVENT_REM
import static com.android.server.display.DisplayDeviceInfo.DIFF_EVERYTHING;
import static com.android.server.display.DisplayDeviceInfo.FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY;
import static com.android.server.display.LogicalDisplayMapper.LOGICAL_DISPLAY_EVENT_ADDED;
+import static com.android.server.display.LogicalDisplayMapper.LOGICAL_DISPLAY_EVENT_COMMITTED_STATE_CHANGED;
import static com.android.server.display.LogicalDisplayMapper.LOGICAL_DISPLAY_EVENT_CONNECTED;
import static com.android.server.display.LogicalDisplayMapper.LOGICAL_DISPLAY_EVENT_DISCONNECTED;
import static com.android.server.display.LogicalDisplayMapper.LOGICAL_DISPLAY_EVENT_REMOVED;
@@ -1180,13 +1181,21 @@ public class LogicalDisplayMapperTest {
assertEquals(LOGICAL_DISPLAY_EVENT_STATE_CHANGED,
mLogicalDisplayMapper.updateAndGetMaskForDisplayPropertyChanges(newDisplayInfo));
+ // Change the display committed state
+ when(mFlagsMock.isCommittedStateSeparateEventEnabled()).thenReturn(true);
+ newDisplayInfo = new DisplayInfo();
+ newDisplayInfo.committedState = STATE_OFF;
+ assertEquals(LOGICAL_DISPLAY_EVENT_COMMITTED_STATE_CHANGED,
+ mLogicalDisplayMapper.updateAndGetMaskForDisplayPropertyChanges(newDisplayInfo));
// Change multiple properties
newDisplayInfo = new DisplayInfo();
newDisplayInfo.refreshRateOverride = 30;
newDisplayInfo.state = STATE_OFF;
+ newDisplayInfo.committedState = STATE_OFF;
assertEquals(LOGICAL_DISPLAY_EVENT_REFRESH_RATE_CHANGED
- | LOGICAL_DISPLAY_EVENT_STATE_CHANGED,
+ | LOGICAL_DISPLAY_EVENT_STATE_CHANGED
+ | LOGICAL_DISPLAY_EVENT_COMMITTED_STATE_CHANGED,
mLogicalDisplayMapper.updateAndGetMaskForDisplayPropertyChanges(newDisplayInfo));
}
diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/JobStatusTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/JobStatusTest.java
index 2cd105ba5317..67b26c1c0b00 100644
--- a/services/tests/mockingservicestests/src/com/android/server/job/controllers/JobStatusTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/JobStatusTest.java
@@ -60,6 +60,8 @@ import android.content.ComponentName;
import android.content.pm.PackageManagerInternal;
import android.net.Uri;
import android.os.SystemClock;
+import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.SetFlagsRule;
import android.provider.MediaStore;
import android.util.SparseIntArray;
@@ -71,6 +73,7 @@ import com.android.server.job.JobSchedulerService;
import org.junit.After;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
@@ -92,6 +95,9 @@ public class JobStatusTest {
private static final Uri IMAGES_MEDIA_URI = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
private static final Uri VIDEO_MEDIA_URI = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
+ @Rule
+ public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
@Mock
private JobSchedulerInternal mJobSchedulerInternal;
private MockitoSession mMockingSession;
@@ -1373,6 +1379,86 @@ public class JobStatusTest {
assertEquals("@TestNamespace@TestTag:foo", jobStatus.getBatteryName());
}
+ @Test
+ @EnableFlags({
+ com.android.server.job.Flags.FLAG_INCLUDE_TRACE_TAG_IN_JOB_NAME,
+ android.app.job.Flags.FLAG_JOB_DEBUG_INFO_APIS
+ })
+ public void testJobName_NotTagNoNamespace_IncludeTraceTagInJobNameEnabled() {
+ JobInfo jobInfo = new JobInfo.Builder(101, new ComponentName("foo", "bar"))
+ .setTraceTag("TestTraceTag")
+ .build();
+ JobStatus jobStatus = createJobStatus(jobInfo, null, -1, null, null);
+ assertEquals("#TestTraceTag#foo/bar", jobStatus.getBatteryName());
+ }
+
+ @Test
+ @EnableFlags({
+ com.android.server.job.Flags.FLAG_INCLUDE_TRACE_TAG_IN_JOB_NAME,
+ android.app.job.Flags.FLAG_JOB_DEBUG_INFO_APIS
+ })
+ public void testJobName_NoTagWithNamespace_IncludeTraceTagInJobNameEnabled() {
+ JobInfo jobInfo = new JobInfo.Builder(101, new ComponentName("foo", "bar"))
+ .setTraceTag("TestTraceTag")
+ .build();
+ JobStatus jobStatus = createJobStatus(jobInfo, null, -1, "TestNamespace", null);
+ assertEquals("#TestTraceTag#@TestNamespace@foo/bar", jobStatus.getBatteryName());
+ }
+
+ @Test
+ @EnableFlags({
+ com.android.server.job.Flags.FLAG_INCLUDE_TRACE_TAG_IN_JOB_NAME,
+ android.app.job.Flags.FLAG_JOB_DEBUG_INFO_APIS
+ })
+ public void testJobName_WithTagNoNamespace_IncludeTraceTagInJobNameEnabled() {
+ JobInfo jobInfo = new JobInfo.Builder(101, new ComponentName("foo", "bar"))
+ .setTraceTag("TestTraceTag")
+ .build();
+ JobStatus jobStatus = createJobStatus(jobInfo, SOURCE_PACKAGE, 0, null, "TestTag");
+ assertEquals("#TestTraceTag#TestTag:foo", jobStatus.getBatteryName());
+ }
+
+ @Test
+ @EnableFlags({
+ com.android.server.job.Flags.FLAG_INCLUDE_TRACE_TAG_IN_JOB_NAME,
+ android.app.job.Flags.FLAG_JOB_DEBUG_INFO_APIS
+ })
+ public void testJobName_FilteredTraceTagEmail_IncludeTraceTagInJobNameEnabled() {
+ JobInfo jobInfo = new JobInfo.Builder(101, new ComponentName("foo", "bar"))
+ .setTraceTag("test@email.com")
+ .build();
+ JobStatus jobStatus = createJobStatus(jobInfo, SOURCE_PACKAGE, 0, null, "TestTag");
+ assertEquals("#[EMAIL]#TestTag:foo", jobStatus.getBatteryName());
+ }
+
+ @Test
+ @EnableFlags({
+ com.android.server.job.Flags.FLAG_INCLUDE_TRACE_TAG_IN_JOB_NAME,
+ android.app.job.Flags.FLAG_JOB_DEBUG_INFO_APIS
+ })
+ public void testJobName_FilteredTraceTagPhone_IncludeTraceTagInJobNameEnabled() {
+ JobInfo jobInfo = new JobInfo.Builder(101, new ComponentName("foo", "bar"))
+ .setTraceTag("123-456-7890")
+ .build();
+ JobStatus jobStatus = createJobStatus(jobInfo, SOURCE_PACKAGE, 0, null, "TestTag");
+ assertEquals("#[PHONE]#TestTag:foo", jobStatus.getBatteryName());
+ }
+
+ @Test
+ @EnableFlags({
+ com.android.server.job.Flags.FLAG_INCLUDE_TRACE_TAG_IN_JOB_NAME,
+ android.app.job.Flags.FLAG_JOB_DEBUG_INFO_APIS
+ })
+ public void testJobName_WithTagAndNamespace_IncludeTraceTagInJobNameEnabled() {
+ JobInfo jobInfo =
+ new JobInfo.Builder(101, new ComponentName("foo", "bar"))
+ .setTraceTag("TestTraceTag")
+ .build();
+ JobStatus jobStatus =
+ createJobStatus(jobInfo, SOURCE_PACKAGE, 0, "TestNamespace", "TestTag");
+ assertEquals("#TestTraceTag#@TestNamespace@TestTag:foo", jobStatus.getBatteryName());
+ }
+
private void markExpeditedQuotaApproved(JobStatus job, boolean isApproved) {
if (job.isRequestedExpeditedJob()) {
job.setExpeditedJobQuotaApproved(sElapsedRealtimeClock.millis(), isApproved);
diff --git a/services/tests/powerservicetests/src/com/android/server/power/NotifierTest.java b/services/tests/powerservicetests/src/com/android/server/power/NotifierTest.java
index 83a390d7f70b..4e56422ec391 100644
--- a/services/tests/powerservicetests/src/com/android/server/power/NotifierTest.java
+++ b/services/tests/powerservicetests/src/com/android/server/power/NotifierTest.java
@@ -437,6 +437,42 @@ public class NotifierTest {
}
@Test
+ public void testOnGroupChanged_perDisplayWakeByTouchEnabled() {
+ createNotifier();
+ // GIVEN per-display wake by touch is enabled and one display group has been defined with
+ // two displays
+ when(mPowerManagerFlags.isPerDisplayWakeByTouchEnabled()).thenReturn(true);
+ final int groupId = 121;
+ final int displayId1 = 1221;
+ final int displayId2 = 1222;
+ final int[] displays = new int[]{displayId1, displayId2};
+ when(mDisplayManagerInternal.getDisplayIds()).thenReturn(IntArray.wrap(displays));
+ when(mDisplayManagerInternal.getDisplayIdsForGroup(groupId)).thenReturn(displays);
+ SparseArray<int[]> displayIdsByGroupId = new SparseArray<>();
+ displayIdsByGroupId.put(groupId, displays);
+ when(mDisplayManagerInternal.getDisplayIdsByGroupsIds()).thenReturn(displayIdsByGroupId);
+ mNotifier.onGroupWakefulnessChangeStarted(
+ groupId, WAKEFULNESS_AWAKE, PowerManager.WAKE_REASON_TAP, /* eventTime= */ 1000);
+ final SparseBooleanArray expectedDisplayInteractivities = new SparseBooleanArray();
+ expectedDisplayInteractivities.put(displayId1, true);
+ expectedDisplayInteractivities.put(displayId2, true);
+ verify(mInputManagerInternal).setDisplayInteractivities(expectedDisplayInteractivities);
+
+ // WHEN display group is changed to only contain one display
+ SparseArray<int[]> newDisplayIdsByGroupId = new SparseArray<>();
+ newDisplayIdsByGroupId.put(groupId, new int[]{displayId1});
+ when(mDisplayManagerInternal.getDisplayIdsByGroupsIds()).thenReturn(newDisplayIdsByGroupId);
+ mNotifier.onGroupChanged();
+
+ // THEN native input manager is informed that the displays in the group have changed
+ final SparseBooleanArray expectedDisplayInteractivitiesAfterChange =
+ new SparseBooleanArray();
+ expectedDisplayInteractivitiesAfterChange.put(displayId1, true);
+ verify(mInputManagerInternal).setDisplayInteractivities(
+ expectedDisplayInteractivitiesAfterChange);
+ }
+
+ @Test
public void testOnWakeLockReleased_FrameworkStatsLogged_NoChains() {
when(mPowerManagerFlags.isMoveWscLoggingToNotifierEnabled()).thenReturn(true);
createNotifier();
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 29a17e1c85ab..ff6796561926 100644
--- a/services/tests/powerservicetests/src/com/android/server/power/PowerManagerServiceTest.java
+++ b/services/tests/powerservicetests/src/com/android/server/power/PowerManagerServiceTest.java
@@ -2501,6 +2501,49 @@ public class PowerManagerServiceTest {
}
@Test
+ public void testMultiDisplay_twoDisplays_onlyDefaultDisplayCanDream() {
+ final int nonDefaultDisplayGroupId = Display.DEFAULT_DISPLAY_GROUP + 1;
+ final int nonDefaultDisplay = Display.DEFAULT_DISPLAY + 1;
+ final AtomicReference<DisplayManagerInternal.DisplayGroupListener> listener =
+ new AtomicReference<>();
+ doAnswer((Answer<Void>) invocation -> {
+ listener.set(invocation.getArgument(0));
+ return null;
+ }).when(mDisplayManagerInternalMock).registerDisplayGroupListener(any());
+ final DisplayInfo info = new DisplayInfo();
+ info.displayGroupId = nonDefaultDisplayGroupId;
+ when(mDisplayManagerInternalMock.getDisplayInfo(nonDefaultDisplay)).thenReturn(info);
+ when(mBatteryManagerInternalMock.isPowered(anyInt())).thenReturn(true);
+ Settings.Secure.putInt(mContextSpy.getContentResolver(),
+ Settings.Secure.SCREENSAVER_ACTIVATE_ON_SLEEP, 1);
+ doAnswer(inv -> {
+ when(mDreamManagerInternalMock.isDreaming()).thenReturn(true);
+ return null;
+ }).when(mDreamManagerInternalMock).startDream(anyBoolean(), anyString());
+
+ setMinimumScreenOffTimeoutConfig(5);
+ createService();
+ startSystem();
+
+ listener.get().onDisplayGroupAdded(nonDefaultDisplayGroupId);
+
+ assertThat(mService.getWakefulnessLocked(Display.DEFAULT_DISPLAY_GROUP)).isEqualTo(
+ WAKEFULNESS_AWAKE);
+ assertThat(mService.getWakefulnessLocked(nonDefaultDisplayGroupId)).isEqualTo(
+ WAKEFULNESS_AWAKE);
+ assertThat(mService.getGlobalWakefulnessLocked()).isEqualTo(WAKEFULNESS_AWAKE);
+
+ advanceTime(15000);
+
+ // Only the default display group is dreaming.
+ assertThat(mService.getWakefulnessLocked(Display.DEFAULT_DISPLAY_GROUP)).isEqualTo(
+ WAKEFULNESS_DREAMING);
+ assertThat(mService.getWakefulnessLocked(nonDefaultDisplayGroupId)).isEqualTo(
+ WAKEFULNESS_DOZING);
+ assertThat(mService.getGlobalWakefulnessLocked()).isEqualTo(WAKEFULNESS_DREAMING);
+ }
+
+ @Test
public void testMultiDisplay_addNewDisplay_becomeGloballyAwakeButDefaultRemainsDozing() {
final int nonDefaultDisplayGroupId = Display.DEFAULT_DISPLAY_GROUP + 1;
final int nonDefaultDisplay = Display.DEFAULT_DISPLAY + 1;
diff --git a/services/tests/powerstatstests/res/raw/battery-history.zip b/services/tests/powerstatstests/res/raw/battery-history.zip
new file mode 100644
index 000000000000..ed82ac0f79cc
--- /dev/null
+++ b/services/tests/powerstatstests/res/raw/battery-history.zip
Binary files differ
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsProviderPerfTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsProviderPerfTest.java
new file mode 100644
index 000000000000..8fc8c9f677a6
--- /dev/null
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsProviderPerfTest.java
@@ -0,0 +1,179 @@
+/*
+ * 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 com.google.common.truth.Truth.assertThat;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.os.BatteryConsumer;
+import android.os.BatteryUsageStats;
+import android.os.BatteryUsageStatsQuery;
+import android.os.ConditionVariable;
+import android.os.FileUtils;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.perftests.utils.BenchmarkState;
+import android.perftests.utils.PerfStatusReporter;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.LargeTest;
+
+import com.android.internal.os.Clock;
+import com.android.internal.os.CpuScalingPolicies;
+import com.android.internal.os.CpuScalingPolicyReader;
+import com.android.internal.os.MonotonicClock;
+import com.android.internal.os.PowerProfile;
+import com.android.server.power.stats.processor.MultiStatePowerAttributor;
+
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.file.Files;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
+
+@RunWith(AndroidJUnit4.class)
+@LargeTest
+@android.platform.test.annotations.DisabledOnRavenwood(reason = "Performance test")
+@Ignore("Performance experiment. Comment out @Ignore to run")
+public class BatteryUsageStatsProviderPerfTest {
+ @Rule
+ public final PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter();
+
+ private final Clock mClock = new MockClock();
+ private MonotonicClock mMonotonicClock;
+ private PowerProfile mPowerProfile;
+ private CpuScalingPolicies mCpuScalingPolicies;
+ private File mDirectory;
+ private Handler mHandler;
+ private MockBatteryStatsImpl mBatteryStats;
+
+ @Before
+ public void setup() throws Exception {
+ Context context = InstrumentationRegistry.getContext();
+ mPowerProfile = new PowerProfile(context);
+ mCpuScalingPolicies = new CpuScalingPolicyReader().read();
+
+ HandlerThread mHandlerThread = new HandlerThread("batterystats-handler");
+ mHandlerThread.start();
+ mHandler = new Handler(mHandlerThread.getLooper());
+
+ // Extract accumulated battery history to ensure consistent iterations
+ mDirectory = Files.createTempDirectory("BatteryUsageStatsProviderPerfTest").toFile();
+ File historyDirectory = new File(mDirectory, "battery-history");
+ historyDirectory.mkdir();
+
+ long maxMonotonicTime = 0;
+
+ // To recreate battery-history.zip if necessary, perform these commands:
+ // cd /tmp
+ // mkdir battery-history
+ // adb pull /data/system/battery-history
+ // zip battery-history.zip battery-history/*
+ // cp battery-history.zip \
+ // $ANDROID_BUILD_TOP/frameworks/base/services/tests/powerstatstests/res/raw
+ Resources resources = context.getResources();
+ int resId = resources.getIdentifier("battery-history", "raw", context.getPackageName());
+ try (InputStream in = resources.openRawResource(resId)) {
+ try (ZipInputStream zis = new ZipInputStream(in)) {
+ ZipEntry ze;
+ while ((ze = zis.getNextEntry()) != null) {
+ if (!ze.getName().endsWith(".bh")) {
+ continue;
+ }
+ File file = new File(mDirectory, ze.getName());
+ try (OutputStream out = new FileOutputStream(
+ file)) {
+ FileUtils.copy(zis, out);
+ }
+ long timestamp = Long.parseLong(file.getName().replace(".bh", ""));
+ if (timestamp > maxMonotonicTime) {
+ maxMonotonicTime = timestamp;
+ }
+ }
+ }
+ }
+
+ mMonotonicClock = new MonotonicClock(maxMonotonicTime + 1000000000, mClock);
+ mBatteryStats = new MockBatteryStatsImpl(mClock, mDirectory);
+ }
+
+ @Test
+ public void getBatteryUsageStats_accumulated() {
+ BatteryUsageStatsQuery query = new BatteryUsageStatsQuery.Builder()
+ .setMaxStatsAgeMs(0)
+ .includePowerStateData()
+ .includeScreenStateData()
+ .includeProcessStateData()
+ .accumulated()
+ .build();
+
+ double expectedCpuPower = 0;
+ BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ while (state.keepRunning()) {
+ state.pauseTiming();
+
+ waitForBackgroundThread();
+
+ BatteryUsageStatsProvider provider = createBatteryUsageStatsProvider();
+ state.resumeTiming();
+
+ BatteryUsageStats stats = provider.getBatteryUsageStats(mBatteryStats, query);
+ waitForBackgroundThread();
+
+ state.pauseTiming();
+
+ double cpuConsumedPower = stats.getAggregateBatteryConsumer(
+ BatteryUsageStats.AGGREGATE_BATTERY_CONSUMER_SCOPE_DEVICE)
+ .getConsumedPower(BatteryConsumer.POWER_COMPONENT_CPU);
+ assertThat(cpuConsumedPower).isNonZero();
+ if (expectedCpuPower == 0) {
+ expectedCpuPower = cpuConsumedPower;
+ } else {
+ // Verify that all iterations produce the same result
+ assertThat(cpuConsumedPower).isEqualTo(expectedCpuPower);
+ }
+ state.resumeTiming();
+ }
+ }
+
+ private BatteryUsageStatsProvider createBatteryUsageStatsProvider() {
+ Context context = InstrumentationRegistry.getContext();
+
+ PowerStatsStore store = new PowerStatsStore(mDirectory, mHandler);
+ store.reset();
+
+ MultiStatePowerAttributor powerAttributor = new MultiStatePowerAttributor(context, store,
+ mPowerProfile, mCpuScalingPolicies, mPowerProfile::getBatteryCapacity);
+ return new BatteryUsageStatsProvider(context, powerAttributor, mPowerProfile,
+ mCpuScalingPolicies, store, 10000000, mClock, mMonotonicClock);
+ }
+
+ private void waitForBackgroundThread() {
+ ConditionVariable done = new ConditionVariable();
+ mHandler.post(done::open);
+ done.block();
+ }
+}
diff --git a/services/tests/security/intrusiondetection/src/com/android/server/security/intrusiondetection/IntrusionDetectionServiceTest.java b/services/tests/security/intrusiondetection/src/com/android/server/security/intrusiondetection/IntrusionDetectionServiceTest.java
index 298d27e2e8c4..879aa4893802 100644
--- a/services/tests/security/intrusiondetection/src/com/android/server/security/intrusiondetection/IntrusionDetectionServiceTest.java
+++ b/services/tests/security/intrusiondetection/src/com/android/server/security/intrusiondetection/IntrusionDetectionServiceTest.java
@@ -17,7 +17,6 @@
package com.android.server.security.intrusiondetection;
import static android.Manifest.permission.BIND_INTRUSION_DETECTION_EVENT_TRANSPORT_SERVICE;
-import static android.Manifest.permission.INTERNET;
import static android.Manifest.permission.MANAGE_INTRUSION_DETECTION_STATE;
import static android.Manifest.permission.READ_INTRUSION_DETECTION_STATE;
@@ -28,7 +27,6 @@ import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.spy;
@@ -37,8 +35,8 @@ import static org.mockito.Mockito.verify;
import android.annotation.SuppressLint;
import android.app.admin.ConnectEvent;
+import android.app.admin.DevicePolicyManagerInternal;
import android.app.admin.DnsEvent;
-import android.app.admin.SecurityLog;
import android.app.admin.SecurityLog.SecurityEvent;
import android.content.ComponentName;
import android.content.Context;
@@ -53,37 +51,22 @@ import android.os.test.TestLooper;
import android.security.intrusiondetection.IIntrusionDetectionServiceCommandCallback;
import android.security.intrusiondetection.IIntrusionDetectionServiceStateCallback;
import android.security.intrusiondetection.IntrusionDetectionEvent;
-import android.security.keystore.KeyGenParameterSpec;
-import android.security.keystore.KeyProperties;
import android.util.Log;
import androidx.test.core.app.ApplicationProvider;
import com.android.bedstead.harrier.BedsteadJUnit4;
import com.android.bedstead.multiuser.annotations.RequireRunOnSystemUser;
-import com.android.bedstead.nene.TestApis;
-import com.android.bedstead.nene.devicepolicy.DeviceOwner;
import com.android.bedstead.permissions.CommonPermissions;
-import com.android.bedstead.permissions.PermissionContext;
import com.android.bedstead.permissions.annotations.EnsureHasPermission;
import com.android.coretests.apps.testapp.LocalIntrusionDetectionEventTransport;
import com.android.server.ServiceThread;
import org.junit.Before;
-import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
-import org.mockito.invocation.InvocationOnMock;
-import org.mockito.stubbing.Answer;
-
-import java.io.IOException;
-import java.net.HttpURLConnection;
-import java.net.URL;
-import java.security.GeneralSecurityException;
-import java.security.KeyPair;
-import java.security.KeyPairGenerator;
-import java.security.KeyStore;
+
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
@@ -107,7 +90,6 @@ public class IntrusionDetectionServiceTest {
private static final int ERROR_DATA_SOURCE_UNAVAILABLE =
IIntrusionDetectionServiceCommandCallback.ErrorCode.DATA_SOURCE_UNAVAILABLE;
- private static DeviceOwner sDeviceOwner;
private Context mContext;
private IntrusionDetectionEventTransportConnection mIntrusionDetectionEventTransportConnection;
@@ -124,6 +106,8 @@ public class IntrusionDetectionServiceTest {
"com.android.coretests.apps.testapp";
private static final String TEST_SERVICE = TEST_PKG + ".TestLoggingService";
+ DevicePolicyManagerInternal mDevicePolicyManagerInternal;
+
@SuppressLint("VisibleForTests")
@Before
public void setUp() throws Exception {
@@ -189,6 +173,7 @@ public class IntrusionDetectionServiceTest {
}
@Test
+ @EnsureHasPermission(CommonPermissions.MANAGE_DEVICE_POLICY_AUDIT_LOGGING)
public void testAddStateCallback_Disabled_TwoStateCallbacks() throws RemoteException {
StateCallback scb1 = new StateCallback();
assertEquals(STATE_UNKNOWN, scb1.mState);
@@ -204,7 +189,7 @@ public class IntrusionDetectionServiceTest {
}
@Test
- @Ignore("Unit test does not run as system service UID")
+ @EnsureHasPermission(CommonPermissions.MANAGE_DEVICE_POLICY_AUDIT_LOGGING)
public void testRemoveStateCallback() throws RemoteException {
mIntrusionDetectionService.setState(STATE_DISABLED);
StateCallback scb1 = new StateCallback();
@@ -220,15 +205,19 @@ public class IntrusionDetectionServiceTest {
mIntrusionDetectionService.getBinderService().removeStateCallback(scb2);
CommandCallback ccb = new CommandCallback();
+
+ // Enable will fail; caller does not run as system server.
+ doNothing().when(mDataAggregator).enable();
mIntrusionDetectionService.getBinderService().enable(ccb);
+
mTestLooper.dispatchAll();
assertEquals(STATE_ENABLED, scb1.mState);
assertEquals(STATE_DISABLED, scb2.mState);
assertNull(ccb.mErrorCode);
}
- @Ignore("Unit test does not run as system service UID")
@Test
+ @EnsureHasPermission(CommonPermissions.MANAGE_DEVICE_POLICY_AUDIT_LOGGING)
public void testEnable_FromDisabled_TwoStateCallbacks() throws RemoteException {
mIntrusionDetectionService.setState(STATE_DISABLED);
StateCallback scb1 = new StateCallback();
@@ -243,6 +232,9 @@ public class IntrusionDetectionServiceTest {
CommandCallback ccb = new CommandCallback();
mIntrusionDetectionService.getBinderService().enable(ccb);
+
+ // Enable will fail; caller does not run as system server.
+ doNothing().when(mDataAggregator).enable();
mTestLooper.dispatchAll();
verify(mDataAggregator, times(1)).enable();
@@ -319,7 +311,7 @@ public class IntrusionDetectionServiceTest {
assertNull(ccb.mErrorCode);
}
- @Ignore("Enable once the IntrusionDetectionEventTransportConnection is ready")
+ @EnsureHasPermission(CommonPermissions.MANAGE_DEVICE_POLICY_AUDIT_LOGGING)
@Test
public void testEnable_FromDisable_TwoStateCallbacks_TransportUnavailable()
throws RemoteException {
@@ -390,146 +382,6 @@ public class IntrusionDetectionServiceTest {
}
@Test
- @Ignore("Unit test does not run as system service UID")
- @RequireRunOnSystemUser
- @EnsureHasPermission(CommonPermissions.MANAGE_DEVICE_POLICY_AUDIT_LOGGING)
- public void testDataAggregator_AddSecurityEvent() throws Exception {
- mIntrusionDetectionService.setState(STATE_ENABLED);
- ServiceThread mockThread = spy(ServiceThread.class);
- mDataAggregator.setHandler(mLooperOfDataAggregator, mockThread);
-
- // SecurityLogging generates a number of events and callbacks, so create a latch to wait for
- // the given event.
- String eventString = this.getClass().getName() + ".testSecurityEvent";
-
- final CountDownLatch latch = new CountDownLatch(1);
- // TODO: Replace this mock when the IntrusionDetectionEventTransportConnection is ready.
- doAnswer(
- new Answer<Boolean>() {
- @Override
- public Boolean answer(InvocationOnMock input) {
- List<IntrusionDetectionEvent> receivedEvents =
- (List<IntrusionDetectionEvent>) input.getArguments()[0];
- for (IntrusionDetectionEvent event : receivedEvents) {
- if (event.getType() == IntrusionDetectionEvent.SECURITY_EVENT) {
- SecurityEvent securityEvent = event.getSecurityEvent();
- Object[] eventData = (Object[]) securityEvent.getData();
- if (securityEvent.getTag() == SecurityLog.TAG_KEY_GENERATED
- && eventData[1].equals(eventString)) {
- latch.countDown();
- }
- }
- }
- return true;
- }
- })
- .when(mIntrusionDetectionEventTransportConnection).addData(any());
- mDataAggregator.enable();
-
- // Generate the security event.
- generateSecurityEvent(eventString);
- TestApis.devicePolicy().forceSecurityLogs();
-
- // Verify the event is received.
- mTestLooper.startAutoDispatch();
- assertTrue(latch.await(1, TimeUnit.SECONDS));
- mTestLooper.stopAutoDispatch();
-
- mDataAggregator.disable();
- }
-
- @Test
- @RequireRunOnSystemUser
- @Ignore("Unit test does not run as system service UID")
- @EnsureHasPermission(CommonPermissions.MANAGE_DEVICE_POLICY_AUDIT_LOGGING)
- public void testDataAggregator_AddNetworkEvent() throws Exception {
- mIntrusionDetectionService.setState(STATE_ENABLED);
- ServiceThread mockThread = spy(ServiceThread.class);
- mDataAggregator.setHandler(mLooperOfDataAggregator, mockThread);
-
- // Network logging may log multiple and callbacks, so create a latch to wait for
- // the given event.
- // eventServer must be a valid domain to generate a network log event.
- String eventServer = "google.com";
- final CountDownLatch latch = new CountDownLatch(1);
- // TODO: Replace this mock when the IntrusionDetectionEventTransportConnection is ready.
- doAnswer(
- new Answer<Boolean>() {
- @Override
- public Boolean answer(InvocationOnMock input) {
- List<IntrusionDetectionEvent> receivedEvents =
- (List<IntrusionDetectionEvent>) input.getArguments()[0];
- for (IntrusionDetectionEvent event : receivedEvents) {
- if (event.getType()
- == IntrusionDetectionEvent.NETWORK_EVENT_DNS) {
- DnsEvent dnsEvent = event.getDnsEvent();
- if (dnsEvent.getHostname().equals(eventServer)) {
- latch.countDown();
- }
- }
- }
- return true;
- }
- })
- .when(mIntrusionDetectionEventTransportConnection).addData(any());
- mDataAggregator.enable();
-
- // Generate the network event.
- generateNetworkEvent(eventServer);
- TestApis.devicePolicy().forceNetworkLogs();
-
- // Verify the event is received.
- mTestLooper.startAutoDispatch();
- assertTrue(latch.await(1, TimeUnit.SECONDS));
- mTestLooper.stopAutoDispatch();
-
- mDataAggregator.disable();
- }
-
- /** Emits a given string into security log (if enabled). */
- private void generateSecurityEvent(String eventString)
- throws IllegalArgumentException, GeneralSecurityException, IOException {
- if (eventString == null || eventString.isEmpty()) {
- throw new IllegalArgumentException(
- "Error generating security event: eventString must not be empty");
- }
-
- final KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA", "AndroidKeyStore");
- keyGen.initialize(
- new KeyGenParameterSpec.Builder(eventString, KeyProperties.PURPOSE_SIGN).build());
- // Emit key generation event.
- final KeyPair keyPair = keyGen.generateKeyPair();
- assertNotNull(keyPair);
-
- final KeyStore ks = KeyStore.getInstance("AndroidKeyStore");
- ks.load(null);
- // Emit key destruction event.
- ks.deleteEntry(eventString);
- }
-
- /** Emits a given string into network log (if enabled). */
- private void generateNetworkEvent(String server) throws IllegalArgumentException, IOException {
- if (server == null || server.isEmpty()) {
- throw new IllegalArgumentException(
- "Error generating network event: server must not be empty");
- }
-
- HttpURLConnection urlConnection = null;
- int connectionTimeoutMS = 2_000;
- try (PermissionContext p = TestApis.permissions().withPermission(INTERNET)) {
- final URL url = new URL("http://" + server);
- urlConnection = (HttpURLConnection) url.openConnection();
- urlConnection.setConnectTimeout(connectionTimeoutMS);
- urlConnection.setReadTimeout(connectionTimeoutMS);
- urlConnection.getResponseCode();
- } finally {
- if (urlConnection != null) {
- urlConnection.disconnect();
- }
- }
- }
-
- @Test
@RequireRunOnSystemUser
@EnsureHasPermission(
android.Manifest.permission.BIND_INTRUSION_DETECTION_EVENT_TRANSPORT_SERVICE)
diff --git a/services/tests/servicestests/src/com/android/server/audio/AudioDeviceInventoryTest.java b/services/tests/servicestests/src/com/android/server/audio/AudioDeviceInventoryTest.java
index b5a538fa09f8..c7da27420cbb 100644
--- a/services/tests/servicestests/src/com/android/server/audio/AudioDeviceInventoryTest.java
+++ b/services/tests/servicestests/src/com/android/server/audio/AudioDeviceInventoryTest.java
@@ -103,7 +103,7 @@ public class AudioDeviceInventoryTest {
// NOTE: for now this is only when flag asDeviceConnectionFailure is true
if (asDeviceConnectionFailure()) {
when(mSpyAudioSystem.setDeviceConnectionState(ada, AudioSystem.DEVICE_STATE_AVAILABLE,
- AudioSystem.AUDIO_FORMAT_DEFAULT))
+ AudioSystem.AUDIO_FORMAT_DEFAULT, false /*deviceSwitch*/))
.thenReturn(AudioSystem.AUDIO_STATUS_ERROR);
runWithBluetoothPrivilegedPermission(
() -> mDevInventory.onSetBtActiveDevice(/*btInfo*/ btInfo,
@@ -115,7 +115,7 @@ public class AudioDeviceInventoryTest {
// test that the device is added when AudioSystem returns AUDIO_STATUS_OK
// when setDeviceConnectionState is called for the connection
when(mSpyAudioSystem.setDeviceConnectionState(ada, AudioSystem.DEVICE_STATE_AVAILABLE,
- AudioSystem.AUDIO_FORMAT_DEFAULT))
+ AudioSystem.AUDIO_FORMAT_DEFAULT, false /*deviceSwitch*/))
.thenReturn(AudioSystem.AUDIO_STATUS_OK);
runWithBluetoothPrivilegedPermission(
() -> mDevInventory.onSetBtActiveDevice(/*btInfo*/ btInfo,
diff --git a/services/tests/servicestests/src/com/android/server/audio/NoOpAudioSystemAdapter.java b/services/tests/servicestests/src/com/android/server/audio/NoOpAudioSystemAdapter.java
index ce59a86c6ca3..39e7d727f7c5 100644
--- a/services/tests/servicestests/src/com/android/server/audio/NoOpAudioSystemAdapter.java
+++ b/services/tests/servicestests/src/com/android/server/audio/NoOpAudioSystemAdapter.java
@@ -51,9 +51,9 @@ public class NoOpAudioSystemAdapter extends AudioSystemAdapter {
// Overrides of AudioSystemAdapter
@Override
public int setDeviceConnectionState(AudioDeviceAttributes attributes, int state,
- int codecFormat) {
- Log.i(TAG, String.format("setDeviceConnectionState(0x%s, %d, 0x%s",
- attributes.toString(), state, Integer.toHexString(codecFormat)));
+ int codecFormat, boolean deviceSwitch) {
+ Log.i(TAG, String.format("setDeviceConnectionState(0x%s, %d, 0x%s %b",
+ attributes.toString(), state, Integer.toHexString(codecFormat), deviceSwitch));
return AudioSystem.AUDIO_STATUS_OK;
}
diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java
index 30aa8cebdff6..e0023e59af50 100644
--- a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java
@@ -1768,6 +1768,7 @@ public class DevicePolicyManagerTest extends DpmTestBase {
}
@Test
+ @Ignore // b/396073342
public void testCertificateDisclosure() throws Exception {
final int userId = CALLER_USER_HANDLE;
final UserHandle user = UserHandle.of(userId);
@@ -4612,6 +4613,7 @@ public class DevicePolicyManagerTest extends DpmTestBase {
}
@Test
+ @Ignore // b/396073342
public void testGetLastBugReportRequestTime() throws Exception {
mContext.binder.callingUid = DpmMockContext.CALLER_SYSTEM_USER_UID;
setupDeviceOwner();
@@ -4659,6 +4661,7 @@ public class DevicePolicyManagerTest extends DpmTestBase {
}
@Test
+ @Ignore // b/396073342
public void testGetLastNetworkLogRetrievalTime() throws Exception {
mContext.binder.callingUid = DpmMockContext.CALLER_SYSTEM_USER_UID;
setupDeviceOwner();
@@ -6441,6 +6444,7 @@ public class DevicePolicyManagerTest extends DpmTestBase {
}
@Test
+ @Ignore // b/396073342
public void testGetOwnerInstalledCaCertsForDeviceOwner() throws Exception {
mServiceContext.packageName = mRealTestContext.getPackageName();
mServiceContext.applicationInfo = new ApplicationInfo();
@@ -6452,6 +6456,7 @@ public class DevicePolicyManagerTest extends DpmTestBase {
}
@Test
+ @Ignore // b/396073342
public void testGetOwnerInstalledCaCertsForProfileOwner() throws Exception {
mServiceContext.packageName = mRealTestContext.getPackageName();
mServiceContext.applicationInfo = new ApplicationInfo();
@@ -6464,6 +6469,7 @@ public class DevicePolicyManagerTest extends DpmTestBase {
}
@Test
+ @Ignore // b/396073342
public void testGetOwnerInstalledCaCertsForDelegate() throws Exception {
mServiceContext.packageName = mRealTestContext.getPackageName();
mServiceContext.applicationInfo = new ApplicationInfo();
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/BaseAbsoluteVolumeBehaviorTest.java b/services/tests/servicestests/src/com/android/server/hdmi/BaseAbsoluteVolumeBehaviorTest.java
index fca0cfbc7d2f..cf2c15c5daca 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/BaseAbsoluteVolumeBehaviorTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/BaseAbsoluteVolumeBehaviorTest.java
@@ -432,7 +432,7 @@ public abstract class BaseAbsoluteVolumeBehaviorTest {
.setMaxVolumeIndex(AudioStatus.MAX_VOLUME)
.setMinVolumeIndex(AudioStatus.MIN_VOLUME)
.build()),
- any(), any(), anyBoolean());
+ anyBoolean(), any(), any());
}
@Test
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/BaseTvToAudioSystemAvbTest.java b/services/tests/servicestests/src/com/android/server/hdmi/BaseTvToAudioSystemAvbTest.java
index ec44a918f8e8..f44517a47f55 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/BaseTvToAudioSystemAvbTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/BaseTvToAudioSystemAvbTest.java
@@ -112,7 +112,7 @@ public abstract class BaseTvToAudioSystemAvbTest extends BaseAbsoluteVolumeBehav
.setMaxVolumeIndex(AudioStatus.MAX_VOLUME)
.setMinVolumeIndex(AudioStatus.MIN_VOLUME)
.build()),
- any(), any(), anyBoolean());
+ anyBoolean(), any(), any());
}
@@ -135,7 +135,7 @@ public abstract class BaseTvToAudioSystemAvbTest extends BaseAbsoluteVolumeBehav
.setMaxVolumeIndex(AudioStatus.MAX_VOLUME)
.setMinVolumeIndex(AudioStatus.MIN_VOLUME)
.build()),
- any(), any(), anyBoolean());
+ anyBoolean(), any(), any());
}
@Test
@@ -160,7 +160,7 @@ public abstract class BaseTvToAudioSystemAvbTest extends BaseAbsoluteVolumeBehav
.setMaxVolumeIndex(AudioStatus.MAX_VOLUME)
.setMinVolumeIndex(AudioStatus.MIN_VOLUME)
.build()),
- any(), any(), anyBoolean());
+ anyBoolean(), any(), any());
}
@Test
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/FakeAudioFramework.java b/services/tests/servicestests/src/com/android/server/hdmi/FakeAudioFramework.java
index 7294ba62cdae..90f94cb4b596 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/FakeAudioFramework.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/FakeAudioFramework.java
@@ -183,9 +183,9 @@ public class FakeAudioFramework {
public void setDeviceAbsoluteVolumeBehavior(
@NonNull AudioDeviceAttributes device,
@NonNull VolumeInfo volume,
+ boolean handlesVolumeAdjustment,
@NonNull @CallbackExecutor Executor executor,
- @NonNull OnAudioDeviceVolumeChangedListener vclistener,
- boolean handlesVolumeAdjustment) {
+ @NonNull OnAudioDeviceVolumeChangedListener vclistener) {
setVolumeBehaviorHelper(device, AudioManager.DEVICE_VOLUME_BEHAVIOR_ABSOLUTE);
}
@@ -193,9 +193,9 @@ public class FakeAudioFramework {
public void setDeviceAbsoluteVolumeAdjustOnlyBehavior(
@NonNull AudioDeviceAttributes device,
@NonNull VolumeInfo volume,
+ boolean handlesVolumeAdjustment,
@NonNull @CallbackExecutor Executor executor,
- @NonNull OnAudioDeviceVolumeChangedListener vclistener,
- boolean handlesVolumeAdjustment) {
+ @NonNull OnAudioDeviceVolumeChangedListener vclistener) {
setVolumeBehaviorHelper(device,
AudioManager.DEVICE_VOLUME_BEHAVIOR_ABSOLUTE_ADJUST_ONLY);
}
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java
index f74e2ace7ae3..563baacf5811 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java
@@ -66,6 +66,7 @@ import androidx.test.InstrumentationRegistry;
import androidx.test.filters.SmallTest;
import org.junit.Before;
+import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
@@ -1033,6 +1034,7 @@ public class HdmiCecLocalDeviceTvTest {
}
@Test
+ @Ignore("b/360768278")
public void onHotplug_doNotSend_systemAudioModeRequestWithParameter(){
// Add a device to the network and assert that this device is included in the list of
// devices.
diff --git a/services/tests/uiservicestests/Android.bp b/services/tests/uiservicestests/Android.bp
index 0eb20eb22380..66d7611a29c6 100644
--- a/services/tests/uiservicestests/Android.bp
+++ b/services/tests/uiservicestests/Android.bp
@@ -32,6 +32,7 @@ android_test {
"androidx.test.rules",
"hamcrest-library",
"mockito-target-inline-minus-junit4",
+ "mockito-target-extended",
"platform-compat-test-rules",
"platform-test-annotations",
"platformprotosnano",
diff --git a/services/tests/uiservicestests/src/android/app/NotificationManagerZenTest.java b/services/tests/uiservicestests/src/android/app/NotificationManagerZenTest.java
index 779fa1aa2f72..dbbe40fd42e6 100644
--- a/services/tests/uiservicestests/src/android/app/NotificationManagerZenTest.java
+++ b/services/tests/uiservicestests/src/android/app/NotificationManagerZenTest.java
@@ -80,7 +80,7 @@ public class NotificationManagerZenTest {
}
@Test
- @RequiresFlagsEnabled({Flags.FLAG_MODES_API, Flags.FLAG_MODES_UI})
+ @RequiresFlagsEnabled(Flags.FLAG_MODES_UI)
public void setAutomaticZenRuleState_manualActivation() {
AutomaticZenRule ruleToCreate = createZenRule("rule");
String ruleId = mNotificationManager.addAutomaticZenRule(ruleToCreate);
@@ -111,7 +111,7 @@ public class NotificationManagerZenTest {
}
@Test
- @RequiresFlagsEnabled({Flags.FLAG_MODES_API, Flags.FLAG_MODES_UI})
+ @RequiresFlagsEnabled(Flags.FLAG_MODES_UI)
public void setAutomaticZenRuleState_manualDeactivation() {
AutomaticZenRule ruleToCreate = createZenRule("rule");
String ruleId = mNotificationManager.addAutomaticZenRule(ruleToCreate);
@@ -145,7 +145,7 @@ public class NotificationManagerZenTest {
}
@Test
- @RequiresFlagsEnabled({Flags.FLAG_MODES_API, Flags.FLAG_MODES_UI})
+ @RequiresFlagsEnabled(Flags.FLAG_MODES_UI)
public void setAutomaticZenRuleState_respectsManuallyActivated() {
AutomaticZenRule ruleToCreate = createZenRule("rule");
String ruleId = mNotificationManager.addAutomaticZenRule(ruleToCreate);
@@ -178,7 +178,7 @@ public class NotificationManagerZenTest {
}
@Test
- @RequiresFlagsEnabled({Flags.FLAG_MODES_API, Flags.FLAG_MODES_UI})
+ @RequiresFlagsEnabled(Flags.FLAG_MODES_UI)
public void setAutomaticZenRuleState_respectsManuallyDeactivated() {
AutomaticZenRule ruleToCreate = createZenRule("rule");
String ruleId = mNotificationManager.addAutomaticZenRule(ruleToCreate);
@@ -212,7 +212,7 @@ public class NotificationManagerZenTest {
}
@Test
- @RequiresFlagsEnabled({Flags.FLAG_MODES_API, Flags.FLAG_MODES_UI})
+ @RequiresFlagsEnabled(Flags.FLAG_MODES_UI)
public void setAutomaticZenRuleState_manualActivationFromApp() {
AutomaticZenRule ruleToCreate = createZenRule("rule");
String ruleId = mNotificationManager.addAutomaticZenRule(ruleToCreate);
@@ -244,7 +244,7 @@ public class NotificationManagerZenTest {
}
@Test
- @RequiresFlagsEnabled({Flags.FLAG_MODES_API, Flags.FLAG_MODES_UI})
+ @RequiresFlagsEnabled(Flags.FLAG_MODES_UI)
public void setAutomaticZenRuleState_manualDeactivationFromApp() {
AutomaticZenRule ruleToCreate = createZenRule("rule");
String ruleId = mNotificationManager.addAutomaticZenRule(ruleToCreate);
diff --git a/services/tests/uiservicestests/src/com/android/server/UiModeManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/UiModeManagerServiceTest.java
index c4b8599a483c..9930c9f07ed8 100644
--- a/services/tests/uiservicestests/src/com/android/server/UiModeManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/UiModeManagerServiceTest.java
@@ -70,7 +70,6 @@ import android.Manifest;
import android.app.Activity;
import android.app.ActivityManager;
import android.app.AlarmManager;
-import android.app.Flags;
import android.app.IOnProjectionStateChangedListener;
import android.app.IUiModeManager;
import android.content.BroadcastReceiver;
@@ -91,7 +90,6 @@ import android.os.Process;
import android.os.RemoteException;
import android.os.UserHandle;
import android.os.test.FakePermissionEnforcer;
-import android.platform.test.annotations.EnableFlags;
import android.platform.test.flag.junit.SetFlagsRule;
import android.provider.Settings;
import android.service.dreams.DreamManagerInternal;
@@ -1508,13 +1506,11 @@ public class UiModeManagerServiceTest extends UiServiceTestCase {
}
@Test
- @EnableFlags(Flags.FLAG_MODES_API)
public void testAttentionModeThemeOverlay_nightModeDisabled() throws RemoteException {
testAttentionModeThemeOverlay(false);
}
@Test
- @EnableFlags(Flags.FLAG_MODES_API)
public void testAttentionModeThemeOverlay_nightModeEnabled() throws RemoteException {
testAttentionModeThemeOverlay(true);
}
diff --git a/services/tests/uiservicestests/src/com/android/server/UiServiceTestCase.java b/services/tests/uiservicestests/src/com/android/server/UiServiceTestCase.java
index b3ec2153542a..c9d5241c57b7 100644
--- a/services/tests/uiservicestests/src/com/android/server/UiServiceTestCase.java
+++ b/services/tests/uiservicestests/src/com/android/server/UiServiceTestCase.java
@@ -30,6 +30,7 @@ import android.testing.TestableContext;
import androidx.test.InstrumentationRegistry;
+import com.android.server.pm.UserManagerInternal;
import com.android.server.uri.UriGrantsManagerInternal;
import org.junit.After;
@@ -41,6 +42,7 @@ import org.mockito.MockitoAnnotations;
public class UiServiceTestCase {
@Mock protected PackageManagerInternal mPmi;
+ @Mock protected UserManagerInternal mUmi;
@Mock protected UriGrantsManagerInternal mUgmInternal;
protected static final String PKG_N_MR1 = "com.example.n_mr1";
@@ -92,6 +94,8 @@ public class UiServiceTestCase {
}
});
+ LocalServices.removeServiceForTest(UserManagerInternal.class);
+ LocalServices.addService(UserManagerInternal.class, mUmi);
LocalServices.removeServiceForTest(UriGrantsManagerInternal.class);
LocalServices.addService(UriGrantsManagerInternal.class, mUgmInternal);
when(mUgmInternal.checkGrantUriPermission(
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/DefaultDeviceEffectsApplierTest.java b/services/tests/uiservicestests/src/com/android/server/notification/DefaultDeviceEffectsApplierTest.java
index 1890879da69d..5ce9a3e8d4d4 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/DefaultDeviceEffectsApplierTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/DefaultDeviceEffectsApplierTest.java
@@ -47,7 +47,6 @@ import android.content.Intent;
import android.content.IntentFilter;
import android.hardware.display.ColorDisplayManager;
import android.os.PowerManager;
-import android.platform.test.annotations.EnableFlags;
import android.platform.test.flag.junit.SetFlagsRule;
import android.service.notification.ZenDeviceEffects;
import android.testing.TestableContext;
@@ -102,8 +101,6 @@ public class DefaultDeviceEffectsApplierTest {
@Test
public void apply_appliesEffects() {
- mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
-
ZenDeviceEffects effects = new ZenDeviceEffects.Builder()
.setShouldSuppressAmbientDisplay(true)
.setShouldDimWallpaper(true)
@@ -119,7 +116,6 @@ public class DefaultDeviceEffectsApplierTest {
}
@Test
- @EnableFlags(android.app.Flags.FLAG_MODES_API)
public void apply_logsToZenLog() {
when(mPowerManager.isInteractive()).thenReturn(true);
ArgumentCaptor<BroadcastReceiver> broadcastReceiverCaptor =
@@ -155,8 +151,6 @@ public class DefaultDeviceEffectsApplierTest {
@Test
public void apply_removesEffects() {
- mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
-
ZenDeviceEffects previousEffects = new ZenDeviceEffects.Builder()
.setShouldSuppressAmbientDisplay(true)
.setShouldDimWallpaper(true)
@@ -180,8 +174,6 @@ public class DefaultDeviceEffectsApplierTest {
@Test
public void apply_removesOnlyPreviouslyAppliedEffects() {
- mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
-
ZenDeviceEffects previousEffects = new ZenDeviceEffects.Builder()
.setShouldSuppressAmbientDisplay(true)
.build();
@@ -197,7 +189,6 @@ public class DefaultDeviceEffectsApplierTest {
@Test
public void apply_missingSomeServices_okay() {
- mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
mContext.addMockSystemService(ColorDisplayManager.class, null);
mContext.addMockSystemService(WallpaperManager.class, null);
mApplier = new DefaultDeviceEffectsApplier(mContext);
@@ -216,7 +207,6 @@ public class DefaultDeviceEffectsApplierTest {
@Test
public void apply_disabledWallpaperService_dimWallpaperNotApplied() {
- mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
WallpaperManager disabledWallpaperService = mock(WallpaperManager.class);
when(mWallpaperManager.isWallpaperSupported()).thenReturn(false);
mContext.addMockSystemService(WallpaperManager.class, disabledWallpaperService);
@@ -236,8 +226,6 @@ public class DefaultDeviceEffectsApplierTest {
@Test
public void apply_someEffects_onlyThoseEffectsApplied() {
- mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
-
ZenDeviceEffects effects = new ZenDeviceEffects.Builder()
.setShouldDimWallpaper(true)
.setShouldDisplayGrayscale(true)
@@ -253,8 +241,6 @@ public class DefaultDeviceEffectsApplierTest {
@Test
public void apply_onlyEffectDeltaApplied() {
- mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
-
mApplier.apply(new ZenDeviceEffects.Builder().setShouldDimWallpaper(true).build(),
ORIGIN_USER_IN_SYSTEMUI);
verify(mWallpaperManager).setWallpaperDimAmount(eq(0.6f));
@@ -272,7 +258,6 @@ public class DefaultDeviceEffectsApplierTest {
@Test
public void apply_nightModeFromApp_appliedOnScreenOff() {
- mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
ArgumentCaptor<BroadcastReceiver> broadcastReceiverCaptor =
ArgumentCaptor.forClass(BroadcastReceiver.class);
ArgumentCaptor<IntentFilter> intentFilterCaptor =
@@ -301,8 +286,6 @@ public class DefaultDeviceEffectsApplierTest {
@Test
public void apply_nightModeWithScreenOff_appliedImmediately(
@TestParameter ZenChangeOrigin origin) {
- mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
-
when(mPowerManager.isInteractive()).thenReturn(false);
mApplier.apply(new ZenDeviceEffects.Builder().setShouldUseNightMode(true).build(),
@@ -314,7 +297,6 @@ public class DefaultDeviceEffectsApplierTest {
}
@Test
- @EnableFlags({android.app.Flags.FLAG_MODES_API, android.app.Flags.FLAG_MODES_UI})
public void apply_nightModeWithScreenOnAndKeyguardShowing_appliedImmediately(
@TestParameter ZenChangeOrigin origin) {
@@ -334,8 +316,6 @@ public class DefaultDeviceEffectsApplierTest {
"{origin: ORIGIN_INIT}", "{origin: ORIGIN_INIT_USER}"})
public void apply_nightModeWithScreenOn_appliedImmediatelyBasedOnOrigin(
ZenChangeOrigin origin) {
- mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
-
when(mPowerManager.isInteractive()).thenReturn(true);
mApplier.apply(new ZenDeviceEffects.Builder().setShouldUseNightMode(true).build(),
@@ -351,8 +331,6 @@ public class DefaultDeviceEffectsApplierTest {
"{origin: ORIGIN_SYSTEM}", "{origin: ORIGIN_UNKNOWN}"})
public void apply_nightModeWithScreenOn_willBeAppliedLaterBasedOnOrigin(
ZenChangeOrigin origin) {
- mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
-
when(mPowerManager.isInteractive()).thenReturn(true);
mApplier.apply(new ZenDeviceEffects.Builder().setShouldUseNightMode(true).build(),
@@ -367,8 +345,6 @@ public class DefaultDeviceEffectsApplierTest {
@Test
public void apply_servicesThrow_noCrash() {
- mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
-
doThrow(new RuntimeException()).when(mPowerManager)
.suppressAmbientDisplay(anyString(), anyBoolean());
doThrow(new RuntimeException()).when(mColorDisplayManager).setSaturationLevel(anyInt());
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java
index e5c42082ab97..98440ecdad82 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java
@@ -17,12 +17,17 @@ package com.android.server.notification;
import static android.content.Context.DEVICE_POLICY_SERVICE;
import static android.app.Flags.FLAG_LIFETIME_EXTENSION_REFACTOR;
+import static android.os.UserHandle.USER_ALL;
+import static android.os.UserHandle.USER_CURRENT;
import static android.os.UserManager.USER_TYPE_FULL_SECONDARY;
import static android.os.UserManager.USER_TYPE_PROFILE_CLONE;
import static android.os.UserManager.USER_TYPE_PROFILE_MANAGED;
import static android.os.UserManager.USER_TYPE_PROFILE_PRIVATE;
import static android.service.notification.NotificationListenerService.META_DATA_DEFAULT_AUTOBIND;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
+import static com.android.server.notification.Flags.FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER;
+import static com.android.server.notification.Flags.managedServicesConcurrentMultiuser;
import static com.android.server.notification.ManagedServices.APPROVAL_BY_COMPONENT;
import static com.android.server.notification.ManagedServices.APPROVAL_BY_PACKAGE;
import static com.android.server.notification.NotificationManagerService.privateSpaceFlagsEnabled;
@@ -66,7 +71,9 @@ import android.os.IInterface;
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.provider.Settings;
import android.text.TextUtils;
import android.util.ArrayMap;
@@ -83,6 +90,7 @@ import com.android.server.UiServiceTestCase;
import com.google.android.collect.Lists;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
@@ -105,6 +113,9 @@ import java.util.concurrent.CountDownLatch;
public class ManagedServicesTest extends UiServiceTestCase {
+ @Rule
+ public SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
@Mock
private IPackageManager mIpm;
@Mock
@@ -155,6 +166,7 @@ public class ManagedServicesTest extends UiServiceTestCase {
users.add(new UserInfo(11, "11", 0));
users.add(new UserInfo(12, "12", 0));
users.add(new UserInfo(13, "13", 0));
+ users.add(new UserInfo(99, "99", 0));
for (UserInfo user : users) {
when(mUm.getUserInfo(eq(user.id))).thenReturn(user);
}
@@ -804,6 +816,7 @@ public class ManagedServicesTest extends UiServiceTestCase {
}
@Test
+ @DisableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER)
public void rebindServices_onlyBindsExactMatchesIfComponent() throws Exception {
// If the primary and secondary lists contain component names, only those components within
// the package should be matched
@@ -841,6 +854,45 @@ public class ManagedServicesTest extends UiServiceTestCase {
}
@Test
+ @EnableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER)
+ public void rebindServices_onlyBindsExactMatchesIfComponent_concurrent_multiUser()
+ throws Exception {
+ // If the primary and secondary lists contain component names, only those components within
+ // the package should be matched
+ ManagedServices service = new TestManagedServices(getContext(), mLock, mUserProfiles,
+ mIpm,
+ ManagedServices.APPROVAL_BY_COMPONENT);
+
+ List<String> packages = new ArrayList<>();
+ packages.add("package");
+ packages.add("anotherPackage");
+ addExpectedServices(service, packages, 0);
+
+ // only 2 components are approved per package
+ mExpectedPrimaryComponentNames.clear();
+ mExpectedPrimaryComponentNames.put(0, "package/C1:package/C2");
+ mExpectedSecondaryComponentNames.clear();
+ mExpectedSecondaryComponentNames.put(0, "anotherPackage/C1:anotherPackage/C2");
+
+ loadXml(service);
+ // verify the 2 components per package are enabled (bound)
+ verifyExpectedBoundEntries(service, true, 0);
+ verifyExpectedBoundEntries(service, false, 0);
+
+ // verify the last component per package is not enabled/we don't try to bind to it
+ for (String pkg : packages) {
+ ComponentName unapprovedAdditionalComponent =
+ ComponentName.unflattenFromString(pkg + "/C3");
+ assertFalse(
+ service.isComponentEnabledForUser(
+ unapprovedAdditionalComponent, 0));
+ verify(mIpm, never()).getServiceInfo(
+ eq(unapprovedAdditionalComponent), anyLong(), anyInt());
+ }
+ }
+
+ @Test
+ @DisableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER)
public void rebindServices_bindsEverythingInAPackage() throws Exception {
// If the primary and secondary lists contain packages, all components within those packages
// should be bound
@@ -866,6 +918,32 @@ public class ManagedServicesTest extends UiServiceTestCase {
}
@Test
+ @EnableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER)
+ public void rebindServices_bindsEverythingInAPackage_concurrent_multiUser() throws Exception {
+ // If the primary and secondary lists contain packages, all components within those packages
+ // should be bound
+ ManagedServices service = new TestManagedServices(getContext(), mLock, mUserProfiles, mIpm,
+ APPROVAL_BY_PACKAGE);
+
+ List<String> packages = new ArrayList<>();
+ packages.add("package");
+ packages.add("packagea");
+ addExpectedServices(service, packages, 0);
+
+ // 2 approved packages
+ mExpectedPrimaryPackages.clear();
+ mExpectedPrimaryPackages.put(0, "package");
+ mExpectedSecondaryPackages.clear();
+ mExpectedSecondaryPackages.put(0, "packagea");
+
+ loadXml(service);
+
+ // verify the 3 components per package are enabled (bound)
+ verifyExpectedBoundEntries(service, true, 0);
+ verifyExpectedBoundEntries(service, false, 0);
+ }
+
+ @Test
public void reregisterService_checksAppIsApproved_pkg() throws Exception {
Context context = mock(Context.class);
PackageManager pm = mock(PackageManager.class);
@@ -1118,6 +1196,7 @@ public class ManagedServicesTest extends UiServiceTestCase {
}
@Test
+ @DisableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER)
public void testUpgradeAppBindsNewServices() throws Exception {
// If the primary and secondary lists contain component names, only those components within
// the package should be matched
@@ -1159,6 +1238,49 @@ public class ManagedServicesTest extends UiServiceTestCase {
}
@Test
+ @EnableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER)
+ public void testUpgradeAppBindsNewServices_concurrent_multiUser() throws Exception {
+ // If the primary and secondary lists contain component names, only those components within
+ // the package should be matched
+ ManagedServices service = new TestManagedServices(getContext(), mLock, mUserProfiles,
+ mIpm,
+ ManagedServices.APPROVAL_BY_PACKAGE);
+
+ List<String> packages = new ArrayList<>();
+ packages.add("package");
+ addExpectedServices(service, packages, 0);
+
+ // only 2 components are approved per package
+ mExpectedPrimaryComponentNames.clear();
+ mExpectedPrimaryPackages.clear();
+ mExpectedPrimaryComponentNames.put(0, "package/C1:package/C2");
+ mExpectedSecondaryComponentNames.clear();
+ mExpectedSecondaryPackages.clear();
+
+ loadXml(service);
+
+ // new component expected
+ mExpectedPrimaryComponentNames.put(0, "package/C1:package/C2:package/C3");
+
+ service.onPackagesChanged(false, new String[]{"package"}, new int[]{0});
+
+ // verify the 3 components per package are enabled (bound)
+ verifyExpectedBoundEntries(service, true, 0);
+
+ // verify the last component per package is not enabled/we don't try to bind to it
+ for (String pkg : packages) {
+ ComponentName unapprovedAdditionalComponent =
+ ComponentName.unflattenFromString(pkg + "/C3");
+ assertFalse(
+ service.isComponentEnabledForUser(
+ unapprovedAdditionalComponent, 0));
+ verify(mIpm, never()).getServiceInfo(
+ eq(unapprovedAdditionalComponent), anyLong(), anyInt());
+ }
+ }
+
+ @Test
+ @DisableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER)
public void testUpgradeAppNoPermissionNoRebind() throws Exception {
Context context = spy(getContext());
doReturn(true).when(context).bindServiceAsUser(any(), any(), anyInt(), any());
@@ -1211,6 +1333,59 @@ public class ManagedServicesTest extends UiServiceTestCase {
}
@Test
+ @EnableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER)
+ public void testUpgradeAppNoPermissionNoRebind_concurrent_multiUser() throws Exception {
+ Context context = spy(getContext());
+ doReturn(true).when(context).bindServiceAsUser(any(), any(), anyInt(), any());
+
+ ManagedServices service = new TestManagedServices(context, mLock, mUserProfiles,
+ mIpm,
+ APPROVAL_BY_COMPONENT);
+
+ List<String> packages = new ArrayList<>();
+ packages.add("package");
+ addExpectedServices(service, packages, 0);
+
+ final ComponentName unapprovedComponent = ComponentName.unflattenFromString("package/C1");
+ final ComponentName approvedComponent = ComponentName.unflattenFromString("package/C2");
+
+ // Both components are approved initially
+ mExpectedPrimaryComponentNames.clear();
+ mExpectedPrimaryPackages.clear();
+ mExpectedPrimaryComponentNames.put(0, "package/C1:package/C2");
+ mExpectedSecondaryComponentNames.clear();
+ mExpectedSecondaryPackages.clear();
+
+ loadXml(service);
+
+ //Component package/C1 loses bind permission
+ when(mIpm.getServiceInfo(any(), anyLong(), anyInt())).thenAnswer(
+ (Answer<ServiceInfo>) invocation -> {
+ ComponentName invocationCn = invocation.getArgument(0);
+ if (invocationCn != null) {
+ ServiceInfo serviceInfo = new ServiceInfo();
+ serviceInfo.packageName = invocationCn.getPackageName();
+ serviceInfo.name = invocationCn.getClassName();
+ if (invocationCn.equals(unapprovedComponent)) {
+ serviceInfo.permission = "none";
+ } else {
+ serviceInfo.permission = service.getConfig().bindPermission;
+ }
+ serviceInfo.metaData = null;
+ return serviceInfo;
+ }
+ return null;
+ }
+ );
+
+ // Trigger package update
+ service.onPackagesChanged(false, new String[]{"package"}, new int[]{0});
+
+ assertFalse(service.isComponentEnabledForUser(unapprovedComponent, 0));
+ assertTrue(service.isComponentEnabledForUser(approvedComponent, 0));
+ }
+
+ @Test
public void testSetPackageOrComponentEnabled() throws Exception {
for (int approvalLevel : new int[] {APPROVAL_BY_COMPONENT, APPROVAL_BY_PACKAGE}) {
ManagedServices service = new TestManagedServices(getContext(), mLock, mUserProfiles,
@@ -1517,6 +1692,201 @@ public class ManagedServicesTest extends UiServiceTestCase {
assertTrue(componentsToBind.get(10).contains(ComponentName.unflattenFromString("c/c")));
}
+ @SuppressWarnings("GuardedBy")
+ @Test
+ @EnableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER)
+ public void testPopulateComponentsToBindWithNonProfileUser() {
+ ManagedServices service = new TestManagedServices(getContext(), mLock, mUserProfiles, mIpm,
+ APPROVAL_BY_COMPONENT);
+ spyOn(service);
+
+ SparseArray<ArraySet<ComponentName>> approvedComponentsByUser = new SparseArray<>();
+ ArraySet<ComponentName> allowed0 = new ArraySet<>();
+ allowed0.add(ComponentName.unflattenFromString("a/a"));
+ approvedComponentsByUser.put(0, allowed0);
+ ArraySet<ComponentName> allowed10 = new ArraySet<>();
+ allowed10.add(ComponentName.unflattenFromString("b/b"));
+ approvedComponentsByUser.put(10, allowed10);
+
+ int nonProfileUser = 99;
+ ArraySet<ComponentName> allowedForNonProfileUser = new ArraySet<>();
+ allowedForNonProfileUser.add(ComponentName.unflattenFromString("c/c"));
+ approvedComponentsByUser.put(nonProfileUser, allowedForNonProfileUser);
+
+ IntArray users = new IntArray();
+ users.add(nonProfileUser);
+ users.add(10);
+ users.add(0);
+
+ SparseArray<Set<ComponentName>> componentsToBind = new SparseArray<>();
+ spyOn(service.mUmInternal);
+ when(service.mUmInternal.isVisibleBackgroundFullUser(nonProfileUser)).thenReturn(true);
+
+ service.populateComponentsToBind(componentsToBind, users, approvedComponentsByUser);
+
+ assertTrue(service.isComponentEnabledForUser(
+ ComponentName.unflattenFromString("a/a"), 0));
+ assertTrue(service.isComponentEnabledForPackage("a", 0));
+ assertTrue(service.isComponentEnabledForUser(
+ ComponentName.unflattenFromString("b/b"), 10));
+ assertTrue(service.isComponentEnabledForPackage("b", 0));
+ assertTrue(service.isComponentEnabledForPackage("b", 10));
+ assertTrue(service.isComponentEnabledForUser(
+ ComponentName.unflattenFromString("c/c"), nonProfileUser));
+ assertTrue(service.isComponentEnabledForPackage("c", nonProfileUser));
+ }
+
+
+ @Test
+ @EnableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER)
+ public void testRebindService_profileUser() throws Exception {
+ final int profileUserId = 10;
+ when(mUserProfiles.isProfileUser(profileUserId, mContext)).thenReturn(true);
+ spyOn(mService);
+ ArgumentCaptor<IntArray> captor = ArgumentCaptor.forClass(
+ IntArray.class);
+ when(mService.allowRebindForParentUser()).thenReturn(true);
+
+ mService.rebindServices(false, profileUserId);
+
+ verify(mService).populateComponentsToBind(any(), captor.capture(), any());
+ assertTrue(captor.getValue().contains(0));
+ assertTrue(captor.getValue().contains(profileUserId));
+ }
+
+ @Test
+ @EnableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER)
+ public void testRebindService_nonProfileUser() throws Exception {
+ final int userId = 99;
+ when(mUserProfiles.isProfileUser(userId, mContext)).thenReturn(false);
+ spyOn(mService);
+ ArgumentCaptor<IntArray> captor = ArgumentCaptor.forClass(
+ IntArray.class);
+ when(mService.allowRebindForParentUser()).thenReturn(true);
+
+ mService.rebindServices(false, userId);
+
+ verify(mService).populateComponentsToBind(any(), captor.capture(), any());
+ assertFalse(captor.getValue().contains(0));
+ assertTrue(captor.getValue().contains(userId));
+ }
+
+ @Test
+ @EnableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER)
+ public void testRebindService_userAll() throws Exception {
+ final int userId = 99;
+ spyOn(mService);
+ spyOn(mService.mUmInternal);
+ when(mService.mUmInternal.isVisibleBackgroundFullUser(userId)).thenReturn(true);
+ ArgumentCaptor<IntArray> captor = ArgumentCaptor.forClass(
+ IntArray.class);
+ when(mService.allowRebindForParentUser()).thenReturn(true);
+
+ mService.rebindServices(false, USER_ALL);
+
+ verify(mService).populateComponentsToBind(any(), captor.capture(), any());
+ assertTrue(captor.getValue().contains(0));
+ assertTrue(captor.getValue().contains(userId));
+ }
+
+ @Test
+ @EnableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER)
+ public void testOnUserStoppedWithVisibleBackgroundUser() throws Exception {
+ ManagedServices service = new TestManagedServices(getContext(), mLock, mUserProfiles, mIpm,
+ APPROVAL_BY_COMPONENT);
+ spyOn(service);
+ int userId = 99;
+ SparseArray<ArraySet<ComponentName>> approvedComponentsByUser = new SparseArray<>();
+ ArraySet<ComponentName> allowedForNonProfileUser = new ArraySet<>();
+ allowedForNonProfileUser.add(ComponentName.unflattenFromString("a/a"));
+ approvedComponentsByUser.put(userId, allowedForNonProfileUser);
+ IntArray users = new IntArray();
+ users.add(userId);
+ SparseArray<Set<ComponentName>> componentsToBind = new SparseArray<>();
+ spyOn(service.mUmInternal);
+ when(service.mUmInternal.isVisibleBackgroundFullUser(userId)).thenReturn(true);
+ service.populateComponentsToBind(componentsToBind, users, approvedComponentsByUser);
+ assertTrue(service.isComponentEnabledForUser(
+ ComponentName.unflattenFromString("a/a"), userId));
+ assertTrue(service.isComponentEnabledForPackage("a", userId));
+
+ service.onUserStopped(userId);
+
+ assertFalse(service.isComponentEnabledForUser(
+ ComponentName.unflattenFromString("a/a"), userId));
+ assertFalse(service.isComponentEnabledForPackage("a", userId));
+ verify(service).unbindUserServices(eq(userId));
+ }
+
+ @Test
+ @EnableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER)
+ public void testUnbindServicesImpl_serviceOfForegroundUser() throws Exception {
+ int switchingUserId = 10;
+ int userId = 99;
+
+ ManagedServices service = new TestManagedServices(getContext(), mLock, mUserProfiles, mIpm,
+ APPROVAL_BY_COMPONENT);
+ spyOn(service);
+ spyOn(service.mUmInternal);
+ when(service.mUmInternal.isVisibleBackgroundFullUser(userId)).thenReturn(false);
+
+ IInterface iInterface = mock(IInterface.class);
+ when(iInterface.asBinder()).thenReturn(mock(IBinder.class));
+
+ ManagedServices.ManagedServiceInfo serviceInfo = service.new ManagedServiceInfo(
+ iInterface, ComponentName.unflattenFromString("a/a"), userId, false,
+ mock(ServiceConnection.class), 26, 34);
+
+ Set<ManagedServices.ManagedServiceInfo> removableBoundServices = new ArraySet<>();
+ removableBoundServices.add(serviceInfo);
+
+ when(service.getRemovableConnectedServices()).thenReturn(removableBoundServices);
+ ArgumentCaptor<SparseArray<Set<ComponentName>>> captor = ArgumentCaptor.forClass(
+ SparseArray.class);
+
+ service.unbindServicesImpl(switchingUserId, true);
+
+ verify(service).unbindFromServices(captor.capture());
+
+ assertEquals(captor.getValue().size(), 1);
+ assertTrue(captor.getValue().indexOfKey(userId) != -1);
+ assertTrue(captor.getValue().get(userId).contains(
+ ComponentName.unflattenFromString("a/a")));
+ }
+
+ @Test
+ @EnableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER)
+ public void testUnbindServicesImpl_serviceOfVisibleBackgroundUser() throws Exception {
+ int switchingUserId = 10;
+ int userId = 99;
+
+ ManagedServices service = new TestManagedServices(getContext(), mLock, mUserProfiles, mIpm,
+ APPROVAL_BY_COMPONENT);
+ spyOn(service);
+ spyOn(service.mUmInternal);
+ when(service.mUmInternal.isVisibleBackgroundFullUser(userId)).thenReturn(true);
+
+ IInterface iInterface = mock(IInterface.class);
+ when(iInterface.asBinder()).thenReturn(mock(IBinder.class));
+
+ ManagedServices.ManagedServiceInfo serviceInfo = service.new ManagedServiceInfo(
+ iInterface, ComponentName.unflattenFromString("a/a"), userId,
+ false, mock(ServiceConnection.class), 26, 34);
+
+ Set<ManagedServices.ManagedServiceInfo> removableBoundServices = new ArraySet<>();
+ removableBoundServices.add(serviceInfo);
+
+ when(service.getRemovableConnectedServices()).thenReturn(removableBoundServices);
+ ArgumentCaptor<SparseArray<Set<ComponentName>>> captor = ArgumentCaptor.forClass(
+ SparseArray.class);
+
+ service.unbindServicesImpl(switchingUserId, true);
+
+ verify(service).unbindFromServices(captor.capture());
+
+ assertEquals(captor.getValue().size(), 0);
+ }
+
@Test
public void testOnNullBinding() throws Exception {
Context context = mock(Context.class);
@@ -1681,6 +2051,7 @@ public class ManagedServicesTest extends UiServiceTestCase {
assertFalse(service.isBound(cn, mZero.id));
assertFalse(service.isBound(cn, mTen.id));
}
+
@Test
public void testOnPackagesChanged_nullValuesPassed_noNullPointers() {
for (int approvalLevel : new int[] {APPROVAL_BY_COMPONENT, APPROVAL_BY_PACKAGE}) {
@@ -2012,6 +2383,7 @@ public class ManagedServicesTest extends UiServiceTestCase {
}
@Test
+ @DisableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER)
public void isComponentEnabledForCurrentProfiles_isThreadSafe() throws InterruptedException {
for (UserInfo userInfo : mUm.getUsers()) {
mService.addApprovedList("pkg1/cmp1:pkg2/cmp2:pkg3/cmp3", userInfo.id, true);
@@ -2024,6 +2396,20 @@ public class ManagedServicesTest extends UiServiceTestCase {
}
@Test
+ @EnableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER)
+ public void isComponentEnabledForUser_isThreadSafe() throws InterruptedException {
+ for (UserInfo userInfo : mUm.getUsers()) {
+ mService.addApprovedList("pkg1/cmp1:pkg2/cmp2:pkg3/cmp3", userInfo.id, true);
+ }
+ testThreadSafety(() -> {
+ mService.rebindServices(false, 0);
+ assertThat(mService.isComponentEnabledForUser(
+ new ComponentName("pkg1", "cmp1"), 0)).isTrue();
+ }, 20, 30);
+ }
+
+ @Test
+ @DisableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER)
public void isComponentEnabledForCurrentProfiles_profileUserId() {
final int profileUserId = 10;
when(mUserProfiles.isProfileUser(profileUserId, mContext)).thenReturn(true);
@@ -2037,6 +2423,24 @@ public class ManagedServicesTest extends UiServiceTestCase {
}
@Test
+ @EnableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER)
+ public void isComponentEnabledForUser_profileUserId() {
+ final int profileUserId = 10;
+ when(mUserProfiles.isProfileUser(profileUserId, mContext)).thenReturn(true);
+ spyOn(mService);
+ doReturn(USER_CURRENT).when(mService).resolveUserId(anyInt());
+
+ // Only approve for parent user (0)
+ mService.addApprovedList("pkg1/cmp1:pkg2/cmp2:pkg3/cmp3", 0, true);
+
+ // Test that the component is enabled after calling rebindServices with profile userId (10)
+ mService.rebindServices(false, profileUserId);
+ assertThat(mService.isComponentEnabledForUser(
+ new ComponentName("pkg1", "cmp1"), profileUserId)).isTrue();
+ }
+
+ @Test
+ @DisableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER)
public void isComponentEnabledForCurrentProfiles_profileUserId_NAS() {
final int profileUserId = 10;
when(mUserProfiles.isProfileUser(profileUserId, mContext)).thenReturn(true);
@@ -2054,6 +2458,25 @@ public class ManagedServicesTest extends UiServiceTestCase {
}
@Test
+ @EnableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER)
+ public void isComponentEnabledForUser_profileUserId_NAS() {
+ final int profileUserId = 10;
+ when(mUserProfiles.isProfileUser(profileUserId, mContext)).thenReturn(true);
+ // Do not rebind for parent users (NAS use-case)
+ ManagedServices service = spy(mService);
+ when(service.allowRebindForParentUser()).thenReturn(false);
+ doReturn(USER_CURRENT).when(service).resolveUserId(anyInt());
+
+ // Only approve for parent user (0)
+ service.addApprovedList("pkg1/cmp1:pkg2/cmp2:pkg3/cmp3", 0, true);
+
+ // Test that the component is disabled after calling rebindServices with profile userId (10)
+ service.rebindServices(false, profileUserId);
+ assertThat(service.isComponentEnabledForUser(
+ new ComponentName("pkg1", "cmp1"), profileUserId)).isFalse();
+ }
+
+ @Test
@EnableFlags(FLAG_LIFETIME_EXTENSION_REFACTOR)
public void testManagedServiceInfoIsSystemUi() {
ManagedServices service = new TestManagedServices(getContext(), mLock, mUserProfiles, mIpm,
@@ -2069,6 +2492,48 @@ public class ManagedServicesTest extends UiServiceTestCase {
assertThat(service0.isSystemUi()).isFalse();
}
+ @Test
+ @EnableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER)
+ public void testUserMatchesAndEnabled_profileUser() throws Exception {
+ int currentUserId = 10;
+ int profileUserId = 11;
+
+ ManagedServices service = new TestManagedServices(getContext(), mLock, mUserProfiles, mIpm,
+ APPROVAL_BY_COMPONENT);
+ ManagedServices.ManagedServiceInfo listener = spy(service.new ManagedServiceInfo(
+ mock(IInterface.class), ComponentName.unflattenFromString("a/a"), currentUserId,
+ false, mock(ServiceConnection.class), 26, 34));
+
+ doReturn(currentUserId).when(service.mUmInternal).getProfileParentId(profileUserId);
+ doReturn(currentUserId).when(service.mUmInternal).getProfileParentId(currentUserId);
+ doReturn(true).when(listener).isEnabledForUser();
+ doReturn(true).when(mUserProfiles).isCurrentProfile(anyInt());
+
+ assertThat(listener.enabledAndUserMatches(profileUserId)).isTrue();
+ }
+
+ @Test
+ @EnableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER)
+ public void testUserMatchesAndDisabled_visibleBackgroudUser() throws Exception {
+ int currentUserId = 10;
+ int profileUserId = 11;
+ int visibleBackgroundUserId = 12;
+
+ ManagedServices service = new TestManagedServices(getContext(), mLock, mUserProfiles, mIpm,
+ APPROVAL_BY_COMPONENT);
+ ManagedServices.ManagedServiceInfo listener = spy(service.new ManagedServiceInfo(
+ mock(IInterface.class), ComponentName.unflattenFromString("a/a"), profileUserId,
+ false, mock(ServiceConnection.class), 26, 34));
+
+ doReturn(currentUserId).when(service.mUmInternal).getProfileParentId(profileUserId);
+ doReturn(currentUserId).when(service.mUmInternal).getProfileParentId(currentUserId);
+ doReturn(visibleBackgroundUserId).when(service.mUmInternal)
+ .getProfileParentId(visibleBackgroundUserId);
+ doReturn(true).when(listener).isEnabledForUser();
+
+ assertThat(listener.enabledAndUserMatches(visibleBackgroundUserId)).isFalse();
+ }
+
private void mockServiceInfoWithMetaData(List<ComponentName> componentNames,
ManagedServices service, ArrayMap<ComponentName, Bundle> metaDatas)
throws RemoteException {
@@ -2247,26 +2712,47 @@ public class ManagedServicesTest extends UiServiceTestCase {
private void verifyExpectedBoundEntries(ManagedServices service, boolean primary)
throws Exception {
+ verifyExpectedBoundEntries(service, primary, UserHandle.USER_CURRENT);
+ }
+
+ private void verifyExpectedBoundEntries(ManagedServices service, boolean primary,
+ int targetUserId) throws Exception {
ArrayMap<Integer, String> verifyMap = primary ? mExpectedPrimary.get(service.mApprovalLevel)
: mExpectedSecondary.get(service.mApprovalLevel);
for (int userId : verifyMap.keySet()) {
for (String packageOrComponent : verifyMap.get(userId).split(":")) {
if (!TextUtils.isEmpty(packageOrComponent)) {
if (service.mApprovalLevel == APPROVAL_BY_PACKAGE) {
- assertTrue(packageOrComponent,
- service.isComponentEnabledForPackage(packageOrComponent));
+ if (managedServicesConcurrentMultiuser()) {
+ assertTrue(packageOrComponent,
+ service.isComponentEnabledForPackage(packageOrComponent,
+ targetUserId));
+ } else {
+ assertTrue(packageOrComponent,
+ service.isComponentEnabledForPackage(packageOrComponent));
+ }
for (int i = 1; i <= 3; i++) {
ComponentName componentName = ComponentName.unflattenFromString(
packageOrComponent +"/C" + i);
- assertTrue(service.isComponentEnabledForCurrentProfiles(
- componentName));
+ if (managedServicesConcurrentMultiuser()) {
+ assertTrue(service.isComponentEnabledForUser(
+ componentName, targetUserId));
+ } else {
+ assertTrue(service.isComponentEnabledForCurrentProfiles(
+ componentName));
+ }
verify(mIpm, times(1)).getServiceInfo(
eq(componentName), anyLong(), anyInt());
}
} else {
ComponentName componentName =
ComponentName.unflattenFromString(packageOrComponent);
- assertTrue(service.isComponentEnabledForCurrentProfiles(componentName));
+ if (managedServicesConcurrentMultiuser()) {
+ assertTrue(service.isComponentEnabledForUser(componentName,
+ targetUserId));
+ } else {
+ assertTrue(service.isComponentEnabledForCurrentProfiles(componentName));
+ }
verify(mIpm, times(1)).getServiceInfo(
eq(componentName), anyLong(), anyInt());
}
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 415e3accfa39..37ab541f12da 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -140,6 +140,7 @@ import static com.android.server.am.PendingIntentRecord.FLAG_ACTIVITY_SENDER;
import static com.android.server.am.PendingIntentRecord.FLAG_BROADCAST_SENDER;
import static com.android.server.am.PendingIntentRecord.FLAG_SERVICE_SENDER;
import static com.android.server.notification.Flags.FLAG_ALL_NOTIFS_NEED_TTL;
+import static com.android.server.notification.Flags.FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER;
import static com.android.server.notification.Flags.FLAG_REJECT_OLD_NOTIFICATIONS;
import static com.android.server.notification.GroupHelper.AUTOGROUP_KEY;
import static com.android.server.notification.NotificationManagerService.BITMAP_DURATION;
@@ -867,7 +868,8 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
&& filter.hasAction(Intent.ACTION_PACKAGES_SUSPENDED)) {
mPackageIntentReceiver = broadcastReceivers.get(i);
}
- if (filter.hasAction(Intent.ACTION_USER_SWITCHED)
+ if (filter.hasAction(Intent.ACTION_USER_STOPPED)
+ || filter.hasAction(Intent.ACTION_USER_SWITCHED)
|| filter.hasAction(Intent.ACTION_PROFILE_UNAVAILABLE)
|| filter.hasAction(Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE)) {
// There may be multiple receivers, get the NMS one
@@ -11028,7 +11030,6 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
}
@Test
- @EnableFlags(android.app.Flags.FLAG_MODES_API)
public void testAddAutomaticZenRule_typeManagedCanBeUsedByDeviceOwners() throws Exception {
ZenModeHelper zenModeHelper = setUpMockZenTest();
mService.setCallerIsNormalPackage();
@@ -11046,20 +11047,17 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
}
@Test
- @EnableFlags(android.app.Flags.FLAG_MODES_API)
public void testAddAutomaticZenRule_typeManagedCanBeUsedBySystem() throws Exception {
addAutomaticZenRule_restrictedRuleTypeCanBeUsedBySystem(AutomaticZenRule.TYPE_MANAGED);
}
@Test
- @EnableFlags(android.app.Flags.FLAG_MODES_API)
public void testAddAutomaticZenRule_typeManagedCannotBeUsedByRegularApps() throws Exception {
addAutomaticZenRule_restrictedRuleTypeCannotBeUsedByRegularApps(
AutomaticZenRule.TYPE_MANAGED);
}
@Test
- @EnableFlags(android.app.Flags.FLAG_MODES_API)
public void testAddAutomaticZenRule_typeBedtimeCanBeUsedByWellbeing() throws Exception {
ZenModeHelper zenModeHelper = setUpMockZenTest();
mService.setCallerIsNormalPackage();
@@ -11082,7 +11080,6 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
}
@Test
- @EnableFlags(android.app.Flags.FLAG_MODES_API)
public void testAddAutomaticZenRule_typeBedtimeCanBeUsedBySystem() throws Exception {
reset(mPackageManagerInternal);
when(mPackageManagerInternal.isSameApp(eq(mPkg), eq(mUid), anyInt())).thenReturn(true);
@@ -11090,7 +11087,6 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
}
@Test
- @EnableFlags(android.app.Flags.FLAG_MODES_API)
public void testAddAutomaticZenRule_typeBedtimeCannotBeUsedByRegularApps() throws Exception {
reset(mPackageManagerInternal);
when(mPackageManagerInternal.isSameApp(eq(mPkg), eq(mUid), anyInt())).thenReturn(true);
@@ -11133,7 +11129,6 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
}
@Test
- @EnableFlags(android.app.Flags.FLAG_MODES_API)
public void addAutomaticZenRule_fromUser_mappedToOriginUser() throws Exception {
ZenModeHelper zenModeHelper = setUpMockZenTest();
mService.isSystemUid = true;
@@ -11145,7 +11140,6 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
}
@Test
- @EnableFlags(android.app.Flags.FLAG_MODES_API)
public void addAutomaticZenRule_fromSystemNotUser_mappedToOriginSystem() throws Exception {
ZenModeHelper zenModeHelper = setUpMockZenTest();
mService.isSystemUid = true;
@@ -11157,7 +11151,6 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
}
@Test
- @EnableFlags(android.app.Flags.FLAG_MODES_API)
public void addAutomaticZenRule_fromApp_mappedToOriginApp() throws Exception {
ZenModeHelper zenModeHelper = setUpMockZenTest();
mService.setCallerIsNormalPackage();
@@ -11169,7 +11162,6 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
}
@Test
- @EnableFlags(android.app.Flags.FLAG_MODES_API)
public void addAutomaticZenRule_fromAppFromUser_blocked() throws Exception {
setUpMockZenTest();
mService.setCallerIsNormalPackage();
@@ -11179,7 +11171,6 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
}
@Test
- @EnableFlags(android.app.Flags.FLAG_MODES_API)
public void updateAutomaticZenRule_fromUserFromSystem_allowed() throws Exception {
ZenModeHelper zenModeHelper = setUpMockZenTest();
mService.isSystemUid = true;
@@ -11191,7 +11182,6 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
}
@Test
- @EnableFlags(android.app.Flags.FLAG_MODES_API)
public void updateAutomaticZenRule_fromUserFromApp_blocked() throws Exception {
setUpMockZenTest();
mService.setCallerIsNormalPackage();
@@ -11201,7 +11191,6 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
}
@Test
- @EnableFlags(android.app.Flags.FLAG_MODES_API)
public void removeAutomaticZenRule_fromUserFromSystem_allowed() throws Exception {
ZenModeHelper zenModeHelper = setUpMockZenTest();
mService.isSystemUid = true;
@@ -11213,7 +11202,6 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
}
@Test
- @EnableFlags(android.app.Flags.FLAG_MODES_API)
public void removeAutomaticZenRule_fromUserFromApp_blocked() throws Exception {
setUpMockZenTest();
mService.setCallerIsNormalPackage();
@@ -11223,7 +11211,6 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
}
@Test
- @EnableFlags(android.app.Flags.FLAG_MODES_API)
public void setAutomaticZenRuleState_fromAppWithConditionFromUser_originUserInApp()
throws Exception {
ZenModeHelper zenModeHelper = setUpMockZenTest();
@@ -11238,7 +11225,6 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
}
@Test
- @EnableFlags(android.app.Flags.FLAG_MODES_API)
public void setAutomaticZenRuleState_fromAppWithConditionNotFromUser_originApp()
throws Exception {
ZenModeHelper zenModeHelper = setUpMockZenTest();
@@ -11253,7 +11239,6 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
}
@Test
- @EnableFlags(android.app.Flags.FLAG_MODES_API)
public void setAutomaticZenRuleState_fromSystemWithConditionFromUser_originUserInSystemUi()
throws Exception {
ZenModeHelper zenModeHelper = setUpMockZenTest();
@@ -11267,7 +11252,6 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
eq(ZenModeConfig.ORIGIN_USER_IN_SYSTEMUI), anyInt());
}
@Test
- @EnableFlags(android.app.Flags.FLAG_MODES_API)
public void setAutomaticZenRuleState_fromSystemWithConditionNotFromUser_originSystem()
throws Exception {
ZenModeHelper zenModeHelper = setUpMockZenTest();
@@ -11438,7 +11422,6 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
}
@Test
- @EnableFlags(android.app.Flags.FLAG_MODES_API)
public void onAutomaticRuleStatusChanged_sendsBroadcastToRuleOwner() throws Exception {
mService.mZenModeHelper.getCallbacks().forEach(c -> c.onAutomaticRuleStatusChanged(
mUserId, "rule.owner.pkg", "rule_id", AUTOMATIC_RULE_STATUS_ACTIVATED));
@@ -16302,7 +16285,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
InOrder inOrder = inOrder(mPreferencesHelper, mService.mZenModeHelper);
inOrder.verify(mService.mZenModeHelper).onUserSwitched(eq(20));
- inOrder.verify(mPreferencesHelper).syncChannelsBypassingDnd();
+ inOrder.verify(mPreferencesHelper).syncHasPriorityChannels();
inOrder.verifyNoMoreInteractions();
}
@@ -16318,11 +16301,25 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
InOrder inOrder = inOrder(mPreferencesHelper, mService.mZenModeHelper);
inOrder.verify(mService.mZenModeHelper).onUserSwitched(eq(20));
- inOrder.verify(mPreferencesHelper).syncChannelsBypassingDnd();
+ inOrder.verify(mPreferencesHelper).syncHasPriorityChannels();
inOrder.verifyNoMoreInteractions();
}
@Test
+ @EnableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER)
+ public void onUserStopped_callBackToListeners() {
+ Intent intent = new Intent(Intent.ACTION_USER_STOPPED);
+ intent.putExtra(Intent.EXTRA_USER_HANDLE, 20);
+
+ mUserIntentReceiver.onReceive(mContext, intent);
+
+ verify(mConditionProviders).onUserStopped(eq(20));
+ verify(mListeners).onUserStopped(eq(20));
+ verify(mAssistants).onUserStopped(eq(20));
+ }
+
+ @Test
+ @DisableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER)
public void isNotificationPolicyAccessGranted_invalidPackage() throws Exception {
final String notReal = "NOT REAL";
final var checker = mService.permissionChecker;
@@ -16339,6 +16336,25 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
}
@Test
+ @EnableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER)
+ public void isNotificationPolicyAccessGranted_invalidPackage_concurrent_multiUser()
+ throws Exception {
+ final String notReal = "NOT REAL";
+ final var checker = mService.permissionChecker;
+
+ when(mPackageManagerClient.getPackageUidAsUser(eq(notReal), anyInt())).thenThrow(
+ PackageManager.NameNotFoundException.class);
+
+ assertThat(mBinderService.isNotificationPolicyAccessGranted(notReal)).isFalse();
+ verify(mPackageManagerClient).getPackageUidAsUser(eq(notReal), anyInt());
+ verify(checker, never()).check(any(), anyInt(), anyInt(), anyBoolean());
+ verify(mConditionProviders, never()).isPackageOrComponentAllowed(eq(notReal), anyInt());
+ verify(mListeners, never()).isComponentEnabledForPackage(any(), anyInt());
+ verify(mDevicePolicyManager, never()).isActiveDeviceOwner(anyInt());
+ }
+
+ @Test
+ @DisableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER)
public void isNotificationPolicyAccessGranted_hasPermission() throws Exception {
final String packageName = "target";
final int uid = 123;
@@ -16357,6 +16373,27 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
}
@Test
+ @EnableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER)
+ public void isNotificationPolicyAccessGranted_hasPermission_concurrent_multiUser()
+ throws Exception {
+ final String packageName = "target";
+ final int uid = 123;
+ final var checker = mService.permissionChecker;
+
+ when(mPackageManagerClient.getPackageUidAsUser(eq(packageName), anyInt())).thenReturn(uid);
+ when(checker.check(android.Manifest.permission.MANAGE_NOTIFICATIONS, uid, -1, true))
+ .thenReturn(PackageManager.PERMISSION_GRANTED);
+
+ assertThat(mBinderService.isNotificationPolicyAccessGranted(packageName)).isTrue();
+ verify(mPackageManagerClient).getPackageUidAsUser(eq(packageName), anyInt());
+ verify(checker).check(android.Manifest.permission.MANAGE_NOTIFICATIONS, uid, -1, true);
+ verify(mConditionProviders, never()).isPackageOrComponentAllowed(eq(packageName), anyInt());
+ verify(mListeners, never()).isComponentEnabledForPackage(any(), anyInt());
+ verify(mDevicePolicyManager, never()).isActiveDeviceOwner(anyInt());
+ }
+
+ @Test
+ @DisableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER)
public void isNotificationPolicyAccessGranted_isPackageAllowed() throws Exception {
final String packageName = "target";
final int uid = 123;
@@ -16375,6 +16412,27 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
}
@Test
+ @EnableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER)
+ public void isNotificationPolicyAccessGranted_isPackageAllowed_concurrent_multiUser()
+ throws Exception {
+ final String packageName = "target";
+ final int uid = 123;
+ final var checker = mService.permissionChecker;
+
+ when(mPackageManagerClient.getPackageUidAsUser(eq(packageName), anyInt())).thenReturn(uid);
+ when(mConditionProviders.isPackageOrComponentAllowed(eq(packageName), anyInt()))
+ .thenReturn(true);
+
+ assertThat(mBinderService.isNotificationPolicyAccessGranted(packageName)).isTrue();
+ verify(mPackageManagerClient).getPackageUidAsUser(eq(packageName), anyInt());
+ verify(checker).check(android.Manifest.permission.MANAGE_NOTIFICATIONS, uid, -1, true);
+ verify(mConditionProviders).isPackageOrComponentAllowed(eq(packageName), anyInt());
+ verify(mListeners, never()).isComponentEnabledForPackage(any(), anyInt());
+ verify(mDevicePolicyManager, never()).isActiveDeviceOwner(anyInt());
+ }
+
+ @Test
+ @DisableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER)
public void isNotificationPolicyAccessGranted_isComponentEnabled() throws Exception {
final String packageName = "target";
final int uid = 123;
@@ -16392,6 +16450,26 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
}
@Test
+ @EnableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER)
+ public void isNotificationPolicyAccessGranted_isComponentEnabled_concurrent_multiUser()
+ throws Exception {
+ final String packageName = "target";
+ final int uid = 123;
+ final var checker = mService.permissionChecker;
+
+ when(mPackageManagerClient.getPackageUidAsUser(eq(packageName), anyInt())).thenReturn(uid);
+ when(mListeners.isComponentEnabledForPackage(packageName, mUserId)).thenReturn(true);
+
+ assertThat(mBinderService.isNotificationPolicyAccessGranted(packageName)).isTrue();
+ verify(mPackageManagerClient).getPackageUidAsUser(eq(packageName), anyInt());
+ verify(checker).check(android.Manifest.permission.MANAGE_NOTIFICATIONS, uid, -1, true);
+ verify(mConditionProviders).isPackageOrComponentAllowed(eq(packageName), anyInt());
+ verify(mListeners).isComponentEnabledForPackage(packageName, mUserId);
+ verify(mDevicePolicyManager, never()).isActiveDeviceOwner(anyInt());
+ }
+
+ @Test
+ @DisableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER)
public void isNotificationPolicyAccessGranted_isDeviceOwner() throws Exception {
final String packageName = "target";
final int uid = 123;
@@ -16408,10 +16486,30 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
verify(mDevicePolicyManager).isActiveDeviceOwner(uid);
}
+ @Test
+ @EnableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER)
+ public void isNotificationPolicyAccessGranted_isDeviceOwner_concurrent_multiUser()
+ throws Exception {
+ final String packageName = "target";
+ final int uid = 123;
+ final var checker = mService.permissionChecker;
+
+ when(mPackageManagerClient.getPackageUidAsUser(eq(packageName), anyInt())).thenReturn(uid);
+ when(mDevicePolicyManager.isActiveDeviceOwner(uid)).thenReturn(true);
+
+ assertThat(mBinderService.isNotificationPolicyAccessGranted(packageName)).isTrue();
+ verify(mPackageManagerClient).getPackageUidAsUser(eq(packageName), anyInt());
+ verify(checker).check(android.Manifest.permission.MANAGE_NOTIFICATIONS, uid, -1, true);
+ verify(mConditionProviders).isPackageOrComponentAllowed(eq(packageName), anyInt());
+ verify(mListeners).isComponentEnabledForPackage(packageName, mUserId);
+ verify(mDevicePolicyManager).isActiveDeviceOwner(uid);
+ }
+
/**
* b/292163859
*/
@Test
+ @DisableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER)
public void isNotificationPolicyAccessGranted_callerIsDeviceOwner() throws Exception {
final String packageName = "target";
final int uid = 123;
@@ -16430,7 +16528,32 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
verify(mDevicePolicyManager, never()).isActiveDeviceOwner(callingUid);
}
+ /**
+ * b/292163859
+ */
@Test
+ @EnableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER)
+ public void isNotificationPolicyAccessGranted_callerIsDeviceOwner_concurrent_multiUser()
+ throws Exception {
+ final String packageName = "target";
+ final int uid = 123;
+ final int callingUid = Binder.getCallingUid();
+ final var checker = mService.permissionChecker;
+
+ when(mPackageManagerClient.getPackageUidAsUser(eq(packageName), anyInt())).thenReturn(uid);
+ when(mDevicePolicyManager.isActiveDeviceOwner(callingUid)).thenReturn(true);
+
+ assertThat(mBinderService.isNotificationPolicyAccessGranted(packageName)).isFalse();
+ verify(mPackageManagerClient).getPackageUidAsUser(eq(packageName), anyInt());
+ verify(checker).check(android.Manifest.permission.MANAGE_NOTIFICATIONS, uid, -1, true);
+ verify(mConditionProviders).isPackageOrComponentAllowed(eq(packageName), anyInt());
+ verify(mListeners).isComponentEnabledForPackage(packageName, mUserId);
+ verify(mDevicePolicyManager).isActiveDeviceOwner(uid);
+ verify(mDevicePolicyManager, never()).isActiveDeviceOwner(callingUid);
+ }
+
+ @Test
+ @DisableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER)
public void isNotificationPolicyAccessGranted_notGranted() throws Exception {
final String packageName = "target";
final int uid = 123;
@@ -16447,6 +16570,24 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
}
@Test
+ @EnableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER)
+ public void isNotificationPolicyAccessGranted_notGranted_concurrent_multiUser()
+ throws Exception {
+ final String packageName = "target";
+ final int uid = 123;
+ final var checker = mService.permissionChecker;
+
+ when(mPackageManagerClient.getPackageUidAsUser(eq(packageName), anyInt())).thenReturn(uid);
+
+ assertThat(mBinderService.isNotificationPolicyAccessGranted(packageName)).isFalse();
+ verify(mPackageManagerClient).getPackageUidAsUser(eq(packageName), anyInt());
+ verify(checker).check(android.Manifest.permission.MANAGE_NOTIFICATIONS, uid, -1, true);
+ verify(mConditionProviders).isPackageOrComponentAllowed(eq(packageName), anyInt());
+ verify(mListeners).isComponentEnabledForPackage(packageName, mUserId);
+ verify(mDevicePolicyManager).isActiveDeviceOwner(uid);
+ }
+
+ @Test
public void testResetDefaultDnd() {
TestableNotificationManagerService service = spy(mService);
UserInfo user = new UserInfo(0, "owner", 0);
@@ -16481,7 +16622,6 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
}
@Test
- @EnableFlags(android.app.Flags.FLAG_MODES_API)
public void setDeviceEffectsApplier_succeeds() throws Exception {
initNMS(SystemService.PHASE_SYSTEM_SERVICES_READY);
@@ -16492,7 +16632,6 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
}
@Test
- @EnableFlags(android.app.Flags.FLAG_MODES_API)
public void setDeviceEffectsApplier_tooLate_throws() throws Exception {
initNMS(SystemService.PHASE_THIRD_PARTY_APPS_CAN_START);
@@ -16501,7 +16640,6 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
}
@Test
- @EnableFlags(android.app.Flags.FLAG_MODES_API)
public void setDeviceEffectsApplier_calledTwice_throws() throws Exception {
initNMS(SystemService.PHASE_SYSTEM_SERVICES_READY);
@@ -16513,7 +16651,6 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
@Test
@EnableCompatChanges(NotificationManagerService.MANAGE_GLOBAL_ZEN_VIA_IMPLICIT_RULES)
public void setNotificationPolicy_mappedToImplicitRule() throws RemoteException {
- mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
mService.setCallerIsNormalPackage();
ZenModeHelper zenHelper = mock(ZenModeHelper.class);
mService.mZenModeHelper = zenHelper;
@@ -16530,7 +16667,6 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
@Test
@EnableCompatChanges(NotificationManagerService.MANAGE_GLOBAL_ZEN_VIA_IMPLICIT_RULES)
public void setNotificationPolicy_systemCaller_setsGlobalPolicy() throws RemoteException {
- mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
ZenModeHelper zenModeHelper = mock(ZenModeHelper.class);
mService.mZenModeHelper = zenModeHelper;
when(mConditionProviders.isPackageOrComponentAllowed(anyString(), anyInt()))
@@ -16570,7 +16706,6 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
private void setNotificationPolicy_dependingOnCompanionAppDevice_maySetGlobalPolicy(
@AssociationRequest.DeviceProfile String deviceProfile, boolean canSetGlobalPolicy)
throws RemoteException {
- mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
mService.setCallerIsNormalPackage();
ZenModeHelper zenModeHelper = mock(ZenModeHelper.class);
mService.mZenModeHelper = zenModeHelper;
@@ -16597,7 +16732,6 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
@Test
@DisableCompatChanges(NotificationManagerService.MANAGE_GLOBAL_ZEN_VIA_IMPLICIT_RULES)
public void setNotificationPolicy_withoutCompat_setsGlobalPolicy() throws RemoteException {
- mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
mService.setCallerIsNormalPackage();
ZenModeHelper zenModeHelper = mock(ZenModeHelper.class);
mService.mZenModeHelper = zenModeHelper;
@@ -16613,7 +16747,6 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
@Test
@EnableCompatChanges(NotificationManagerService.MANAGE_GLOBAL_ZEN_VIA_IMPLICIT_RULES)
public void getNotificationPolicy_mappedFromImplicitRule() throws RemoteException {
- mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
mService.setCallerIsNormalPackage();
ZenModeHelper zenHelper = mock(ZenModeHelper.class);
mService.mZenModeHelper = zenHelper;
@@ -16628,7 +16761,6 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
@Test
@EnableCompatChanges(NotificationManagerService.MANAGE_GLOBAL_ZEN_VIA_IMPLICIT_RULES)
public void setInterruptionFilter_mappedToImplicitRule() throws RemoteException {
- mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
mService.setCallerIsNormalPackage();
ZenModeHelper zenHelper = mock(ZenModeHelper.class);
mService.mZenModeHelper = zenHelper;
@@ -16644,7 +16776,6 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
@Test
@EnableCompatChanges(NotificationManagerService.MANAGE_GLOBAL_ZEN_VIA_IMPLICIT_RULES)
public void setInterruptionFilter_systemCaller_setsGlobalPolicy() throws RemoteException {
- mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
mService.setCallerIsNormalPackage();
ZenModeHelper zenModeHelper = mock(ZenModeHelper.class);
mService.mZenModeHelper = zenModeHelper;
@@ -16683,7 +16814,6 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
private void setInterruptionFilter_dependingOnCompanionAppDevice_maySetGlobalZen(
@AssociationRequest.DeviceProfile String deviceProfile, boolean canSetGlobalPolicy)
throws RemoteException {
- mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
ZenModeHelper zenModeHelper = mock(ZenModeHelper.class);
mService.mZenModeHelper = zenModeHelper;
mService.setCallerIsNormalPackage();
@@ -16708,7 +16838,6 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
}
@Test
- @EnableFlags(android.app.Flags.FLAG_MODES_API)
@EnableCompatChanges(NotificationManagerService.MANAGE_GLOBAL_ZEN_VIA_IMPLICIT_RULES)
public void requestInterruptionFilterFromListener_fromApp_doesNotSetGlobalZen()
throws Exception {
@@ -16726,7 +16855,6 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
}
@Test
- @EnableFlags(android.app.Flags.FLAG_MODES_API)
@EnableCompatChanges(NotificationManagerService.MANAGE_GLOBAL_ZEN_VIA_IMPLICIT_RULES)
public void requestInterruptionFilterFromListener_fromSystem_setsGlobalZen()
throws Exception {
@@ -16745,24 +16873,6 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
}
@Test
- @DisableFlags(android.app.Flags.FLAG_MODES_API)
- public void requestInterruptionFilterFromListener_flagOff_callsRequestFromListener()
- throws Exception {
- mService.setCallerIsNormalPackage();
- mService.mZenModeHelper = mock(ZenModeHelper.class);
- ManagedServices.ManagedServiceInfo info = mock(ManagedServices.ManagedServiceInfo.class);
- when(mListeners.checkServiceTokenLocked(any())).thenReturn(info);
- info.component = new ComponentName("pkg", "cls");
-
- mBinderService.requestInterruptionFilterFromListener(mock(INotificationListener.class),
- INTERRUPTION_FILTER_PRIORITY);
-
- verify(mService.mZenModeHelper).requestFromListener(eq(info.component),
- eq(INTERRUPTION_FILTER_PRIORITY), eq(mUid), /* fromSystemOrSystemUi= */ eq(false));
- }
-
- @Test
- @EnableFlags(android.app.Flags.FLAG_MODES_API)
@EnableCompatChanges(NotificationManagerService.MANAGE_GLOBAL_ZEN_VIA_IMPLICIT_RULES)
public void updateAutomaticZenRule_implicitRuleWithoutCPS_disallowedFromApp() throws Exception {
setUpRealZenTest();
@@ -16788,7 +16898,6 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
}
@Test
- @EnableFlags(android.app.Flags.FLAG_MODES_API)
@EnableCompatChanges(NotificationManagerService.MANAGE_GLOBAL_ZEN_VIA_IMPLICIT_RULES)
public void updateAutomaticZenRule_implicitRuleWithoutCPS_allowedFromSystem() throws Exception {
setUpRealZenTest();
@@ -16814,7 +16923,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
}
@Test
- @EnableFlags({android.app.Flags.FLAG_MODES_API, android.app.Flags.FLAG_MODES_UI})
+ @EnableFlags(android.app.Flags.FLAG_MODES_UI)
public void setNotificationPolicy_fromSystemApp_appliesPriorityChannelsAllowed()
throws Exception {
setUpRealZenTest();
@@ -16844,7 +16953,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
}
@Test
- @EnableFlags({android.app.Flags.FLAG_MODES_API, android.app.Flags.FLAG_MODES_UI})
+ @EnableFlags(android.app.Flags.FLAG_MODES_UI)
@DisableCompatChanges(NotificationManagerService.MANAGE_GLOBAL_ZEN_VIA_IMPLICIT_RULES)
public void setNotificationPolicy_fromRegularAppThatCanModifyPolicy_ignoresState()
throws Exception {
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 640de174ba20..5dea44d6ebf4 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
@@ -363,7 +363,7 @@ public class PreferencesHelperTest extends UiServiceTestCase {
.when(mTestIContentProvider).uncanonicalize(any(), eq(CANONICAL_SOUND_URI));
mTestNotificationPolicy = new NotificationManager.Policy(0, 0, 0, 0,
- NotificationManager.Policy.STATE_CHANNELS_BYPASSING_DND, 0);
+ NotificationManager.Policy.STATE_HAS_PRIORITY_CHANNELS, 0);
when(mMockZenModeHelper.getNotificationPolicy(any())).thenReturn(mTestNotificationPolicy);
when(mAppOpsManager.noteOpNoThrow(anyInt(), anyInt(),
anyString(), eq(null), anyString())).thenReturn(MODE_DEFAULT);
@@ -2733,7 +2733,7 @@ public class PreferencesHelperTest extends UiServiceTestCase {
NotificationChannel channel = new NotificationChannel("id1", "name1", IMPORTANCE_LOW);
mHelper.createNotificationChannel(PKG_N_MR1, uid, channel, true, false,
uid, false);
- assertFalse(mHelper.areChannelsBypassingDnd());
+ assertFalse(mHelper.hasPriorityChannels());
if (android.app.Flags.modesUi()) {
verify(mMockZenModeHelper, never()).updateHasPriorityChannels(any(), anyBoolean());
} else {
@@ -2748,7 +2748,7 @@ public class PreferencesHelperTest extends UiServiceTestCase {
channel2.setBypassDnd(true);
mHelper.createNotificationChannel(PKG_N_MR1, uid, channel2, true, true,
uid, false);
- assertTrue(mHelper.areChannelsBypassingDnd());
+ assertTrue(mHelper.hasPriorityChannels());
if (android.app.Flags.modesUi()) {
verify(mMockZenModeHelper, times(1)).updateHasPriorityChannels(eq(UserHandle.CURRENT),
eq(true));
@@ -2760,7 +2760,7 @@ public class PreferencesHelperTest extends UiServiceTestCase {
// delete channels
mHelper.deleteNotificationChannel(PKG_N_MR1, uid, channel.getId(), uid, false);
- assertTrue(mHelper.areChannelsBypassingDnd()); // channel2 can still bypass DND
+ assertTrue(mHelper.hasPriorityChannels()); // channel2 can still bypass DND
if (android.app.Flags.modesUi()) {
verify(mMockZenModeHelper, never()).updateHasPriorityChannels(any(), anyBoolean());
} else {
@@ -2770,7 +2770,7 @@ public class PreferencesHelperTest extends UiServiceTestCase {
resetZenModeHelper();
mHelper.deleteNotificationChannel(PKG_N_MR1, uid, channel2.getId(), uid, false);
- assertFalse(mHelper.areChannelsBypassingDnd());
+ assertFalse(mHelper.hasPriorityChannels());
if (android.app.Flags.modesUi()) {
verify(mMockZenModeHelper, times(1)).updateHasPriorityChannels(eq(UserHandle.CURRENT),
eq(false));
@@ -2792,7 +2792,7 @@ public class PreferencesHelperTest extends UiServiceTestCase {
NotificationChannel channel = new NotificationChannel("id1", "name1", IMPORTANCE_LOW);
mHelper.createNotificationChannel(PKG_N_MR1, uid, channel, true, false,
uid, false);
- assertFalse(mHelper.areChannelsBypassingDnd());
+ assertFalse(mHelper.hasPriorityChannels());
if (android.app.Flags.modesUi()) {
verify(mMockZenModeHelper, never()).updateHasPriorityChannels(any(), anyBoolean());
} else {
@@ -2807,7 +2807,7 @@ public class PreferencesHelperTest extends UiServiceTestCase {
mHelper.createNotificationChannel(PKG_N_MR1, uid, update, true, true,
uid, false);
- assertTrue(mHelper.areChannelsBypassingDnd());
+ assertTrue(mHelper.hasPriorityChannels());
if (android.app.Flags.modesUi()) {
verify(mMockZenModeHelper, times(1)).updateHasPriorityChannels(eq(UserHandle.CURRENT),
eq(true));
@@ -2829,7 +2829,7 @@ public class PreferencesHelperTest extends UiServiceTestCase {
NotificationChannel channel = new NotificationChannel("id1", "name1", IMPORTANCE_LOW);
mHelper.createNotificationChannel(PKG_N_MR1, uid, channel, true, false,
uid, false);
- assertFalse(mHelper.areChannelsBypassingDnd());
+ assertFalse(mHelper.hasPriorityChannels());
if (android.app.Flags.modesUi()) {
verify(mMockZenModeHelper, never()).updateHasPriorityChannels(any(), anyBoolean());
} else {
@@ -2844,7 +2844,7 @@ public class PreferencesHelperTest extends UiServiceTestCase {
channel2.setBypassDnd(true);
mHelper.createNotificationChannel(PKG_N_MR1, uid, channel2, true, true,
uid, false);
- assertTrue(mHelper.areChannelsBypassingDnd());
+ assertTrue(mHelper.hasPriorityChannels());
if (android.app.Flags.modesUi()) {
verify(mMockZenModeHelper, times(1)).updateHasPriorityChannels(eq(UserHandle.CURRENT),
eq(true));
@@ -2856,7 +2856,7 @@ public class PreferencesHelperTest extends UiServiceTestCase {
// delete channels
mHelper.deleteNotificationChannel(PKG_N_MR1, uid, channel.getId(), uid, false);
- assertTrue(mHelper.areChannelsBypassingDnd()); // channel2 can still bypass DND
+ assertTrue(mHelper.hasPriorityChannels()); // channel2 can still bypass DND
if (android.app.Flags.modesUi()) {
verify(mMockZenModeHelper, never()).updateHasPriorityChannels(any(), anyBoolean());
} else {
@@ -2866,7 +2866,7 @@ public class PreferencesHelperTest extends UiServiceTestCase {
resetZenModeHelper();
mHelper.deleteNotificationChannel(PKG_N_MR1, uid, channel2.getId(), uid, false);
- assertFalse(mHelper.areChannelsBypassingDnd());
+ assertFalse(mHelper.hasPriorityChannels());
if (android.app.Flags.modesUi()) {
verify(mMockZenModeHelper, times(1)).updateHasPriorityChannels(eq(UserHandle.CURRENT),
eq(false));
@@ -2884,9 +2884,9 @@ public class PreferencesHelperTest extends UiServiceTestCase {
// start in a 'allowed to bypass dnd state'
mTestNotificationPolicy = new NotificationManager.Policy(0, 0, 0, 0,
- NotificationManager.Policy.STATE_CHANNELS_BYPASSING_DND, 0);
+ NotificationManager.Policy.STATE_HAS_PRIORITY_CHANNELS, 0);
when(mMockZenModeHelper.getNotificationPolicy(any())).thenReturn(mTestNotificationPolicy);
- mHelper.syncChannelsBypassingDnd();
+ mHelper.syncHasPriorityChannels();
// create notification channel that can bypass dnd, but app is blocked
// expected result: areChannelsBypassingDnd = false
@@ -2899,7 +2899,7 @@ public class PreferencesHelperTest extends UiServiceTestCase {
channel2.setBypassDnd(true);
mHelper.createNotificationChannel(PKG_N_MR1, uid, channel2, true, true,
uid, false);
- assertFalse(mHelper.areChannelsBypassingDnd());
+ assertFalse(mHelper.hasPriorityChannels());
if (android.app.Flags.modesUi()) {
verify(mMockZenModeHelper, times(1)).updateHasPriorityChannels(eq(UserHandle.CURRENT),
eq(false));
@@ -2917,9 +2917,9 @@ public class PreferencesHelperTest extends UiServiceTestCase {
// start in a 'allowed to bypass dnd state'
mTestNotificationPolicy = new NotificationManager.Policy(0, 0, 0, 0,
- NotificationManager.Policy.STATE_CHANNELS_BYPASSING_DND, 0);
+ NotificationManager.Policy.STATE_HAS_PRIORITY_CHANNELS, 0);
when(mMockZenModeHelper.getNotificationPolicy(any())).thenReturn(mTestNotificationPolicy);
- mHelper.syncChannelsBypassingDnd();
+ mHelper.syncHasPriorityChannels();
// create notification channel that can bypass dnd, but app is blocked
// expected result: areChannelsBypassingDnd = false
@@ -2927,7 +2927,7 @@ public class PreferencesHelperTest extends UiServiceTestCase {
channel2.setBypassDnd(true);
mHelper.createNotificationChannel(PKG_N_MR1, uid, channel2, true, true,
uid, false);
- assertFalse(mHelper.areChannelsBypassingDnd());
+ assertFalse(mHelper.hasPriorityChannels());
if (android.app.Flags.modesUi()) {
verify(mMockZenModeHelper, times(1)).updateHasPriorityChannels(eq(UserHandle.CURRENT),
eq(false));
@@ -2945,9 +2945,9 @@ public class PreferencesHelperTest extends UiServiceTestCase {
// start in a 'allowed to bypass dnd state'
mTestNotificationPolicy = new NotificationManager.Policy(0, 0, 0, 0,
- NotificationManager.Policy.STATE_CHANNELS_BYPASSING_DND, 0);
+ NotificationManager.Policy.STATE_HAS_PRIORITY_CHANNELS, 0);
when(mMockZenModeHelper.getNotificationPolicy(any())).thenReturn(mTestNotificationPolicy);
- mHelper.syncChannelsBypassingDnd();
+ mHelper.syncHasPriorityChannels();
// create notification channel that can bypass dnd, but app is blocked
// expected result: areChannelsBypassingDnd = false
@@ -2955,7 +2955,7 @@ public class PreferencesHelperTest extends UiServiceTestCase {
channel2.setBypassDnd(true);
mHelper.createNotificationChannel(PKG_N_MR1, uid, channel2, true, true,
uid, false);
- assertFalse(mHelper.areChannelsBypassingDnd());
+ assertFalse(mHelper.hasPriorityChannels());
if (android.app.Flags.modesUi()) {
verify(mMockZenModeHelper, times(1)).updateHasPriorityChannels(eq(UserHandle.CURRENT),
eq(false));
@@ -2977,7 +2977,7 @@ public class PreferencesHelperTest extends UiServiceTestCase {
NotificationChannel channel = new NotificationChannel("id1", "name1", IMPORTANCE_LOW);
mHelper.createNotificationChannel(PKG_N_MR1, uid, channel, true, false,
uid, false);
- assertFalse(mHelper.areChannelsBypassingDnd());
+ assertFalse(mHelper.hasPriorityChannels());
if (android.app.Flags.modesUi()) {
verify(mMockZenModeHelper, never()).updateHasPriorityChannels(any(), anyBoolean());
} else {
@@ -2990,7 +2990,7 @@ public class PreferencesHelperTest extends UiServiceTestCase {
// expected result: areChannelsBypassingDnd = true
channel.setBypassDnd(true);
mHelper.updateNotificationChannel(PKG_N_MR1, uid, channel, true, SYSTEM_UID, true);
- assertTrue(mHelper.areChannelsBypassingDnd());
+ assertTrue(mHelper.hasPriorityChannels());
if (android.app.Flags.modesUi()) {
verify(mMockZenModeHelper, times(1)).updateHasPriorityChannels(eq(UserHandle.CURRENT),
eq(true));
@@ -3004,7 +3004,7 @@ public class PreferencesHelperTest extends UiServiceTestCase {
// expected result: areChannelsBypassingDnd = false
channel.setBypassDnd(false);
mHelper.updateNotificationChannel(PKG_N_MR1, uid, channel, true, SYSTEM_UID, true);
- assertFalse(mHelper.areChannelsBypassingDnd());
+ assertFalse(mHelper.hasPriorityChannels());
if (android.app.Flags.modesUi()) {
verify(mMockZenModeHelper, times(1)).updateHasPriorityChannels(eq(UserHandle.CURRENT),
eq(false));
@@ -3020,10 +3020,10 @@ public class PreferencesHelperTest extends UiServiceTestCase {
// start notification policy off with mAreChannelsBypassingDnd = true, but
// RankingHelper should change to false
mTestNotificationPolicy = new NotificationManager.Policy(0, 0, 0, 0,
- NotificationManager.Policy.STATE_CHANNELS_BYPASSING_DND, 0);
+ NotificationManager.Policy.STATE_HAS_PRIORITY_CHANNELS, 0);
when(mMockZenModeHelper.getNotificationPolicy(any())).thenReturn(mTestNotificationPolicy);
- mHelper.syncChannelsBypassingDnd();
- assertFalse(mHelper.areChannelsBypassingDnd());
+ mHelper.syncHasPriorityChannels();
+ assertFalse(mHelper.hasPriorityChannels());
if (android.app.Flags.modesUi()) {
verify(mMockZenModeHelper, times(1)).updateHasPriorityChannels(eq(UserHandle.CURRENT),
eq(false));
@@ -3039,7 +3039,7 @@ public class PreferencesHelperTest extends UiServiceTestCase {
// start notification policy off with mAreChannelsBypassingDnd = false
mTestNotificationPolicy = new NotificationManager.Policy(0, 0, 0, 0, 0, 0);
when(mMockZenModeHelper.getNotificationPolicy(any())).thenReturn(mTestNotificationPolicy);
- assertFalse(mHelper.areChannelsBypassingDnd());
+ assertFalse(mHelper.hasPriorityChannels());
if (android.app.Flags.modesUi()) {
verify(mMockZenModeHelper, never()).updateHasPriorityChannels(any(), anyBoolean());
} else {
@@ -3050,7 +3050,7 @@ public class PreferencesHelperTest extends UiServiceTestCase {
}
@Test
- public void syncChannelsBypassingDnd_includesProfilesOfCurrentUser() throws Exception {
+ public void syncHasPriorityChannels_includesProfilesOfCurrentUser() throws Exception {
when(mUserProfiles.getCurrentProfileIds()).thenReturn(IntArray.wrap(new int[] {0, 10}));
when(mPermissionHelper.hasPermission(anyInt())).thenReturn(true);
ApplicationInfo appInfo = new ApplicationInfo();
@@ -3067,13 +3067,13 @@ public class PreferencesHelperTest extends UiServiceTestCase {
mHelper.createNotificationChannel("com.example", UserHandle.getUid(10, 444), withBypass,
false, false, Process.SYSTEM_UID, true);
- mHelper.syncChannelsBypassingDnd();
+ mHelper.syncHasPriorityChannels();
- assertThat(mHelper.areChannelsBypassingDnd()).isTrue();
+ assertThat(mHelper.hasPriorityChannels()).isTrue();
}
@Test
- public void syncChannelsBypassingDnd_excludesOtherUsers() throws Exception {
+ public void syncHasPriorityChannels_excludesOtherUsers() throws Exception {
when(mUserProfiles.getCurrentProfileIds()).thenReturn(IntArray.wrap(new int[] {0}));
when(mPermissionHelper.hasPermission(anyInt())).thenReturn(true);
ApplicationInfo appInfo = new ApplicationInfo();
@@ -3090,9 +3090,9 @@ public class PreferencesHelperTest extends UiServiceTestCase {
mHelper.createNotificationChannel("com.example", UserHandle.getUid(10, 444), withBypass,
false, false, Process.SYSTEM_UID, true);
- mHelper.syncChannelsBypassingDnd();
+ mHelper.syncHasPriorityChannels();
- assertThat(mHelper.areChannelsBypassingDnd()).isFalse();
+ assertThat(mHelper.hasPriorityChannels()).isFalse();
}
@Test
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/RankingHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/RankingHelperTest.java
index f90034614383..ec428d506e7b 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/RankingHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/RankingHelperTest.java
@@ -17,9 +17,10 @@ package com.android.server.notification;
import static android.app.NotificationManager.IMPORTANCE_DEFAULT;
import static android.app.NotificationManager.IMPORTANCE_LOW;
-
import static android.platform.test.flag.junit.SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT;
+
import static com.google.common.truth.Truth.assertThat;
+
import static junit.framework.TestCase.assertEquals;
import static org.junit.Assert.assertTrue;
@@ -29,7 +30,6 @@ import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
-import android.app.Flags;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
@@ -40,7 +40,6 @@ import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.Signature;
-import android.media.AudioAttributes;
import android.net.Uri;
import android.os.Build;
import android.os.UserHandle;
@@ -67,7 +66,6 @@ import org.mockito.MockitoAnnotations;
import java.util.ArrayList;
import java.util.Collections;
-import java.util.List;
@SmallTest
@RunWith(AndroidJUnit4.class)
@@ -154,7 +152,7 @@ public class RankingHelperTest extends UiServiceTestCase {
.thenReturn(SOUND_URI);
mTestNotificationPolicy = new NotificationManager.Policy(0, 0, 0, 0,
- NotificationManager.Policy.STATE_CHANNELS_BYPASSING_DND, 0);
+ NotificationManager.Policy.STATE_HAS_PRIORITY_CHANNELS, 0);
when(mMockZenModeHelper.getNotificationPolicy(any())).thenReturn(mTestNotificationPolicy);
mHelper = new RankingHelper(getContext(), mHandler, mConfig, mMockZenModeHelper,
mUsageStats, new String[] {ImportanceExtractor.class.getName()},
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenAdaptersTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenAdaptersTest.java
index 75552bc433c5..f3813437a9c5 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenAdaptersTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenAdaptersTest.java
@@ -20,10 +20,7 @@ import static android.service.notification.ZenAdapters.notificationPolicyToZenPo
import static com.google.common.truth.Truth.assertThat;
-import android.app.Flags;
import android.app.NotificationManager.Policy;
-import android.platform.test.annotations.DisableFlags;
-import android.platform.test.annotations.EnableFlags;
import android.platform.test.flag.junit.SetFlagsRule;
import android.service.notification.ZenPolicy;
@@ -137,8 +134,7 @@ public class ZenAdaptersTest extends UiServiceTestCase {
}
@Test
- @EnableFlags(Flags.FLAG_MODES_API)
- public void notificationPolicyToZenPolicy_modesApi_priorityChannels() {
+ public void notificationPolicyToZenPolicy_priorityChannels() {
Policy policy = new Policy(0, 0, 0, 0,
Policy.policyState(false, true), 0);
@@ -151,20 +147,4 @@ public class ZenAdaptersTest extends UiServiceTestCase {
assertThat(zenPolicyNotAllowed.getPriorityChannelsAllowed()).isEqualTo(
ZenPolicy.STATE_DISALLOW);
}
-
- @Test
- @DisableFlags(Flags.FLAG_MODES_API)
- public void notificationPolicyToZenPolicy_noModesApi_priorityChannelsUnset() {
- Policy policy = new Policy(0, 0, 0, 0,
- Policy.policyState(false, true), 0);
-
- ZenPolicy zenPolicy = notificationPolicyToZenPolicy(policy);
- assertThat(zenPolicy.getPriorityChannelsAllowed()).isEqualTo(ZenPolicy.STATE_UNSET);
-
- Policy notAllowed = new Policy(0, 0, 0, 0,
- Policy.policyState(false, false), 0);
- ZenPolicy zenPolicyNotAllowed = notificationPolicyToZenPolicy(notAllowed);
- assertThat(zenPolicyNotAllowed.getPriorityChannelsAllowed()).isEqualTo(
- ZenPolicy.STATE_UNSET);
- }
}
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenDeviceEffectsTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenDeviceEffectsTest.java
index af911e811e5e..9a2b748a9bcc 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenDeviceEffectsTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenDeviceEffectsTest.java
@@ -20,7 +20,6 @@ import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
-import android.app.Flags;
import android.os.Parcel;
import android.platform.test.flag.junit.SetFlagsRule;
import android.service.notification.ZenDeviceEffects;
@@ -31,7 +30,6 @@ import com.android.server.UiServiceTestCase;
import com.google.common.collect.ImmutableSet;
-import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -42,11 +40,6 @@ public class ZenDeviceEffectsTest extends UiServiceTestCase {
@Rule
public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
- @Before
- public final void setUp() {
- mSetFlagsRule.enableFlags(Flags.FLAG_MODES_API);
- }
-
@Test
public void builder() {
ZenDeviceEffects deviceEffects =
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java
index b42a6a5a7382..67efb9e76692 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java
@@ -174,7 +174,7 @@ public class ZenModeConfigTest extends UiServiceTestCase {
}
assertTrue(ZenModeConfig.areAllPriorityOnlyRingerSoundsMuted(config));
- config.areChannelsBypassingDnd = true;
+ config.hasPriorityChannels = true;
assertTrue(ZenModeConfig.areAllPriorityOnlyRingerSoundsMuted(config));
if (Flags.modesUi()) {
@@ -187,7 +187,7 @@ public class ZenModeConfigTest extends UiServiceTestCase {
assertFalse(ZenModeConfig.areAllPriorityOnlyRingerSoundsMuted(config));
- config.areChannelsBypassingDnd = false;
+ config.hasPriorityChannels = false;
if (Flags.modesUi()) {
config.manualRule.zenPolicy = new ZenPolicy.Builder(config.manualRule.zenPolicy)
.allowPriorityChannels(false)
@@ -417,7 +417,7 @@ public class ZenModeConfigTest extends UiServiceTestCase {
assertTrue(ZenModeConfig.areAllPriorityOnlyRingerSoundsMuted(config));
assertTrue(ZenModeConfig.areAllZenBehaviorSoundsMuted(config));
- config.areChannelsBypassingDnd = true;
+ config.hasPriorityChannels = true;
if (Flags.modesUi()) {
config.manualRule.zenPolicy = new ZenPolicy.Builder(config.manualRule.zenPolicy)
.allowPriorityChannels(true)
@@ -429,7 +429,7 @@ public class ZenModeConfigTest extends UiServiceTestCase {
assertFalse(ZenModeConfig.areAllPriorityOnlyRingerSoundsMuted(config));
assertFalse(ZenModeConfig.areAllZenBehaviorSoundsMuted(config));
- config.areChannelsBypassingDnd = false;
+ config.hasPriorityChannels = false;
if (Flags.modesUi()) {
config.manualRule.zenPolicy = new ZenPolicy.Builder(config.manualRule.zenPolicy)
.allowPriorityChannels(false)
@@ -488,33 +488,33 @@ public class ZenModeConfigTest extends UiServiceTestCase {
ZenModeConfig.ZenRule rule = new ZenModeConfig.ZenRule();
rule.zenPolicy = null;
rule.zenDeviceEffects = null;
- assertThat(rule.canBeUpdatedByApp()).isTrue();
+ assertThat(rule.isUserModified()).isFalse();
rule.userModifiedFields = 1;
- assertThat(rule.canBeUpdatedByApp()).isFalse();
+ assertThat(rule.isUserModified()).isTrue();
}
@Test
public void testCanBeUpdatedByApp_policyModified() throws Exception {
ZenModeConfig.ZenRule rule = new ZenModeConfig.ZenRule();
rule.zenPolicy = new ZenPolicy();
- assertThat(rule.canBeUpdatedByApp()).isTrue();
+ assertThat(rule.isUserModified()).isFalse();
rule.zenPolicyUserModifiedFields = 1;
- assertThat(rule.canBeUpdatedByApp()).isFalse();
+ assertThat(rule.isUserModified()).isTrue();
}
@Test
public void testCanBeUpdatedByApp_deviceEffectsModified() throws Exception {
ZenModeConfig.ZenRule rule = new ZenModeConfig.ZenRule();
rule.zenDeviceEffects = new ZenDeviceEffects.Builder().build();
- assertThat(rule.canBeUpdatedByApp()).isTrue();
+ assertThat(rule.isUserModified()).isFalse();
rule.zenDeviceEffectsUserModifiedFields = 1;
- assertThat(rule.canBeUpdatedByApp()).isFalse();
+ assertThat(rule.isUserModified()).isTrue();
}
@Test
@@ -548,7 +548,6 @@ public class ZenModeConfigTest extends UiServiceTestCase {
rule.creationTime = 123;
rule.id = "id";
rule.zenMode = INTERRUPTION_FILTER;
- rule.modified = true;
rule.name = NAME;
rule.setConditionOverride(OVERRIDE_DEACTIVATE);
rule.pkg = OWNER.getPackageName();
@@ -564,6 +563,9 @@ public class ZenModeConfigTest extends UiServiceTestCase {
rule.deletionInstant = Instant.ofEpochMilli(1701790147000L);
if (Flags.modesUi()) {
rule.disabledOrigin = ZenModeConfig.ORIGIN_USER_IN_SYSTEMUI;
+ if (Flags.modesCleanupImplicit()) {
+ rule.lastActivation = Instant.ofEpochMilli(456);
+ }
}
config.automaticRules.put(rule.id, rule);
@@ -585,7 +587,6 @@ public class ZenModeConfigTest extends UiServiceTestCase {
assertEquals(rule.condition, ruleActual.condition);
assertEquals(rule.enabled, ruleActual.enabled);
assertEquals(rule.creationTime, ruleActual.creationTime);
- assertEquals(rule.modified, ruleActual.modified);
assertEquals(rule.conditionId, ruleActual.conditionId);
assertEquals(rule.name, ruleActual.name);
assertEquals(rule.zenMode, ruleActual.zenMode);
@@ -602,6 +603,9 @@ public class ZenModeConfigTest extends UiServiceTestCase {
assertEquals(rule.deletionInstant, ruleActual.deletionInstant);
if (Flags.modesUi()) {
assertEquals(rule.disabledOrigin, ruleActual.disabledOrigin);
+ if (Flags.modesCleanupImplicit()) {
+ assertEquals(rule.lastActivation, ruleActual.lastActivation);
+ }
}
if (Flags.backupRestoreLogging()) {
verify(logger).logItemsBackedUp(DATA_TYPE_ZEN_RULES, 2);
@@ -620,7 +624,6 @@ public class ZenModeConfigTest extends UiServiceTestCase {
rule.creationTime = 123;
rule.id = "id";
rule.zenMode = INTERRUPTION_FILTER;
- rule.modified = true;
rule.name = NAME;
rule.setConditionOverride(OVERRIDE_DEACTIVATE);
rule.pkg = OWNER.getPackageName();
@@ -636,6 +639,9 @@ public class ZenModeConfigTest extends UiServiceTestCase {
rule.deletionInstant = Instant.ofEpochMilli(1701790147000L);
if (Flags.modesUi()) {
rule.disabledOrigin = ZenModeConfig.ORIGIN_USER_IN_SYSTEMUI;
+ if (Flags.modesCleanupImplicit()) {
+ rule.lastActivation = Instant.ofEpochMilli(789);
+ }
}
Parcel parcel = Parcel.obtain();
@@ -651,7 +657,6 @@ public class ZenModeConfigTest extends UiServiceTestCase {
assertEquals(rule.condition, parceled.condition);
assertEquals(rule.enabled, parceled.enabled);
assertEquals(rule.creationTime, parceled.creationTime);
- assertEquals(rule.modified, parceled.modified);
assertEquals(rule.conditionId, parceled.conditionId);
assertEquals(rule.name, parceled.name);
assertEquals(rule.zenMode, parceled.zenMode);
@@ -668,6 +673,9 @@ public class ZenModeConfigTest extends UiServiceTestCase {
assertEquals(rule.deletionInstant, parceled.deletionInstant);
if (Flags.modesUi()) {
assertEquals(rule.disabledOrigin, parceled.disabledOrigin);
+ if (Flags.modesCleanupImplicit()) {
+ assertEquals(rule.lastActivation, parceled.lastActivation);
+ }
}
assertEquals(rule, parceled);
@@ -685,7 +693,6 @@ public class ZenModeConfigTest extends UiServiceTestCase {
rule.creationTime = 123;
rule.id = "id";
rule.zenMode = Settings.Global.ZEN_MODE_ALARMS;
- rule.modified = true;
rule.name = "name";
rule.snoozing = true;
rule.pkg = "b";
@@ -705,7 +712,6 @@ public class ZenModeConfigTest extends UiServiceTestCase {
assertEquals(rule.condition, fromXml.condition);
assertEquals(rule.enabled, fromXml.enabled);
assertEquals(rule.creationTime, fromXml.creationTime);
- assertEquals(rule.modified, fromXml.modified);
assertEquals(rule.conditionId, fromXml.conditionId);
assertEquals(rule.name, fromXml.name);
assertEquals(rule.zenMode, fromXml.zenMode);
@@ -721,7 +727,6 @@ public class ZenModeConfigTest extends UiServiceTestCase {
rule.enabled = ENABLED;
rule.id = "id";
rule.zenMode = INTERRUPTION_FILTER;
- rule.modified = true;
rule.name = NAME;
rule.setConditionOverride(OVERRIDE_DEACTIVATE);
rule.pkg = OWNER.getPackageName();
@@ -753,6 +758,9 @@ public class ZenModeConfigTest extends UiServiceTestCase {
rule.deletionInstant = Instant.ofEpochMilli(1701790147000L);
if (Flags.modesUi()) {
rule.disabledOrigin = ZenModeConfig.ORIGIN_APP;
+ if (Flags.modesCleanupImplicit()) {
+ rule.lastActivation = Instant.ofEpochMilli(123);
+ }
}
ByteArrayOutputStream baos = new ByteArrayOutputStream();
@@ -770,7 +778,6 @@ public class ZenModeConfigTest extends UiServiceTestCase {
assertEquals(rule.condition, fromXml.condition);
assertEquals(rule.enabled, fromXml.enabled);
assertEquals(rule.creationTime, fromXml.creationTime);
- assertEquals(rule.modified, fromXml.modified);
assertEquals(rule.conditionId, fromXml.conditionId);
assertEquals(rule.name, fromXml.name);
assertEquals(rule.zenMode, fromXml.zenMode);
@@ -789,6 +796,9 @@ public class ZenModeConfigTest extends UiServiceTestCase {
assertEquals(rule.deletionInstant, fromXml.deletionInstant);
if (Flags.modesUi()) {
assertEquals(rule.disabledOrigin, fromXml.disabledOrigin);
+ if (Flags.modesCleanupImplicit()) {
+ assertEquals(rule.lastActivation, fromXml.lastActivation);
+ }
}
}
@@ -916,7 +926,7 @@ public class ZenModeConfigTest extends UiServiceTestCase {
ZenModeConfig.ZenRule rule = new ZenModeConfig.ZenRule();
rule.userModifiedFields |= AutomaticZenRule.FIELD_NAME;
assertThat(rule.userModifiedFields).isEqualTo(1);
- assertThat(rule.canBeUpdatedByApp()).isFalse();
+ assertThat(rule.isUserModified()).isTrue();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
writeRuleXml(rule, baos);
@@ -924,7 +934,7 @@ public class ZenModeConfigTest extends UiServiceTestCase {
ZenModeConfig.ZenRule fromXml = readRuleXml(bais);
assertThat(fromXml.userModifiedFields).isEqualTo(rule.userModifiedFields);
- assertThat(fromXml.canBeUpdatedByApp()).isFalse();
+ assertThat(fromXml.isUserModified()).isTrue();
}
@Test
@@ -1259,7 +1269,6 @@ public class ZenModeConfigTest extends UiServiceTestCase {
rule.creationTime = 123;
rule.id = "id";
rule.zenMode = ZEN_MODE_IMPORTANT_INTERRUPTIONS;
- rule.modified = true;
rule.name = "name";
rule.pkg = "b";
config.automaticRules.put("key", rule);
@@ -1348,7 +1357,7 @@ public class ZenModeConfigTest extends UiServiceTestCase {
config.setSuppressedVisualEffects(0);
config.setAllowPriorityChannels(false);
}
- config.areChannelsBypassingDnd = false;
+ config.hasPriorityChannels = false;
return config;
}
@@ -1383,7 +1392,7 @@ public class ZenModeConfigTest extends UiServiceTestCase {
config.setSuppressedVisualEffects(0);
config.setAllowPriorityChannels(true);
}
- config.areChannelsBypassingDnd = false;
+ config.hasPriorityChannels = false;
return config;
}
@@ -1410,7 +1419,7 @@ public class ZenModeConfigTest extends UiServiceTestCase {
config.setAllowConversationsFrom(CONVERSATION_SENDERS_NONE);
config.setSuppressedVisualEffects(0);
}
- config.areChannelsBypassingDnd = false;
+ config.hasPriorityChannels = false;
return config;
}
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeDiffTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeDiffTest.java
index b138c72875a6..6d0bf8b322fd 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeDiffTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeDiffTest.java
@@ -16,7 +16,6 @@
package com.android.server.notification;
-import static android.app.Flags.FLAG_MODES_API;
import static android.app.Flags.FLAG_MODES_UI;
import static com.google.common.truth.Truth.assertThat;
@@ -64,7 +63,6 @@ import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
-import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Optional;
@@ -78,20 +76,14 @@ public class ZenModeDiffTest extends UiServiceTestCase {
// version is not included in the diff; manual & automatic rules have special handling;
// deleted rules are not included in the diff.
public static final Set<String> ZEN_MODE_CONFIG_EXEMPT_FIELDS =
- android.app.Flags.modesApi()
- ? Set.of("version", "manualRule", "automaticRules", "deletedRules")
- : Set.of("version", "manualRule", "automaticRules");
-
- // allowPriorityChannels is flagged by android.app.modes_api
- public static final Set<String> ZEN_MODE_CONFIG_FLAGGED_FIELDS =
- Set.of("allowPriorityChannels");
+ Set.of("version", "manualRule", "automaticRules", "deletedRules");
@Rule
public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
@Parameters(name = "{0}")
public static List<FlagsParameterization> getParams() {
- return FlagsParameterization.progressionOf(FLAG_MODES_API, FLAG_MODES_UI);
+ return FlagsParameterization.progressionOf(FLAG_MODES_UI);
}
public ZenModeDiffTest(FlagsParameterization flags) {
@@ -147,7 +139,7 @@ public class ZenModeDiffTest extends UiServiceTestCase {
}
@Test
- @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI})
+ @EnableFlags(FLAG_MODES_UI)
public void testRuleDiff_toStringNoChangeAddRemove() throws Exception {
// Start with two identical rules
ZenModeConfig.ZenRule r1 = makeRule();
@@ -164,7 +156,7 @@ public class ZenModeDiffTest extends UiServiceTestCase {
}
@Test
- @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI})
+ @EnableFlags(FLAG_MODES_UI)
public void testRuleDiff_toString() throws Exception {
// Start with two identical rules
ZenModeConfig.ZenRule r1 = makeRule();
@@ -218,7 +210,6 @@ public class ZenModeDiffTest extends UiServiceTestCase {
+ "mPriorityCalls:2->1, "
+ "mConversationSenders:2->1, "
+ "mAllowChannels:2->1}, "
- + "modified:true->false, "
+ "pkg:string1->string2, "
+ "zenDeviceEffects:ZenDeviceEffectsDiff{"
+ "mGrayscale:true->false, "
@@ -241,7 +232,7 @@ public class ZenModeDiffTest extends UiServiceTestCase {
}
@Test
- @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI})
+ @EnableFlags(FLAG_MODES_UI)
public void testRuleDiff_toStringNullStartPolicy() throws Exception {
// Start with two identical rules
ZenModeConfig.ZenRule r1 = makeRule();
@@ -278,7 +269,6 @@ public class ZenModeDiffTest extends UiServiceTestCase {
+ "creationTime:200->100, "
+ "enabler:string1->string2, "
+ "zenPolicy:ZenPolicyDiff{added}, "
- + "modified:true->false, "
+ "pkg:string1->string2, "
+ "zenDeviceEffects:ZenDeviceEffectsDiff{added}, "
+ "triggerDescription:string1->string2, "
@@ -485,16 +475,10 @@ public class ZenModeDiffTest extends UiServiceTestCase {
// "Metadata" fields are never compared.
Set<String> exemptFields = new LinkedHashSet<>(
Set.of("userModifiedFields", "zenPolicyUserModifiedFields",
- "zenDeviceEffectsUserModifiedFields", "deletionInstant", "disabledOrigin"));
+ "zenDeviceEffectsUserModifiedFields", "deletionInstant", "disabledOrigin",
+ "lastActivation"));
// Flagged fields are only compared if their flag is on.
- if (!Flags.modesApi()) {
- exemptFields.addAll(
- Set.of(RuleDiff.FIELD_TYPE, RuleDiff.FIELD_TRIGGER_DESCRIPTION,
- RuleDiff.FIELD_ICON_RES, RuleDiff.FIELD_ALLOW_MANUAL,
- RuleDiff.FIELD_ZEN_DEVICE_EFFECTS,
- RuleDiff.FIELD_LEGACY_SUPPRESSED_EFFECTS));
- }
- if (Flags.modesApi() && Flags.modesUi()) {
+ if (Flags.modesUi()) {
exemptFields.add(RuleDiff.FIELD_SNOOZING); // Obsolete.
} else {
exemptFields.add(RuleDiff.FIELD_CONDITION_OVERRIDE);
@@ -530,35 +514,6 @@ public class ZenModeDiffTest extends UiServiceTestCase {
ArrayMap<String, Object> expectedFrom = new ArrayMap<>();
ArrayMap<String, Object> expectedTo = new ArrayMap<>();
List<Field> fieldsForDiff = getFieldsForDiffCheck(
- ZenModeConfig.class, getConfigExemptAndFlaggedFields(), false);
- generateFieldDiffs(c1, c2, fieldsForDiff, expectedFrom, expectedTo);
-
- ZenModeDiff.ConfigDiff d = new ZenModeDiff.ConfigDiff(c1, c2);
- assertTrue(d.hasDiff());
-
- // Now diff them and check that each of the fields has a diff
- for (Field f : fieldsForDiff) {
- String name = f.getName();
- assertNotNull("diff not found for field: " + name, d.getDiffForField(name));
- assertTrue(d.getDiffForField(name).hasDiff());
- assertTrue("unexpected field: " + name, expectedFrom.containsKey(name));
- assertTrue("unexpected field: " + name, expectedTo.containsKey(name));
- assertEquals(expectedFrom.get(name), d.getDiffForField(name).from());
- assertEquals(expectedTo.get(name), d.getDiffForField(name).to());
- }
- }
-
- @Test
- @EnableFlags(FLAG_MODES_API)
- public void testConfigDiff_fieldDiffs_flagOn() throws Exception {
- // these two start the same
- ZenModeConfig c1 = new ZenModeConfig();
- ZenModeConfig c2 = new ZenModeConfig();
-
- // maps mapping field name -> expected output value as we set diffs
- ArrayMap<String, Object> expectedFrom = new ArrayMap<>();
- ArrayMap<String, Object> expectedTo = new ArrayMap<>();
- List<Field> fieldsForDiff = getFieldsForDiffCheck(
ZenModeConfig.class, ZEN_MODE_CONFIG_EXEMPT_FIELDS, false);
generateFieldDiffs(c1, c2, fieldsForDiff, expectedFrom, expectedTo);
@@ -656,14 +611,6 @@ public class ZenModeDiffTest extends UiServiceTestCase {
assertEquals("different", automaticDiffs.get("ruleId").getDiffForField("pkg").to());
}
- // Helper method that merges the base exempt fields with fields that are flagged
- private Set getConfigExemptAndFlaggedFields() {
- Set merged = new HashSet();
- merged.addAll(ZEN_MODE_CONFIG_EXEMPT_FIELDS);
- merged.addAll(ZEN_MODE_CONFIG_FLAGGED_FIELDS);
- return merged;
- }
-
// Helper methods for working with configs, policies, rules
// Just makes a zen rule with fields filled in
private ZenModeConfig.ZenRule makeRule() {
@@ -676,20 +623,17 @@ public class ZenModeDiffTest extends UiServiceTestCase {
rule.creationTime = 123;
rule.id = "ruleId";
rule.zenMode = Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS;
- rule.modified = false;
rule.name = "name";
rule.setConditionOverride(ZenModeConfig.ZenRule.OVERRIDE_DEACTIVATE);
rule.pkg = "a";
- if (android.app.Flags.modesApi()) {
- rule.allowManualInvocation = true;
- rule.type = AutomaticZenRule.TYPE_SCHEDULE_TIME;
- rule.iconResName = "res";
- rule.triggerDescription = "At night";
- rule.zenDeviceEffects = new ZenDeviceEffects.Builder()
- .setShouldDimWallpaper(true)
- .build();
- rule.userModifiedFields = AutomaticZenRule.FIELD_NAME;
- }
+ rule.allowManualInvocation = true;
+ rule.type = AutomaticZenRule.TYPE_SCHEDULE_TIME;
+ rule.iconResName = "res";
+ rule.triggerDescription = "At night";
+ rule.zenDeviceEffects = new ZenDeviceEffects.Builder()
+ .setShouldDimWallpaper(true)
+ .build();
+ rule.userModifiedFields = AutomaticZenRule.FIELD_NAME;
return rule;
}
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeFilteringTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeFilteringTest.java
index a49f5a89b11b..2f0b3ecb593a 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeFilteringTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeFilteringTest.java
@@ -37,7 +37,6 @@ import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
-import android.app.Flags;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager.Policy;
@@ -492,8 +491,6 @@ public class ZenModeFilteringTest extends UiServiceTestCase {
@Test
public void testAllowChannels_priorityPackage() {
- mSetFlagsRule.enableFlags(Flags.FLAG_MODES_API);
-
// Notification with package priority = PRIORITY_MAX (assigned to indicate canBypassDnd)
NotificationRecord r = getNotificationRecord();
r.setPackagePriority(Notification.PRIORITY_MAX);
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
index 31b9cf72584c..0ab11e0cbe3d 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
@@ -23,7 +23,7 @@ import static android.app.AutomaticZenRule.TYPE_SCHEDULE_TIME;
import static android.app.AutomaticZenRule.TYPE_THEATER;
import static android.app.AutomaticZenRule.TYPE_UNKNOWN;
import static android.app.Flags.FLAG_BACKUP_RESTORE_LOGGING;
-import static android.app.Flags.FLAG_MODES_API;
+import static android.app.Flags.FLAG_MODES_CLEANUP_IMPLICIT;
import static android.app.Flags.FLAG_MODES_MULTIUSER;
import static android.app.Flags.FLAG_MODES_UI;
import static android.app.NotificationManager.AUTOMATIC_RULE_STATUS_ACTIVATED;
@@ -85,6 +85,7 @@ import static android.service.notification.ZenPolicy.VISUAL_EFFECT_LIGHTS;
import static android.service.notification.ZenPolicy.VISUAL_EFFECT_PEEK;
import static com.android.internal.config.sysui.SystemUiSystemPropertiesFlags.NotificationFlags.LOG_DND_STATE_EVENTS;
+import static com.android.os.dnd.DNDProtoEnums.CONV_IMPORTANT;
import static com.android.os.dnd.DNDProtoEnums.PEOPLE_STARRED;
import static com.android.os.dnd.DNDProtoEnums.ROOT_CONFIG;
import static com.android.os.dnd.DNDProtoEnums.STATE_ALLOW;
@@ -124,7 +125,10 @@ import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.withSettings;
+import static java.time.temporal.ChronoUnit.DAYS;
+
import android.Manifest;
+import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SuppressLint;
import android.app.AlarmManager;
@@ -219,11 +223,11 @@ import java.io.Reader;
import java.io.StringWriter;
import java.time.Instant;
import java.time.ZoneOffset;
-import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.LinkedList;
import java.util.List;
+import java.util.Map;
import java.util.Objects;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
@@ -713,7 +717,6 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags(FLAG_MODES_API)
public void testTotalSilence_consolidatedPolicyDisallowsAll() {
// Start with zen mode off just to make sure global/manual mode isn't doing anything.
mZenModeHelper.mZenMode = ZEN_MODE_OFF;
@@ -746,7 +749,6 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags(FLAG_MODES_API)
public void testAlarmsOnly_consolidatedPolicyOnlyAllowsAlarmsAndMedia() {
// Start with zen mode off just to make sure global/manual mode isn't doing anything.
mZenModeHelper.mZenMode = ZEN_MODE_OFF;
@@ -1136,8 +1138,7 @@ public class ZenModeHelperTest extends UiServiceTestCase {
@Test
public void testProto() throws InvalidProtocolBufferException {
mZenModeHelper.setManualZenMode(UserHandle.CURRENT, ZEN_MODE_IMPORTANT_INTERRUPTIONS, null,
- Flags.modesApi() ? ORIGIN_USER_IN_SYSTEMUI : ORIGIN_SYSTEM, null,
- "test", CUSTOM_PKG_UID);
+ ORIGIN_USER_IN_SYSTEMUI, null, "test", CUSTOM_PKG_UID);
mZenModeHelper.mConfig.automaticRules = new ArrayMap<>(); // no automatic rules
@@ -1262,7 +1263,6 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags(FLAG_MODES_API)
public void testProtoWithAutoRuleCustomPolicy() throws Exception {
setupZenConfig();
// clear any automatic rules just to make sure
@@ -1304,7 +1304,6 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags(FLAG_MODES_API)
public void testProtoWithAutoRuleWithModifiedFields() throws Exception {
setupZenConfig();
mZenModeHelper.mConfig.automaticRules = new ArrayMap<>();
@@ -2005,7 +2004,6 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags(FLAG_MODES_API)
public void testReadXml_onModesApi_noUpgrade() throws Exception {
// When reading XML for something that is already on the modes API system, make sure no
// rules' policies get changed.
@@ -2053,7 +2051,6 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags(FLAG_MODES_API)
public void testReadXml_upgradeToModesApi_makesCustomPolicies() throws Exception {
// When reading in an XML file written from a pre-modes-API version, confirm that we create
// a custom policy matching the global config for any automatic rule with no specified
@@ -2105,7 +2102,6 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags(FLAG_MODES_API)
public void testReadXml_upgradeToModesApi_fillsInCustomPolicies() throws Exception {
// When reading in an XML file written from a pre-modes-API version, confirm that for an
// underspecified ZenPolicy, we fill in all of the gaps with things from the global config
@@ -2165,7 +2161,6 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags(FLAG_MODES_API)
public void testReadXml_upgradeToModesApi_existingDefaultRulesGetCustomPolicy()
throws Exception {
setupZenConfig();
@@ -2227,7 +2222,7 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI})
+ @EnableFlags(FLAG_MODES_UI)
public void testReadXml_upgradeToModesUi_resetsImplicitRuleIcon() throws Exception {
setupZenConfig();
mZenModeHelper.mConfig.automaticRules.clear();
@@ -2241,13 +2236,12 @@ public class ZenModeHelperTest extends UiServiceTestCase {
mZenModeHelper.mConfig.automaticRules.put(implicitRuleBeforeModesUi.id,
implicitRuleBeforeModesUi);
// Plus one other normal rule.
- ZenRule anotherRule = newZenRule("other_pkg", Instant.now(), null);
- anotherRule.id = "other_rule";
+ ZenRule anotherRule = newZenRule("other_rule", "other_pkg", Instant.now());
anotherRule.iconResName = "other_icon";
anotherRule.type = TYPE_IMMERSIVE;
mZenModeHelper.mConfig.automaticRules.put(anotherRule.id, anotherRule);
- // Write with pre-modes-ui = (modes_api) version, then re-read.
+ // Write with pre-modes-ui version, then re-read.
ByteArrayOutputStream baos = writeXmlAndPurge(ZenModeConfig.XML_VERSION_MODES_API);
TypedXmlPullParser parser = Xml.newFastPullParser();
parser.setInput(new BufferedInputStream(
@@ -2265,7 +2259,7 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI})
+ @EnableFlags(FLAG_MODES_UI)
public void testReadXml_onModesUi_implicitRulesUntouched() throws Exception {
setupZenConfig();
mZenModeHelper.mConfig.automaticRules.clear();
@@ -2279,8 +2273,7 @@ public class ZenModeHelperTest extends UiServiceTestCase {
implicitRuleWithModesUi);
// Plus one other normal rule.
- ZenRule anotherRule = newZenRule("other_pkg", Instant.now(), null);
- anotherRule.id = "other_rule";
+ ZenRule anotherRule = newZenRule("other_rule", "other_pkg", Instant.now());
anotherRule.iconResName = "other_icon";
anotherRule.type = TYPE_IMMERSIVE;
mZenModeHelper.mConfig.automaticRules.put(anotherRule.id, anotherRule);
@@ -2342,7 +2335,7 @@ public class ZenModeHelperTest extends UiServiceTestCase {
// shouldn't update rule that's been modified
ZenModeConfig.ZenRule updatedDefaultRule = new ZenModeConfig.ZenRule();
- updatedDefaultRule.modified = true;
+ updatedDefaultRule.userModifiedFields = AutomaticZenRule.FIELD_NAME;
updatedDefaultRule.enabled = false;
updatedDefaultRule.creationTime = 0;
updatedDefaultRule.id = SCHEDULE_DEFAULT_RULE_ID;
@@ -2370,8 +2363,8 @@ public class ZenModeHelperTest extends UiServiceTestCase {
// will update rule that is not enabled and modified
ZenModeConfig.ZenRule customDefaultRule = new ZenModeConfig.ZenRule();
customDefaultRule.pkg = SystemZenRules.PACKAGE_ANDROID;
+ customDefaultRule.userModifiedFields = AutomaticZenRule.FIELD_NAME;
customDefaultRule.enabled = false;
- customDefaultRule.modified = false;
customDefaultRule.creationTime = 0;
customDefaultRule.id = SCHEDULE_DEFAULT_RULE_ID;
customDefaultRule.name = "Schedule Default Rule";
@@ -2391,7 +2384,7 @@ public class ZenModeHelperTest extends UiServiceTestCase {
ZenModeConfig.ZenRule ruleAfterUpdating =
mZenModeHelper.mConfig.automaticRules.get(SCHEDULE_DEFAULT_RULE_ID);
assertEquals(customDefaultRule.enabled, ruleAfterUpdating.enabled);
- assertEquals(customDefaultRule.modified, ruleAfterUpdating.modified);
+ assertEquals(customDefaultRule.userModifiedFields, ruleAfterUpdating.userModifiedFields);
assertEquals(customDefaultRule.id, ruleAfterUpdating.id);
assertEquals(customDefaultRule.conditionId, ruleAfterUpdating.conditionId);
assertNotEquals(defaultRuleName, ruleAfterUpdating.name); // update name
@@ -2401,8 +2394,7 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags(FLAG_MODES_API)
- public void testDefaultRulesFromConfig_modesApi_getPolicies() {
+ public void testDefaultRulesFromConfig_getPolicies() {
// After mZenModeHelper was created, set some things in the policy so it's changed from
// default.
setupZenConfig();
@@ -2530,7 +2522,6 @@ public class ZenModeHelperTest extends UiServiceTestCase {
ZenModeConfig.ZenRule ruleInConfig = mZenModeHelper.mConfig.automaticRules.get(id);
assertTrue(ruleInConfig != null);
assertEquals(zenRule.isEnabled(), ruleInConfig.enabled);
- assertEquals(zenRule.isModified(), ruleInConfig.modified);
assertEquals(zenRule.getConditionId(), ruleInConfig.conditionId);
assertEquals(NotificationManager.zenModeFromInterruptionFilter(
zenRule.getInterruptionFilter(), -1), ruleInConfig.zenMode);
@@ -2551,7 +2542,6 @@ public class ZenModeHelperTest extends UiServiceTestCase {
ZenModeConfig.ZenRule ruleInConfig = mZenModeHelper.mConfig.automaticRules.get(id);
assertTrue(ruleInConfig != null);
assertEquals(zenRule.isEnabled(), ruleInConfig.enabled);
- assertEquals(zenRule.isModified(), ruleInConfig.modified);
assertEquals(zenRule.getConditionId(), ruleInConfig.conditionId);
assertEquals(NotificationManager.zenModeFromInterruptionFilter(
zenRule.getInterruptionFilter(), -1), ruleInConfig.zenMode);
@@ -2560,8 +2550,7 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags(FLAG_MODES_API)
- public void testAddAutomaticZenRule_modesApi_fillsInDefaultValues() {
+ public void testAddAutomaticZenRule_fillsInDefaultValues() {
// When a new automatic zen rule is added with only some fields filled in, ensure that
// all unset fields are filled in with device defaults.
@@ -2762,7 +2751,6 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags(FLAG_MODES_API)
public void addAutomaticZenRule_fromApp_ignoresHiddenEffects() {
ZenDeviceEffects zde =
new ZenDeviceEffects.Builder()
@@ -2799,7 +2787,6 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags(FLAG_MODES_API)
public void addAutomaticZenRule_fromSystem_respectsHiddenEffects() {
ZenDeviceEffects zde = new ZenDeviceEffects.Builder()
.setShouldDisplayGrayscale(true)
@@ -2828,7 +2815,6 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags(FLAG_MODES_API)
public void addAutomaticZenRule_fromUser_respectsHiddenEffects() throws Exception {
ZenDeviceEffects zde = new ZenDeviceEffects.Builder()
.setShouldDisplayGrayscale(true)
@@ -2859,7 +2845,6 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags(FLAG_MODES_API)
public void updateAutomaticZenRule_fromApp_preservesPreviousHiddenEffects() {
ZenDeviceEffects original = new ZenDeviceEffects.Builder()
.setShouldDisableTapToWake(true)
@@ -2896,7 +2881,6 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags(FLAG_MODES_API)
public void updateAutomaticZenRule_fromSystem_updatesHiddenEffects() {
ZenDeviceEffects original = new ZenDeviceEffects.Builder()
.setShouldDisableTapToWake(true)
@@ -2925,7 +2909,6 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags(FLAG_MODES_API)
public void updateAutomaticZenRule_fromUser_updatesHiddenEffects() {
ZenDeviceEffects original = new ZenDeviceEffects.Builder()
.setShouldDisableTapToWake(true)
@@ -2958,7 +2941,6 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags(FLAG_MODES_API)
public void updateAutomaticZenRule_nullPolicy_doesNothing() {
// Test that when updateAutomaticZenRule is called with a null policy, nothing changes
// about the existing policy.
@@ -2985,7 +2967,6 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags(FLAG_MODES_API)
public void updateAutomaticZenRule_overwritesExistingPolicy() {
// Test that when updating an automatic zen rule with an existing policy, the newly set
// fields overwrite those from the previous policy, but unset fields in the new policy
@@ -3024,7 +3005,6 @@ public class ZenModeHelperTest extends UiServiceTestCase {
@Test
- @EnableFlags(FLAG_MODES_API)
public void addAutomaticZenRule_withTypeBedtime_replacesDisabledSleeping() {
ZenRule sleepingRule = createCustomAutomaticRule(ZEN_MODE_IMPORTANT_INTERRUPTIONS,
ZenModeConfig.EVERY_NIGHT_DEFAULT_RULE_ID);
@@ -3044,7 +3024,6 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags(FLAG_MODES_API)
public void addAutomaticZenRule_withTypeBedtime_keepsEnabledSleeping() {
ZenRule sleepingRule = createCustomAutomaticRule(ZEN_MODE_IMPORTANT_INTERRUPTIONS,
ZenModeConfig.EVERY_NIGHT_DEFAULT_RULE_ID);
@@ -3065,7 +3044,6 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags(FLAG_MODES_API)
public void addAutomaticZenRule_withTypeBedtime_keepsCustomizedSleeping() {
ZenRule sleepingRule = createCustomAutomaticRule(ZEN_MODE_IMPORTANT_INTERRUPTIONS,
ZenModeConfig.EVERY_NIGHT_DEFAULT_RULE_ID);
@@ -3086,7 +3064,7 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI})
+ @EnableFlags(FLAG_MODES_UI)
public void updateAutomaticZenRule_withTypeBedtime_replacesDisabledSleeping() {
ZenRule sleepingRule = createCustomAutomaticRule(ZEN_MODE_IMPORTANT_INTERRUPTIONS,
ZenModeConfig.EVERY_NIGHT_DEFAULT_RULE_ID);
@@ -3113,7 +3091,34 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags(FLAG_MODES_API)
+ public void getAutomaticZenRules_returnsOwnedRules() {
+ AutomaticZenRule myRule1 = new AutomaticZenRule.Builder("My Rule 1", Uri.parse("1"))
+ .setPackage(mPkg)
+ .setConfigurationActivity(new ComponentName(mPkg, "myActivity"))
+ .build();
+ AutomaticZenRule myRule2 = new AutomaticZenRule.Builder("My Rule 2", Uri.parse("2"))
+ .setPackage(mPkg)
+ .setConfigurationActivity(new ComponentName(mPkg, "myActivity"))
+ .build();
+ AutomaticZenRule otherPkgRule = new AutomaticZenRule.Builder("Other", Uri.parse("3"))
+ .setPackage("com.other.package")
+ .setConfigurationActivity(new ComponentName("com.other.package", "theirActivity"))
+ .build();
+
+ String rule1Id = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, mPkg, myRule1,
+ ORIGIN_APP, "reason", CUSTOM_PKG_UID);
+ String rule2Id = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, mPkg, myRule2,
+ ORIGIN_APP, "reason", CUSTOM_PKG_UID);
+ String otherRuleId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT,
+ "com.other.package", otherPkgRule, ORIGIN_APP, "reason", CUSTOM_PKG_UID);
+
+ Map<String, AutomaticZenRule> rules = mZenModeHelper.getAutomaticZenRules(
+ UserHandle.CURRENT, CUSTOM_PKG_UID);
+
+ assertThat(rules.keySet()).containsExactly(rule1Id, rule2Id);
+ }
+
+ @Test
public void testSetManualZenMode() {
setupZenConfig();
@@ -3133,7 +3138,6 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags(FLAG_MODES_API)
@DisableFlags(FLAG_MODES_UI)
public void setManualZenMode_off_snoozesActiveRules() {
for (ZenChangeOrigin origin : ZenChangeOrigin.values()) {
@@ -3172,7 +3176,7 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI})
+ @EnableFlags(FLAG_MODES_UI)
public void setManualZenMode_off_doesNotSnoozeRulesIfFromUserInSystemUi() {
for (ZenChangeOrigin origin : ZenChangeOrigin.values()) {
// Start with an active rule and an inactive rule
@@ -3246,7 +3250,7 @@ public class ZenModeHelperTest extends UiServiceTestCase {
// Turn zen mode on (to important_interruptions)
// Need to additionally call the looper in order to finish the post-apply-config process
mZenModeHelper.setManualZenMode(UserHandle.CURRENT, ZEN_MODE_IMPORTANT_INTERRUPTIONS, null,
- Flags.modesApi() ? ORIGIN_USER_IN_SYSTEMUI : ORIGIN_SYSTEM, "", null, SYSTEM_UID);
+ ORIGIN_USER_IN_SYSTEMUI, "", null, SYSTEM_UID);
// Now turn zen mode off, but via a different package UID -- this should get registered as
// "not an action by the user" because some other app is changing zen mode
@@ -3273,14 +3277,13 @@ public class ZenModeHelperTest extends UiServiceTestCase {
assertEquals(ZEN_MODE_IMPORTANT_INTERRUPTIONS, mZenModeEventLogger.getNewZenMode(0));
assertEquals(DNDProtoEnums.MANUAL_RULE, mZenModeEventLogger.getChangedRuleType(0));
assertEquals(1, mZenModeEventLogger.getNumRulesActive(0));
- assertThat(mZenModeEventLogger.getFromSystemOrSystemUi(0)).isEqualTo(
- !(Flags.modesUi() || Flags.modesApi()));
+ assertThat(mZenModeEventLogger.getFromSystemOrSystemUi(0)).isFalse();
assertTrue(mZenModeEventLogger.getIsUserAction(0));
assertEquals(SYSTEM_UID, mZenModeEventLogger.getPackageUid(0));
checkDndProtoMatchesSetupZenConfig(mZenModeEventLogger.getPolicyProto(0));
// change origin should be populated only under modes_ui
assertThat(mZenModeEventLogger.getChangeOrigin(0)).isEqualTo(
- (Flags.modesApi() && Flags.modesUi()) ? ORIGIN_USER_IN_SYSTEMUI : 0);
+ (Flags.modesUi()) ? ORIGIN_USER_IN_SYSTEMUI : 0);
// and from turning zen mode off:
// - event ID: DND_TURNED_OFF
@@ -3298,11 +3301,7 @@ public class ZenModeHelperTest extends UiServiceTestCase {
assertEquals(0, mZenModeEventLogger.getNumRulesActive(1));
assertFalse(mZenModeEventLogger.getIsUserAction(1));
assertEquals(CUSTOM_PKG_UID, mZenModeEventLogger.getPackageUid(1));
- if (Flags.modesApi()) {
- assertThat(mZenModeEventLogger.getPolicyProto(1)).isNull();
- } else {
- checkDndProtoMatchesSetupZenConfig(mZenModeEventLogger.getPolicyProto(1));
- }
+ assertThat(mZenModeEventLogger.getPolicyProto(1)).isNull();
assertThat(mZenModeEventLogger.getChangeOrigin(1)).isEqualTo(
Flags.modesUi() ? ORIGIN_APP : 0);
}
@@ -3334,8 +3333,7 @@ public class ZenModeHelperTest extends UiServiceTestCase {
// Event 2: "User" turns off the automatic rule (sets it to not enabled)
zenRule.setEnabled(false);
mZenModeHelper.updateAutomaticZenRule(UserHandle.CURRENT, id, zenRule,
- Flags.modesApi() ? ORIGIN_USER_IN_SYSTEMUI : ORIGIN_SYSTEM, "",
- SYSTEM_UID);
+ ORIGIN_USER_IN_SYSTEMUI, "", SYSTEM_UID);
AutomaticZenRule systemRule = new AutomaticZenRule("systemRule",
null,
@@ -3345,8 +3343,7 @@ public class ZenModeHelperTest extends UiServiceTestCase {
NotificationManager.INTERRUPTION_FILTER_PRIORITY, true);
String systemId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT,
mContext.getPackageName(), systemRule,
- Flags.modesApi() ? ORIGIN_USER_IN_SYSTEMUI : ORIGIN_SYSTEM, "test",
- SYSTEM_UID);
+ ORIGIN_USER_IN_SYSTEMUI, "test", SYSTEM_UID);
// Event 3: turn on the system rule
mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, systemId,
@@ -3355,8 +3352,7 @@ public class ZenModeHelperTest extends UiServiceTestCase {
// Event 4: "User" deletes the rule
mZenModeHelper.removeAutomaticZenRule(UserHandle.CURRENT, systemId,
- Flags.modesApi() ? ORIGIN_USER_IN_SYSTEMUI : ORIGIN_SYSTEM, "",
- SYSTEM_UID);
+ ORIGIN_USER_IN_SYSTEMUI, "", SYSTEM_UID);
// In total, this represents 4 events
assertEquals(4, mZenModeEventLogger.numLoggedChanges());
@@ -3394,11 +3390,7 @@ public class ZenModeHelperTest extends UiServiceTestCase {
assertTrue(mZenModeEventLogger.getIsUserAction(1));
assertThat(mZenModeEventLogger.getPackageUid(1)).isEqualTo(
Flags.modesUi() ? CUSTOM_PKG_UID : SYSTEM_UID);
- if (Flags.modesApi()) {
- assertThat(mZenModeEventLogger.getPolicyProto(1)).isNull();
- } else {
- checkDndProtoMatchesSetupZenConfig(mZenModeEventLogger.getPolicyProto(1));
- }
+ assertThat(mZenModeEventLogger.getPolicyProto(1)).isNull();
assertThat(mZenModeEventLogger.getChangeOrigin(1)).isEqualTo(
Flags.modesUi() ? ORIGIN_USER_IN_SYSTEMUI : 0);
@@ -3426,7 +3418,6 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags(FLAG_MODES_API)
public void testZenModeEventLog_automaticRuleActivatedFromAppByAppAndUser()
throws IllegalArgumentException {
mTestFlagResolver.setFlagOverride(LOG_DND_STATE_EVENTS, true);
@@ -3816,13 +3807,13 @@ public class ZenModeHelperTest extends UiServiceTestCase {
// Now change apps bypassing to true
ZenModeConfig newConfig = mZenModeHelper.mConfig.copy();
- newConfig.areChannelsBypassingDnd = true;
+ newConfig.hasPriorityChannels = true;
mZenModeHelper.setNotificationPolicy(UserHandle.CURRENT, newConfig.toNotificationPolicy(),
ORIGIN_SYSTEM, SYSTEM_UID);
assertEquals(2, mZenModeEventLogger.numLoggedChanges());
// and then back to false, all without changing anything else
- newConfig.areChannelsBypassingDnd = false;
+ newConfig.hasPriorityChannels = false;
mZenModeHelper.setNotificationPolicy(UserHandle.CURRENT, newConfig.toNotificationPolicy(),
ORIGIN_SYSTEM, SYSTEM_UID);
assertEquals(3, mZenModeEventLogger.numLoggedChanges());
@@ -3869,10 +3860,9 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags(FLAG_MODES_API)
public void testZenModeEventLog_policyAllowChannels() {
- // when modes_api flag is on, ensure that any change in allow_channels gets logged,
- // even when there are no other changes.
+ // Ensure that any change in allow_channels gets logged, even when there are no other
+ // changes.
mTestFlagResolver.setFlagOverride(LOG_DND_STATE_EVENTS, true);
// Default zen config has allow channels = priority (aka on)
@@ -3919,7 +3909,6 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags(FLAG_MODES_API)
public void testZenModeEventLog_ruleWithInterruptionFilterAll_notLoggedAsDndChange() {
mTestFlagResolver.setFlagOverride(LOG_DND_STATE_EVENTS, true);
setupZenConfig();
@@ -3961,7 +3950,6 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags(FLAG_MODES_API)
public void testZenModeEventLog_activeRuleTypes() {
mTestFlagResolver.setFlagOverride(LOG_DND_STATE_EVENTS, true);
setupZenConfig();
@@ -4050,43 +4038,7 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @DisableFlags(FLAG_MODES_API)
- public void testUpdateConsolidatedPolicy_preModesApiDefaultRulesOnly_takesGlobalDefault() {
- setupZenConfig();
- // When there's one automatic rule active and it doesn't specify a policy, test that the
- // resulting consolidated policy is one that matches the default rule settings.
- AutomaticZenRule zenRule = new AutomaticZenRule("name",
- null,
- new ComponentName(CUSTOM_PKG_NAME, "ScheduleConditionProvider"),
- ZenModeConfig.toScheduleConditionId(new ScheduleInfo()),
- null,
- NotificationManager.INTERRUPTION_FILTER_PRIORITY, true);
- String id = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT,
- mContext.getPackageName(), zenRule, ORIGIN_SYSTEM, "test", SYSTEM_UID);
-
- // enable the rule
- mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, id,
- new Condition(zenRule.getConditionId(), "", STATE_TRUE),
- ORIGIN_SYSTEM, SYSTEM_UID);
-
- assertEquals(mZenModeHelper.getNotificationPolicy(UserHandle.CURRENT),
- mZenModeHelper.getConsolidatedNotificationPolicy());
-
- // inspect the consolidated policy. Based on setupZenConfig() values.
- assertFalse(mZenModeHelper.mConsolidatedPolicy.allowAlarms());
- assertFalse(mZenModeHelper.mConsolidatedPolicy.allowMedia());
- assertFalse(mZenModeHelper.mConsolidatedPolicy.allowSystem());
- assertTrue(mZenModeHelper.mConsolidatedPolicy.allowReminders());
- assertTrue(mZenModeHelper.mConsolidatedPolicy.allowCalls());
- assertEquals(PRIORITY_SENDERS_STARRED, mZenModeHelper.mConsolidatedPolicy.allowCallsFrom());
- assertTrue(mZenModeHelper.mConsolidatedPolicy.allowMessages());
- assertTrue(mZenModeHelper.mConsolidatedPolicy.allowConversations());
- assertTrue(mZenModeHelper.mConsolidatedPolicy.allowRepeatCallers());
- assertFalse(mZenModeHelper.mConsolidatedPolicy.showBadges());
- }
-
- @Test
- public void testUpdateConsolidatedPolicy_modesApiDefaultRulesOnly_takesDefault() {
+ public void testUpdateConsolidatedPolicy_defaultRulesOnly_takesDefault() {
setupZenConfig();
// When there's one automatic rule active and it doesn't specify a policy, test that the
@@ -4113,53 +4065,7 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @DisableFlags(FLAG_MODES_API)
- public void testUpdateConsolidatedPolicy_preModesApiCustomPolicyOnly_fillInWithGlobal() {
- setupZenConfig();
-
- // when there's only one automatic rule active and it has a custom policy, make sure that's
- // what the consolidated policy reflects whether or not it's stricter than what the global
- // config would specify.
- ZenPolicy customPolicy = new ZenPolicy.Builder()
- .allowAlarms(true) // more lenient than default
- .allowMedia(true) // more lenient than default
- .allowRepeatCallers(false) // more restrictive than default
- .allowCalls(ZenPolicy.PEOPLE_TYPE_NONE) // more restrictive than default
- .showBadges(true) // more lenient
- .showPeeking(false) // more restrictive
- .build();
-
- AutomaticZenRule zenRule = new AutomaticZenRule("name",
- null,
- new ComponentName(CUSTOM_PKG_NAME, "ScheduleConditionProvider"),
- ZenModeConfig.toScheduleConditionId(new ScheduleInfo()),
- customPolicy,
- NotificationManager.INTERRUPTION_FILTER_PRIORITY, true);
- String id = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT,
- mContext.getPackageName(), zenRule, ORIGIN_SYSTEM, "test", SYSTEM_UID);
-
- // enable the rule; this will update the consolidated policy
- mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, id,
- new Condition(zenRule.getConditionId(), "", STATE_TRUE), ORIGIN_SYSTEM, SYSTEM_UID);
-
- // since this is the only active rule, the consolidated policy should match the custom
- // policy for every field specified, and take default values (from device default or
- // manual policy) for unspecified things
- assertTrue(mZenModeHelper.mConsolidatedPolicy.allowAlarms()); // custom
- assertTrue(mZenModeHelper.mConsolidatedPolicy.allowMedia()); // custom
- assertFalse(mZenModeHelper.mConsolidatedPolicy.allowSystem()); // default
- assertTrue(mZenModeHelper.mConsolidatedPolicy.allowReminders()); // default
- assertFalse(mZenModeHelper.mConsolidatedPolicy.allowCalls()); // custom
- assertTrue(mZenModeHelper.mConsolidatedPolicy.allowMessages()); // default
- assertTrue(mZenModeHelper.mConsolidatedPolicy.allowConversations()); // default
- assertFalse(mZenModeHelper.mConsolidatedPolicy.allowRepeatCallers()); // custom
- assertTrue(mZenModeHelper.mConsolidatedPolicy.showBadges()); // custom
- assertFalse(mZenModeHelper.mConsolidatedPolicy.showPeeking()); // custom
- }
-
- @Test
- @EnableFlags(FLAG_MODES_API)
- public void testUpdateConsolidatedPolicy_modesApiCustomPolicyOnly_fillInWithDefault() {
+ public void testUpdateConsolidatedPolicy_customPolicyOnly_fillInWithDefault() {
setupZenConfig();
// when there's only one automatic rule active and it has a custom policy, make sure that's
@@ -4204,68 +4110,7 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @DisableFlags(FLAG_MODES_API)
- public void testUpdateConsolidatedPolicy_preModesApiDefaultAndCustomActive_mergesWithGlobal() {
- setupZenConfig();
-
- // when there are two rules active, one inheriting the default policy and one setting its
- // own custom policy, they should be merged to form the most restrictive combination.
-
- // rule 1: no custom policy
- AutomaticZenRule zenRule = new AutomaticZenRule("name",
- null,
- new ComponentName(CUSTOM_PKG_NAME, "ScheduleConditionProvider"),
- ZenModeConfig.toScheduleConditionId(new ScheduleInfo()),
- null,
- NotificationManager.INTERRUPTION_FILTER_PRIORITY, true);
- String id = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT,
- mContext.getPackageName(), zenRule, ORIGIN_SYSTEM, "test", SYSTEM_UID);
-
- // enable rule 1
- mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, id,
- new Condition(zenRule.getConditionId(), "", STATE_TRUE), ORIGIN_SYSTEM, SYSTEM_UID);
-
- // custom policy for rule 2
- ZenPolicy customPolicy = new ZenPolicy.Builder()
- .allowAlarms(true) // more lenient than default
- .allowMedia(true) // more lenient than default
- .allowRepeatCallers(false) // more restrictive than default
- .allowCalls(ZenPolicy.PEOPLE_TYPE_NONE) // more restrictive than default
- .showBadges(true) // more lenient
- .showPeeking(false) // more restrictive
- .build();
-
- AutomaticZenRule zenRule2 = new AutomaticZenRule("name2",
- null,
- new ComponentName(CUSTOM_PKG_NAME, "ScheduleConditionProvider"),
- ZenModeConfig.toScheduleConditionId(new ScheduleInfo()),
- customPolicy,
- NotificationManager.INTERRUPTION_FILTER_PRIORITY, true);
- String id2 = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT,
- mContext.getPackageName(), zenRule2, ORIGIN_SYSTEM, "test", SYSTEM_UID);
-
- // enable rule 2; this will update the consolidated policy
- mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, id2,
- new Condition(zenRule2.getConditionId(), "", STATE_TRUE),
- ORIGIN_SYSTEM, SYSTEM_UID);
-
- // now both rules should be on, and the consolidated policy should reflect the most
- // restrictive option of each of the two
- assertFalse(mZenModeHelper.mConsolidatedPolicy.allowAlarms()); // default stricter
- assertFalse(mZenModeHelper.mConsolidatedPolicy.allowMedia()); // default stricter
- assertFalse(mZenModeHelper.mConsolidatedPolicy.allowSystem()); // default, unset in custom
- assertTrue(mZenModeHelper.mConsolidatedPolicy.allowReminders()); // default
- assertFalse(mZenModeHelper.mConsolidatedPolicy.allowCalls()); // custom stricter
- assertTrue(mZenModeHelper.mConsolidatedPolicy.allowMessages()); // default, unset in custom
- assertTrue(mZenModeHelper.mConsolidatedPolicy.allowConversations()); // default
- assertFalse(mZenModeHelper.mConsolidatedPolicy.allowRepeatCallers()); // custom stricter
- assertFalse(mZenModeHelper.mConsolidatedPolicy.showBadges()); // default stricter
- assertFalse(mZenModeHelper.mConsolidatedPolicy.showPeeking()); // custom stricter
- }
-
- @Test
- @EnableFlags(FLAG_MODES_API)
- public void testUpdateConsolidatedPolicy_modesApiDefaultAndCustomActive_mergesWithDefault() {
+ public void testUpdateConsolidatedPolicy_defaultAndCustomActive_mergesWithDefault() {
setupZenConfig();
// when there are two rules active, one inheriting the default policy and one setting its
@@ -4328,7 +4173,6 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags(FLAG_MODES_API)
public void testUpdateConsolidatedPolicy_allowChannels() {
setupZenConfig();
@@ -4377,7 +4221,6 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags(FLAG_MODES_API)
public void testUpdateConsolidatedPolicy_ignoresActiveRulesWithInterruptionFilterAll() {
setupZenConfig();
@@ -4428,7 +4271,6 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags(FLAG_MODES_API)
public void zenRuleToAutomaticZenRule_allFields() {
when(mPackageManager.getPackagesForUid(anyInt())).thenReturn(
new String[]{OWNER.getPackageName()});
@@ -4442,7 +4284,6 @@ public class ZenModeHelperTest extends UiServiceTestCase {
rule.creationTime = 123;
rule.id = "id";
rule.zenMode = INTERRUPTION_FILTER_ZR;
- rule.modified = true;
rule.name = NAME;
rule.setConditionOverride(OVERRIDE_DEACTIVATE);
rule.pkg = OWNER.getPackageName();
@@ -4473,7 +4314,6 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags(FLAG_MODES_API)
public void automaticZenRuleToZenRule_allFields() {
when(mPackageManager.getPackagesForUid(anyInt())).thenReturn(
new String[]{OWNER.getPackageName()});
@@ -4515,7 +4355,6 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags(FLAG_MODES_API)
public void updateAutomaticZenRule_fromApp_updatesNameUnlessUserModified() {
// Add a starting rule with the name OriginalName.
AutomaticZenRule azrBase = new AutomaticZenRule.Builder("OriginalName", CONDITION_ID)
@@ -4573,7 +4412,6 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags(FLAG_MODES_API)
public void updateAutomaticZenRule_fromUser_updatesBitmaskAndValue() {
// Adds a starting rule with empty zen policies and device effects
AutomaticZenRule azrBase = new AutomaticZenRule.Builder(NAME, CONDITION_ID)
@@ -4627,7 +4465,6 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags(FLAG_MODES_API)
public void updateAutomaticZenRule_fromSystemUi_updatesValues() {
// Adds a starting rule with empty zen policies and device effects
AutomaticZenRule azrBase = new AutomaticZenRule.Builder(NAME, CONDITION_ID)
@@ -4678,7 +4515,6 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags(FLAG_MODES_API)
public void updateAutomaticZenRule_fromApp_updatesValuesIfRuleNotUserModified() {
// Adds a starting rule with empty zen policies and device effects
AutomaticZenRule azrBase = new AutomaticZenRule.Builder(NAME, CONDITION_ID)
@@ -4754,7 +4590,6 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags(FLAG_MODES_API)
public void addAutomaticZenRule_updatesValues() {
// Adds a starting rule with empty zen policies and device effects
AutomaticZenRule azrBase = new AutomaticZenRule.Builder(NAME, CONDITION_ID)
@@ -4777,11 +4612,10 @@ public class ZenModeHelperTest extends UiServiceTestCase {
assertThat(rule.getDeviceEffects().shouldDisplayGrayscale()).isTrue();
ZenRule storedRule = mZenModeHelper.mConfig.automaticRules.get(ruleId);
- assertThat(storedRule.canBeUpdatedByApp()).isTrue();
+ assertThat(storedRule.isUserModified()).isFalse();
}
@Test
- @EnableFlags(FLAG_MODES_API)
public void updateAutomaticZenRule_nullDeviceEffectsUpdate() {
// Adds a starting rule with empty zen policies and device effects
ZenDeviceEffects zde = new ZenDeviceEffects.Builder().setShouldUseNightMode(true).build();
@@ -4809,7 +4643,6 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags(FLAG_MODES_API)
public void updateAutomaticZenRule_nullPolicyUpdate() {
// Adds a starting rule with set zen policy and empty device effects
AutomaticZenRule azrBase = new AutomaticZenRule.Builder(NAME, CONDITION_ID)
@@ -4838,7 +4671,6 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags(FLAG_MODES_API)
public void automaticZenRuleToZenRule_nullToNonNullPolicyUpdate() {
when(mContext.checkCallingPermission(anyString()))
.thenReturn(PackageManager.PERMISSION_GRANTED);
@@ -4888,7 +4720,7 @@ public class ZenModeHelperTest extends UiServiceTestCase {
STATE_DISALLOW);
ZenRule storedRule = mZenModeHelper.mConfig.automaticRules.get(ruleId);
- assertThat(storedRule.canBeUpdatedByApp()).isFalse();
+ assertThat(storedRule.isUserModified()).isTrue();
assertThat(storedRule.zenPolicyUserModifiedFields).isEqualTo(
ZenPolicy.FIELD_ALLOW_CHANNELS
| ZenPolicy.FIELD_PRIORITY_CATEGORY_REMINDERS
@@ -4902,7 +4734,6 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags(FLAG_MODES_API)
public void automaticZenRuleToZenRule_nullToNonNullDeviceEffectsUpdate() {
// Adds a starting rule with empty zen policies and device effects
AutomaticZenRule azrBase = new AutomaticZenRule.Builder(NAME, CONDITION_ID)
@@ -4931,7 +4762,7 @@ public class ZenModeHelperTest extends UiServiceTestCase {
assertThat(rule.getDeviceEffects().shouldDisplayGrayscale()).isTrue();
ZenRule storedRule = mZenModeHelper.mConfig.automaticRules.get(ruleId);
- assertThat(storedRule.canBeUpdatedByApp()).isFalse();
+ assertThat(storedRule.isUserModified()).isTrue();
assertThat(storedRule.zenDeviceEffectsUserModifiedFields).isEqualTo(
ZenDeviceEffects.FIELD_GRAYSCALE);
}
@@ -5007,7 +4838,6 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags(FLAG_MODES_API)
public void testUpdateAutomaticRule_activated_triggersBroadcast() throws Exception {
setupZenConfig();
@@ -5047,7 +4877,6 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags(FLAG_MODES_API)
public void testUpdateAutomaticRule_deactivatedByUser_triggersBroadcast() throws Exception {
setupZenConfig();
@@ -5091,7 +4920,6 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags(FLAG_MODES_API)
public void testUpdateAutomaticRule_deactivatedByApp_triggersBroadcast() throws Exception {
setupZenConfig();
@@ -5167,7 +4995,6 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags(FLAG_MODES_API)
public void updateAutomaticZenRule_ruleChanged_deactivatesRule() {
assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_OFF);
AutomaticZenRule rule = new AutomaticZenRule.Builder("rule", CONDITION_ID)
@@ -5191,7 +5018,6 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags(FLAG_MODES_API)
public void updateAutomaticZenRule_ruleNotChanged_doesNotDeactivateRule() {
assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_OFF);
AutomaticZenRule rule = new AutomaticZenRule.Builder("rule", CONDITION_ID)
@@ -5214,7 +5040,7 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI})
+ @EnableFlags(FLAG_MODES_UI)
public void updateAutomaticZenRule_ruleChangedByUser_doesNotDeactivateRule() {
assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_OFF);
AutomaticZenRule rule = new AutomaticZenRule.Builder("rule", CONDITION_ID)
@@ -5239,7 +5065,6 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags(FLAG_MODES_API)
public void updateAutomaticZenRule_ruleChangedByUser_doesNotDeactivateRule_forWatch() {
when(mContext.getPackageManager()).thenReturn(mPackageManager);
when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WATCH)).thenReturn(true);
@@ -5266,7 +5091,7 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI})
+ @EnableFlags(FLAG_MODES_UI)
public void updateAutomaticZenRule_ruleDisabledByUser_doesNotReactivateOnReenable() {
assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_OFF);
AutomaticZenRule rule = new AutomaticZenRule.Builder("rule", CONDITION_ID)
@@ -5291,7 +5116,7 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI})
+ @EnableFlags(FLAG_MODES_UI)
public void updateAutomaticZenRule_changeOwnerForSystemRule_allowed() {
when(mContext.checkCallingPermission(anyString()))
.thenReturn(PackageManager.PERMISSION_GRANTED);
@@ -5314,7 +5139,7 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI})
+ @EnableFlags(FLAG_MODES_UI)
public void updateAutomaticZenRule_changeOwnerForAppOwnedRule_ignored() {
AutomaticZenRule original = new AutomaticZenRule.Builder("Rule", Uri.EMPTY)
.setOwner(new ComponentName(mContext.getPackageName(), "old.third.party.cps"))
@@ -5335,7 +5160,6 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags(FLAG_MODES_API)
public void removeAutomaticZenRule_propagatesOriginToEffectsApplier() {
mZenModeHelper.setDeviceEffectsApplier(mDeviceEffectsApplier);
reset(mDeviceEffectsApplier);
@@ -5358,7 +5182,6 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags(FLAG_MODES_API)
public void testDeviceEffects_applied() {
mZenModeHelper.setDeviceEffectsApplier(mDeviceEffectsApplier);
verify(mDeviceEffectsApplier).apply(eq(NO_EFFECTS), eq(ORIGIN_INIT));
@@ -5384,7 +5207,6 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags(FLAG_MODES_API)
public void testSettingDeviceEffects_throwsExceptionIfAlreadySet() {
mZenModeHelper.setDeviceEffectsApplier(mDeviceEffectsApplier);
@@ -5394,7 +5216,6 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags(FLAG_MODES_API)
public void testDeviceEffects_onDeactivateRule_applied() {
mZenModeHelper.setDeviceEffectsApplier(mDeviceEffectsApplier);
@@ -5413,7 +5234,6 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags(FLAG_MODES_API)
public void testDeviceEffects_changeToConsolidatedEffects_applied() {
mZenModeHelper.setDeviceEffectsApplier(mDeviceEffectsApplier);
verify(mDeviceEffectsApplier).apply(eq(NO_EFFECTS), eq(ORIGIN_INIT));
@@ -5453,7 +5273,6 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags(FLAG_MODES_API)
public void testDeviceEffects_noChangeToConsolidatedEffects_notApplied() {
mZenModeHelper.setDeviceEffectsApplier(mDeviceEffectsApplier);
verify(mDeviceEffectsApplier).apply(eq(NO_EFFECTS), eq(ORIGIN_INIT));
@@ -5478,7 +5297,6 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags(FLAG_MODES_API)
public void testDeviceEffects_activeBeforeApplierProvided_appliedWhenProvided() {
ZenDeviceEffects zde = new ZenDeviceEffects.Builder().setShouldUseNightMode(true).build();
String ruleId = addRuleWithEffects(zde);
@@ -5494,7 +5312,6 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags(FLAG_MODES_API)
public void testDeviceEffects_onUserSwitch_appliedImmediately() {
mZenModeHelper.setDeviceEffectsApplier(mDeviceEffectsApplier);
verify(mDeviceEffectsApplier).apply(eq(NO_EFFECTS), eq(ORIGIN_INIT));
@@ -5522,7 +5339,7 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI, FLAG_PREVENT_ZEN_DEVICE_EFFECTS_WHILE_DRIVING})
+ @EnableFlags({FLAG_MODES_UI, FLAG_PREVENT_ZEN_DEVICE_EFFECTS_WHILE_DRIVING})
public void testDeviceEffects_allowsGrayscale() {
mZenModeHelper.setDeviceEffectsApplier(mDeviceEffectsApplier);
reset(mDeviceEffectsApplier);
@@ -5539,7 +5356,7 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI, FLAG_PREVENT_ZEN_DEVICE_EFFECTS_WHILE_DRIVING})
+ @EnableFlags({FLAG_MODES_UI, FLAG_PREVENT_ZEN_DEVICE_EFFECTS_WHILE_DRIVING})
public void testDeviceEffects_whileDriving_avoidsGrayscale() {
mZenModeHelper.setDeviceEffectsApplier(mDeviceEffectsApplier);
reset(mDeviceEffectsApplier);
@@ -5563,7 +5380,7 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI, FLAG_PREVENT_ZEN_DEVICE_EFFECTS_WHILE_DRIVING})
+ @EnableFlags({FLAG_MODES_UI, FLAG_PREVENT_ZEN_DEVICE_EFFECTS_WHILE_DRIVING})
public void testDeviceEffects_whileDrivingWithGrayscale_allowsGrayscale() {
mZenModeHelper.setDeviceEffectsApplier(mDeviceEffectsApplier);
reset(mDeviceEffectsApplier);
@@ -5594,7 +5411,6 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags(FLAG_MODES_API)
public void removeAndAddAutomaticZenRule_wasCustomized_isRestored() {
// Start with a rule.
mZenModeHelper.mConfig.automaticRules.clear();
@@ -5651,7 +5467,6 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags(FLAG_MODES_API)
public void removeAndAddAutomaticZenRule_wasNotCustomized_isNotRestored() {
// Start with a single rule.
mZenModeHelper.mConfig.automaticRules.clear();
@@ -5685,7 +5500,6 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags(FLAG_MODES_API)
public void removeAndAddAutomaticZenRule_recreatedButNotByApp_isNotRestored() {
// Start with a single rule.
mZenModeHelper.mConfig.automaticRules.clear();
@@ -5736,7 +5550,6 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags(FLAG_MODES_API)
public void removeAndAddAutomaticZenRule_removedByUser_isNotRestored() {
// Start with a single rule.
mZenModeHelper.mConfig.automaticRules.clear();
@@ -5779,7 +5592,7 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI})
+ @EnableFlags(FLAG_MODES_UI)
public void removeAndAddAutomaticZenRule_ifChangingComponent_isAllowedAndDoesNotRestore() {
// Start with a rule.
mZenModeHelper.mConfig.automaticRules.clear();
@@ -5824,7 +5637,6 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags(FLAG_MODES_API)
public void removeAutomaticZenRule_preservedForRestoringByPackageAndConditionId() {
mContext.getTestablePermissions().setPermission(Manifest.permission.MANAGE_NOTIFICATIONS,
PERMISSION_GRANTED); // So that canManageAZR passes although packages don't match.
@@ -5874,7 +5686,6 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags(FLAG_MODES_API)
public void removeAllZenRules_preservedForRestoring() {
mZenModeHelper.mConfig.automaticRules.clear();
@@ -5898,14 +5709,13 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags(FLAG_MODES_API)
public void removeAllZenRules_fromSystem_deletesPreservedRulesToo() {
mZenModeHelper.mConfig.automaticRules.clear();
// Start with deleted rules from 2 different packages.
Instant now = Instant.ofEpochMilli(1701796461000L);
- ZenRule pkg1Rule = newZenRule("pkg1", now.minus(1, ChronoUnit.DAYS), now);
- ZenRule pkg2Rule = newZenRule("pkg2", now.minus(2, ChronoUnit.DAYS), now);
+ ZenRule pkg1Rule = newDeletedZenRule("1", "pkg1", now.minus(1, DAYS), now);
+ ZenRule pkg2Rule = newDeletedZenRule("2", "pkg2", now.minus(2, DAYS), now);
mZenModeHelper.mConfig.deletedRules.put(ZenModeConfig.deletedRuleKey(pkg1Rule), pkg1Rule);
mZenModeHelper.mConfig.deletedRules.put(ZenModeConfig.deletedRuleKey(pkg2Rule), pkg2Rule);
@@ -5918,7 +5728,6 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags(FLAG_MODES_API)
public void removeAndAddAutomaticZenRule_wasActive_isRestoredAsInactive() {
// Start with a rule.
mZenModeHelper.mConfig.automaticRules.clear();
@@ -5968,7 +5777,6 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags(FLAG_MODES_API)
public void removeAndAddAutomaticZenRule_wasSnoozed_isRestoredAsInactive() {
// Start with a rule.
mZenModeHelper.mConfig.automaticRules.clear();
@@ -6023,12 +5831,11 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags(FLAG_MODES_API)
public void testRuleCleanup() throws Exception {
Instant now = Instant.ofEpochMilli(1701796461000L);
- Instant yesterday = now.minus(1, ChronoUnit.DAYS);
- Instant aWeekAgo = now.minus(7, ChronoUnit.DAYS);
- Instant twoMonthsAgo = now.minus(60, ChronoUnit.DAYS);
+ Instant yesterday = now.minus(1, DAYS);
+ Instant aWeekAgo = now.minus(7, DAYS);
+ Instant twoMonthsAgo = now.minus(60, DAYS);
mTestClock.setNowMillis(now.toEpochMilli());
when(mPackageManager.getPackageInfo(eq("good_pkg"), anyInt()))
@@ -6041,24 +5848,28 @@ public class ZenModeHelperTest extends UiServiceTestCase {
config.user = 42;
mZenModeHelper.mConfigs.put(42, config);
// okay rules (not deleted, package exists, with a range of creation dates).
- config.automaticRules.put("ar1", newZenRule("good_pkg", now, null));
- config.automaticRules.put("ar2", newZenRule("good_pkg", yesterday, null));
- config.automaticRules.put("ar3", newZenRule("good_pkg", twoMonthsAgo, null));
+ config.automaticRules.put("ar1", newZenRule("ar1", "good_pkg", now));
+ config.automaticRules.put("ar2", newZenRule("ar2", "good_pkg", yesterday));
+ config.automaticRules.put("ar3", newZenRule("ar3", "good_pkg", twoMonthsAgo));
// newish rules for a missing package
- config.automaticRules.put("ar4", newZenRule("bad_pkg", yesterday, null));
+ config.automaticRules.put("ar4", newZenRule("ar4", "bad_pkg", yesterday));
// oldish rules belonging to a missing package
- config.automaticRules.put("ar5", newZenRule("bad_pkg", aWeekAgo, null));
+ config.automaticRules.put("ar5", newZenRule("ar5", "bad_pkg", aWeekAgo));
// rules deleted recently
- config.deletedRules.put("del1", newZenRule("good_pkg", twoMonthsAgo, yesterday));
- config.deletedRules.put("del2", newZenRule("good_pkg", twoMonthsAgo, aWeekAgo));
+ config.deletedRules.put("del1",
+ newDeletedZenRule("del1", "good_pkg", twoMonthsAgo, yesterday));
+ config.deletedRules.put("del2",
+ newDeletedZenRule("del2", "good_pkg", twoMonthsAgo, aWeekAgo));
// rules deleted a long time ago
- config.deletedRules.put("del3", newZenRule("good_pkg", twoMonthsAgo, twoMonthsAgo));
+ config.deletedRules.put("del3",
+ newDeletedZenRule("del3", "good_pkg", twoMonthsAgo, twoMonthsAgo));
// rules for a missing package, created recently and deleted recently
- config.deletedRules.put("del4", newZenRule("bad_pkg", yesterday, now));
+ config.deletedRules.put("del4", newDeletedZenRule("del4", "bad_pkg", yesterday, now));
// rules for a missing package, created a long time ago and deleted recently
- config.deletedRules.put("del5", newZenRule("bad_pkg", twoMonthsAgo, now));
+ config.deletedRules.put("del5", newDeletedZenRule("del5", "bad_pkg", twoMonthsAgo, now));
// rules for a missing package, created a long time ago and deleted a long time ago
- config.deletedRules.put("del6", newZenRule("bad_pkg", twoMonthsAgo, twoMonthsAgo));
+ config.deletedRules.put("del6",
+ newDeletedZenRule("del6", "bad_pkg", twoMonthsAgo, twoMonthsAgo));
mZenModeHelper.onUserSwitched(42); // copies config and cleans it up.
@@ -6068,20 +5879,120 @@ public class ZenModeHelperTest extends UiServiceTestCase {
.containsExactly("del1", "del2", "del4");
}
- private static ZenRule newZenRule(String pkg, Instant createdAt, @Nullable Instant deletedAt) {
+ @Test
+ @EnableFlags({FLAG_MODES_UI, FLAG_MODES_CLEANUP_IMPLICIT})
+ public void testRuleCleanup_removesNotRecentlyUsedNotModifiedImplicitRules() throws Exception {
+ Instant now = Instant.ofEpochMilli(1701796461000L);
+ Instant yesterday = now.minus(1, DAYS);
+ Instant aWeekAgo = now.minus(7, DAYS);
+ Instant twoMonthsAgo = now.minus(60, DAYS);
+ Instant aYearAgo = now.minus(365, DAYS);
+ mTestClock.setNowMillis(now.toEpochMilli());
+ when(mPackageManager.getPackageInfo(anyString(), anyInt())).thenReturn(new PackageInfo());
+
+ // Set up a config to be loaded, containing a bunch of implicit rules
+ ZenModeConfig config = new ZenModeConfig();
+ config.user = 42;
+ mZenModeHelper.mConfigs.put(42, config);
+ // used recently
+ ZenRule usedRecently1 = newImplicitZenRule("pkg1", aYearAgo, yesterday);
+ ZenRule usedRecently2 = newImplicitZenRule("pkg2", aYearAgo, aWeekAgo);
+ config.automaticRules.put(usedRecently1.id, usedRecently1);
+ config.automaticRules.put(usedRecently2.id, usedRecently2);
+ // not used in a long time
+ ZenRule longUnused = newImplicitZenRule("pkg3", aYearAgo, twoMonthsAgo);
+ config.automaticRules.put(longUnused.id, longUnused);
+ // created a long time ago, before lastActivation tracking
+ ZenRule oldAndLastUsageUnknown = newImplicitZenRule("pkg4", twoMonthsAgo, null);
+ config.automaticRules.put(oldAndLastUsageUnknown.id, oldAndLastUsageUnknown);
+ // created a short time ago, before lastActivation tracking
+ ZenRule newAndLastUsageUnknown = newImplicitZenRule("pkg5", aWeekAgo, null);
+ config.automaticRules.put(newAndLastUsageUnknown.id, newAndLastUsageUnknown);
+ // not used in a long time, but was customized by user
+ ZenRule longUnusedButCustomized = newImplicitZenRule("pkg6", aYearAgo, twoMonthsAgo);
+ longUnusedButCustomized.zenPolicyUserModifiedFields = ZenPolicy.FIELD_CONVERSATIONS;
+ config.automaticRules.put(longUnusedButCustomized.id, longUnusedButCustomized);
+ // created a long time ago, before lastActivation tracking, and was customized by user
+ ZenRule oldAndLastUsageUnknownAndCustomized = newImplicitZenRule("pkg7", twoMonthsAgo,
+ null);
+ oldAndLastUsageUnknownAndCustomized.userModifiedFields = AutomaticZenRule.FIELD_ICON;
+ config.automaticRules.put(oldAndLastUsageUnknownAndCustomized.id,
+ oldAndLastUsageUnknownAndCustomized);
+
+ mZenModeHelper.onUserSwitched(42); // copies config and cleans it up.
+
+ // The recently used OR modified OR last-used-unknown rules stay.
+ assertThat(mZenModeHelper.mConfig.automaticRules.values())
+ .comparingElementsUsing(IGNORE_METADATA)
+ .containsExactly(usedRecently1, usedRecently2, oldAndLastUsageUnknown,
+ newAndLastUsageUnknown, longUnusedButCustomized,
+ oldAndLastUsageUnknownAndCustomized);
+ }
+
+ @Test
+ @EnableFlags({FLAG_MODES_UI, FLAG_MODES_CLEANUP_IMPLICIT})
+ public void testRuleCleanup_assignsLastActivationToImplicitRules() throws Exception {
+ Instant now = Instant.ofEpochMilli(1701796461000L);
+ Instant aWeekAgo = now.minus(7, DAYS);
+ Instant aYearAgo = now.minus(365, DAYS);
+ mTestClock.setNowMillis(now.toEpochMilli());
+ when(mPackageManager.getPackageInfo(anyString(), anyInt())).thenReturn(new PackageInfo());
+
+ // Set up a config to be loaded, containing implicit rules.
+ ZenModeConfig config = new ZenModeConfig();
+ config.user = 42;
+ mZenModeHelper.mConfigs.put(42, config);
+ // with last activation known
+ ZenRule usedRecently = newImplicitZenRule("pkg1", aYearAgo, aWeekAgo);
+ config.automaticRules.put(usedRecently.id, usedRecently);
+ // created a long time ago, with last activation unknown
+ ZenRule oldAndLastUsageUnknown = newImplicitZenRule("pkg4", aYearAgo, null);
+ config.automaticRules.put(oldAndLastUsageUnknown.id, oldAndLastUsageUnknown);
+ // created a short time ago, with last activation unknown
+ ZenRule newAndLastUsageUnknown = newImplicitZenRule("pkg5", aWeekAgo, null);
+ config.automaticRules.put(newAndLastUsageUnknown.id, newAndLastUsageUnknown);
+
+ mZenModeHelper.onUserSwitched(42); // copies config and cleans it up.
+
+ // All rules stayed.
+ usedRecently = getZenRule(usedRecently.id);
+ oldAndLastUsageUnknown = getZenRule(oldAndLastUsageUnknown.id);
+ newAndLastUsageUnknown = getZenRule(newAndLastUsageUnknown.id);
+
+ // The rules with an unknown last usage have been assigned a placeholder one.
+ assertThat(usedRecently.lastActivation).isEqualTo(aWeekAgo);
+ assertThat(oldAndLastUsageUnknown.lastActivation).isEqualTo(now);
+ assertThat(newAndLastUsageUnknown.lastActivation).isEqualTo(now);
+ }
+
+ private static ZenRule newDeletedZenRule(String id, String pkg, Instant createdAt,
+ @NonNull Instant deletedAt) {
+ ZenRule rule = newZenRule(id, pkg, createdAt);
+ rule.deletionInstant = deletedAt;
+ return rule;
+ }
+
+ private static ZenRule newImplicitZenRule(String pkg, @NonNull Instant createdAt,
+ @Nullable Instant lastActivatedAt) {
+ ZenRule implicitRule = newZenRule(implicitRuleId(pkg), pkg, createdAt);
+ implicitRule.lastActivation = lastActivatedAt;
+ return implicitRule;
+ }
+
+ private static ZenRule newZenRule(String id, String pkg, Instant createdAt) {
ZenRule rule = new ZenRule();
+ rule.id = id;
rule.pkg = pkg;
rule.creationTime = createdAt.toEpochMilli();
rule.enabled = true;
- rule.deletionInstant = deletedAt;
+ rule.deletionInstant = null;
// Plus stuff so that isValidAutomaticRule() passes
- rule.name = "A rule from " + pkg + " created on " + createdAt;
+ rule.name = "Rule " + id;
rule.conditionId = Uri.parse(rule.name);
return rule;
}
@Test
- @EnableFlags(FLAG_MODES_API)
public void getAutomaticZenRuleState_ownedRule_returnsRuleState() {
String id = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT,
mContext.getPackageName(),
@@ -6112,14 +6023,13 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags(FLAG_MODES_API)
public void getAutomaticZenRuleState_notOwnedRule_returnsStateUnknown() {
// Assume existence of a system-owned rule that is currently ACTIVE.
- ZenRule systemRule = newZenRule("android", Instant.now(), null);
+ ZenRule systemRule = newZenRule("systemRule", "android", Instant.now());
systemRule.zenMode = ZEN_MODE_ALARMS;
systemRule.condition = new Condition(systemRule.conditionId, "on", Condition.STATE_TRUE);
ZenModeConfig config = mZenModeHelper.mConfig.copy();
- config.automaticRules.put("systemRule", systemRule);
+ config.automaticRules.put(systemRule.id, systemRule);
mZenModeHelper.setConfig(config, null, ORIGIN_INIT, "", SYSTEM_UID);
assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_ALARMS);
@@ -6128,15 +6038,14 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags(FLAG_MODES_API)
public void setAutomaticZenRuleState_idForNotOwnedRule_ignored() {
// Assume existence of an other-package-owned rule that is currently ACTIVE.
assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_OFF);
- ZenRule otherRule = newZenRule("another.package", Instant.now(), null);
+ ZenRule otherRule = newZenRule("otherRule", "another.package", Instant.now());
otherRule.zenMode = ZEN_MODE_ALARMS;
otherRule.condition = new Condition(otherRule.conditionId, "on", Condition.STATE_TRUE);
ZenModeConfig config = mZenModeHelper.mConfig.copy();
- config.automaticRules.put("otherRule", otherRule);
+ config.automaticRules.put(otherRule.id, otherRule);
mZenModeHelper.setConfig(config, null, ORIGIN_INIT, "", SYSTEM_UID);
assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_ALARMS);
@@ -6149,15 +6058,14 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags(FLAG_MODES_API)
public void setAutomaticZenRuleStateFromConditionProvider_conditionForNotOwnedRule_ignored() {
// Assume existence of an other-package-owned rule that is currently ACTIVE.
assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_OFF);
- ZenRule otherRule = newZenRule("another.package", Instant.now(), null);
+ ZenRule otherRule = newZenRule("otherRule", "another.package", Instant.now());
otherRule.zenMode = ZEN_MODE_ALARMS;
otherRule.condition = new Condition(otherRule.conditionId, "on", Condition.STATE_TRUE);
ZenModeConfig config = mZenModeHelper.mConfig.copy();
- config.automaticRules.put("otherRule", otherRule);
+ config.automaticRules.put(otherRule.id, otherRule);
mZenModeHelper.setConfig(config, null, ORIGIN_INIT, "", SYSTEM_UID);
assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_ALARMS);
@@ -6171,7 +6079,6 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags(FLAG_MODES_API)
public void testCallbacks_policy() throws Exception {
setupZenConfig();
assertThat(mZenModeHelper.getNotificationPolicy(UserHandle.CURRENT).allowReminders())
@@ -6193,7 +6100,6 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags(FLAG_MODES_API)
public void testCallbacks_consolidatedPolicy() throws Exception {
assertThat(mZenModeHelper.getConsolidatedNotificationPolicy().allowMedia()).isTrue();
SettableFuture<Policy> futureConsolidatedPolicy = SettableFuture.create();
@@ -6219,7 +6125,6 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags(FLAG_MODES_API)
public void applyGlobalZenModeAsImplicitZenRule_createsImplicitRuleAndActivatesIt() {
mZenModeHelper.mConfig.automaticRules.clear();
@@ -6235,7 +6140,6 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags(FLAG_MODES_API)
public void applyGlobalZenModeAsImplicitZenRule_updatesImplicitRuleAndActivatesIt() {
mZenModeHelper.mConfig.automaticRules.clear();
@@ -6257,7 +6161,6 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags(FLAG_MODES_API)
public void applyGlobalZenModeAsImplicitZenRule_ruleCustomized_doesNotUpdateRule() {
mZenModeHelper.mConfig.automaticRules.clear();
String pkg = mContext.getPackageName();
@@ -6290,7 +6193,6 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags(FLAG_MODES_API)
public void applyGlobalZenModeAsImplicitZenRule_ruleCustomizedButNotFilter_updatesRule() {
mZenModeHelper.mConfig.automaticRules.clear();
String pkg = mContext.getPackageName();
@@ -6322,7 +6224,6 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags(FLAG_MODES_API)
public void applyGlobalZenModeAsImplicitZenRule_modeOff_deactivatesImplicitRule() {
mZenModeHelper.mConfig.automaticRules.clear();
mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(UserHandle.CURRENT, mPkg, CUSTOM_PKG_UID,
@@ -6339,7 +6240,6 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags(FLAG_MODES_API)
public void applyGlobalZenModeAsImplicitZenRule_modeOffButNoPreviousRule_ignored() {
mZenModeHelper.mConfig.automaticRules.clear();
@@ -6350,7 +6250,6 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags(FLAG_MODES_API)
public void applyGlobalZenModeAsImplicitZenRule_update_unsnoozesRule() {
mZenModeHelper.mConfig.automaticRules.clear();
@@ -6375,7 +6274,7 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI})
+ @EnableFlags(FLAG_MODES_UI)
public void applyGlobalZenModeAsImplicitZenRule_again_refreshesRuleName() {
mZenModeHelper.mConfig.automaticRules.clear();
mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(UserHandle.CURRENT,
@@ -6394,7 +6293,7 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI})
+ @EnableFlags(FLAG_MODES_UI)
public void applyGlobalZenModeAsImplicitZenRule_again_doesNotChangeCustomizedRuleName() {
mZenModeHelper.mConfig.automaticRules.clear();
mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(UserHandle.CURRENT,
@@ -6420,19 +6319,6 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @DisableFlags(FLAG_MODES_API)
- public void applyGlobalZenModeAsImplicitZenRule_flagOff_ignored() {
- mZenModeHelper.mConfig.automaticRules.clear();
-
- withoutWtfCrash(
- () -> mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(UserHandle.CURRENT,
- CUSTOM_PKG_NAME, CUSTOM_PKG_UID, ZEN_MODE_IMPORTANT_INTERRUPTIONS));
-
- assertThat(mZenModeHelper.mConfig.automaticRules).isEmpty();
- }
-
- @Test
- @EnableFlags(FLAG_MODES_API)
public void applyGlobalPolicyAsImplicitZenRule_createsImplicitRule() {
mZenModeHelper.mConfig.automaticRules.clear();
@@ -6457,7 +6343,6 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags(FLAG_MODES_API)
public void applyGlobalPolicyAsImplicitZenRule_updatesImplicitRule() {
mZenModeHelper.mConfig.automaticRules.clear();
@@ -6489,7 +6374,6 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags(FLAG_MODES_API)
public void applyGlobalPolicyAsImplicitZenRule_ruleCustomized_doesNotUpdateRule() {
mZenModeHelper.mConfig.automaticRules.clear();
String pkg = mContext.getPackageName();
@@ -6532,7 +6416,6 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags(FLAG_MODES_API)
public void applyGlobalPolicyAsImplicitZenRule_ruleCustomizedButNotZenPolicy_updatesRule() {
mZenModeHelper.mConfig.automaticRules.clear();
String pkg = mContext.getPackageName();
@@ -6572,7 +6455,7 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI})
+ @EnableFlags(FLAG_MODES_UI)
public void applyGlobalPolicyAsImplicitZenRule_again_refreshesRuleName() {
mZenModeHelper.mConfig.automaticRules.clear();
mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(UserHandle.CURRENT,
@@ -6591,7 +6474,7 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI})
+ @EnableFlags(FLAG_MODES_UI)
public void applyGlobalPolicyAsImplicitZenRule_again_doesNotChangeCustomizedRuleName() {
mZenModeHelper.mConfig.automaticRules.clear();
mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(UserHandle.CURRENT,
@@ -6617,19 +6500,6 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @DisableFlags(FLAG_MODES_API)
- public void applyGlobalPolicyAsImplicitZenRule_flagOff_ignored() {
- mZenModeHelper.mConfig.automaticRules.clear();
-
- withoutWtfCrash(
- () -> mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(UserHandle.CURRENT,
- CUSTOM_PKG_NAME, CUSTOM_PKG_UID, new Policy(0, 0, 0)));
-
- assertThat(mZenModeHelper.mConfig.automaticRules).isEmpty();
- }
-
- @Test
- @EnableFlags(FLAG_MODES_API)
public void getNotificationPolicyFromImplicitZenRule_returnsSetPolicy() {
Policy writtenPolicy = new Policy(PRIORITY_CATEGORY_CALLS | PRIORITY_CATEGORY_CONVERSATIONS,
PRIORITY_SENDERS_CONTACTS, PRIORITY_SENDERS_STARRED,
@@ -6645,7 +6515,6 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags(FLAG_MODES_API)
@DisableFlags(FLAG_MODES_UI)
public void getNotificationPolicyFromImplicitZenRule_ruleWithoutPolicy_copiesGlobalPolicy() {
// Implicit rule will get the global policy at the time of rule creation.
@@ -6665,7 +6534,6 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags(FLAG_MODES_API)
public void getNotificationPolicyFromImplicitZenRule_noImplicitRule_returnsGlobalPolicy() {
Policy policy = new Policy(PRIORITY_CATEGORY_CALLS, PRIORITY_SENDERS_STARRED, 0);
mZenModeHelper.setNotificationPolicy(UserHandle.CURRENT, policy, ORIGIN_APP,
@@ -6680,7 +6548,6 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags(FLAG_MODES_API)
@DisableFlags(FLAG_MODES_UI)
public void setNotificationPolicy_updatesRulePolicies_ifRulePolicyIsDefaultOrGlobalPolicy() {
ZenPolicy defaultZenPolicy = mZenModeHelper.getDefaultZenPolicy();
@@ -6726,7 +6593,6 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags(Flags.FLAG_MODES_API)
public void addRule_iconIdWithResourceNameTooLong_ignoresIcon() {
int resourceId = 999;
String veryLongResourceName = "com.android.server.notification:drawable/"
@@ -6745,7 +6611,7 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI})
+ @EnableFlags(FLAG_MODES_UI)
public void setManualZenRuleDeviceEffects_noPreexistingMode() {
ZenDeviceEffects effects = new ZenDeviceEffects.Builder()
.setShouldDimWallpaper(true)
@@ -6759,7 +6625,7 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI})
+ @EnableFlags(FLAG_MODES_UI)
public void setManualZenRuleDeviceEffects_preexistingMode() {
mZenModeHelper.setManualZenMode(UserHandle.CURRENT, ZEN_MODE_OFF, Uri.EMPTY,
ORIGIN_USER_IN_SYSTEMUI, "create manual rule", "settings", SYSTEM_UID);
@@ -6776,7 +6642,7 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI})
+ @EnableFlags(FLAG_MODES_UI)
public void addAutomaticZenRule_startsDisabled_recordsDisabledOrigin() {
AutomaticZenRule startsDisabled = new AutomaticZenRule.Builder("Rule", Uri.EMPTY)
.setOwner(new ComponentName(mPkg, "SomeProvider"))
@@ -6792,7 +6658,7 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI})
+ @EnableFlags(FLAG_MODES_UI)
public void updateAutomaticZenRule_disabling_recordsDisabledOrigin() {
AutomaticZenRule startsEnabled = new AutomaticZenRule.Builder("Rule", Uri.EMPTY)
.setOwner(new ComponentName(mPkg, "SomeProvider"))
@@ -6815,7 +6681,7 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI})
+ @EnableFlags(FLAG_MODES_UI)
public void updateAutomaticZenRule_keepingDisabled_preservesPreviousDisabledOrigin() {
AutomaticZenRule startsEnabled = new AutomaticZenRule.Builder("Rule", Uri.EMPTY)
.setOwner(new ComponentName(mPkg, "SomeProvider"))
@@ -6845,7 +6711,7 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI})
+ @EnableFlags(FLAG_MODES_UI)
public void updateAutomaticZenRule_enabling_clearsDisabledOrigin() {
AutomaticZenRule startsEnabled = new AutomaticZenRule.Builder("Rule", Uri.EMPTY)
.setOwner(new ComponentName(mPkg, "SomeProvider"))
@@ -6875,7 +6741,7 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI})
+ @EnableFlags(FLAG_MODES_UI)
public void setAutomaticZenRuleState_manualActivation_appliesOverride() {
AutomaticZenRule rule = new AutomaticZenRule.Builder("Rule", Uri.parse("cond"))
.setPackage(mPkg)
@@ -6893,7 +6759,7 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI})
+ @EnableFlags(FLAG_MODES_UI)
public void setAutomaticZenRuleState_manualActivationAndThenDeactivation_removesOverride() {
AutomaticZenRule rule = new AutomaticZenRule.Builder("Rule", Uri.parse("cond"))
.setPackage(mPkg)
@@ -6930,7 +6796,7 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI})
+ @EnableFlags(FLAG_MODES_UI)
public void setAutomaticZenRuleState_manualDeactivationAndThenReactivation_removesOverride() {
AutomaticZenRule rule = new AutomaticZenRule.Builder("Rule", Uri.parse("cond"))
.setPackage(mPkg)
@@ -6976,7 +6842,7 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI})
+ @EnableFlags(FLAG_MODES_UI)
public void setAutomaticZenRuleState_manualDeactivation_appliesOverride() {
AutomaticZenRule rule = new AutomaticZenRule.Builder("Rule", Uri.parse("cond"))
.setPackage(mPkg)
@@ -7002,7 +6868,7 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI})
+ @EnableFlags(FLAG_MODES_UI)
public void setAutomaticZenRuleState_ifManualActive_appCannotDeactivateBeforeActivating() {
AutomaticZenRule rule = new AutomaticZenRule.Builder("Rule", Uri.parse("cond"))
.setPackage(mPkg)
@@ -7039,7 +6905,7 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI})
+ @EnableFlags(FLAG_MODES_UI)
public void setAutomaticZenRuleState_ifManualInactive_appCannotReactivateBeforeDeactivating() {
AutomaticZenRule rule = new AutomaticZenRule.Builder("Rule", Uri.parse("cond"))
.setPackage(mPkg)
@@ -7085,7 +6951,7 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI})
+ @EnableFlags(FLAG_MODES_UI)
public void setAutomaticZenRuleState_withActivationOverride_userActionFromAppCanDeactivate() {
AutomaticZenRule rule = new AutomaticZenRule.Builder("Rule", Uri.parse("cond"))
.setPackage(mPkg)
@@ -7109,7 +6975,7 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI})
+ @EnableFlags(FLAG_MODES_UI)
public void setAutomaticZenRuleState_withDeactivationOverride_userActionFromAppCanActivate() {
AutomaticZenRule rule = new AutomaticZenRule.Builder("Rule", Uri.parse("cond"))
.setPackage(mPkg)
@@ -7140,7 +7006,7 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI})
+ @EnableFlags(FLAG_MODES_UI)
public void setAutomaticZenRuleState_manualActionFromApp_isNotOverride() {
AutomaticZenRule rule = new AutomaticZenRule.Builder("Rule", Uri.parse("cond"))
.setPackage(mPkg)
@@ -7165,7 +7031,7 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI})
+ @EnableFlags(FLAG_MODES_UI)
public void setAutomaticZenRuleState_implicitRuleManualActivation_doesNotUseOverride() {
mContext.getTestablePermissions().setPermission(Manifest.permission.MANAGE_NOTIFICATIONS,
PERMISSION_GRANTED); // So that canManageAZR succeeds.
@@ -7188,7 +7054,7 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI})
+ @EnableFlags(FLAG_MODES_UI)
public void setAutomaticZenRuleState_implicitRuleManualDeactivation_doesNotUseOverride() {
mContext.getTestablePermissions().setPermission(Manifest.permission.MANAGE_NOTIFICATIONS,
PERMISSION_GRANTED); // So that canManageAZR succeeds.
@@ -7214,24 +7080,8 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @DisableFlags({FLAG_MODES_API, FLAG_MODES_UI})
- public void testDefaultConfig_preModesApi_rulesAreBare() {
- // Create a new user, which should get a copy of the default policy.
- mZenModeHelper.onUserSwitched(101);
-
- ZenRule eventsRule = mZenModeHelper.mConfig.automaticRules.get(
- ZenModeConfig.EVENTS_OBSOLETE_RULE_ID);
-
- assertThat(eventsRule).isNotNull();
- assertThat(eventsRule.zenPolicy).isNull();
- assertThat(eventsRule.type).isEqualTo(TYPE_UNKNOWN);
- assertThat(eventsRule.triggerDescription).isNull();
- }
-
- @Test
- @EnableFlags(FLAG_MODES_API)
@DisableFlags(FLAG_MODES_UI)
- public void testDefaultConfig_modesApi_rulesHaveFullPolicy() {
+ public void testDefaultConfig_rulesHaveFullPolicy() {
// Create a new user, which should get a copy of the default policy.
mZenModeHelper.onUserSwitched(201);
@@ -7245,7 +7095,7 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI})
+ @EnableFlags(FLAG_MODES_UI)
public void testDefaultConfig_modesUi_rulesHaveFullPolicy() {
// Create a new user, which should get a copy of the default policy.
mZenModeHelper.onUserSwitched(301);
@@ -7260,7 +7110,7 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI})
+ @EnableFlags(FLAG_MODES_UI)
public void setAutomaticZenRuleState_withManualActivation_activeOnReboot()
throws Exception {
AutomaticZenRule rule = new AutomaticZenRule.Builder("Rule", Uri.parse("cond"))
@@ -7298,7 +7148,7 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI})
+ @EnableFlags(FLAG_MODES_UI)
public void setAutomaticZenRuleState_withManualDeactivation_clearedOnReboot()
throws Exception {
AutomaticZenRule rule = new AutomaticZenRule.Builder("Rule", Uri.parse("cond"))
@@ -7336,7 +7186,6 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags(FLAG_MODES_API)
public void addAutomaticZenRule_withoutPolicy_getsItsOwnInstanceOfDefaultPolicy() {
// Add a rule without policy -> uses default config
AutomaticZenRule azr = new AutomaticZenRule.Builder("Rule", Uri.parse("cond"))
@@ -7352,7 +7201,7 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI})
+ @EnableFlags(FLAG_MODES_UI)
public void readXml_withDisabledEventsRule_deletesIt() throws Exception {
ZenRule rule = new ZenRule();
rule.id = ZenModeConfig.EVENTS_OBSOLETE_RULE_ID;
@@ -7372,7 +7221,7 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI})
+ @EnableFlags(FLAG_MODES_UI)
public void readXml_withEnabledEventsRule_keepsIt() throws Exception {
ZenRule rule = new ZenRule();
rule.id = ZenModeConfig.EVENTS_OBSOLETE_RULE_ID;
@@ -7392,7 +7241,7 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI})
+ @EnableFlags(FLAG_MODES_UI)
public void updateHasPriorityChannels_keepsChannelSettings() {
setupZenConfig();
@@ -7512,6 +7361,125 @@ public class ZenModeHelperTest extends UiServiceTestCase {
"config: setAzrStateFromCps: cond/cond (ORIGIN_APP) from uid " + CUSTOM_PKG_UID);
}
+ @Test
+ @EnableFlags({FLAG_MODES_UI, FLAG_MODES_CLEANUP_IMPLICIT})
+ public void setAutomaticZenRuleState_updatesLastActivation() {
+ String ruleOne = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, mPkg,
+ new AutomaticZenRule.Builder("rule", CONDITION_ID)
+ .setConfigurationActivity(new ComponentName(mPkg, "cls"))
+ .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
+ .build(),
+ ORIGIN_APP, "reason", CUSTOM_PKG_UID);
+ String ruleTwo = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, mPkg,
+ new AutomaticZenRule.Builder("unrelated", Uri.parse("other.condition"))
+ .setConfigurationActivity(new ComponentName(mPkg, "cls"))
+ .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
+ .build(),
+ ORIGIN_APP, "reason", CUSTOM_PKG_UID);
+
+ assertThat(getZenRule(ruleOne).lastActivation).isNull();
+ assertThat(getZenRule(ruleTwo).lastActivation).isNull();
+
+ Instant firstActivation = Instant.ofEpochMilli(100);
+ mTestClock.setNow(firstActivation);
+ mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, ruleOne, CONDITION_TRUE,
+ ORIGIN_APP, CUSTOM_PKG_UID);
+
+ assertThat(getZenRule(ruleOne).lastActivation).isEqualTo(firstActivation);
+ assertThat(getZenRule(ruleTwo).lastActivation).isNull();
+
+ mTestClock.setNow(Instant.ofEpochMilli(300));
+ mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, ruleOne, CONDITION_FALSE,
+ ORIGIN_APP, CUSTOM_PKG_UID);
+
+ assertThat(getZenRule(ruleOne).lastActivation).isEqualTo(firstActivation);
+ assertThat(getZenRule(ruleTwo).lastActivation).isNull();
+
+ Instant secondActivation = Instant.ofEpochMilli(500);
+ mTestClock.setNow(secondActivation);
+ mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, ruleOne, CONDITION_TRUE,
+ ORIGIN_APP, CUSTOM_PKG_UID);
+
+ assertThat(getZenRule(ruleOne).lastActivation).isEqualTo(secondActivation);
+ assertThat(getZenRule(ruleTwo).lastActivation).isNull();
+ }
+
+ @Test
+ @EnableFlags({FLAG_MODES_UI, FLAG_MODES_CLEANUP_IMPLICIT})
+ public void setManualZenMode_updatesLastActivation() {
+ assertThat(mZenModeHelper.mConfig.manualRule.lastActivation).isNull();
+ Instant instant = Instant.ofEpochMilli(100);
+ mTestClock.setNow(instant);
+
+ mZenModeHelper.setManualZenMode(UserHandle.CURRENT, ZEN_MODE_ALARMS, null,
+ ORIGIN_USER_IN_SYSTEMUI, "reason", "systemui", SYSTEM_UID);
+
+ assertThat(mZenModeHelper.mConfig.manualRule.lastActivation).isEqualTo(instant);
+ }
+
+ @Test
+ @EnableFlags({FLAG_MODES_UI, FLAG_MODES_CLEANUP_IMPLICIT})
+ public void applyGlobalZenModeAsImplicitZenRule_updatesLastActivation() {
+ Instant instant = Instant.ofEpochMilli(100);
+ mTestClock.setNow(instant);
+
+ mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(UserHandle.CURRENT, CUSTOM_PKG_NAME,
+ CUSTOM_PKG_UID, ZEN_MODE_ALARMS);
+
+ ZenRule implicitRule = getZenRule(implicitRuleId(CUSTOM_PKG_NAME));
+ assertThat(implicitRule.lastActivation).isEqualTo(instant);
+ }
+
+ @Test
+ @EnableFlags({FLAG_MODES_UI, FLAG_MODES_CLEANUP_IMPLICIT})
+ public void setAutomaticZenRuleState_notChangingActiveState_doesNotUpdateLastActivation() {
+ String ruleId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, mPkg,
+ new AutomaticZenRule.Builder("rule", CONDITION_ID)
+ .setConfigurationActivity(new ComponentName(mPkg, "cls"))
+ .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
+ .build(),
+ ORIGIN_APP, "reason", CUSTOM_PKG_UID);
+
+ assertThat(getZenRule(ruleId).lastActivation).isNull();
+
+ // Manual activation comes first
+ Instant firstActivation = Instant.ofEpochMilli(100);
+ mTestClock.setNow(firstActivation);
+ mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, ruleId, CONDITION_TRUE,
+ ORIGIN_USER_IN_SYSTEMUI, SYSTEM_UID);
+
+ assertThat(getZenRule(ruleId).lastActivation).isEqualTo(firstActivation);
+
+ // Now the app says the rule should be active (assume it's on a schedule, and the app
+ // doesn't listen to broadcasts so it doesn't know an override was present). This doesn't
+ // change the activation state.
+ mTestClock.setNow(Instant.ofEpochMilli(300));
+ mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, ruleId, CONDITION_TRUE,
+ ORIGIN_APP, CUSTOM_PKG_UID);
+
+ assertThat(getZenRule(ruleId).lastActivation).isEqualTo(firstActivation);
+ }
+
+ @Test
+ @EnableFlags({FLAG_MODES_UI, FLAG_MODES_CLEANUP_IMPLICIT})
+ public void addOrUpdateRule_doesNotUpdateLastActivation() {
+ AutomaticZenRule azr = new AutomaticZenRule.Builder("rule", CONDITION_ID)
+ .setConfigurationActivity(new ComponentName(mPkg, "cls"))
+ .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
+ .build();
+
+ String ruleId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, mPkg, azr,
+ ORIGIN_APP, "reason", CUSTOM_PKG_UID);
+
+ assertThat(getZenRule(ruleId).lastActivation).isNull();
+
+ mZenModeHelper.updateAutomaticZenRule(UserHandle.CURRENT, ruleId,
+ new AutomaticZenRule.Builder(azr).setName("New name").build(), ORIGIN_APP, "reason",
+ CUSTOM_PKG_UID);
+
+ assertThat(getZenRule(ruleId).lastActivation).isNull();
+ }
+
private static void addZenRule(ZenModeConfig config, String id, String ownerPkg, int zenMode,
@Nullable ZenPolicy zenPolicy) {
ZenRule rule = new ZenRule();
@@ -7529,22 +7497,27 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
private static final Correspondence<ZenRule, ZenRule> IGNORE_METADATA =
- Correspondence.transforming(zr -> {
- Parcel p = Parcel.obtain();
- try {
- zr.writeToParcel(p, 0);
- p.setDataPosition(0);
- ZenRule copy = new ZenRule(p);
- copy.creationTime = 0;
- copy.userModifiedFields = 0;
- copy.zenPolicyUserModifiedFields = 0;
- copy.zenDeviceEffectsUserModifiedFields = 0;
- return copy;
- } finally {
- p.recycle();
- }
- },
- "Ignoring timestamp and userModifiedFields");
+ Correspondence.transforming(
+ ZenModeHelperTest::cloneWithoutMetadata,
+ ZenModeHelperTest::cloneWithoutMetadata,
+ "Ignoring timestamps and userModifiedFields");
+
+ private static ZenRule cloneWithoutMetadata(ZenRule rule) {
+ Parcel p = Parcel.obtain();
+ try {
+ rule.writeToParcel(p, 0);
+ p.setDataPosition(0);
+ ZenRule copy = new ZenRule(p);
+ copy.creationTime = 0;
+ copy.userModifiedFields = 0;
+ copy.zenPolicyUserModifiedFields = 0;
+ copy.zenDeviceEffectsUserModifiedFields = 0;
+ copy.lastActivation = null;
+ return copy;
+ } finally {
+ p.recycle();
+ }
+ }
private ZenRule expectedImplicitRule(String ownerPkg, int zenMode, ZenPolicy policy,
@Nullable Boolean conditionActive) {
@@ -7574,7 +7547,6 @@ public class ZenModeHelperTest extends UiServiceTestCase {
return rule;
}
- // TODO: b/310620812 - Update setup methods to include allowChannels() when MODES_API is inlined
private void setupZenConfig() {
Policy customPolicy = new Policy(PRIORITY_CATEGORY_REMINDERS
| PRIORITY_CATEGORY_CALLS | PRIORITY_CATEGORY_MESSAGES
@@ -7583,7 +7555,7 @@ public class ZenModeHelperTest extends UiServiceTestCase {
PRIORITY_SENDERS_STARRED,
PRIORITY_SENDERS_STARRED,
SUPPRESSED_EFFECT_BADGE,
- 0,
+ 0, // allows priority channels.
CONVERSATION_SENDERS_IMPORTANT);
mZenModeHelper.setNotificationPolicy(UserHandle.CURRENT, customPolicy, ORIGIN_UNKNOWN, 1);
if (!Flags.modesUi()) {
@@ -7599,8 +7571,11 @@ public class ZenModeHelperTest extends UiServiceTestCase {
assertEquals(STATE_ALLOW, dndProto.getCalls().getNumber());
assertEquals(PEOPLE_STARRED, dndProto.getAllowCallsFrom().getNumber());
assertEquals(STATE_ALLOW, dndProto.getMessages().getNumber());
+ assertEquals(PEOPLE_STARRED, dndProto.getAllowMessagesFrom().getNumber());
assertEquals(STATE_ALLOW, dndProto.getEvents().getNumber());
assertEquals(STATE_ALLOW, dndProto.getRepeatCallers().getNumber());
+ assertEquals(STATE_ALLOW, dndProto.getConversations().getNumber());
+ assertEquals(CONV_IMPORTANT, dndProto.getAllowConversationsFrom().getNumber());
assertEquals(STATE_ALLOW, dndProto.getFullscreen().getNumber());
assertEquals(STATE_ALLOW, dndProto.getLights().getNumber());
assertEquals(STATE_ALLOW, dndProto.getPeek().getNumber());
@@ -7616,7 +7591,7 @@ public class ZenModeHelperTest extends UiServiceTestCase {
return;
}
- // When modes_api flag is on, the default zen config is the device defaults.
+ // The default zen config is the device defaults.
assertThat(dndProto.getAlarms().getNumber()).isEqualTo(STATE_ALLOW);
assertThat(dndProto.getMedia().getNumber()).isEqualTo(STATE_ALLOW);
assertThat(dndProto.getSystem().getNumber()).isEqualTo(STATE_DISALLOW);
@@ -7627,6 +7602,8 @@ public class ZenModeHelperTest extends UiServiceTestCase {
assertThat(dndProto.getAllowMessagesFrom().getNumber()).isEqualTo(PEOPLE_STARRED);
assertThat(dndProto.getEvents().getNumber()).isEqualTo(STATE_DISALLOW);
assertThat(dndProto.getRepeatCallers().getNumber()).isEqualTo(STATE_ALLOW);
+ assertThat(dndProto.getConversations().getNumber()).isEqualTo(STATE_ALLOW);
+ assertThat(dndProto.getAllowConversationsFrom().getNumber()).isEqualTo(CONV_IMPORTANT);
assertThat(dndProto.getFullscreen().getNumber()).isEqualTo(STATE_DISALLOW);
assertThat(dndProto.getLights().getNumber()).isEqualTo(STATE_DISALLOW);
assertThat(dndProto.getPeek().getNumber()).isEqualTo(STATE_DISALLOW);
@@ -7634,6 +7611,8 @@ public class ZenModeHelperTest extends UiServiceTestCase {
assertThat(dndProto.getBadge().getNumber()).isEqualTo(STATE_ALLOW);
assertThat(dndProto.getAmbient().getNumber()).isEqualTo(STATE_DISALLOW);
assertThat(dndProto.getNotificationList().getNumber()).isEqualTo(STATE_ALLOW);
+ assertThat(dndProto.getAllowChannels().getNumber()).isEqualTo(
+ DNDProtoEnums.CHANNEL_POLICY_PRIORITY);
}
private static String getZenLog() {
@@ -7642,16 +7621,6 @@ public class ZenModeHelperTest extends UiServiceTestCase {
return zenLogWriter.toString();
}
- private static void withoutWtfCrash(Runnable test) {
- Log.TerribleFailureHandler oldHandler = Log.setWtfHandler((tag, what, system) -> {
- });
- try {
- test.run();
- } finally {
- Log.setWtfHandler(oldHandler);
- }
- }
-
/**
* Wrapper to use TypedXmlPullParser as XmlResourceParser for Resources.getXml()
*/
@@ -7954,6 +7923,10 @@ public class ZenModeHelperTest extends UiServiceTestCase {
return mNowMillis;
}
+ private void setNow(Instant instant) {
+ mNowMillis = instant.toEpochMilli();
+ }
+
private void setNowMillis(long millis) {
mNowMillis = millis;
}
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenPolicyTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenPolicyTest.java
index 6433b76defc3..8b9376454c0f 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenPolicyTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenPolicyTest.java
@@ -21,7 +21,6 @@ import static com.google.common.truth.Truth.assertThat;
import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.fail;
-import android.app.Flags;
import android.os.Parcel;
import android.platform.test.flag.junit.SetFlagsRule;
import android.service.notification.ZenPolicy;
@@ -34,7 +33,6 @@ import com.android.server.UiServiceTestCase;
import com.google.protobuf.nano.InvalidProtocolBufferNanoException;
-import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -50,11 +48,6 @@ public class ZenPolicyTest extends UiServiceTestCase {
@Rule
public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
- @Before
- public final void setUp() {
- mSetFlagsRule.enableFlags(Flags.FLAG_MODES_API);
- }
-
@Test
public void testZenPolicyApplyAllowedToDisallowed() {
ZenPolicy.Builder builder = new ZenPolicy.Builder();
@@ -207,8 +200,6 @@ public class ZenPolicyTest extends UiServiceTestCase {
@Test
public void testZenPolicyApplyChannels_applyUnset() {
- mSetFlagsRule.enableFlags(Flags.FLAG_MODES_API);
-
ZenPolicy.Builder builder = new ZenPolicy.Builder();
ZenPolicy unset = builder.build();
@@ -223,8 +214,6 @@ public class ZenPolicyTest extends UiServiceTestCase {
@Test
public void testZenPolicyApplyChannels_applyStricter() {
- mSetFlagsRule.enableFlags(Flags.FLAG_MODES_API);
-
ZenPolicy.Builder builder = new ZenPolicy.Builder();
builder.allowPriorityChannels(false);
ZenPolicy none = builder.build();
@@ -239,8 +228,6 @@ public class ZenPolicyTest extends UiServiceTestCase {
@Test
public void testZenPolicyApplyChannels_applyLooser() {
- mSetFlagsRule.enableFlags(Flags.FLAG_MODES_API);
-
ZenPolicy.Builder builder = new ZenPolicy.Builder();
builder.allowPriorityChannels(false);
ZenPolicy none = builder.build();
@@ -255,8 +242,6 @@ public class ZenPolicyTest extends UiServiceTestCase {
@Test
public void testZenPolicyApplyChannels_applySet() {
- mSetFlagsRule.enableFlags(Flags.FLAG_MODES_API);
-
ZenPolicy.Builder builder = new ZenPolicy.Builder();
ZenPolicy unset = builder.build();
@@ -270,8 +255,6 @@ public class ZenPolicyTest extends UiServiceTestCase {
@Test
public void testZenPolicyOverwrite_allUnsetPolicies() {
- mSetFlagsRule.enableFlags(Flags.FLAG_MODES_API);
-
ZenPolicy.Builder builder = new ZenPolicy.Builder();
ZenPolicy unset = builder.build();
@@ -292,8 +275,6 @@ public class ZenPolicyTest extends UiServiceTestCase {
@Test
public void testZenPolicyOverwrite_someOverlappingFields_takeNewPolicy() {
- mSetFlagsRule.enableFlags(Flags.FLAG_MODES_API);
-
ZenPolicy p1 = new ZenPolicy.Builder()
.allowCalls(ZenPolicy.PEOPLE_TYPE_CONTACTS)
.allowMessages(ZenPolicy.PEOPLE_TYPE_STARRED)
@@ -375,7 +356,6 @@ public class ZenPolicyTest extends UiServiceTestCase {
@Test
public void testEmptyZenPolicy_emptyChannels() {
- mSetFlagsRule.enableFlags(Flags.FLAG_MODES_API);
ZenPolicy.Builder builder = new ZenPolicy.Builder();
ZenPolicy policy = builder.build();
@@ -688,22 +668,7 @@ public class ZenPolicyTest extends UiServiceTestCase {
}
@Test
- public void testAllowChannels_noFlag() {
- mSetFlagsRule.disableFlags(Flags.FLAG_MODES_API);
-
- // allowChannels should be unset, not be modifiable, and not show up in any output
- ZenPolicy.Builder builder = new ZenPolicy.Builder();
- builder.allowPriorityChannels(true);
- ZenPolicy policy = builder.build();
-
- assertThat(policy.getPriorityChannelsAllowed()).isEqualTo(ZenPolicy.STATE_UNSET);
- assertThat(policy.toString().contains("allowChannels")).isFalse();
- }
-
- @Test
public void testAllowChannels() {
- mSetFlagsRule.enableFlags(Flags.FLAG_MODES_API);
-
// allow priority channels
ZenPolicy.Builder builder = new ZenPolicy.Builder();
builder.allowPriorityChannels(true);
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityTaskSupervisorTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityTaskSupervisorTests.java
index 3c74ad06a21f..a9be47d71213 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityTaskSupervisorTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityTaskSupervisorTests.java
@@ -509,6 +509,32 @@ public class ActivityTaskSupervisorTests extends WindowTestsBase {
assertThat(mSupervisor.mOpaqueContainerHelper.isOpaque(rootTask)).isTrue();
}
+ @Test
+ public void testOpaque_nonLeafTaskFragmentWithDirectActivity_opaque() {
+ final ActivityRecord directChildActivity = new ActivityBuilder(mAtm).setCreateTask(true)
+ .build();
+ directChildActivity.setOccludesParent(true);
+ final Task nonLeafTask = directChildActivity.getTask();
+ final TaskFragment directChildFragment = new TaskFragment(mAtm, new Binder(),
+ true /* createdByOrganizer */, false /* isEmbedded */);
+ nonLeafTask.addChild(directChildFragment, 0);
+
+ assertThat(mSupervisor.mOpaqueContainerHelper.isOpaque(nonLeafTask)).isTrue();
+ }
+
+ @Test
+ public void testOpaque_nonLeafTaskFragmentWithDirectActivity_transparent() {
+ final ActivityRecord directChildActivity = new ActivityBuilder(mAtm).setCreateTask(true)
+ .build();
+ directChildActivity.setOccludesParent(false);
+ final Task nonLeafTask = directChildActivity.getTask();
+ final TaskFragment directChildFragment = new TaskFragment(mAtm, new Binder(),
+ true /* createdByOrganizer */, false /* isEmbedded */);
+ nonLeafTask.addChild(directChildFragment, 0);
+
+ assertThat(mSupervisor.mOpaqueContainerHelper.isOpaque(nonLeafTask)).isFalse();
+ }
+
@NonNull
private TaskFragment createChildTaskFragment(@NonNull Task parent,
@WindowConfiguration.WindowingMode int windowingMode,
diff --git a/services/tests/wmtests/src/com/android/server/wm/DesktopModeHelperTest.java b/services/tests/wmtests/src/com/android/server/wm/DesktopModeHelperTest.java
index eaffc481098e..1e91bedb5c18 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DesktopModeHelperTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DesktopModeHelperTest.java
@@ -16,8 +16,6 @@
package com.android.server.wm;
-import static android.provider.Settings.Global.DEVELOPMENT_OVERRIDE_DESKTOP_MODE_FEATURES;
-
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.doReturn;
@@ -30,7 +28,6 @@ import android.platform.test.annotations.DisableFlags;
import android.platform.test.annotations.EnableFlags;
import android.platform.test.annotations.Presubmit;
import android.platform.test.flag.junit.SetFlagsRule;
-import android.provider.Settings;
import android.window.DesktopModeFlags;
import androidx.test.filters.SmallTest;
@@ -74,14 +71,12 @@ public class DesktopModeHelperTest {
doReturn(mContext.getContentResolver()).when(mMockContext).getContentResolver();
resetDesktopModeFlagsCache();
resetEnforceDeviceRestriction();
- resetFlagOverride();
}
@After
public void tearDown() throws Exception {
resetDesktopModeFlagsCache();
resetEnforceDeviceRestriction();
- resetFlagOverride();
}
@DisableFlags({Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE,
@@ -167,7 +162,8 @@ public class DesktopModeHelperTest {
@DisableFlags(Flags.FLAG_ENABLE_DESKTOP_MODE_THROUGH_DEV_OPTION)
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
@Test
- public void canEnterDesktopMode_DWFlagEnabled_configDevOptionOn_flagOverrideOn_returnsTrue() {
+ public void canEnterDesktopMode_DWFlagEnabled_configDevOptionOn_flagOverrideOn_returnsTrue()
+ throws Exception {
doReturn(true).when(mMockResources).getBoolean(
eq(R.bool.config_isDesktopModeDevOptionSupported)
);
@@ -177,22 +173,41 @@ public class DesktopModeHelperTest {
}
@Test
- public void isDeviceEligibleForDesktopMode_configDEModeOn_returnsTrue() {
+ public void isDeviceEligibleForDesktopMode_configDEModeOnAndIntDispHostsDesktop_returnsTrue() {
+ doReturn(true).when(mMockResources).getBoolean(eq(R.bool.config_isDesktopModeSupported));
+ doReturn(true).when(mMockResources)
+ .getBoolean(eq(R.bool.config_canInternalDisplayHostDesktops));
+
+ assertThat(DesktopModeHelper.isDeviceEligibleForDesktopMode(mMockContext)).isTrue();
+ }
+
+ @Test
+ public void isDeviceEligibleForDesktopMode_configDEModeOffAndIntDispHostsDesktop_returnsFalse() {
+ doReturn(true).when(mMockResources).getBoolean(eq(R.bool.config_isDesktopModeSupported));
+ doReturn(false).when(mMockResources)
+ .getBoolean(eq(R.bool.config_canInternalDisplayHostDesktops));
+
+ assertThat(DesktopModeHelper.isDeviceEligibleForDesktopMode(mMockContext)).isFalse();
+ }
+
+ @Test
+ public void isDeviceEligibleForDesktopMode_configDEModeOnAndIntDispHostsDesktopOff_returnsFalse() {
+ doReturn(false).when(mMockResources).getBoolean(eq(R.bool.config_isDesktopModeSupported));
doReturn(true).when(mMockResources).getBoolean(eq(R.bool.config_canInternalDisplayHostDesktops));
- assertThat(DesktopModeHelper.isInternalDisplayEligibleToHostDesktops(mMockContext)).isTrue();
+ assertThat(DesktopModeHelper.isDeviceEligibleForDesktopMode(mMockContext)).isFalse();
}
@DisableFlags(Flags.FLAG_ENABLE_DESKTOP_MODE_THROUGH_DEV_OPTION)
@Test
public void isDeviceEligibleForDesktopMode_supportFlagOff_returnsFalse() {
- assertThat(DesktopModeHelper.isInternalDisplayEligibleToHostDesktops(mMockContext)).isFalse();
+ assertThat(DesktopModeHelper.isDeviceEligibleForDesktopMode(mMockContext)).isFalse();
}
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_MODE_THROUGH_DEV_OPTION)
@Test
public void isDeviceEligibleForDesktopMode_supportFlagOn_returnsFalse() {
- assertThat(DesktopModeHelper.isInternalDisplayEligibleToHostDesktops(mMockContext)).isFalse();
+ assertThat(DesktopModeHelper.isDeviceEligibleForDesktopMode(mMockContext)).isFalse();
}
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_MODE_THROUGH_DEV_OPTION)
@@ -202,7 +217,7 @@ public class DesktopModeHelperTest {
eq(R.bool.config_isDesktopModeDevOptionSupported)
);
- assertThat(DesktopModeHelper.isInternalDisplayEligibleToHostDesktops(mMockContext)).isTrue();
+ assertThat(DesktopModeHelper.isDeviceEligibleForDesktopMode(mMockContext)).isTrue();
}
private void resetEnforceDeviceRestriction() throws Exception {
@@ -227,13 +242,10 @@ public class DesktopModeHelperTest {
cachedToggleOverride.set(/* obj= */ null, /* value= */ null);
}
- private void resetFlagOverride() {
- Settings.Global.putString(mContext.getContentResolver(),
- DEVELOPMENT_OVERRIDE_DESKTOP_MODE_FEATURES, null);
- }
-
- private void setFlagOverride(DesktopModeFlags.ToggleOverride override) {
- Settings.Global.putInt(mContext.getContentResolver(),
- DEVELOPMENT_OVERRIDE_DESKTOP_MODE_FEATURES, override.getSetting());
+ private void setFlagOverride(DesktopModeFlags.ToggleOverride override) throws Exception {
+ Field cachedToggleOverride = DesktopModeFlags.class.getDeclaredField(
+ "sCachedToggleOverride");
+ cachedToggleOverride.setAccessible(true);
+ cachedToggleOverride.set(/* obj= */ null, /* value= */ override);
}
} \ No newline at end of file
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 82435b24dad6..6e109a8d4eaf 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
@@ -164,6 +164,7 @@ import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
+import java.util.function.BooleanSupplier;
/**
* Tests for the {@link DisplayContent} class.
@@ -2674,16 +2675,67 @@ public class DisplayContentTests extends WindowTestsBase {
public void testKeyguardGoingAwayWhileAodShown() {
mDisplayContent.getDisplayPolicy().setAwake(true);
- final WindowState appWin = newWindowBuilder("appWin", TYPE_APPLICATION).setDisplay(
- mDisplayContent).build();
- final ActivityRecord activity = appWin.mActivityRecord;
+ final KeyguardController keyguard = mAtm.mKeyguardController;
+ final ActivityRecord activity = new ActivityBuilder(mAtm).setCreateTask(true).build();
+ final int displayId = mDisplayContent.getDisplayId();
+
+ final BooleanSupplier keyguardShowing = () -> keyguard.isKeyguardShowing(displayId);
+ final BooleanSupplier keyguardGoingAway = () -> keyguard.isKeyguardGoingAway(displayId);
+ final BooleanSupplier appVisible = activity::isVisibleRequested;
+
+ // Begin locked and in AOD
+ keyguard.setKeyguardShown(displayId, true /* keyguard */, true /* aod */);
+ assertFalse(keyguardGoingAway.getAsBoolean());
+ assertFalse(appVisible.getAsBoolean());
+
+ // Start unlocking from AOD.
+ keyguard.keyguardGoingAway(displayId, 0x0 /* flags */);
+ assertTrue(keyguardGoingAway.getAsBoolean());
+ assertTrue(appVisible.getAsBoolean());
- mAtm.mKeyguardController.setKeyguardShown(appWin.getDisplayId(), true /* keyguardShowing */,
- true /* aodShowing */);
- assertFalse(activity.isVisibleRequested());
+ // Clear AOD. This does *not* clear the going-away status.
+ keyguard.setKeyguardShown(displayId, true /* keyguard */, false /* aod */);
+ assertTrue(keyguardGoingAway.getAsBoolean());
+ assertTrue(appVisible.getAsBoolean());
+
+ // Finish unlock
+ keyguard.setKeyguardShown(displayId, false /* keyguard */, false /* aod */);
+ assertFalse(keyguardGoingAway.getAsBoolean());
+ assertTrue(appVisible.getAsBoolean());
+ }
+
+ @Test
+ public void testKeyguardGoingAwayCanceledWhileAodShown() {
+ mDisplayContent.getDisplayPolicy().setAwake(true);
- mAtm.mKeyguardController.keyguardGoingAway(appWin.getDisplayId(), 0 /* flags */);
- assertTrue(activity.isVisibleRequested());
+ final KeyguardController keyguard = mAtm.mKeyguardController;
+ final ActivityRecord activity = new ActivityBuilder(mAtm).setCreateTask(true).build();
+ final int displayId = mDisplayContent.getDisplayId();
+
+ final BooleanSupplier keyguardShowing = () -> keyguard.isKeyguardShowing(displayId);
+ final BooleanSupplier keyguardGoingAway = () -> keyguard.isKeyguardGoingAway(displayId);
+ final BooleanSupplier appVisible = activity::isVisibleRequested;
+
+ // Begin locked and in AOD
+ keyguard.setKeyguardShown(displayId, true /* keyguard */, true /* aod */);
+ assertFalse(keyguardGoingAway.getAsBoolean());
+ assertFalse(appVisible.getAsBoolean());
+
+ // Start unlocking from AOD.
+ keyguard.keyguardGoingAway(displayId, 0x0 /* flags */);
+ assertTrue(keyguardGoingAway.getAsBoolean());
+ assertTrue(appVisible.getAsBoolean());
+
+ // Clear AOD. This does *not* clear the going-away status.
+ keyguard.setKeyguardShown(displayId, true /* keyguard */, false /* aod */);
+ assertTrue(keyguardGoingAway.getAsBoolean());
+ assertTrue(appVisible.getAsBoolean());
+
+ // Same API call a second time cancels the unlock, because AOD isn't changing.
+ keyguard.setKeyguardShown(displayId, true /* keyguard */, false /* aod */);
+ assertTrue(keyguardShowing.getAsBoolean());
+ assertFalse(keyguardGoingAway.getAsBoolean());
+ assertFalse(appVisible.getAsBoolean());
}
@Test
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 95bca2b17efb..1dc32b00acba 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
@@ -4486,6 +4486,49 @@ public class SizeCompatTests extends WindowTestsBase {
}
@Test
+ @EnableFlags(Flags.FLAG_EXCLUDE_CAPTION_FROM_APP_BOUNDS)
+ @DisableCompatChanges({ActivityInfo.INSETS_DECOUPLED_CONFIGURATION_ENFORCED})
+ public void testInFreeform_boundsSandboxedToAppBounds() {
+ allowDesktopMode();
+ final int dw = 2800;
+ final int dh = 1400;
+ final int notchHeight = 100;
+ final DisplayContent display = new TestDisplayContent.Builder(mAtm, dw, dh)
+ .setNotch(notchHeight)
+ .build();
+ setUpApp(display);
+ prepareUnresizable(mActivity, SCREEN_ORIENTATION_PORTRAIT);
+
+ mTask.mDisplayContent.getDefaultTaskDisplayArea()
+ .setWindowingMode(WindowConfiguration.WINDOWING_MODE_FREEFORM);
+ mTask.setWindowingMode(WINDOWING_MODE_FREEFORM);
+ Rect appBounds = new Rect(0, 0, 1000, 500);
+ Rect bounds = new Rect(0, 0, 1000, 600);
+ mTask.getWindowConfiguration().setAppBounds(appBounds);
+ mTask.getWindowConfiguration().setBounds(bounds);
+ mActivity.onConfigurationChanged(mTask.getConfiguration());
+
+ // Bounds are sandboxed to appBounds in freeform.
+ assertDownScaled();
+ assertEquals(mActivity.getWindowConfiguration().getAppBounds(),
+ mActivity.getWindowConfiguration().getBounds());
+
+ // Exit freeform.
+ mTask.mDisplayContent.getDefaultTaskDisplayArea()
+ .setWindowingMode(WindowConfiguration.WINDOWING_MODE_FULLSCREEN);
+ mTask.setWindowingMode(WINDOWING_MODE_FULLSCREEN);
+ mTask.getWindowConfiguration().setBounds(new Rect(0, 0, dw, dh));
+ mActivity.onConfigurationChanged(mTask.getConfiguration());
+ assertFitted();
+ appBounds = mActivity.getWindowConfiguration().getAppBounds();
+ bounds = mActivity.getWindowConfiguration().getBounds();
+ // Bounds are not sandboxed to appBounds.
+ assertNotEquals(appBounds, bounds);
+ assertEquals(notchHeight, appBounds.top - bounds.top);
+ }
+
+
+ @Test
@EnableFlags(Flags.FLAG_IGNORE_ASPECT_RATIO_RESTRICTIONS_FOR_RESIZEABLE_FREEFORM_ACTIVITIES)
public void testUserAspectRatioOverridesNotAppliedToResizeableFreeformActivity() {
final TaskBuilder taskBuilder =
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowTracingPerfettoTest.java b/services/tests/wmtests/src/com/android/server/wm/WindowTracingPerfettoTest.java
index 12b744546f5e..9367941e32a3 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowTracingPerfettoTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowTracingPerfettoTest.java
@@ -18,12 +18,16 @@ package com.android.server.wm;
import static android.tools.traces.Utils.busyWaitForDataSourceRegistration;
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+
import static com.android.dx.mockito.inline.extended.ExtendedMockito.times;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.verifyZeroInteractions;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
@@ -34,11 +38,15 @@ import android.platform.test.annotations.Presubmit;
import android.tools.ScenarioBuilder;
import android.tools.traces.io.ResultWriter;
import android.tools.traces.monitors.PerfettoTraceMonitor;
+import android.util.Log;
import android.view.Choreographer;
+import androidx.test.filters.FlakyTest;
import androidx.test.filters.SmallTest;
+import androidx.test.uiautomator.UiDevice;
import org.junit.After;
+import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
@@ -51,14 +59,15 @@ import java.io.IOException;
/**
* Test class for {@link WindowTracingPerfetto}.
*/
+@FlakyTest(bugId = 372558379)
@SmallTest
@Presubmit
public class WindowTracingPerfettoTest {
private static final String TEST_DATA_SOURCE_NAME = "android.windowmanager.test";
private static WindowManagerService sWmMock;
- private static Choreographer sChoreographer;
private static WindowTracing sWindowTracing;
+ private static Boolean sIsDataSourceRegisteredSuccessfully;
private PerfettoTraceMonitor mTraceMonitor;
@@ -66,19 +75,39 @@ public class WindowTracingPerfettoTest {
public static void setUpOnce() throws Exception {
sWmMock = Mockito.mock(WindowManagerService.class);
Mockito.doNothing().when(sWmMock).dumpDebugLocked(Mockito.any(), Mockito.anyInt());
- sChoreographer = Mockito.mock(Choreographer.class);
- sWindowTracing = new WindowTracingPerfetto(sWmMock, sChoreographer,
+ sWindowTracing = new WindowTracingPerfetto(sWmMock, Mockito.mock(Choreographer.class),
new WindowManagerGlobalLock(), TEST_DATA_SOURCE_NAME);
- busyWaitForDataSourceRegistration(TEST_DATA_SOURCE_NAME);
+ }
+
+ @AfterClass
+ public static void tearDownOnce() {
+ sWmMock = null;
+ sWindowTracing = null;
}
@Before
public void setUp() throws IOException {
- Mockito.clearInvocations(sWmMock);
+ if (sIsDataSourceRegisteredSuccessfully != null) {
+ assumeTrue("Failed to register data source", sIsDataSourceRegisteredSuccessfully);
+ return;
+ }
+ try {
+ busyWaitForDataSourceRegistration(TEST_DATA_SOURCE_NAME);
+ sIsDataSourceRegisteredSuccessfully = true;
+ } catch (Exception e) {
+ sIsDataSourceRegisteredSuccessfully = false;
+ final String perfettoStatus = UiDevice.getInstance(getInstrumentation())
+ .executeShellCommand("perfetto --query");
+ Log.e(WindowTracingPerfettoTest.class.getSimpleName(),
+ "Failed to register data source: " + perfettoStatus);
+ // Only fail once. The rest tests will be skipped by assumeTrue.
+ fail("Failed to register data source");
+ }
}
@After
public void tearDown() throws IOException {
+ Mockito.clearInvocations(sWmMock);
stopTracing();
}
diff --git a/telephony/java/android/telephony/CarrierConfigManager.java b/telephony/java/android/telephony/CarrierConfigManager.java
index 0b3d720bf52a..1a932859b750 100644
--- a/telephony/java/android/telephony/CarrierConfigManager.java
+++ b/telephony/java/android/telephony/CarrierConfigManager.java
@@ -10207,6 +10207,17 @@ public class CarrierConfigManager {
"carrier_supported_satellite_notification_hysteresis_sec_int";
/**
+ * Satellite notification display restriction reset time in seconds.
+ *
+ * The device shows a notification when it connects to a satellite. If the user interacts
+ * with the notification, it won't be shown again immediately. Instead, the notification
+ * will only reappear after below key mentioned amount of time has passed.
+ */
+ @FlaggedApi(Flags.FLAG_SATELLITE_25Q4_APIS)
+ public static final String KEY_SATELLITE_CONNECTED_NOTIFICATION_THROTTLE_MILLIS_INT =
+ "satellite_connected_notification_throttle_millis_int";
+
+ /**
* An integer key holds the timeout duration in seconds used to determine whether to exit
* carrier-roaming NB-IOT satellite mode.
*
@@ -11428,6 +11439,10 @@ public class CarrierConfigManager {
sDefaults.putInt(KEY_CARRIER_ROAMING_NTN_EMERGENCY_CALL_TO_SATELLITE_HANDOVER_TYPE_INT,
SatelliteManager.EMERGENCY_CALL_TO_SATELLITE_HANDOVER_TYPE_T911);
sDefaults.putInt(KEY_CARRIER_SUPPORTED_SATELLITE_NOTIFICATION_HYSTERESIS_SEC_INT, 180);
+ if (Flags.starlinkDataBugfix()) {
+ sDefaults.putLong(KEY_SATELLITE_CONNECTED_NOTIFICATION_THROTTLE_MILLIS_INT,
+ TimeUnit.DAYS.toMillis(7));
+ }
sDefaults.putInt(KEY_SATELLITE_ROAMING_SCREEN_OFF_INACTIVITY_TIMEOUT_SEC_INT, 30);
sDefaults.putInt(KEY_SATELLITE_ROAMING_P2P_SMS_INACTIVITY_TIMEOUT_SEC_INT, 180);
sDefaults.putInt(KEY_SATELLITE_ROAMING_ESOS_INACTIVITY_TIMEOUT_SEC_INT, 600);
diff --git a/telephony/java/android/telephony/satellite/SatelliteManager.java b/telephony/java/android/telephony/satellite/SatelliteManager.java
index b7b209b78300..100690dcbb65 100644
--- a/telephony/java/android/telephony/satellite/SatelliteManager.java
+++ b/telephony/java/android/telephony/satellite/SatelliteManager.java
@@ -821,6 +821,25 @@ public final class SatelliteManager {
"android.telephony.METADATA_SATELLITE_MANUAL_CONNECT_P2P_SUPPORT";
/**
+ * A boolean value indicating whether application is optimized to utilize low bandwidth
+ * satellite data.
+ * The applications that are optimized for low bandwidth satellite data should set this
+ * property to {@code true} in the manifest to indicate to platform about the same.
+ * {@code
+ * <application>
+ * <meta-data
+ * android:name="android.telephony.PROPERTY_SATELLITE_DATA_OPTIMIZED"
+ * android:value="true"/>
+ * </application>
+ * }
+ * <p>
+ * When {@code true}, satellite data optimized network is available for applications.
+ */
+ @FlaggedApi(Flags.FLAG_SATELLITE_25Q4_APIS)
+ public static final String PROPERTY_SATELLITE_DATA_OPTIMIZED =
+ "android.telephony.PROPERTY_SATELLITE_DATA_OPTIMIZED";
+
+ /**
* Registers a {@link SatelliteStateChangeListener} to receive callbacks when the satellite
* state may have changed.
*
@@ -3840,6 +3859,35 @@ public final class SatelliteManager {
}
}
+ /**
+ * Get list of application packages name that are optimized for low bandwidth satellite data.
+ *
+ * @return List of application packages name with data optimized network property.
+ *
+ * {@link #PROPERTY_SATELLITE_DATA_OPTIMIZED}
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION)
+ @FlaggedApi(Flags.FLAG_SATELLITE_25Q4_APIS)
+ public @NonNull List<String> getSatelliteDataOptimizedApps() {
+ List<String> appsNames = new ArrayList<>();
+ try {
+ ITelephony telephony = getITelephony();
+ if (telephony != null) {
+ appsNames = telephony.getSatelliteDataOptimizedApps();
+ } else {
+ throw new IllegalStateException("telephony service is null.");
+ }
+ } catch (RemoteException ex) {
+ loge("getSatelliteDataOptimizedApps() RemoteException:" + ex);
+ ex.rethrowAsRuntimeException();
+ }
+
+ return appsNames;
+ }
+
@Nullable
private static ITelephony getITelephony() {
ITelephony binder = ITelephony.Stub.asInterface(TelephonyFrameworkInitializer
diff --git a/telephony/java/com/android/internal/telephony/ITelephony.aidl b/telephony/java/com/android/internal/telephony/ITelephony.aidl
index 08c003027c5b..1c6652daf498 100644
--- a/telephony/java/com/android/internal/telephony/ITelephony.aidl
+++ b/telephony/java/com/android/internal/telephony/ITelephony.aidl
@@ -3596,4 +3596,15 @@ interface ITelephony {
* @hide
*/
int getCarrierIdFromIdentifier(in CarrierIdentifier carrierIdentifier);
+
+
+ /**
+ * Get list of applications that are optimized for low bandwidth satellite data.
+ *
+ * @return List of Application Name with data optimized network property.
+ * {@link #PROPERTY_SATELLITE_DATA_OPTIMIZED}
+ */
+ @JavaPassthrough(annotation="@android.annotation.RequiresPermission("
+ + "android.Manifest.permission.SATELLITE_COMMUNICATION)")
+ List<String> getSatelliteDataOptimizedApps();
}
diff --git a/tests/AppJankTest/res/values/strings.xml b/tests/AppJankTest/res/values/strings.xml
new file mode 100644
index 000000000000..ab2d18fa9d53
--- /dev/null
+++ b/tests/AppJankTest/res/values/strings.xml
@@ -0,0 +1,3 @@
+<resources>
+ <string name="continue_test">Continue Test</string>
+</resources> \ No newline at end of file
diff --git a/tests/AppJankTest/src/android/app/jank/tests/IntegrationTests.java b/tests/AppJankTest/src/android/app/jank/tests/IntegrationTests.java
index fe9f63615757..3498974b348e 100644
--- a/tests/AppJankTest/src/android/app/jank/tests/IntegrationTests.java
+++ b/tests/AppJankTest/src/android/app/jank/tests/IntegrationTests.java
@@ -209,7 +209,8 @@ public class IntegrationTests {
JankTrackerActivity.class);
resumeJankTracker.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
mEmptyActivity.startActivity(resumeJankTracker);
- mDevice.wait(Until.findObject(By.text("Edit Text")), WAIT_FOR_TIMEOUT_MS);
+ mDevice.wait(Until.findObject(By.text(mEmptyActivity.getString(R.string.continue_test))),
+ WAIT_FOR_TIMEOUT_MS);
assertTrue(jankTracker.shouldTrack());
}
diff --git a/tests/AppJankTest/src/android/app/jank/tests/JankTrackerActivity.java b/tests/AppJankTest/src/android/app/jank/tests/JankTrackerActivity.java
index 80ab6ad3e587..686758200853 100644
--- a/tests/AppJankTest/src/android/app/jank/tests/JankTrackerActivity.java
+++ b/tests/AppJankTest/src/android/app/jank/tests/JankTrackerActivity.java
@@ -18,15 +18,41 @@ package android.app.jank.tests;
import android.app.Activity;
import android.os.Bundle;
+import android.widget.EditText;
public class JankTrackerActivity extends Activity {
+ private static final int CONTINUE_TEST_DELAY_MS = 4000;
+
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.jank_tracker_activity_layout);
}
+
+ /**
+ * In IntegrationTests#jankTrackingResumed_whenActivityBecomesVisibleAgain this activity is
+ * placed into the background and then resumed via an intent. The test waits until the
+ * `continue_test` string is visible on the screen before validating that Jank tracking has
+ * resumed.
+ *
+ * <p>The 4 second delay allows JankTracker to re-register its callbacks and start receiving
+ * JankData before the test proceeds.
+ */
+ @Override
+ protected void onResume() {
+ super.onResume();
+ getActivityThread().getHandler().postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ EditText editTextView = findViewById(R.id.edit_text);
+ if (editTextView != null) {
+ editTextView.setText(R.string.continue_test);
+ }
+ }
+ }, CONTINUE_TEST_DELAY_MS);
+ }
}
diff --git a/tests/AttestationVerificationTest/src/com/android/server/security/CertificateRevocationStatusManagerTest.java b/tests/AttestationVerificationTest/src/com/android/server/security/CertificateRevocationStatusManagerTest.java
index c38517ace5e6..586bb76388f6 100644
--- a/tests/AttestationVerificationTest/src/com/android/server/security/CertificateRevocationStatusManagerTest.java
+++ b/tests/AttestationVerificationTest/src/com/android/server/security/CertificateRevocationStatusManagerTest.java
@@ -277,6 +277,72 @@ public class CertificateRevocationStatusManagerTest {
}
}
+ @Test
+ public void checkRevocationStatus_allCertificatesRecentlyChecked_doesNotFetchRemoteCrl()
+ throws Exception {
+ copyFromAssetToFile(
+ REVOCATION_LIST_WITHOUT_CERTIFICATES_USED_IN_THIS_TEST, mRevocationListFile);
+ mCertificateRevocationStatusManager =
+ new CertificateRevocationStatusManager(
+ mContext, mRevocationListUrl, mRevocationStatusFile, false);
+ mCertificateRevocationStatusManager.checkRevocationStatus(mCertificates1);
+ // indirectly verifies the remote list is not fetched by simulating a remote revocation
+ copyFromAssetToFile(
+ REVOCATION_LIST_WITH_CERTIFICATES_USED_IN_THIS_TEST, mRevocationListFile);
+
+ // no exception
+ mCertificateRevocationStatusManager.checkRevocationStatus(mCertificates1);
+ }
+
+ @Test
+ public void checkRevocationStatus_allCertificatesBarelyRecentlyChecked_doesNotFetchRemoteCrl()
+ throws Exception {
+ copyFromAssetToFile(
+ REVOCATION_LIST_WITH_CERTIFICATES_USED_IN_THIS_TEST, mRevocationListFile);
+ mCertificateRevocationStatusManager =
+ new CertificateRevocationStatusManager(
+ mContext, mRevocationListUrl, mRevocationStatusFile, false);
+ Map<String, LocalDateTime> lastCheckedDates = new HashMap<>();
+ LocalDateTime barelyRecently =
+ LocalDateTime.now()
+ .minusHours(
+ CertificateRevocationStatusManager.NUM_HOURS_BEFORE_NEXT_CHECK - 1);
+ for (X509Certificate certificate : mCertificates1) {
+ lastCheckedDates.put(getSerialNumber(certificate), barelyRecently);
+ }
+ mCertificateRevocationStatusManager.storeLastRevocationCheckData(lastCheckedDates);
+
+ // Indirectly verify the remote CRL is not checked by checking there is no exception despite
+ // a certificate being revoked. This test differs from the next only in the lastCheckedDate,
+ // one before the NUM_HOURS_BEFORE_NEXT_CHECK cutoff and one after
+ mCertificateRevocationStatusManager.checkRevocationStatus(mCertificates1);
+ }
+
+ @Test
+ public void checkRevocationStatus_certificatesRevokedAfterCheck_throwsException()
+ throws Exception {
+ copyFromAssetToFile(
+ REVOCATION_LIST_WITH_CERTIFICATES_USED_IN_THIS_TEST, mRevocationListFile);
+ mCertificateRevocationStatusManager =
+ new CertificateRevocationStatusManager(
+ mContext, mRevocationListUrl, mRevocationStatusFile, false);
+ Map<String, LocalDateTime> lastCheckedDates = new HashMap<>();
+ // To save network use, we do not check the remote CRL if all the certificates are recently
+ // checked, so we set the lastCheckDate to some time not recent.
+ LocalDateTime notRecently =
+ LocalDateTime.now()
+ .minusHours(
+ CertificateRevocationStatusManager.NUM_HOURS_BEFORE_NEXT_CHECK + 1);
+ for (X509Certificate certificate : mCertificates1) {
+ lastCheckedDates.put(getSerialNumber(certificate), notRecently);
+ }
+ mCertificateRevocationStatusManager.storeLastRevocationCheckData(lastCheckedDates);
+
+ assertThrows(
+ CertPathValidatorException.class,
+ () -> mCertificateRevocationStatusManager.checkRevocationStatus(mCertificates1));
+ }
+
private List<X509Certificate> getCertificateChain(String fileName) throws Exception {
Collection<? extends Certificate> certificates =
mFactory.generateCertificates(mContext.getResources().getAssets().open(fileName));
diff --git a/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt b/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt
index 6caf3f973618..a2f6f0051116 100644
--- a/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt
+++ b/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt
@@ -333,7 +333,7 @@ class InputManagerServiceTests {
fun testKeyActivenessNotifyEventsLifecycle() {
service.systemRunning()
- fakePermissionEnforcer.grant(android.Manifest.permission.LISTEN_FOR_KEY_ACTIVITY);
+ fakePermissionEnforcer.grant(android.Manifest.permission.LISTEN_FOR_KEY_ACTIVITY)
val inputManager = context.getSystemService(InputManager::class.java)
diff --git a/tests/utils/testutils/java/android/os/test/TestLooper.java b/tests/utils/testutils/java/android/os/test/TestLooper.java
index 83d22d923c78..4d379e45a81a 100644
--- a/tests/utils/testutils/java/android/os/test/TestLooper.java
+++ b/tests/utils/testutils/java/android/os/test/TestLooper.java
@@ -18,18 +18,24 @@ package android.os.test;
import static org.junit.Assert.assertTrue;
+import android.os.Build;
import android.os.Handler;
import android.os.HandlerExecutor;
import android.os.Looper;
import android.os.Message;
import android.os.MessageQueue;
import android.os.SystemClock;
+import android.os.TestLooperManager;
import android.util.Log;
+import androidx.test.platform.app.InstrumentationRegistry;
+
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
+import java.util.ArrayDeque;
+import java.util.Queue;
import java.util.concurrent.Executor;
/**
@@ -44,7 +50,9 @@ import java.util.concurrent.Executor;
* The Robolectric class also allows advancing time.
*/
public class TestLooper {
- protected final Looper mLooper;
+ private final Looper mLooper;
+ private final TestLooperManager mTestLooperManager;
+ private final Clock mClock;
private static final Constructor<Looper> LOOPER_CONSTRUCTOR;
private static final Field THREAD_LOCAL_LOOPER_FIELD;
@@ -54,24 +62,46 @@ public class TestLooper {
private static final Method MESSAGE_MARK_IN_USE_METHOD;
private static final String TAG = "TestLooper";
- private final Clock mClock;
-
private AutoDispatchThread mAutoDispatchThread;
+ /**
+ * Baklava introduces new {@link TestLooperManager} APIs that we can use instead of reflection.
+ */
+ private static boolean isAtLeastBaklava() {
+ Method[] methods = TestLooperManager.class.getMethods();
+ for (Method method : methods) {
+ if (method.getName().equals("peekWhen")) {
+ return true;
+ }
+ }
+ return false;
+ // TODO(shayba): delete the above, uncomment the below.
+ // SDK_INT has not yet ramped to Baklava in all 25Q2 builds.
+ // return Build.VERSION.SDK_INT >= Build.VERSION_CODES.BAKLAVA;
+ }
+
static {
try {
LOOPER_CONSTRUCTOR = Looper.class.getDeclaredConstructor(Boolean.TYPE);
LOOPER_CONSTRUCTOR.setAccessible(true);
THREAD_LOCAL_LOOPER_FIELD = Looper.class.getDeclaredField("sThreadLocal");
THREAD_LOCAL_LOOPER_FIELD.setAccessible(true);
- MESSAGE_QUEUE_MESSAGES_FIELD = MessageQueue.class.getDeclaredField("mMessages");
- MESSAGE_QUEUE_MESSAGES_FIELD.setAccessible(true);
- MESSAGE_NEXT_FIELD = Message.class.getDeclaredField("next");
- MESSAGE_NEXT_FIELD.setAccessible(true);
- MESSAGE_WHEN_FIELD = Message.class.getDeclaredField("when");
- MESSAGE_WHEN_FIELD.setAccessible(true);
- MESSAGE_MARK_IN_USE_METHOD = Message.class.getDeclaredMethod("markInUse");
- MESSAGE_MARK_IN_USE_METHOD.setAccessible(true);
+
+ if (isAtLeastBaklava()) {
+ MESSAGE_QUEUE_MESSAGES_FIELD = null;
+ MESSAGE_NEXT_FIELD = null;
+ MESSAGE_WHEN_FIELD = null;
+ MESSAGE_MARK_IN_USE_METHOD = null;
+ } else {
+ MESSAGE_QUEUE_MESSAGES_FIELD = MessageQueue.class.getDeclaredField("mMessages");
+ MESSAGE_QUEUE_MESSAGES_FIELD.setAccessible(true);
+ MESSAGE_NEXT_FIELD = Message.class.getDeclaredField("next");
+ MESSAGE_NEXT_FIELD.setAccessible(true);
+ MESSAGE_WHEN_FIELD = Message.class.getDeclaredField("when");
+ MESSAGE_WHEN_FIELD.setAccessible(true);
+ MESSAGE_MARK_IN_USE_METHOD = Message.class.getDeclaredMethod("markInUse");
+ MESSAGE_MARK_IN_USE_METHOD.setAccessible(true);
+ }
} catch (NoSuchFieldException | NoSuchMethodException e) {
throw new RuntimeException("Failed to initialize TestLooper", e);
}
@@ -106,6 +136,13 @@ public class TestLooper {
throw new RuntimeException("Reflection error constructing or accessing looper", e);
}
+ if (isAtLeastBaklava()) {
+ mTestLooperManager =
+ InstrumentationRegistry.getInstrumentation().acquireLooperManager(mLooper);
+ } else {
+ mTestLooperManager = null;
+ }
+
mClock = clock;
}
@@ -117,19 +154,61 @@ public class TestLooper {
return new HandlerExecutor(new Handler(getLooper()));
}
- private Message getMessageLinkedList() {
+ private Message getMessageLinkedListLegacy() {
try {
MessageQueue queue = mLooper.getQueue();
return (Message) MESSAGE_QUEUE_MESSAGES_FIELD.get(queue);
} catch (IllegalAccessException e) {
throw new RuntimeException("Access failed in TestLooper: get - MessageQueue.mMessages",
- e);
+ e);
}
}
public void moveTimeForward(long milliSeconds) {
+ if (isAtLeastBaklava()) {
+ moveTimeForwardBaklava(milliSeconds);
+ } else {
+ moveTimeForwardLegacy(milliSeconds);
+ }
+ }
+
+ private void moveTimeForwardBaklava(long milliSeconds) {
+ // Drain all Messages from the queue.
+ Queue<Message> messages = new ArrayDeque<>();
+ while (true) {
+ Message message = mTestLooperManager.poll();
+ if (message == null) {
+ break;
+ }
+ messages.add(message);
+ }
+
+ // Repost all Messages back to the queue with a new time.
+ while (true) {
+ Message message = messages.poll();
+ if (message == null) {
+ break;
+ }
+
+ // Ugly trick to reset the Message's "in use" flag.
+ // This is needed because the Message cannot be re-enqueued if it's
+ // marked in use.
+ message.copyFrom(message);
+
+ // Adjust the Message's delivery time.
+ long newWhen = message.getWhen() - milliSeconds;
+ if (newWhen < 0) {
+ newWhen = 0;
+ }
+
+ // Send the Message back to its Handler to be re-enqueued.
+ message.getTarget().sendMessageAtTime(message, newWhen);
+ }
+ }
+
+ private void moveTimeForwardLegacy(long milliSeconds) {
try {
- Message msg = getMessageLinkedList();
+ Message msg = getMessageLinkedListLegacy();
while (msg != null) {
long updatedWhen = msg.getWhen() - milliSeconds;
if (updatedWhen < 0) {
@@ -147,12 +226,12 @@ public class TestLooper {
return mClock.uptimeMillis();
}
- private Message messageQueueNext() {
+ private Message messageQueueNextLegacy() {
try {
long now = currentTime();
Message prevMsg = null;
- Message msg = getMessageLinkedList();
+ Message msg = getMessageLinkedListLegacy();
if (msg != null && msg.getTarget() == null) {
// Stalled by a barrier. Find the next asynchronous message in
// the queue.
@@ -185,18 +264,46 @@ public class TestLooper {
/**
* @return true if there are pending messages in the message queue
*/
- public synchronized boolean isIdle() {
- Message messageList = getMessageLinkedList();
+ public boolean isIdle() {
+ if (isAtLeastBaklava()) {
+ return isIdleBaklava();
+ } else {
+ return isIdleLegacy();
+ }
+ }
+
+ private boolean isIdleBaklava() {
+ Long when = mTestLooperManager.peekWhen();
+ return when != null && currentTime() >= when;
+ }
+ private synchronized boolean isIdleLegacy() {
+ Message messageList = getMessageLinkedListLegacy();
return messageList != null && currentTime() >= messageList.getWhen();
}
/**
* @return the next message in the Looper's message queue or null if there is none
*/
- public synchronized Message nextMessage() {
+ public Message nextMessage() {
+ if (isAtLeastBaklava()) {
+ return nextMessageBaklava();
+ } else {
+ return nextMessageLegacy();
+ }
+ }
+
+ private Message nextMessageBaklava() {
+ if (isIdle()) {
+ return mTestLooperManager.poll();
+ } else {
+ return null;
+ }
+ }
+
+ private synchronized Message nextMessageLegacy() {
if (isIdle()) {
- return messageQueueNext();
+ return messageQueueNextLegacy();
} else {
return null;
}
@@ -206,9 +313,26 @@ public class TestLooper {
* Dispatch the next message in the queue
* Asserts that there is a message in the queue
*/
- public synchronized void dispatchNext() {
+ public void dispatchNext() {
+ if (isAtLeastBaklava()) {
+ dispatchNextBaklava();
+ } else {
+ dispatchNextLegacy();
+ }
+ }
+
+ private void dispatchNextBaklava() {
+ assertTrue(isIdle());
+ Message msg = mTestLooperManager.poll();
+ if (msg == null) {
+ return;
+ }
+ msg.getTarget().dispatchMessage(msg);
+ }
+
+ private synchronized void dispatchNextLegacy() {
assertTrue(isIdle());
- Message msg = messageQueueNext();
+ Message msg = messageQueueNextLegacy();
if (msg == null) {
return;
}
diff --git a/tests/vcn/Android.bp b/tests/vcn/Android.bp
index 51a300bff7ea..661ed07a5669 100644
--- a/tests/vcn/Android.bp
+++ b/tests/vcn/Android.bp
@@ -16,13 +16,19 @@ android_test {
name: "FrameworksVcnTests",
// For access hidden connectivity methods in tests
defaults: ["framework-connectivity-test-defaults"],
+
+ // TODO: b/374174952 Use 36 after Android B finalization
+ min_sdk_version: "35",
+
srcs: [
"java/**/*.java",
"java/**/*.kt",
],
platform_apis: true,
- test_suites: ["device-tests"],
- certificate: "platform",
+ test_suites: [
+ "general-tests",
+ "mts-tethering",
+ ],
static_libs: [
"android.net.vcn.flags-aconfig-java-export",
"androidx.test.rules",
diff --git a/tests/vcn/AndroidManifest.xml b/tests/vcn/AndroidManifest.xml
index a8f657c89f76..08effbd1f7cf 100644
--- a/tests/vcn/AndroidManifest.xml
+++ b/tests/vcn/AndroidManifest.xml
@@ -16,8 +16,9 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.frameworks.tests.vcn">
- <uses-sdk android:minSdkVersion="33"
- android:targetSdkVersion="33"/>
+ <!-- TODO: b/374174952 Use 36 after Android B finalization -->
+ <uses-sdk android:minSdkVersion="35" android:targetSdkVersion="35" />
+
<application>
<uses-library android:name="android.test.runner" />
</application>
diff --git a/tests/vcn/AndroidTest.xml b/tests/vcn/AndroidTest.xml
index dc521fd7bcd9..9c8362f36cb2 100644
--- a/tests/vcn/AndroidTest.xml
+++ b/tests/vcn/AndroidTest.xml
@@ -14,12 +14,20 @@
limitations under the License.
-->
<configuration description="Runs VCN Tests.">
- <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup">
+ <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+ <option name="cleanup-apks" value="true" />
<option name="test-file-name" value="FrameworksVcnTests.apk" />
</target_preparer>
<option name="test-suite-tag" value="apct" />
<option name="test-tag" value="FrameworksVcnTests" />
+
+ <!-- Run tests in MTS only if the Tethering Mainline module is installed. -->
+ <object type="module_controller"
+ class="com.android.tradefed.testtype.suite.module.MainlineTestModuleController">
+ <option name="mainline-module-package-name" value="com.google.android.tethering" />
+ </object>
+
<test class="com.android.tradefed.testtype.AndroidJUnitTest" >
<option name="package" value="com.android.frameworks.tests.vcn" />
<option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
diff --git a/tests/vcn/java/android/net/vcn/VcnCellUnderlyingNetworkTemplateTest.java b/tests/vcn/java/android/net/vcn/VcnCellUnderlyingNetworkTemplateTest.java
index 156961312323..0fa11ae1fe7d 100644
--- a/tests/vcn/java/android/net/vcn/VcnCellUnderlyingNetworkTemplateTest.java
+++ b/tests/vcn/java/android/net/vcn/VcnCellUnderlyingNetworkTemplateTest.java
@@ -23,11 +23,24 @@ import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.fail;
+import android.os.Build;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
+
import org.junit.Test;
+import org.junit.runner.RunWith;
import java.util.HashSet;
import java.util.Set;
+// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on
+// Android B/B+
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+@SmallTest
public class VcnCellUnderlyingNetworkTemplateTest extends VcnUnderlyingNetworkTemplateTestBase {
private static final Set<String> ALLOWED_PLMN_IDS = new HashSet<>();
private static final Set<Integer> ALLOWED_CARRIER_IDS = new HashSet<>();
diff --git a/tests/vcn/java/android/net/vcn/VcnConfigTest.java b/tests/vcn/java/android/net/vcn/VcnConfigTest.java
index 73a0a6183cb6..fa97de0aff45 100644
--- a/tests/vcn/java/android/net/vcn/VcnConfigTest.java
+++ b/tests/vcn/java/android/net/vcn/VcnConfigTest.java
@@ -29,11 +29,14 @@ import static org.mockito.Mockito.mock;
import android.annotation.NonNull;
import android.content.Context;
+import android.os.Build;
import android.os.Parcel;
import android.util.ArraySet;
import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
import org.junit.Before;
import org.junit.Test;
@@ -42,7 +45,10 @@ import org.junit.runner.RunWith;
import java.util.Collections;
import java.util.Set;
-@RunWith(AndroidJUnit4.class)
+// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on
+// Android B/B+
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
@SmallTest
public class VcnConfigTest {
private static final String TEST_PACKAGE_NAME = VcnConfigTest.class.getPackage().getName();
diff --git a/tests/vcn/java/android/net/vcn/VcnGatewayConnectionConfigTest.java b/tests/vcn/java/android/net/vcn/VcnGatewayConnectionConfigTest.java
index 59dc68900100..990cc74caf6c 100644
--- a/tests/vcn/java/android/net/vcn/VcnGatewayConnectionConfigTest.java
+++ b/tests/vcn/java/android/net/vcn/VcnGatewayConnectionConfigTest.java
@@ -34,10 +34,13 @@ import android.net.ipsec.ike.IkeSessionParams;
import android.net.ipsec.ike.IkeTunnelConnectionParams;
import android.net.vcn.persistablebundleutils.IkeSessionParamsUtilsTest;
import android.net.vcn.persistablebundleutils.TunnelConnectionParamsUtilsTest;
+import android.os.Build;
import android.os.PersistableBundle;
import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -49,7 +52,10 @@ import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
-@RunWith(AndroidJUnit4.class)
+// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on
+// Android B/B+
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
@SmallTest
public class VcnGatewayConnectionConfigTest {
// Public for use in VcnGatewayConnectionTest
diff --git a/tests/vcn/java/android/net/vcn/VcnManagerTest.java b/tests/vcn/java/android/net/vcn/VcnManagerTest.java
index 8461de6d877b..1739fbc0fa6d 100644
--- a/tests/vcn/java/android/net/vcn/VcnManagerTest.java
+++ b/tests/vcn/java/android/net/vcn/VcnManagerTest.java
@@ -38,16 +38,28 @@ import android.net.NetworkCapabilities;
import android.net.vcn.VcnManager.VcnStatusCallback;
import android.net.vcn.VcnManager.VcnStatusCallbackBinder;
import android.net.vcn.VcnManager.VcnUnderlyingNetworkPolicyListener;
+import android.os.Build;
import android.os.ParcelUuid;
+import androidx.test.filters.SmallTest;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
+
import org.junit.Before;
import org.junit.Test;
+import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import java.net.UnknownHostException;
import java.util.UUID;
import java.util.concurrent.Executor;
+// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on
+// Android B/B+
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+@SmallTest
public class VcnManagerTest {
private static final ParcelUuid SUB_GROUP = new ParcelUuid(new UUID(0, 0));
private static final String GATEWAY_CONNECTION_NAME = "gatewayConnectionName";
diff --git a/tests/vcn/java/android/net/vcn/VcnTransportInfoTest.java b/tests/vcn/java/android/net/vcn/VcnTransportInfoTest.java
index 7bc9970629a6..52952eb3f2cc 100644
--- a/tests/vcn/java/android/net/vcn/VcnTransportInfoTest.java
+++ b/tests/vcn/java/android/net/vcn/VcnTransportInfoTest.java
@@ -30,12 +30,24 @@ import static org.junit.Assert.fail;
import android.net.NetworkCapabilities;
import android.net.wifi.WifiConfiguration;
import android.net.wifi.WifiInfo;
+import android.os.Build;
import android.os.Parcel;
+import androidx.test.filters.SmallTest;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
+
import org.junit.Test;
+import org.junit.runner.RunWith;
import java.util.Arrays;
+// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on
+// Android B/B+
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+@SmallTest
public class VcnTransportInfoTest {
private static final int SUB_ID = 1;
private static final int NETWORK_ID = 5;
diff --git a/tests/vcn/java/android/net/vcn/VcnUnderlyingNetworkPolicyTest.java b/tests/vcn/java/android/net/vcn/VcnUnderlyingNetworkPolicyTest.java
index a674425efea3..c82d2003dbf6 100644
--- a/tests/vcn/java/android/net/vcn/VcnUnderlyingNetworkPolicyTest.java
+++ b/tests/vcn/java/android/net/vcn/VcnUnderlyingNetworkPolicyTest.java
@@ -22,9 +22,21 @@ import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import android.net.NetworkCapabilities;
+import android.os.Build;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
import org.junit.Test;
+import org.junit.runner.RunWith;
+// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on
+// Android B/B+
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+@SmallTest
public class VcnUnderlyingNetworkPolicyTest {
private static final VcnUnderlyingNetworkPolicy DEFAULT_NETWORK_POLICY =
new VcnUnderlyingNetworkPolicy(
diff --git a/tests/vcn/java/android/net/vcn/VcnUnderlyingNetworkSpecifierTest.java b/tests/vcn/java/android/net/vcn/VcnUnderlyingNetworkSpecifierTest.java
index 2110d6ee7c86..22361cc71f12 100644
--- a/tests/vcn/java/android/net/vcn/VcnUnderlyingNetworkSpecifierTest.java
+++ b/tests/vcn/java/android/net/vcn/VcnUnderlyingNetworkSpecifierTest.java
@@ -22,14 +22,20 @@ import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import android.net.TelephonyNetworkSpecifier;
+import android.os.Build;
import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
import org.junit.Test;
import org.junit.runner.RunWith;
-@RunWith(AndroidJUnit4.class)
+// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on
+// Android B/B+
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
@SmallTest
public class VcnUnderlyingNetworkSpecifierTest {
private static final int[] TEST_SUB_IDS = new int[] {1, 2, 3, 5};
diff --git a/tests/vcn/java/android/net/vcn/VcnUtilsTest.java b/tests/vcn/java/android/net/vcn/VcnUtilsTest.java
index 3ce6c8f9386d..fb040d8f9b91 100644
--- a/tests/vcn/java/android/net/vcn/VcnUtilsTest.java
+++ b/tests/vcn/java/android/net/vcn/VcnUtilsTest.java
@@ -30,13 +30,25 @@ import android.net.Network;
import android.net.NetworkCapabilities;
import android.net.TelephonyNetworkSpecifier;
import android.net.wifi.WifiInfo;
+import android.os.Build;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
import org.junit.Before;
import org.junit.Test;
+import org.junit.runner.RunWith;
import java.util.Arrays;
import java.util.Collections;
+// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on
+// Android B/B+
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+@SmallTest
public class VcnUtilsTest {
private static final int SUB_ID = 1;
diff --git a/tests/vcn/java/android/net/vcn/VcnWifiUnderlyingNetworkTemplateTest.java b/tests/vcn/java/android/net/vcn/VcnWifiUnderlyingNetworkTemplateTest.java
index 4063178e005d..2c072e1cbc88 100644
--- a/tests/vcn/java/android/net/vcn/VcnWifiUnderlyingNetworkTemplateTest.java
+++ b/tests/vcn/java/android/net/vcn/VcnWifiUnderlyingNetworkTemplateTest.java
@@ -22,10 +22,23 @@ import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
+import android.os.Build;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
+
import org.junit.Test;
+import org.junit.runner.RunWith;
import java.util.Set;
+// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on
+// Android B/B+
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+@SmallTest
public class VcnWifiUnderlyingNetworkTemplateTest extends VcnUnderlyingNetworkTemplateTestBase {
private static final String SSID = "TestWifi";
diff --git a/tests/vcn/java/android/net/vcn/persistablebundleutils/EapSessionConfigUtilsTest.java b/tests/vcn/java/android/net/vcn/persistablebundleutils/EapSessionConfigUtilsTest.java
index bc8e9d3200b6..01e9ac2ac3cf 100644
--- a/tests/vcn/java/android/net/vcn/persistablebundleutils/EapSessionConfigUtilsTest.java
+++ b/tests/vcn/java/android/net/vcn/persistablebundleutils/EapSessionConfigUtilsTest.java
@@ -21,11 +21,14 @@ import static android.telephony.TelephonyManager.APPTYPE_USIM;
import static org.junit.Assert.assertEquals;
import android.net.eap.EapSessionConfig;
+import android.os.Build;
import android.os.PersistableBundle;
import androidx.test.InstrumentationRegistry;
import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -35,7 +38,10 @@ import java.nio.charset.StandardCharsets;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
-@RunWith(AndroidJUnit4.class)
+// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on
+// Android B/B+
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
@SmallTest
public class EapSessionConfigUtilsTest {
private static final byte[] EAP_ID = "test@android.net".getBytes(StandardCharsets.US_ASCII);
diff --git a/tests/vcn/java/android/net/vcn/persistablebundleutils/IkeIdentificationUtilsTest.java b/tests/vcn/java/android/net/vcn/persistablebundleutils/IkeIdentificationUtilsTest.java
index 4f3930f9b5af..821e5a6c94cb 100644
--- a/tests/vcn/java/android/net/vcn/persistablebundleutils/IkeIdentificationUtilsTest.java
+++ b/tests/vcn/java/android/net/vcn/persistablebundleutils/IkeIdentificationUtilsTest.java
@@ -25,10 +25,13 @@ import android.net.ipsec.ike.IkeIpv4AddrIdentification;
import android.net.ipsec.ike.IkeIpv6AddrIdentification;
import android.net.ipsec.ike.IkeKeyIdIdentification;
import android.net.ipsec.ike.IkeRfc822AddrIdentification;
+import android.os.Build;
import android.os.PersistableBundle;
import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -39,7 +42,10 @@ import java.net.InetAddress;
import javax.security.auth.x500.X500Principal;
-@RunWith(AndroidJUnit4.class)
+// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on
+// Android B/B+
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
@SmallTest
public class IkeIdentificationUtilsTest {
private static void verifyPersistableBundleEncodeDecodeIsLossless(IkeIdentification id) {
diff --git a/tests/vcn/java/android/net/vcn/persistablebundleutils/IkeSessionParamsUtilsTest.java b/tests/vcn/java/android/net/vcn/persistablebundleutils/IkeSessionParamsUtilsTest.java
index 9f7d2390938f..7200aee1c012 100644
--- a/tests/vcn/java/android/net/vcn/persistablebundleutils/IkeSessionParamsUtilsTest.java
+++ b/tests/vcn/java/android/net/vcn/persistablebundleutils/IkeSessionParamsUtilsTest.java
@@ -29,14 +29,16 @@ import android.net.InetAddresses;
import android.net.eap.EapSessionConfig;
import android.net.ipsec.ike.IkeFqdnIdentification;
import android.net.ipsec.ike.IkeSessionParams;
+import android.os.Build;
import android.os.PersistableBundle;
import androidx.test.InstrumentationRegistry;
import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
import com.android.internal.org.bouncycastle.util.io.pem.PemObject;
import com.android.internal.org.bouncycastle.util.io.pem.PemReader;
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -52,7 +54,10 @@ import java.security.cert.X509Certificate;
import java.security.interfaces.RSAPrivateKey;
import java.util.concurrent.TimeUnit;
-@RunWith(AndroidJUnit4.class)
+// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on
+// Android B/B+
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
@SmallTest
public class IkeSessionParamsUtilsTest {
// Public for use in VcnGatewayConnectionConfigTest, EncryptedTunnelParamsUtilsTest
diff --git a/tests/vcn/java/android/net/vcn/persistablebundleutils/IkeTrafficSelectorUtilsTest.java b/tests/vcn/java/android/net/vcn/persistablebundleutils/IkeTrafficSelectorUtilsTest.java
index 28cf38a2a583..957e785d70c0 100644
--- a/tests/vcn/java/android/net/vcn/persistablebundleutils/IkeTrafficSelectorUtilsTest.java
+++ b/tests/vcn/java/android/net/vcn/persistablebundleutils/IkeTrafficSelectorUtilsTest.java
@@ -20,17 +20,23 @@ import static org.junit.Assert.assertEquals;
import android.net.InetAddresses;
import android.net.ipsec.ike.IkeTrafficSelector;
+import android.os.Build;
import android.os.PersistableBundle;
import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.net.InetAddress;
-@RunWith(AndroidJUnit4.class)
+// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on
+// Android B/B+
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
@SmallTest
public class IkeTrafficSelectorUtilsTest {
private static final int START_PORT = 16;
diff --git a/tests/vcn/java/android/net/vcn/persistablebundleutils/SaProposalUtilsTest.java b/tests/vcn/java/android/net/vcn/persistablebundleutils/SaProposalUtilsTest.java
index 664044a9e7d4..1e8f5ff2dc07 100644
--- a/tests/vcn/java/android/net/vcn/persistablebundleutils/SaProposalUtilsTest.java
+++ b/tests/vcn/java/android/net/vcn/persistablebundleutils/SaProposalUtilsTest.java
@@ -21,15 +21,21 @@ import static org.junit.Assert.assertEquals;
import android.net.ipsec.ike.ChildSaProposal;
import android.net.ipsec.ike.IkeSaProposal;
import android.net.ipsec.ike.SaProposal;
+import android.os.Build;
import android.os.PersistableBundle;
import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
import org.junit.Test;
import org.junit.runner.RunWith;
-@RunWith(AndroidJUnit4.class)
+// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on
+// Android B/B+
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
@SmallTest
public class SaProposalUtilsTest {
/** Package private so that IkeSessionParamsUtilsTest can use it */
diff --git a/tests/vcn/java/android/net/vcn/persistablebundleutils/TunnelConnectionParamsUtilsTest.java b/tests/vcn/java/android/net/vcn/persistablebundleutils/TunnelConnectionParamsUtilsTest.java
index f9dc9eb4d5ae..7d17724112ec 100644
--- a/tests/vcn/java/android/net/vcn/persistablebundleutils/TunnelConnectionParamsUtilsTest.java
+++ b/tests/vcn/java/android/net/vcn/persistablebundleutils/TunnelConnectionParamsUtilsTest.java
@@ -20,14 +20,20 @@ import static org.junit.Assert.assertEquals;
import android.net.ipsec.ike.IkeSessionParams;
import android.net.ipsec.ike.IkeTunnelConnectionParams;
+import android.os.Build;
import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
import org.junit.Test;
import org.junit.runner.RunWith;
-@RunWith(AndroidJUnit4.class)
+// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on
+// Android B/B+
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
@SmallTest
public class TunnelConnectionParamsUtilsTest {
// Public for use in VcnGatewayConnectionConfigTest
diff --git a/tests/vcn/java/android/net/vcn/persistablebundleutils/TunnelModeChildSessionParamsUtilsTest.java b/tests/vcn/java/android/net/vcn/persistablebundleutils/TunnelModeChildSessionParamsUtilsTest.java
index e0b5f0ef0381..3d7348a79b8c 100644
--- a/tests/vcn/java/android/net/vcn/persistablebundleutils/TunnelModeChildSessionParamsUtilsTest.java
+++ b/tests/vcn/java/android/net/vcn/persistablebundleutils/TunnelModeChildSessionParamsUtilsTest.java
@@ -25,10 +25,13 @@ import android.net.InetAddresses;
import android.net.ipsec.ike.ChildSaProposal;
import android.net.ipsec.ike.IkeTrafficSelector;
import android.net.ipsec.ike.TunnelModeChildSessionParams;
+import android.os.Build;
import android.os.PersistableBundle;
import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -37,7 +40,10 @@ import java.net.Inet4Address;
import java.net.Inet6Address;
import java.util.concurrent.TimeUnit;
-@RunWith(AndroidJUnit4.class)
+// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on
+// Android B/B+
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
@SmallTest
public class TunnelModeChildSessionParamsUtilsTest {
// Package private for use in EncryptedTunnelParamsUtilsTest
diff --git a/tests/vcn/java/android/net/vcn/util/MtuUtilsTest.java b/tests/vcn/java/android/net/vcn/util/MtuUtilsTest.java
index 47638b002f37..99c7aa72146b 100644
--- a/tests/vcn/java/android/net/vcn/util/MtuUtilsTest.java
+++ b/tests/vcn/java/android/net/vcn/util/MtuUtilsTest.java
@@ -33,9 +33,12 @@ import static org.junit.Assert.assertTrue;
import static java.util.Collections.emptyList;
import android.net.ipsec.ike.ChildSaProposal;
+import android.os.Build;
import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -43,7 +46,10 @@ import org.junit.runner.RunWith;
import java.util.Arrays;
import java.util.List;
-@RunWith(AndroidJUnit4.class)
+// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on
+// Android B/B+
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
@SmallTest
public class MtuUtilsTest {
private void verifyUnderlyingMtuZero(boolean isIpv4) {
diff --git a/tests/vcn/java/android/net/vcn/util/PersistableBundleUtilsTest.java b/tests/vcn/java/android/net/vcn/util/PersistableBundleUtilsTest.java
index c84e60086b37..f7786af840ee 100644
--- a/tests/vcn/java/android/net/vcn/util/PersistableBundleUtilsTest.java
+++ b/tests/vcn/java/android/net/vcn/util/PersistableBundleUtilsTest.java
@@ -21,10 +21,13 @@ import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
+import android.os.Build;
import android.os.PersistableBundle;
import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -35,7 +38,10 @@ import java.util.LinkedHashMap;
import java.util.List;
import java.util.Objects;
-@RunWith(AndroidJUnit4.class)
+// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on
+// Android B/B+
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
@SmallTest
public class PersistableBundleUtilsTest {
private static final String TEST_KEY = "testKey";
diff --git a/tests/vcn/java/com/android/server/VcnManagementServiceTest.java b/tests/vcn/java/com/android/server/VcnManagementServiceTest.java
index 26a2a0636792..a97f9a837bab 100644
--- a/tests/vcn/java/com/android/server/VcnManagementServiceTest.java
+++ b/tests/vcn/java/com/android/server/VcnManagementServiceTest.java
@@ -79,6 +79,7 @@ import android.net.vcn.VcnManager;
import android.net.vcn.VcnUnderlyingNetworkPolicy;
import android.net.vcn.util.PersistableBundleUtils;
import android.net.vcn.util.PersistableBundleUtils.PersistableBundleWrapper;
+import android.os.Build;
import android.os.IBinder;
import android.os.ParcelUuid;
import android.os.PersistableBundle;
@@ -93,7 +94,6 @@ import android.telephony.TelephonyManager;
import android.util.ArraySet;
import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
import com.android.server.VcnManagementService.VcnCallback;
import com.android.server.VcnManagementService.VcnStatusCallbackInfo;
@@ -101,6 +101,8 @@ import com.android.server.vcn.TelephonySubscriptionTracker;
import com.android.server.vcn.Vcn;
import com.android.server.vcn.VcnContext;
import com.android.server.vcn.VcnNetworkProvider;
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
import org.junit.Before;
import org.junit.Rule;
@@ -117,8 +119,10 @@ import java.util.Map.Entry;
import java.util.Set;
import java.util.UUID;
-/** Tests for {@link VcnManagementService}. */
-@RunWith(AndroidJUnit4.class)
+// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on
+// Android B/B+
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
@SmallTest
public class VcnManagementServiceTest {
@Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
diff --git a/tests/vcn/java/com/android/server/vcn/TelephonySubscriptionTrackerTest.java b/tests/vcn/java/com/android/server/vcn/TelephonySubscriptionTrackerTest.java
index 77f82f0d8cf4..6276be27fbf5 100644
--- a/tests/vcn/java/com/android/server/vcn/TelephonySubscriptionTrackerTest.java
+++ b/tests/vcn/java/com/android/server/vcn/TelephonySubscriptionTrackerTest.java
@@ -54,6 +54,7 @@ import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.vcn.VcnManager;
+import android.os.Build;
import android.os.Handler;
import android.os.ParcelUuid;
import android.os.PersistableBundle;
@@ -69,9 +70,10 @@ import android.util.ArrayMap;
import android.util.ArraySet;
import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
import com.android.modules.utils.HandlerExecutor;
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
import org.junit.Before;
import org.junit.Test;
@@ -87,8 +89,10 @@ import java.util.Map;
import java.util.Set;
import java.util.UUID;
-/** Tests for TelephonySubscriptionTracker */
-@RunWith(AndroidJUnit4.class)
+// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on
+// Android B/B+
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
@SmallTest
public class TelephonySubscriptionTrackerTest {
private static final String PACKAGE_NAME =
diff --git a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionConnectedStateTest.java b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionConnectedStateTest.java
index 74db6a5211a0..6608dda95a4b 100644
--- a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionConnectedStateTest.java
+++ b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionConnectedStateTest.java
@@ -70,16 +70,18 @@ import android.net.vcn.VcnGatewayConnectionConfigTest;
import android.net.vcn.VcnManager.VcnErrorCode;
import android.net.vcn.VcnTransportInfo;
import android.net.vcn.util.MtuUtils;
+import android.os.Build;
import android.os.PersistableBundle;
import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
import com.android.server.vcn.VcnGatewayConnection.VcnChildSessionCallback;
import com.android.server.vcn.VcnGatewayConnection.VcnChildSessionConfiguration;
import com.android.server.vcn.VcnGatewayConnection.VcnIkeSession;
import com.android.server.vcn.VcnGatewayConnection.VcnNetworkAgent;
import com.android.server.vcn.routeselection.UnderlyingNetworkRecord;
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
import org.junit.Before;
import org.junit.Test;
@@ -94,8 +96,10 @@ import java.util.Collections;
import java.util.List;
import java.util.function.Consumer;
-/** Tests for VcnGatewayConnection.ConnectedState */
-@RunWith(AndroidJUnit4.class)
+// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on
+// Android B/B+
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
@SmallTest
public class VcnGatewayConnectionConnectedStateTest extends VcnGatewayConnectionTestBase {
private static final int PARALLEL_SA_COUNT = 4;
diff --git a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionConnectingStateTest.java b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionConnectingStateTest.java
index 3c70759a2fa6..f6123d29f35a 100644
--- a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionConnectingStateTest.java
+++ b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionConnectingStateTest.java
@@ -26,17 +26,22 @@ import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import android.net.ipsec.ike.IkeSessionParams;
+import android.os.Build;
import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
-/** Tests for VcnGatewayConnection.ConnectingState */
-@RunWith(AndroidJUnit4.class)
+// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on
+// Android B/B+
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
@SmallTest
public class VcnGatewayConnectionConnectingStateTest extends VcnGatewayConnectionTestBase {
private VcnIkeSession mIkeSession;
diff --git a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionDisconnectedStateTest.java b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionDisconnectedStateTest.java
index f3eb82f46de7..7cfaf5be5111 100644
--- a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionDisconnectedStateTest.java
+++ b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionDisconnectedStateTest.java
@@ -30,16 +30,21 @@ import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import android.net.IpSecManager;
+import android.os.Build;
import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
-/** Tests for VcnGatewayConnection.DisconnectedState */
-@RunWith(AndroidJUnit4.class)
+// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on
+// Android B/B+
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
@SmallTest
public class VcnGatewayConnectionDisconnectedStateTest extends VcnGatewayConnectionTestBase {
@Before
diff --git a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionDisconnectingStateTest.java b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionDisconnectingStateTest.java
index 78aefad9f8ff..9132d830c54e 100644
--- a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionDisconnectingStateTest.java
+++ b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionDisconnectingStateTest.java
@@ -23,15 +23,21 @@ import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
+import android.os.Build;
+
import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
-/** Tests for VcnGatewayConnection.DisconnectedState */
-@RunWith(AndroidJUnit4.class)
+// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on
+// Android B/B+
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
@SmallTest
public class VcnGatewayConnectionDisconnectingStateTest extends VcnGatewayConnectionTestBase {
@Before
diff --git a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionRetryTimeoutStateTest.java b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionRetryTimeoutStateTest.java
index 6568cdd44377..d5ef4e028709 100644
--- a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionRetryTimeoutStateTest.java
+++ b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionRetryTimeoutStateTest.java
@@ -27,15 +27,21 @@ import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
+import android.os.Build;
+
import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
-/** Tests for VcnGatewayConnection.RetryTimeoutState */
-@RunWith(AndroidJUnit4.class)
+// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on
+// Android B/B+
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
@SmallTest
public class VcnGatewayConnectionRetryTimeoutStateTest extends VcnGatewayConnectionTestBase {
private long mFirstRetryInterval;
diff --git a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTest.java b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTest.java
index b9fe76a24d20..5283322682ee 100644
--- a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTest.java
+++ b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTest.java
@@ -61,15 +61,17 @@ import android.net.vcn.VcnGatewayConnectionConfigTest;
import android.net.vcn.VcnManager;
import android.net.vcn.VcnTransportInfo;
import android.net.wifi.WifiInfo;
+import android.os.Build;
import android.os.ParcelUuid;
import android.os.Process;
import android.telephony.SubscriptionInfo;
import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
import com.android.server.vcn.TelephonySubscriptionTracker.TelephonySubscriptionSnapshot;
import com.android.server.vcn.routeselection.UnderlyingNetworkRecord;
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
import org.junit.Before;
import org.junit.Test;
@@ -87,8 +89,10 @@ import java.util.UUID;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
-/** Tests for TelephonySubscriptionTracker */
-@RunWith(AndroidJUnit4.class)
+// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on
+// Android B/B+
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
@SmallTest
public class VcnGatewayConnectionTest extends VcnGatewayConnectionTestBase {
private static final int TEST_UID = Process.myUid() + 1;
diff --git a/tests/vcn/java/com/android/server/vcn/VcnNetworkProviderTest.java b/tests/vcn/java/com/android/server/vcn/VcnNetworkProviderTest.java
index e9026e22b6b2..2b92428918db 100644
--- a/tests/vcn/java/com/android/server/vcn/VcnNetworkProviderTest.java
+++ b/tests/vcn/java/com/android/server/vcn/VcnNetworkProviderTest.java
@@ -29,12 +29,14 @@ import android.annotation.NonNull;
import android.content.Context;
import android.net.ConnectivityManager;
import android.net.NetworkRequest;
+import android.os.Build;
import android.os.test.TestLooper;
import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
import com.android.server.vcn.VcnNetworkProvider.NetworkRequestListener;
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
import org.junit.Before;
import org.junit.Test;
@@ -44,8 +46,10 @@ import org.mockito.ArgumentCaptor;
import java.util.ArrayList;
import java.util.List;
-/** Tests for TelephonySubscriptionTracker */
-@RunWith(AndroidJUnit4.class)
+// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on
+// Android B/B+
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
@SmallTest
public class VcnNetworkProviderTest {
private static final int TEST_SCORE_UNSATISFIED = 0;
diff --git a/tests/vcn/java/com/android/server/vcn/VcnTest.java b/tests/vcn/java/com/android/server/vcn/VcnTest.java
index 6d269686e42f..bd4aeba761da 100644
--- a/tests/vcn/java/com/android/server/vcn/VcnTest.java
+++ b/tests/vcn/java/com/android/server/vcn/VcnTest.java
@@ -49,20 +49,26 @@ import android.net.Uri;
import android.net.vcn.VcnConfig;
import android.net.vcn.VcnGatewayConnectionConfig;
import android.net.vcn.VcnGatewayConnectionConfigTest;
+import android.os.Build;
import android.os.ParcelUuid;
import android.os.test.TestLooper;
import android.provider.Settings;
import android.telephony.TelephonyManager;
import android.util.ArraySet;
+import androidx.test.filters.SmallTest;
+
import com.android.server.VcnManagementService.VcnCallback;
import com.android.server.vcn.TelephonySubscriptionTracker.TelephonySubscriptionSnapshot;
import com.android.server.vcn.Vcn.VcnGatewayStatusCallback;
import com.android.server.vcn.Vcn.VcnUserMobileDataStateListener;
import com.android.server.vcn.VcnNetworkProvider.NetworkRequestListener;
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
import org.junit.Before;
import org.junit.Test;
+import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import java.util.ArrayList;
@@ -73,6 +79,11 @@ import java.util.Map.Entry;
import java.util.Set;
import java.util.UUID;
+// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on
+// Android B/B+
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+@SmallTest
public class VcnTest {
private static final String PKG_NAME = VcnTest.class.getPackage().getName();
private static final ParcelUuid TEST_SUB_GROUP = new ParcelUuid(new UUID(0, 0));
diff --git a/tests/vcn/java/com/android/server/vcn/routeselection/IpSecPacketLossDetectorTest.java b/tests/vcn/java/com/android/server/vcn/routeselection/IpSecPacketLossDetectorTest.java
index c11b6bb3435d..53a36d3e4d6a 100644
--- a/tests/vcn/java/com/android/server/vcn/routeselection/IpSecPacketLossDetectorTest.java
+++ b/tests/vcn/java/com/android/server/vcn/routeselection/IpSecPacketLossDetectorTest.java
@@ -44,16 +44,22 @@ import static org.mockito.Mockito.when;
import android.content.BroadcastReceiver;
import android.content.Intent;
import android.net.IpSecTransformState;
+import android.os.Build;
import android.os.OutcomeReceiver;
import android.os.PowerManager;
+import androidx.test.filters.SmallTest;
+
import com.android.server.vcn.routeselection.IpSecPacketLossDetector.PacketLossCalculationResult;
import com.android.server.vcn.routeselection.IpSecPacketLossDetector.PacketLossCalculator;
import com.android.server.vcn.routeselection.NetworkMetricMonitor.IpSecTransformWrapper;
import com.android.server.vcn.routeselection.NetworkMetricMonitor.NetworkMetricMonitorCallback;
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
import org.junit.Before;
import org.junit.Test;
+import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
@@ -63,6 +69,11 @@ import java.util.Arrays;
import java.util.BitSet;
import java.util.concurrent.TimeUnit;
+// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on
+// Android B/B+
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+@SmallTest
public class IpSecPacketLossDetectorTest extends NetworkEvaluationTestBase {
private static final String TAG = IpSecPacketLossDetectorTest.class.getSimpleName();
diff --git a/tests/vcn/java/com/android/server/vcn/routeselection/NetworkPriorityClassifierTest.java b/tests/vcn/java/com/android/server/vcn/routeselection/NetworkPriorityClassifierTest.java
index 4f34f9f8f74c..a9c637f7c943 100644
--- a/tests/vcn/java/com/android/server/vcn/routeselection/NetworkPriorityClassifierTest.java
+++ b/tests/vcn/java/com/android/server/vcn/routeselection/NetworkPriorityClassifierTest.java
@@ -42,16 +42,28 @@ import android.net.vcn.VcnGatewayConnectionConfig;
import android.net.vcn.VcnManager;
import android.net.vcn.VcnUnderlyingNetworkTemplate;
import android.net.vcn.VcnWifiUnderlyingNetworkTemplate;
+import android.os.Build;
import android.os.PersistableBundle;
import android.util.ArraySet;
+import androidx.test.filters.SmallTest;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
+
import org.junit.Before;
import org.junit.Test;
+import org.junit.runner.RunWith;
import java.util.Collections;
import java.util.List;
import java.util.Set;
+// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on
+// Android B/B+
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+@SmallTest
public class NetworkPriorityClassifierTest extends NetworkEvaluationTestBase {
private UnderlyingNetworkRecord mWifiNetworkRecord;
private UnderlyingNetworkRecord mCellNetworkRecord;
diff --git a/tests/vcn/java/com/android/server/vcn/routeselection/UnderlyingNetworkControllerTest.java b/tests/vcn/java/com/android/server/vcn/routeselection/UnderlyingNetworkControllerTest.java
index e540932d0e1f..99c508c139ec 100644
--- a/tests/vcn/java/com/android/server/vcn/routeselection/UnderlyingNetworkControllerTest.java
+++ b/tests/vcn/java/com/android/server/vcn/routeselection/UnderlyingNetworkControllerTest.java
@@ -58,6 +58,7 @@ import android.net.vcn.VcnCellUnderlyingNetworkTemplate;
import android.net.vcn.VcnCellUnderlyingNetworkTemplateTest;
import android.net.vcn.VcnGatewayConnectionConfigTest;
import android.net.vcn.VcnUnderlyingNetworkTemplate;
+import android.os.Build;
import android.os.ParcelUuid;
import android.os.test.TestLooper;
import android.telephony.CarrierConfigManager;
@@ -65,6 +66,8 @@ import android.telephony.SubscriptionInfo;
import android.telephony.TelephonyManager;
import android.util.ArraySet;
+import androidx.test.filters.SmallTest;
+
import com.android.server.vcn.TelephonySubscriptionTracker.TelephonySubscriptionSnapshot;
import com.android.server.vcn.VcnContext;
import com.android.server.vcn.VcnNetworkProvider;
@@ -73,9 +76,12 @@ import com.android.server.vcn.routeselection.UnderlyingNetworkController.Network
import com.android.server.vcn.routeselection.UnderlyingNetworkController.UnderlyingNetworkControllerCallback;
import com.android.server.vcn.routeselection.UnderlyingNetworkController.UnderlyingNetworkListener;
import com.android.server.vcn.routeselection.UnderlyingNetworkEvaluator.NetworkEvaluatorCallback;
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
import org.junit.Before;
import org.junit.Test;
+import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
@@ -89,6 +95,11 @@ import java.util.List;
import java.util.Set;
import java.util.UUID;
+// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on
+// Android B/B+
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+@SmallTest
public class UnderlyingNetworkControllerTest {
private static final ParcelUuid SUB_GROUP = new ParcelUuid(new UUID(0, 0));
private static final int INITIAL_SUB_ID_1 = 1;
diff --git a/tests/vcn/java/com/android/server/vcn/routeselection/UnderlyingNetworkEvaluatorTest.java b/tests/vcn/java/com/android/server/vcn/routeselection/UnderlyingNetworkEvaluatorTest.java
index a315b0690ec5..27c1bc105bde 100644
--- a/tests/vcn/java/com/android/server/vcn/routeselection/UnderlyingNetworkEvaluatorTest.java
+++ b/tests/vcn/java/com/android/server/vcn/routeselection/UnderlyingNetworkEvaluatorTest.java
@@ -38,19 +38,30 @@ import static org.mockito.Mockito.when;
import android.net.IpSecTransform;
import android.net.vcn.VcnGatewayConnectionConfig;
+import android.os.Build;
+
+import androidx.test.filters.SmallTest;
import com.android.server.vcn.routeselection.NetworkMetricMonitor.NetworkMetricMonitorCallback;
import com.android.server.vcn.routeselection.UnderlyingNetworkEvaluator.Dependencies;
import com.android.server.vcn.routeselection.UnderlyingNetworkEvaluator.NetworkEvaluatorCallback;
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
import org.junit.Before;
import org.junit.Test;
+import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import java.util.concurrent.TimeUnit;
+// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on
+// Android B/B+
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+@SmallTest
public class UnderlyingNetworkEvaluatorTest extends NetworkEvaluationTestBase {
private static final int PENALTY_TIMEOUT_MIN = 10;
private static final long PENALTY_TIMEOUT_MS = TimeUnit.MINUTES.toMillis(PENALTY_TIMEOUT_MIN);
diff --git a/tools/aapt2/Debug.cpp b/tools/aapt2/Debug.cpp
index e24fe07f959b..9ef8b7dc9947 100644
--- a/tools/aapt2/Debug.cpp
+++ b/tools/aapt2/Debug.cpp
@@ -349,20 +349,22 @@ void Debug::PrintTable(const ResourceTable& table, const DebugPrintTableOptions&
value->value->Accept(&body_printer);
printer->Undent();
}
- printer->Println("Flag disabled values:");
- for (const auto& value : entry.flag_disabled_values) {
- printer->Print("(");
- printer->Print(value->config.to_string());
- printer->Print(") ");
- value->value->Accept(&headline_printer);
- if (options.show_sources && !value->value->GetSource().path.empty()) {
- printer->Print(" src=");
- printer->Print(value->value->GetSource().to_string());
+ if (!entry.flag_disabled_values.empty()) {
+ printer->Println("Flag disabled values:");
+ for (const auto& value : entry.flag_disabled_values) {
+ printer->Print("(");
+ printer->Print(value->config.to_string());
+ printer->Print(") ");
+ value->value->Accept(&headline_printer);
+ if (options.show_sources && !value->value->GetSource().path.empty()) {
+ printer->Print(" src=");
+ printer->Print(value->value->GetSource().to_string());
+ }
+ printer->Println();
+ printer->Indent();
+ value->value->Accept(&body_printer);
+ printer->Undent();
}
- printer->Println();
- printer->Indent();
- value->value->Accept(&body_printer);
- printer->Undent();
}
printer->Undent();
}