summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Android.bp1
-rw-r--r--boot/boot-image-profile.txt1
-rw-r--r--boot/preloaded-classes1
-rw-r--r--cmds/incidentd/src/IncidentService.cpp6
-rw-r--r--config/preloaded-classes1
-rw-r--r--core/api/module-lib-current.txt2
-rw-r--r--core/api/system-current.txt11
-rw-r--r--core/java/android/app/AppCompatTaskInfo.java5
-rw-r--r--core/java/android/app/LocaleConfig.java27
-rw-r--r--core/java/android/app/StatusBarManager.java4
-rw-r--r--core/java/android/app/activity_manager.aconfig7
-rw-r--r--core/java/android/app/supervision/SupervisionAppService.java38
-rw-r--r--core/java/android/companion/virtual/flags/flags.aconfig7
-rw-r--r--core/java/android/content/Intent.java7
-rw-r--r--core/java/android/content/flags/flags.aconfig12
-rw-r--r--core/java/android/content/pm/PackageParser.java18
-rw-r--r--core/java/android/content/pm/RegisteredServicesCache.java31
-rw-r--r--core/java/android/content/pm/parsing/FrameworkParsingPackageUtils.java33
-rw-r--r--core/java/android/hardware/biometrics/flags.aconfig8
-rw-r--r--core/java/android/hardware/input/IInputManager.aidl2
-rw-r--r--core/java/android/hardware/input/IKeyGestureHandler.aidl10
-rw-r--r--core/java/android/hardware/input/InputManager.java21
-rw-r--r--core/java/android/hardware/input/InputManagerGlobal.java103
-rw-r--r--core/java/android/net/VpnManager.java26
-rw-r--r--core/java/android/net/flags.aconfig8
-rw-r--r--core/java/android/os/CombinedMessageQueue/MessageQueue.java6
-rw-r--r--core/java/android/os/flags.aconfig9
-rw-r--r--core/java/android/os/image/flags/trade_in_mode_flags.aconfig9
-rw-r--r--core/java/android/util/ArrayMap.java2
-rw-r--r--core/java/android/view/InsetsController.java4
-rw-r--r--core/java/android/view/WindowManagerGlobal.java8
-rw-r--r--core/java/android/view/inspector/WindowInspector.java5
-rw-r--r--core/java/android/view/translation/ListenerGroup.java4
-rw-r--r--core/java/android/widget/DatePickerCalendarDelegate.java48
-rw-r--r--core/java/android/widget/TimePickerClockDelegate.java52
-rw-r--r--core/java/android/window/DesktopModeFlags.java7
-rw-r--r--core/java/android/window/flags/lse_desktop_experience.aconfig20
-rw-r--r--core/java/android/window/flags/responsible_apis.aconfig7
-rw-r--r--core/java/com/android/internal/app/AssistUtils.java2
-rw-r--r--core/java/com/android/internal/app/MediaRouteDialogPresenter.java12
-rw-r--r--core/java/com/android/internal/statusbar/DisableStates.aidl19
-rw-r--r--core/java/com/android/internal/statusbar/DisableStates.java95
-rw-r--r--core/java/com/android/internal/statusbar/IStatusBar.aidl2
-rw-r--r--core/java/com/android/internal/widget/remotecompose/core/CoreDocument.java8
-rw-r--r--core/java/com/android/internal/widget/remotecompose/core/RemoteComposeBuffer.java6
-rw-r--r--core/jni/Android.bp2
-rw-r--r--core/jni/AndroidRuntime.cpp2
-rw-r--r--core/jni/android_media_AudioSystem.cpp30
-rw-r--r--core/jni/android_media_AudioVolumeGroupCallback.cpp176
-rw-r--r--core/jni/android_media_AudioVolumeGroupCallback.h44
-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/values/config.xml2
-rw-r--r--core/res/res/values/config_telephony.xml6
-rw-r--r--core/tests/FileSystemUtilsTest/OWNERS2
-rw-r--r--core/tests/FileSystemUtilsTest/src/com/android/internal/content/FileSystemUtilsTest.java14
-rw-r--r--core/tests/coretests/src/android/content/IntentTest.java44
-rw-r--r--core/tests/coretests/src/android/content/pm/RegisteredServicesCacheUnitTest.java95
-rw-r--r--core/tests/coretests/src/com/android/internal/app/MediaRouteDialogPresenterTest.kt92
-rw-r--r--core/tests/coretests/src/com/android/internal/statusbar/DisableStatesTest.java64
-rw-r--r--graphics/java/android/graphics/Typeface.java20
-rw-r--r--libs/WindowManager/Shell/res/drawable/desktop_mode_ic_handle_menu_restart.xml28
-rw-r--r--libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml16
-rw-r--r--libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu_action_button.xml15
-rw-r--r--libs/WindowManager/Shell/res/values/dimen.xml13
-rw-r--r--libs/WindowManager/Shell/res/values/strings.xml2
-rw-r--r--libs/WindowManager/Shell/res/values/styles.xml8
-rw-r--r--libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/BubbleDropTargetBoundsProvider.kt5
-rw-r--r--libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java3
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java6
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java20
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTransitions.java2
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipBoundsAlgorithm.java7
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipDesktopState.java122
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipDesktopState.kt96
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/split/FlexParallaxSpec.java5
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java17
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/compatui/impl/CompatUIRequests.kt5
-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/desktopmode/DesktopActivityOrientationChangeHandler.kt23
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandler.kt65
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeKeyGestureHandler.kt29
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt28
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt70
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/VisualIndicatorViewContainer.kt129
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DeskTransition.kt23
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksTransitionObserver.kt105
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java2
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java10
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java16
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java3
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/transition/RecentsMixedTransition.java9
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java5
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CarWindowDecoration.java13
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java18
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java19
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt100
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuActionButton.kt19
-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/viewholder/AppHandleAnimator.kt101
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHandleViewHolder.kt51
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt27
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PipDesktopStateTest.java184
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PipDesktopStateTest.kt153
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/FlexParallaxSpecTests.java1
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java29
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopActivityOrientationChangeHandlerTest.kt18
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandlerTest.kt80
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeKeyGestureHandlerTest.kt20
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt131
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/VisualIndicatorViewContainerTest.kt110
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/DesksTransitionObserverTest.kt30
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserverTest.java19
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java48
-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.java10
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt2
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/viewholder/AppHandleViewHolderTest.kt3
-rw-r--r--libs/androidfw/AssetManager2.cpp4
-rw-r--r--libs/androidfw/CursorWindow.cpp2
-rw-r--r--libs/androidfw/ResourceTypes.cpp16
-rw-r--r--libs/androidfw/include/androidfw/ResourceTypes.h5
-rw-r--r--libs/hwui/renderthread/ReliableSurface.cpp24
-rw-r--r--libs/hwui/renderthread/VulkanManager.cpp1
-rw-r--r--media/java/android/media/AudioManager.java96
-rw-r--r--media/java/android/media/AudioSystem.java21
-rw-r--r--media/java/android/media/IAudioService.aidl7
-rw-r--r--media/java/android/media/audiopolicy/AudioVolumeGroupChangeHandler.java166
-rw-r--r--media/java/android/media/audiopolicy/IAudioVolumeChangeDispatcher.aidl31
-rw-r--r--media/java/android/media/quality/MediaQualityContract.java38
-rw-r--r--media/tests/AudioPolicyTest/AndroidManifest.xml1
-rw-r--r--media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioVolumeGroupChangeHandlerTest.java211
-rw-r--r--media/tests/projection/Android.bp8
-rw-r--r--media/tests/projection/AndroidManifest.xml2
-rw-r--r--media/tests/projection/AndroidTest.xml9
-rw-r--r--media/tests/projection/src/android/media/projection/MediaProjectionStoppingTest.java293
-rw-r--r--packages/CredentialManager/wear/AndroidManifest.xml6
-rw-r--r--packages/CredentialManager/wear/res/values/themes.xml24
-rw-r--r--packages/SettingsLib/AdaptiveIcon/Android.bp3
-rw-r--r--packages/SettingsLib/AdaptiveIcon/src/com/android/settingslib/widget/AdaptiveIconShapeDrawable.java3
-rw-r--r--packages/SettingsLib/AdaptiveIcon/src/com/android/settingslib/widget/AdaptiveOutlineDrawable.java2
-rw-r--r--packages/SettingsLib/SettingsTheme/res/layout/settingslib_expressive_collapsable_textview.xml3
-rw-r--r--packages/SettingsLib/SettingsTheme/src/com/android/settingslib/widget/CollapsableTextView.kt1
-rw-r--r--packages/SettingsLib/SettingsTheme/src/com/android/settingslib/widget/SettingsPreferenceGroupAdapter.kt17
-rw-r--r--packages/SettingsLib/Spa/build.gradle.kts6
-rw-r--r--packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt2
-rw-r--r--packages/SettingsLib/Spa/gradle/libs.versions.toml4
-rw-r--r--packages/SettingsLib/Spa/spa/build.gradle.kts8
-rw-r--r--packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Category.kt37
-rw-r--r--packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/ui/CategoryTest.kt17
-rw-r--r--packages/SettingsLib/StatusBannerPreference/res/layout/settingslib_expressive_preference_statusbanner.xml21
-rw-r--r--packages/SettingsLib/StatusBannerPreference/res/values/attrs.xml2
-rw-r--r--packages/SettingsLib/StatusBannerPreference/src/com/android/settingslib/widget/StatusBannerPreference.kt48
-rw-r--r--packages/SettingsLib/aconfig/settingslib.aconfig10
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/bluetooth/AmbientVolumeUiController.java1
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java28
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java122
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java5
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/notification/data/repository/ZenModeRepository.kt25
-rw-r--r--packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java52
-rw-r--r--packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceTest.java105
-rw-r--r--packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CsipDeviceManagerTest.java4
-rw-r--r--packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/data/repository/ZenModeRepositoryTest.kt10
-rw-r--r--packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java17
-rw-r--r--packages/SettingsProvider/test/src/com/android/providers/settings/SettingsHelperTest.java6
-rw-r--r--packages/SystemUI/aconfig/desktop_users_and_accounts.aconfig9
-rw-r--r--packages/SystemUI/aconfig/systemui.aconfig50
-rw-r--r--packages/SystemUI/animation/src/com/android/systemui/animation/ActivityTransitionAnimator.kt6
-rw-r--r--packages/SystemUI/compose/core/src/com/android/compose/gesture/effect/OffsetOverscrollEffect.kt2
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinInputDisplay.kt44
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/LockSection.kt4
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationScrimFlingBehavior.kt73
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt23
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/scene/session/ui/composable/Session.kt92
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt78
-rw-r--r--packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockController.kt6
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerTest.java4
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegateTest.java2
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractorTest.kt15
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorTest.kt23
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardKeyEventInteractorTest.kt24
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractorTest.kt16
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModelTest.kt12
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/model/SysUiStateExtTest.kt3
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/EditTileListStateTest.kt71
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/selection/MutableSelectionStateTest.kt103
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/CastTileTest.java33
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/dialog/CastDetailsViewModelTest.kt77
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt7
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ShadeControllerSceneImplTest.kt37
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/shade/carrier/ShadeCarrierGroupControllerTest.java9
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/CommandQueueTest.java32
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationGroupingUtilTest.kt64
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationShadeDepthControllerTest.kt43
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt11
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelTest.kt5
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractorTest.kt6
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractorTest.kt74
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt57
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractorTest.kt56
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelTest.kt4
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelTest.kt3
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt106
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/ColorizedFgsCoordinatorTest.kt90
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractorTest.kt8
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationsListInteractorTest.kt9
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/icon/domain/interactor/NotificationIconsInteractorTest.kt6
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractorImplTest.kt212
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/PromotedNotificationsInteractorTest.kt535
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java30
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationInfoTest.kt34
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationMenuRowTest.java41
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImplTest.kt18
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManagerImplTest.kt10
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java36
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallbackTest.java3
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerTest.kt4
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractorTest.kt6
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ui/IconManagerTest.kt9
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ui/StatusBarIconControllerTest.java30
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt (renamed from packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt)0
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/unfold/DisplaySwitchLatencyTrackerTest.kt47
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractorTest.kt86
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/user/ui/viewmodel/StatusBarUserChipViewModelTest.kt21
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt22
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/window/ui/viewmodel/WindowRootViewModelTest.kt2
-rw-r--r--packages/SystemUI/res/drawable/unpin_icon.xml10
-rw-r--r--packages/SystemUI/res/drawable/vector_drawable_progress_indeterminate_horizontal_trimmed.xml2
-rw-r--r--packages/SystemUI/res/layout/activity_rear_display_enabled.xml10
-rw-r--r--packages/SystemUI/res/layout/promoted_permission_guts.xml73
-rw-r--r--packages/SystemUI/res/values/dimens.xml2
-rw-r--r--packages/SystemUI/res/values/strings.xml15
-rw-r--r--packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/PreviewPositionHelper.java15
-rw-r--r--packages/SystemUI/src/com/android/keyguard/KeyguardViewController.java2
-rw-r--r--packages/SystemUI/src/com/android/systemui/ambient/touch/scrim/BouncerScrimController.java2
-rw-r--r--packages/SystemUI/src/com/android/systemui/assist/AssistManager.java2
-rw-r--r--packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java2
-rw-r--r--packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractor.kt3
-rw-r--r--packages/SystemUI/src/com/android/systemui/common/ui/compose/gestures/EagerTap.kt91
-rw-r--r--packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalLockIconViewModel.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java73
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/KeyguardWmReorderAtmsCalls.kt53
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/KeyguardWmStateRefactor.kt14
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java3
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt6
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissInteractor.kt5
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt17
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardKeyEventInteractor.kt5
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt46
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractor.kt71
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt50
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt5
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySection.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerUdfpsIconViewModel.kt5
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModel.kt9
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModel.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModel.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/DismissibleHorizontalPager.kt160
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/Media.kt191
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/SwipeToDismiss.kt115
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/SwipeToReveal.kt264
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaSettingsButtonViewModel.kt21
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaViewModel.kt15
-rw-r--r--packages/SystemUI/src/com/android/systemui/model/SceneContainerPlugin.kt25
-rw-r--r--packages/SystemUI/src/com/android/systemui/model/SysUiStateExt.kt14
-rw-r--r--packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt24
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt140
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/composefragment/ui/NotificationScrimClip.kt67
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/composefragment/ui/NotificationScrimClipParams.kt26
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModel.kt5
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/DragAndDropState.kt14
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/EditTileListState.kt60
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/TileDetails.kt3
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt229
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt3
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/MutableSelectionState.kt146
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/Selection.kt74
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/EditTileViewModel.kt3
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/CastTile.java54
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/CastDetailsContent.kt53
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/CastDetailsViewModel.kt78
-rw-r--r--packages/SystemUI/src/com/android/systemui/reardisplay/RearDisplayCoreStartable.kt13
-rw-r--r--packages/SystemUI/src/com/android/systemui/reardisplay/RearDisplayInnerDialogDelegate.kt26
-rw-r--r--packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt26
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java4
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt5
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/carrier/ShadeCarrierGroupController.java178
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImpl.kt36
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt7
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java43
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/NotificationGroupingUtil.java29
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt49
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModel.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/model/NotificationChipModel.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModel.kt89
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractor.kt24
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/domain/model/ScreenRecordChipModel.kt3
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModel.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModel.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChip.kt14
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/OngoingActivityChipModel.kt15
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModel.kt14
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java13
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/ColorizedFgsCoordinator.java11
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt73
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java3
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationListInteractor.kt22
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterView.java34
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewbinder/FooterViewBinder.kt9
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModel.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractor.kt63
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationLogger.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/AODPromotedNotificationInteractor.kt11
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/PromotedNotificationsInteractor.kt180
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/shared/model/PromotedNotificationContentModel.kt25
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java46
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableOutlineView.java3
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java9
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java29
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationMenuRow.java53
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt12
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/PromotedNotificationInfo.java1
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/PromotedPermissionGutsContent.java173
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/ActiveNotificationModel.kt7
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManagerImpl.kt6
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java5
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt18
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java3
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java7
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/DemoStatusIcons.java80
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java15
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java40
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallback.java3
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallModel.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/ui/DarkIconManager.java30
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/ui/IconManager.java79
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/ui/TintedIconManager.java35
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileUiAdapterKairos.kt95
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileViewLogger.kt39
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/StackedMobileBindableIcon.kt14
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt8
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinderKairos.kt290
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/ShadeCarrierBinder.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/ShadeCarrierBinderKairos.kt42
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/StackedMobileIconBinder.kt21
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernShadeCarrierGroupMobileView.kt60
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernStatusBarMobileView.kt62
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelKairos.kt11
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModelKairos.kt28
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/util/FakeMobileMappingsProxy.kt6
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseUserSwitcherAdapter.kt3
-rw-r--r--packages/SystemUI/src/com/android/systemui/unfold/DisplaySwitchLatencyLogger.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/unfold/DisplaySwitchLatencyTracker.kt23
-rw-r--r--packages/SystemUI/src/com/android/systemui/unfold/data/repository/ScreenTimeoutPolicyRepository.kt61
-rw-r--r--packages/SystemUI/src/com/android/systemui/user/data/source/UserRecord.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractor.kt22
-rw-r--r--packages/SystemUI/src/com/android/systemui/user/legacyhelper/data/LegacyUserDataHelper.kt14
-rw-r--r--packages/SystemUI/src/com/android/systemui/user/legacyhelper/ui/LegacyUserUiHelper.kt8
-rw-r--r--packages/SystemUI/src/com/android/systemui/user/shared/model/UserActionModel.kt1
-rw-r--r--packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModel.kt10
-rw-r--r--packages/SystemUI/src/com/android/systemui/util/composable/kairos/ActivatedKairosSpec.kt60
-rw-r--r--packages/SystemUI/src/com/android/systemui/util/composable/kairos/RememberKairosActivatable.kt38
-rw-r--r--packages/SystemUI/src/com/android/systemui/util/composable/kairos/ToComposeState.kt36
-rw-r--r--packages/SystemUI/src/com/android/systemui/window/data/repository/NoopWindowRootViewBlurRepository.kt3
-rw-r--r--packages/SystemUI/src/com/android/systemui/window/data/repository/WindowRootViewBlurRepository.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/window/domain/interactor/WindowRootViewBlurInteractor.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/window/ui/viewmodel/WindowRootViewModel.kt4
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/animation/ActivityTransitionAnimatorTest.kt2
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java1
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTestKt.kt66
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySectionTest.kt3
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt39
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropTest.kt70
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/EditModeTest.kt47
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/ResizingTest.kt2
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/reardisplay/RearDisplayCoreStartableTest.kt6
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/reardisplay/RearDisplayInnerDialogDelegateTest.kt39
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java2
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java55
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestCase.java3
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestCaseExt.kt13
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractorKosmos.kt1
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/startable/SceneContainerStartableKosmos.kt2
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelKosmos.kt6
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/model/ActiveNotificationModelBuilder.kt8
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModelKosmos.kt2
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/FakePromotedNotificationContentExtractor.kt10
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractorKosmos.kt4
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/PromotedNotificationsInteractorKosmos.kt4
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/shared/model/PromotedNotificationContentBuilder.kt33
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/icon/AppIconProviderKosmos.kt3
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/icon/NotificationIconStyleProviderKosmos.kt4
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallTestHelper.kt6
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ui/TintedIconManagerKosmos.kt22
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileUiAdapterKairosKosmos.kt34
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractorKosmos.kt1
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/window/data/repository/WindowRootViewBlurRepositoryKosmos.kt2
-rw-r--r--services/accessibility/accessibility.aconfig10
-rw-r--r--services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java52
-rw-r--r--services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java17
-rw-r--r--services/core/java/com/android/server/adb/AdbService.java26
-rw-r--r--services/core/java/com/android/server/am/ProcessList.java102
-rw-r--r--services/core/java/com/android/server/am/SettingsToPropertiesMapper.java1
-rw-r--r--services/core/java/com/android/server/audio/AudioService.java23
-rw-r--r--services/core/java/com/android/server/audio/AudioSystemAdapter.java24
-rw-r--r--services/core/java/com/android/server/audio/AudioVolumeChangeHandler.java103
-rw-r--r--services/core/java/com/android/server/display/DisplayManagerService.java109
-rw-r--r--services/core/java/com/android/server/display/mode/DisplayModeDirector.java29
-rw-r--r--services/core/java/com/android/server/display/mode/ModeChangeObserver.java8
-rw-r--r--services/core/java/com/android/server/hdmi/HdmiControlService.java9
-rw-r--r--services/core/java/com/android/server/input/AppLaunchShortcutManager.java2
-rw-r--r--services/core/java/com/android/server/input/InputGestureManager.java9
-rw-r--r--services/core/java/com/android/server/input/InputManagerService.java46
-rw-r--r--services/core/java/com/android/server/input/KeyGestureController.java179
-rw-r--r--services/core/java/com/android/server/inputmethod/InputMethodManagerService.java39
-rw-r--r--services/core/java/com/android/server/location/contexthub/ContextHubEndpointBroker.java74
-rw-r--r--services/core/java/com/android/server/media/MediaSessionService.java34
-rw-r--r--services/core/java/com/android/server/media/projection/Android.bp21
-rw-r--r--services/core/java/com/android/server/media/quality/MediaQualityUtils.java258
-rw-r--r--services/core/java/com/android/server/notification/NotificationManagerService.java10
-rw-r--r--services/core/java/com/android/server/pm/DeletePackageHelper.java2
-rw-r--r--services/core/java/com/android/server/pm/InstallPackageHelper.java3
-rw-r--r--services/core/java/com/android/server/pm/InstallRequest.java3
-rw-r--r--services/core/java/com/android/server/policy/PhoneWindowManager.java191
-rw-r--r--services/core/java/com/android/server/statusbar/StatusBarManagerService.java91
-rw-r--r--services/core/java/com/android/server/wm/ActivityRecord.java49
-rw-r--r--services/core/java/com/android/server/wm/AsyncRotationController.java42
-rw-r--r--services/core/java/com/android/server/wm/BackgroundActivityStartCallback.java13
-rw-r--r--services/core/java/com/android/server/wm/BackgroundActivityStartController.java8
-rw-r--r--services/core/java/com/android/server/wm/BackgroundLaunchProcessController.java42
-rw-r--r--services/core/java/com/android/server/wm/DisplayArea.java6
-rw-r--r--services/core/java/com/android/server/wm/OWNERS2
-rw-r--r--services/core/java/com/android/server/wm/Task.java117
-rw-r--r--services/core/java/com/android/server/wm/TaskDisplayArea.java30
-rw-r--r--services/core/java/com/android/server/wm/TaskFpsCallbackController.java5
-rw-r--r--services/core/java/com/android/server/wm/WindowAnimator.java34
-rw-r--r--services/core/java/com/android/server/wm/WindowContainer.java33
-rw-r--r--services/core/java/com/android/server/wm/WindowManagerService.java10
-rw-r--r--services/core/java/com/android/server/wm/WindowState.java12
-rw-r--r--services/credentials/java/com/android/server/credentials/MetricUtilities.java8
-rw-r--r--services/credentials/java/com/android/server/credentials/PrepareGetRequestSession.java8
-rw-r--r--services/credentials/java/com/android/server/credentials/metrics/InitialPhaseMetric.java16
-rw-r--r--services/credentials/java/com/android/server/credentials/metrics/RequestSessionMetric.java16
-rw-r--r--services/java/com/android/server/SystemServer.java31
-rw-r--r--services/permission/java/com/android/server/permission/access/permission/AppIdPermissionPolicy.kt7
-rw-r--r--services/tests/displayservicetests/Android.bp1
-rw-r--r--services/tests/displayservicetests/AndroidManifest.xml1
-rw-r--r--services/tests/displayservicetests/AndroidTest.xml1
-rw-r--r--services/tests/displayservicetests/src/com/android/server/display/DisplayEventDeliveryTest.java244
-rw-r--r--services/tests/displayservicetests/src/com/android/server/display/EventDeliveryTestBase.java259
-rw-r--r--services/tests/displayservicetests/src/com/android/server/display/TopologyUpdateDeliveryTest.java243
-rw-r--r--services/tests/displayservicetests/src/com/android/server/display/mode/AppRequestObserverTest.kt33
-rw-r--r--services/tests/displayservicetests/src/com/android/server/display/mode/ModeChangeObserverTest.kt187
-rw-r--r--services/tests/mockingservicestests/src/com/android/server/am/ProcessObserverTest.java6
-rw-r--r--services/tests/servicestests/Android.bp2
-rw-r--r--services/tests/servicestests/AndroidTest.xml1
-rw-r--r--services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java32
-rw-r--r--services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickControllerTest.java18
-rw-r--r--services/tests/servicestests/src/com/android/server/audio/AudioVolumeChangeHandlerTest.java142
-rw-r--r--services/tests/servicestests/src/com/android/server/location/contexthub/ContextHubEndpointTest.java46
-rw-r--r--services/tests/servicestests/src/com/android/server/statusbar/StatusBarManagerServiceTest.java74
-rw-r--r--services/tests/servicestests/src/com/android/server/systemconfig/SystemConfigTest.java4
-rw-r--r--services/tests/servicestests/test-apps/TopologyTestApp/Android.bp38
-rw-r--r--services/tests/servicestests/test-apps/TopologyTestApp/AndroidManifest.xml27
-rw-r--r--services/tests/servicestests/test-apps/TopologyTestApp/OWNERS3
-rw-r--r--services/tests/servicestests/test-apps/TopologyTestApp/src/com/android/servicestests/apps/topologytestapp/TopologyUpdateActivity.java87
-rw-r--r--services/tests/wmtests/src/com/android/server/policy/KeyGestureEventTests.java148
-rw-r--r--services/tests/wmtests/src/com/android/server/policy/PhoneWindowManagerTests.java86
-rw-r--r--services/tests/wmtests/src/com/android/server/policy/ShortcutKeyTestBase.java20
-rw-r--r--services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java7
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java15
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/AppWindowTokenAnimationTests.java142
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/BackgroundLaunchProcessControllerTests.java8
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/TaskDisplayAreaTests.java20
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/TaskTests.java85
-rw-r--r--telephony/java/android/telephony/CarrierConfigManager.java10
-rw-r--r--telephony/java/com/android/internal/telephony/ITelephony.aidl9
-rw-r--r--tests/Input/src/android/hardware/input/KeyGestureEventHandlerTest.kt115
-rw-r--r--tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt234
-rw-r--r--tests/Input/src/com/android/test/input/AnrTest.kt86
-rw-r--r--tools/aapt2/ResourcesInternal.proto5
-rw-r--r--tools/aapt2/cmd/Compile.cpp43
-rw-r--r--tools/aapt2/cmd/Convert.h14
-rw-r--r--tools/aapt2/cmd/Link.cpp22
-rw-r--r--tools/aapt2/cmd/Link.h9
-rw-r--r--tools/aapt2/cmd/Optimize.h14
-rw-r--r--tools/aapt2/format/binary/TableFlattener.cpp17
-rw-r--r--tools/aapt2/format/binary/TableFlattener_test.cpp122
-rw-r--r--tools/aapt2/format/proto/ProtoDeserialize.cpp1
-rw-r--r--tools/aapt2/format/proto/ProtoSerialize.cpp1
-rw-r--r--tools/aapt2/link/FlaggedResources_test.cpp6
-rw-r--r--tools/aapt2/link/FlaggedXmlVersioner.cpp48
-rw-r--r--tools/aapt2/link/FlaggedXmlVersioner_test.cpp4
-rw-r--r--tools/aapt2/link/TableMerger.cpp13
-rw-r--r--tools/aapt2/readme.md4
496 files changed, 13112 insertions, 4636 deletions
diff --git a/Android.bp b/Android.bp
index 127556f8e075..e7c20418bcef 100644
--- a/Android.bp
+++ b/Android.bp
@@ -427,6 +427,7 @@ java_defaults {
"framework-permission-aidl-java",
"spatializer-aidl-java",
"audiopolicy-aidl-java",
+ "volumegroupcallback-aidl-java",
"sounddose-aidl-java",
"modules-utils-expresslog",
"perfetto_trace_javastream_protos_jarjar",
diff --git a/boot/boot-image-profile.txt b/boot/boot-image-profile.txt
index d7c409fd54b4..4c3e5dce7a95 100644
--- a/boot/boot-image-profile.txt
+++ b/boot/boot-image-profile.txt
@@ -28829,7 +28829,6 @@ Landroid/media/audiopolicy/AudioProductStrategy$AudioAttributesGroup;
Landroid/media/audiopolicy/AudioProductStrategy;
Landroid/media/audiopolicy/AudioVolumeGroup$1;
Landroid/media/audiopolicy/AudioVolumeGroup;
-Landroid/media/audiopolicy/AudioVolumeGroupChangeHandler;
Landroid/media/audiopolicy/IAudioPolicyCallback$Stub$Proxy;
Landroid/media/audiopolicy/IAudioPolicyCallback$Stub;
Landroid/media/audiopolicy/IAudioPolicyCallback;
diff --git a/boot/preloaded-classes b/boot/preloaded-classes
index 7f4b3244c164..048687781774 100644
--- a/boot/preloaded-classes
+++ b/boot/preloaded-classes
@@ -5509,7 +5509,6 @@ android.media.audiopolicy.AudioProductStrategy$AudioAttributesGroup
android.media.audiopolicy.AudioProductStrategy
android.media.audiopolicy.AudioVolumeGroup$1
android.media.audiopolicy.AudioVolumeGroup
-android.media.audiopolicy.AudioVolumeGroupChangeHandler
android.media.audiopolicy.IAudioPolicyCallback$Stub$Proxy
android.media.audiopolicy.IAudioPolicyCallback$Stub
android.media.audiopolicy.IAudioPolicyCallback
diff --git a/cmds/incidentd/src/IncidentService.cpp b/cmds/incidentd/src/IncidentService.cpp
index 5ebf3e2c3047..de35ffc3fdb9 100644
--- a/cmds/incidentd/src/IncidentService.cpp
+++ b/cmds/incidentd/src/IncidentService.cpp
@@ -52,7 +52,11 @@ enum {
#define SKIPPED_DUMPSTATE_SECTIONS { \
1100, 1101, 1102, 1103, 1104, 1105, 1106, 1107, 1108, /* Logs */ \
1200, 1201, 1202, /* Native, hal, java traces */ \
- 3018, /* dumpsys meminfo*/ }
+ /* dumpsys sections except for odpm data (3054- 3056) which are still needed */ \
+ 3000, 3001, 3002, 3003, 3004, 3005, 3006, 3007, 3008, 3009, 3010, 3011, 3012, 3013, \
+ 3014, 3015, 3016, 3017, 3018, 3019, 3020, 3021, 3022, 3023, 3024, 3027, 3028, 3029, \
+ 3030, 3031, 3032, 3033, 3034, 3035, 3036, 3037, 3038, 3039, 3040, 3041, 3042, 3043, \
+ 3044, 3045, 3046, 3047, 3048, 3049, 3050, 3051, 3052, 3053, 4000, 4001,}
namespace android {
namespace os {
diff --git a/config/preloaded-classes b/config/preloaded-classes
index 707acb00b102..3f0e00be75a7 100644
--- a/config/preloaded-classes
+++ b/config/preloaded-classes
@@ -5514,7 +5514,6 @@ android.media.audiopolicy.AudioProductStrategy$AudioAttributesGroup
android.media.audiopolicy.AudioProductStrategy
android.media.audiopolicy.AudioVolumeGroup$1
android.media.audiopolicy.AudioVolumeGroup
-android.media.audiopolicy.AudioVolumeGroupChangeHandler
android.media.audiopolicy.IAudioPolicyCallback$Stub$Proxy
android.media.audiopolicy.IAudioPolicyCallback$Stub
android.media.audiopolicy.IAudioPolicyCallback
diff --git a/core/api/module-lib-current.txt b/core/api/module-lib-current.txt
index 526a213a6003..98570172e43c 100644
--- a/core/api/module-lib-current.txt
+++ b/core/api/module-lib-current.txt
@@ -304,6 +304,8 @@ package android.net {
field public static final int TYPE_VPN_LEGACY = 3; // 0x3
field public static final int TYPE_VPN_NONE = -1; // 0xffffffff
field public static final int TYPE_VPN_OEM = 4; // 0x4
+ field @FlaggedApi("android.net.platform.flags.vpn_type_oem_service_and_legacy") public static final int TYPE_VPN_OEM_LEGACY = 6; // 0x6
+ field @FlaggedApi("android.net.platform.flags.vpn_type_oem_service_and_legacy") public static final int TYPE_VPN_OEM_SERVICE = 5; // 0x5
field public static final int TYPE_VPN_PLATFORM = 2; // 0x2
field public static final int TYPE_VPN_SERVICE = 1; // 0x1
}
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index 2ce3609d77e7..95b9b49dae3d 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -2940,6 +2940,13 @@ package android.app.smartspace.uitemplatedata {
package android.app.supervision {
+ @FlaggedApi("android.app.supervision.flags.enable_supervision_app_service") public class SupervisionAppService extends android.app.Service {
+ ctor public SupervisionAppService();
+ method @Nullable public final android.os.IBinder onBind(@Nullable android.content.Intent);
+ method @FlaggedApi("android.app.supervision.flags.enable_supervision_app_service") public void onDisabled();
+ method @FlaggedApi("android.app.supervision.flags.enable_supervision_app_service") public void onEnabled();
+ }
+
@FlaggedApi("android.app.supervision.flags.supervision_manager_apis") public class SupervisionManager {
method @FlaggedApi("android.app.supervision.flags.supervision_manager_apis") @Nullable @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.QUERY_USERS}) public android.content.Intent createConfirmSupervisionCredentialsIntent();
method @FlaggedApi("android.app.supervision.flags.supervision_manager_apis") @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.QUERY_USERS}) public boolean isSupervisionEnabled();
@@ -7435,7 +7442,7 @@ package android.media {
method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void muteAwaitConnection(@NonNull int[], @NonNull android.media.AudioDeviceAttributes, long, @NonNull java.util.concurrent.TimeUnit) throws java.lang.IllegalStateException;
method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public int registerAudioPolicy(@NonNull android.media.audiopolicy.AudioPolicy);
method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void registerMuteAwaitConnectionCallback(@NonNull java.util.concurrent.Executor, @NonNull android.media.AudioManager.MuteAwaitConnectionCallback);
- method public void registerVolumeGroupCallback(@NonNull java.util.concurrent.Executor, @NonNull android.media.AudioManager.VolumeGroupCallback);
+ method @FlaggedApi("android.media.audio.register_volume_callback_api_hardening") @RequiresPermission("Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED") public void registerVolumeGroupCallback(@NonNull java.util.concurrent.Executor, @NonNull android.media.AudioManager.VolumeGroupCallback);
method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void removeAssistantServicesUids(@NonNull int[]);
method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public boolean removeDeviceAsNonDefaultForStrategy(@NonNull android.media.audiopolicy.AudioProductStrategy, @NonNull android.media.AudioDeviceAttributes);
method @RequiresPermission(anyOf={android.Manifest.permission.MODIFY_AUDIO_ROUTING, "android.permission.QUERY_AUDIO_STATE"}) public void removeOnDevicesForAttributesChangedListener(@NonNull android.media.AudioManager.OnDevicesForAttributesChangedListener);
@@ -7466,7 +7473,7 @@ package android.media {
method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void unregisterAudioPolicy(@NonNull android.media.audiopolicy.AudioPolicy);
method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void unregisterAudioPolicyAsync(@NonNull android.media.audiopolicy.AudioPolicy);
method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void unregisterMuteAwaitConnectionCallback(@NonNull android.media.AudioManager.MuteAwaitConnectionCallback);
- method public void unregisterVolumeGroupCallback(@NonNull android.media.AudioManager.VolumeGroupCallback);
+ method @FlaggedApi("android.media.audio.register_volume_callback_api_hardening") @RequiresPermission("Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED") public void unregisterVolumeGroupCallback(@NonNull android.media.AudioManager.VolumeGroupCallback);
field public static final String ACTION_VOLUME_CHANGED = "android.media.VOLUME_CHANGED_ACTION";
field public static final int AUDIOFOCUS_FLAG_DELAY_OK = 1; // 0x1
field public static final int AUDIOFOCUS_FLAG_LOCK = 4; // 0x4
diff --git a/core/java/android/app/AppCompatTaskInfo.java b/core/java/android/app/AppCompatTaskInfo.java
index 3fd9d8b26611..85621c9c3fab 100644
--- a/core/java/android/app/AppCompatTaskInfo.java
+++ b/core/java/android/app/AppCompatTaskInfo.java
@@ -136,7 +136,7 @@ public class AppCompatTaskInfo implements Parcelable {
private static final int FLAGS_ORGANIZER_INTERESTED = FLAG_IS_FROM_LETTERBOX_DOUBLE_TAP
| FLAG_ELIGIBLE_FOR_USER_ASPECT_RATIO_BUTTON | FLAG_FULLSCREEN_OVERRIDE_SYSTEM
| FLAG_FULLSCREEN_OVERRIDE_USER | FLAG_HAS_MIN_ASPECT_RATIO_OVERRIDE
- | FLAG_OPT_OUT_EDGE_TO_EDGE;
+ | FLAG_OPT_OUT_EDGE_TO_EDGE | FLAG_ENABLE_RESTART_MENU_FOR_DISPLAY_MOVE;
@TopActivityFlag
private static final int FLAGS_COMPAT_UI_INTERESTED = FLAGS_ORGANIZER_INTERESTED
@@ -179,7 +179,8 @@ public class AppCompatTaskInfo implements Parcelable {
*/
public boolean hasCompatUI() {
return isTopActivityInSizeCompat() || eligibleForLetterboxEducation()
- || isLetterboxDoubleTapEnabled() || eligibleForUserAspectRatioButton();
+ || isLetterboxDoubleTapEnabled() || eligibleForUserAspectRatioButton()
+ || isRestartMenuEnabledForDisplayMove();
}
/**
diff --git a/core/java/android/app/LocaleConfig.java b/core/java/android/app/LocaleConfig.java
index f56bf4d434e7..cbfd7fccb7c6 100644
--- a/core/java/android/app/LocaleConfig.java
+++ b/core/java/android/app/LocaleConfig.java
@@ -144,15 +144,12 @@ public class LocaleConfig implements Parcelable {
}
}
Resources res = context.getResources();
- //Get the resource id
int resId = context.getApplicationInfo().getLocaleConfigRes();
if (resId == 0) {
mStatus = STATUS_NOT_SPECIFIED;
return;
}
- try {
- //Get the parser to read XML data
- XmlResourceParser parser = res.getXml(resId);
+ try (XmlResourceParser parser = res.getXml(resId)) {
parseLocaleConfig(parser, res);
} catch (Resources.NotFoundException e) {
Slog.w(TAG, "The resource file pointed to by the given resource ID isn't found.");
@@ -208,22 +205,22 @@ public class LocaleConfig implements Parcelable {
String defaultLocale = null;
if (android.content.res.Flags.defaultLocale()) {
// Read the defaultLocale attribute of the LocaleConfig element
- TypedArray att = res.obtainAttributes(
- attrs, com.android.internal.R.styleable.LocaleConfig);
- defaultLocale = att.getString(
- R.styleable.LocaleConfig_defaultLocale);
- att.recycle();
+ try (TypedArray att = res.obtainAttributes(
+ attrs, com.android.internal.R.styleable.LocaleConfig)) {
+ defaultLocale = att.getString(
+ R.styleable.LocaleConfig_defaultLocale);
+ }
}
Set<String> localeNames = new HashSet<>();
while (XmlUtils.nextElementWithin(parser, outerDepth)) {
if (TAG_LOCALE.equals(parser.getName())) {
- final TypedArray attributes = res.obtainAttributes(
- attrs, com.android.internal.R.styleable.LocaleConfig_Locale);
- String nameAttr = attributes.getString(
- com.android.internal.R.styleable.LocaleConfig_Locale_name);
- localeNames.add(nameAttr);
- attributes.recycle();
+ try (TypedArray attributes = res.obtainAttributes(
+ attrs, com.android.internal.R.styleable.LocaleConfig_Locale)) {
+ String nameAttr = attributes.getString(
+ com.android.internal.R.styleable.LocaleConfig_Locale_name);
+ localeNames.add(nameAttr);
+ }
} else {
XmlUtils.skipCurrentTag(parser);
}
diff --git a/core/java/android/app/StatusBarManager.java b/core/java/android/app/StatusBarManager.java
index 01868cc601fe..927d46999284 100644
--- a/core/java/android/app/StatusBarManager.java
+++ b/core/java/android/app/StatusBarManager.java
@@ -58,8 +58,10 @@ import com.android.internal.statusbar.IStatusBarService;
import com.android.internal.statusbar.IUndoMediaTransferCallback;
import com.android.internal.statusbar.NotificationVisibility;
+import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
@@ -119,6 +121,7 @@ public class StatusBarManager {
| DISABLE_SEARCH | DISABLE_ONGOING_CALL_CHIP;
/** @hide */
+ @Target(ElementType.TYPE_USE)
@IntDef(flag = true, prefix = {"DISABLE_"}, value = {
DISABLE_NONE,
DISABLE_EXPAND,
@@ -161,6 +164,7 @@ public class StatusBarManager {
| DISABLE2_NOTIFICATION_SHADE | DISABLE2_GLOBAL_ACTIONS | DISABLE2_ROTATE_SUGGESTIONS;
/** @hide */
+ @Target(ElementType.TYPE_USE)
@IntDef(flag = true, prefix = { "DISABLE2_" }, value = {
DISABLE2_NONE,
DISABLE2_MASK,
diff --git a/core/java/android/app/activity_manager.aconfig b/core/java/android/app/activity_manager.aconfig
index 29c84ee00b44..a26bfa4f586c 100644
--- a/core/java/android/app/activity_manager.aconfig
+++ b/core/java/android/app/activity_manager.aconfig
@@ -190,3 +190,10 @@ flag {
purpose: PURPOSE_BUGFIX
}
}
+
+flag {
+ name: "enable_process_observer_broadcast_on_process_started"
+ namespace: "system_performance"
+ description: "Enable ProcessObserver's onProcessStarted callbacks."
+ bug: "323959187"
+}
diff --git a/core/java/android/app/supervision/SupervisionAppService.java b/core/java/android/app/supervision/SupervisionAppService.java
index 4530be5c270a..93eb96204444 100644
--- a/core/java/android/app/supervision/SupervisionAppService.java
+++ b/core/java/android/app/supervision/SupervisionAppService.java
@@ -16,7 +16,11 @@
package android.app.supervision;
+import android.annotation.FlaggedApi;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
import android.app.Service;
+import android.app.supervision.flags.Flags;
import android.content.Intent;
import android.os.IBinder;
@@ -26,31 +30,43 @@ import android.os.IBinder;
*
* @hide
*/
+@SystemApi
+@FlaggedApi(Flags.FLAG_ENABLE_SUPERVISION_APP_SERVICE)
public class SupervisionAppService extends Service {
- private final ISupervisionAppService mBinder = new ISupervisionAppService.Stub() {
- @Override
- public void onEnabled() {
- SupervisionAppService.this.onEnabled();
- }
+ private final ISupervisionAppService mBinder =
+ new ISupervisionAppService.Stub() {
+ @Override
+ public void onEnabled() {
+ SupervisionAppService.this.onEnabled();
+ }
- @Override
- public void onDisabled() {
- SupervisionAppService.this.onDisabled();
- }
- };
+ @Override
+ public void onDisabled() {
+ SupervisionAppService.this.onDisabled();
+ }
+ };
+ @Nullable
@Override
- public final IBinder onBind(Intent intent) {
+ public final IBinder onBind(@Nullable Intent intent) {
return mBinder.asBinder();
}
/**
* Called when supervision is enabled.
+ *
+ * @hide
*/
+ @SystemApi
+ @FlaggedApi(Flags.FLAG_ENABLE_SUPERVISION_APP_SERVICE)
public void onEnabled() {}
/**
* Called when supervision is disabled.
+ *
+ * @hide
*/
+ @SystemApi
+ @FlaggedApi(Flags.FLAG_ENABLE_SUPERVISION_APP_SERVICE)
public void onDisabled() {}
}
diff --git a/core/java/android/companion/virtual/flags/flags.aconfig b/core/java/android/companion/virtual/flags/flags.aconfig
index 615a6dffdf99..161f05bc5139 100644
--- a/core/java/android/companion/virtual/flags/flags.aconfig
+++ b/core/java/android/companion/virtual/flags/flags.aconfig
@@ -166,3 +166,10 @@ flag {
bug: "393517834"
is_exported: true
}
+
+flag {
+ name: "external_virtual_cameras"
+ namespace: "virtual_devices"
+ description: "Allow external virtual cameras visible only in the Context of the virtual device"
+ bug: "375609768"
+}
diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java
index bb62ac321202..a253613e060c 100644
--- a/core/java/android/content/Intent.java
+++ b/core/java/android/content/Intent.java
@@ -680,6 +680,7 @@ public class Intent implements Parcelable, Cloneable {
private static final String ATTR_COMPONENT = "component";
private static final String ATTR_DATA = "data";
private static final String ATTR_FLAGS = "flags";
+ private static final String ATTR_PACKAGE = "package";
// ---------------------------------------------------------------------
// ---------------------------------------------------------------------
@@ -12893,6 +12894,9 @@ public class Intent implements Parcelable, Cloneable {
if (mComponent != null) {
out.attribute(null, ATTR_COMPONENT, mComponent.flattenToShortString());
}
+ if (android.content.flags.Flags.intentSaveToXmlPackage() && mPackage != null) {
+ out.attribute(null, ATTR_PACKAGE, mPackage);
+ }
out.attribute(null, ATTR_FLAGS, Integer.toHexString(getFlags()));
if (mCategories != null) {
@@ -12926,6 +12930,9 @@ public class Intent implements Parcelable, Cloneable {
intent.setComponent(ComponentName.unflattenFromString(attrValue));
} else if (ATTR_FLAGS.equals(attrName)) {
intent.setFlags(Integer.parseInt(attrValue, 16));
+ } else if (android.content.flags.Flags.intentSaveToXmlPackage()
+ && ATTR_PACKAGE.equals(attrName)) {
+ intent.setPackage(attrValue);
} else {
Log.e(TAG, "restoreFromXml: unknown attribute=" + attrName);
}
diff --git a/core/java/android/content/flags/flags.aconfig b/core/java/android/content/flags/flags.aconfig
index aac04b3a9d15..148532b62c36 100644
--- a/core/java/android/content/flags/flags.aconfig
+++ b/core/java/android/content/flags/flags.aconfig
@@ -7,4 +7,14 @@ flag {
namespace: "machine_learning"
description: "This flag enables the newly added flag for binding package-private isolated processes."
bug: "312706530"
-} \ No newline at end of file
+}
+
+flag {
+ namespace: "system_performance"
+ name: "intent_save_to_xml_package"
+ description: "Add package to saveToXml so save then restore passes filterEquals."
+ bug: "369856202"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
diff --git a/core/java/android/content/pm/PackageParser.java b/core/java/android/content/pm/PackageParser.java
index b1ea6e9b68eb..219b20428d7a 100644
--- a/core/java/android/content/pm/PackageParser.java
+++ b/core/java/android/content/pm/PackageParser.java
@@ -2628,15 +2628,6 @@ public class PackageParser {
return Build.VERSION_CODES.CUR_DEVELOPMENT;
}
- // STOPSHIP: hack for the pre-release SDK
- if (platformSdkCodenames.length == 0
- && Build.VERSION.KNOWN_CODENAMES.stream().max(String::compareTo).orElse("").equals(
- targetCode)) {
- Slog.w(TAG, "Package requires development platform " + targetCode
- + ", returning current version " + Build.VERSION.SDK_INT);
- return Build.VERSION.SDK_INT;
- }
-
// Otherwise, we're looking at an incompatible pre-release SDK.
if (platformSdkCodenames.length > 0) {
outError[0] = "Requires development platform " + targetCode
@@ -2708,15 +2699,6 @@ public class PackageParser {
return Build.VERSION_CODES.CUR_DEVELOPMENT;
}
- // STOPSHIP: hack for the pre-release SDK
- if (platformSdkCodenames.length == 0
- && Build.VERSION.KNOWN_CODENAMES.stream().max(String::compareTo).orElse("").equals(
- minCode)) {
- Slog.w(TAG, "Package requires min development platform " + minCode
- + ", returning current version " + Build.VERSION.SDK_INT);
- return Build.VERSION.SDK_INT;
- }
-
// Otherwise, we're looking at an incompatible pre-release SDK.
if (platformSdkCodenames.length > 0) {
outError[0] = "Requires development platform " + minCode
diff --git a/core/java/android/content/pm/RegisteredServicesCache.java b/core/java/android/content/pm/RegisteredServicesCache.java
index ded35b23608d..1ddab2c86ec2 100644
--- a/core/java/android/content/pm/RegisteredServicesCache.java
+++ b/core/java/android/content/pm/RegisteredServicesCache.java
@@ -104,14 +104,6 @@ public abstract class RegisteredServicesCache<V> {
private final Handler mBackgroundHandler;
- private final Runnable mClearServiceInfoCachesRunnable = new Runnable() {
- public void run() {
- synchronized (mUserIdToServiceInfoCaches) {
- mUserIdToServiceInfoCaches.clear();
- }
- }
- };
-
private static class UserServices<V> {
@GuardedBy("mServicesLock")
final Map<V, Integer> persistentServices = Maps.newHashMap();
@@ -565,9 +557,11 @@ public abstract class RegisteredServicesCache<V> {
if (Flags.optimizeParsingInRegisteredServicesCache()) {
synchronized (mUserIdToServiceInfoCaches) {
- if (mUserIdToServiceInfoCaches.numMaps() > 0) {
- mBackgroundHandler.removeCallbacks(mClearServiceInfoCachesRunnable);
- mBackgroundHandler.postDelayed(mClearServiceInfoCachesRunnable,
+ if (mUserIdToServiceInfoCaches.numElementsForKey(userId) > 0) {
+ final Integer token = Integer.valueOf(userId);
+ mBackgroundHandler.removeCallbacksAndEqualMessages(token);
+ mBackgroundHandler.postDelayed(
+ new ClearServiceInfoCachesTimeoutRunnable(userId), token,
SERVICE_INFO_CACHES_TIMEOUT_MILLIS);
}
}
@@ -953,4 +947,19 @@ public abstract class RegisteredServicesCache<V> {
return BackgroundThread.getHandler();
}
}
+
+ class ClearServiceInfoCachesTimeoutRunnable implements Runnable {
+ final int mUserId;
+
+ ClearServiceInfoCachesTimeoutRunnable(int userId) {
+ this.mUserId = userId;
+ }
+
+ @Override
+ public void run() {
+ synchronized (mUserIdToServiceInfoCaches) {
+ mUserIdToServiceInfoCaches.delete(mUserId);
+ }
+ }
+ }
}
diff --git a/core/java/android/content/pm/parsing/FrameworkParsingPackageUtils.java b/core/java/android/content/pm/parsing/FrameworkParsingPackageUtils.java
index c7403c0ea98c..153dd9a93490 100644
--- a/core/java/android/content/pm/parsing/FrameworkParsingPackageUtils.java
+++ b/core/java/android/content/pm/parsing/FrameworkParsingPackageUtils.java
@@ -316,15 +316,6 @@ public class FrameworkParsingPackageUtils {
return input.success(Build.VERSION_CODES.CUR_DEVELOPMENT);
}
- // STOPSHIP: hack for the pre-release SDK
- if (platformSdkCodenames.length == 0
- && Build.VERSION.KNOWN_CODENAMES.stream().max(String::compareTo).orElse("").equals(
- minCode)) {
- Slog.w(TAG, "Parsed package requires min development platform " + minCode
- + ", returning current version " + Build.VERSION.SDK_INT);
- return input.success(Build.VERSION.SDK_INT);
- }
-
// Otherwise, we're looking at an incompatible pre-release SDK.
if (platformSdkCodenames.length > 0) {
return input.error(PackageManager.INSTALL_FAILED_OLDER_SDK,
@@ -377,27 +368,19 @@ public class FrameworkParsingPackageUtils {
return input.success(targetVers);
}
- // If it's a pre-release SDK and the codename matches this platform, it
- // definitely targets this SDK.
- if (matchTargetCode(platformSdkCodenames, targetCode)) {
- return input.success(Build.VERSION_CODES.CUR_DEVELOPMENT);
- }
-
- // STOPSHIP: hack for the pre-release SDK
- if (platformSdkCodenames.length == 0
- && Build.VERSION.KNOWN_CODENAMES.stream().max(String::compareTo).orElse("").equals(
- targetCode)) {
- Slog.w(TAG, "Parsed package requires development platform " + targetCode
- + ", returning current version " + Build.VERSION.SDK_INT);
- return input.success(Build.VERSION.SDK_INT);
- }
-
try {
if (allowUnknownCodenames && UnboundedSdkLevel.isAtMost(targetCode)) {
return input.success(Build.VERSION_CODES.CUR_DEVELOPMENT);
}
} catch (IllegalArgumentException e) {
- return input.error(PackageManager.INSTALL_FAILED_OLDER_SDK, "Bad package SDK");
+ // isAtMost() throws it when encountering an older SDK codename
+ return input.error(PackageManager.INSTALL_FAILED_OLDER_SDK, e.getMessage());
+ }
+
+ // If it's a pre-release SDK and the codename matches this platform, it
+ // definitely targets this SDK.
+ if (matchTargetCode(platformSdkCodenames, targetCode)) {
+ return input.success(Build.VERSION_CODES.CUR_DEVELOPMENT);
}
// Otherwise, we're looking at an incompatible pre-release SDK.
diff --git a/core/java/android/hardware/biometrics/flags.aconfig b/core/java/android/hardware/biometrics/flags.aconfig
index 4815f3e4f524..061e6f44c9c7 100644
--- a/core/java/android/hardware/biometrics/flags.aconfig
+++ b/core/java/android/hardware/biometrics/flags.aconfig
@@ -64,3 +64,11 @@ flag {
purpose: PURPOSE_BUGFIX
}
}
+
+flag {
+ name: "move_fm_api_to_bm"
+ is_exported: true
+ namespace: "biometrics_framework"
+ description: "Feature flag for moving some FingerprintManager APIs to BiometricManager to unblock FM removal."
+ bug: "323957939"
+}
diff --git a/core/java/android/hardware/input/IInputManager.aidl b/core/java/android/hardware/input/IInputManager.aidl
index 1c2150f3c09f..5537135f7bfa 100644
--- a/core/java/android/hardware/input/IInputManager.aidl
+++ b/core/java/android/hardware/input/IInputManager.aidl
@@ -273,7 +273,7 @@ interface IInputManager {
@PermissionManuallyEnforced
@JavaPassthrough(annotation="@android.annotation.RequiresPermission(value = "
+ "android.Manifest.permission.MANAGE_KEY_GESTURES)")
- void registerKeyGestureHandler(IKeyGestureHandler handler);
+ void registerKeyGestureHandler(in int[] keyGesturesToHandle, IKeyGestureHandler handler);
@PermissionManuallyEnforced
@JavaPassthrough(annotation="@android.annotation.RequiresPermission(value = "
diff --git a/core/java/android/hardware/input/IKeyGestureHandler.aidl b/core/java/android/hardware/input/IKeyGestureHandler.aidl
index 4da991ee85b1..08b015892710 100644
--- a/core/java/android/hardware/input/IKeyGestureHandler.aidl
+++ b/core/java/android/hardware/input/IKeyGestureHandler.aidl
@@ -20,12 +20,12 @@ import android.hardware.input.AidlKeyGestureEvent;
import android.os.IBinder;
/** @hide */
-interface IKeyGestureHandler {
+oneway interface IKeyGestureHandler {
/**
- * Called when a key gesture starts, ends, or is cancelled. If a handler returns {@code true},
- * it means they intend to handle the full gesture and should handle all the events pertaining
- * to that gesture.
+ * Called when a key gesture starts, ends, or is cancelled. It is only sent to the handler that
+ * registered the callback for that particular gesture type.
+ * {@see IInputManager#registerKeyGestureHandler(int[], IKeyGestureHandler)}
*/
- boolean handleKeyGesture(in AidlKeyGestureEvent event, in IBinder focusedToken);
+ void handleKeyGesture(in AidlKeyGestureEvent event, in IBinder focusedToken);
}
diff --git a/core/java/android/hardware/input/InputManager.java b/core/java/android/hardware/input/InputManager.java
index d6419afb2a5a..a66ac76d7597 100644
--- a/core/java/android/hardware/input/InputManager.java
+++ b/core/java/android/hardware/input/InputManager.java
@@ -1446,16 +1446,18 @@ public final class InputManager {
/**
* Registers a key gesture event handler for {@link KeyGestureEvent} handling.
*
+ * @param keyGesturesToHandle list of KeyGestureTypes to listen to
* @param handler the {@link KeyGestureEventHandler}
- * @throws IllegalArgumentException if {@code handler} has already been registered previously.
+ * @throws IllegalArgumentException if {@code handler} has already been registered previously
+ * or key gestures provided are already registered by some other gesture handler.
* @throws NullPointerException if {@code handler} or {@code executor} is null.
* @hide
* @see #unregisterKeyGestureEventHandler(KeyGestureEventHandler)
*/
@RequiresPermission(Manifest.permission.MANAGE_KEY_GESTURES)
- public void registerKeyGestureEventHandler(@NonNull KeyGestureEventHandler handler)
- throws IllegalArgumentException {
- mGlobal.registerKeyGestureEventHandler(handler);
+ public void registerKeyGestureEventHandler(List<Integer> keyGesturesToHandle,
+ @NonNull KeyGestureEventHandler handler) throws IllegalArgumentException {
+ mGlobal.registerKeyGestureEventHandler(keyGesturesToHandle, handler);
}
/**
@@ -1463,7 +1465,7 @@ public final class InputManager {
*
* @param handler the {@link KeyGestureEventHandler}
* @hide
- * @see #registerKeyGestureEventHandler(KeyGestureEventHandler)
+ * @see #registerKeyGestureEventHandler(List, KeyGestureEventHandler)
*/
@RequiresPermission(Manifest.permission.MANAGE_KEY_GESTURES)
public void unregisterKeyGestureEventHandler(@NonNull KeyGestureEventHandler handler) {
@@ -1741,7 +1743,7 @@ public final class InputManager {
* {@see KeyGestureEventListener} which is to listen to successfully handled key gestures, this
* interface allows system components to register handler for handling key gestures.
*
- * @see #registerKeyGestureEventHandler(KeyGestureEventHandler)
+ * @see #registerKeyGestureEventHandler(List, KeyGestureEventHandler)
* @see #unregisterKeyGestureEventHandler(KeyGestureEventHandler)
*
* <p> NOTE: All callbacks will occur on system main and input threads, so the caller needs
@@ -1750,14 +1752,11 @@ public final class InputManager {
*/
public interface KeyGestureEventHandler {
/**
- * Called when a key gesture event starts, is completed, or is cancelled. If a handler
- * returns {@code true}, it implies that the handler intends to handle the key gesture and
- * only this handler will receive the future events for this key gesture.
+ * Called when a key gesture event starts, is completed, or is cancelled.
*
* @param event the gesture event
*/
- boolean handleKeyGestureEvent(@NonNull KeyGestureEvent event,
- @Nullable IBinder focusedToken);
+ void handleKeyGestureEvent(@NonNull KeyGestureEvent event, @Nullable IBinder focusedToken);
}
/** @hide */
diff --git a/core/java/android/hardware/input/InputManagerGlobal.java b/core/java/android/hardware/input/InputManagerGlobal.java
index c4b4831ba76e..754182ce3d11 100644
--- a/core/java/android/hardware/input/InputManagerGlobal.java
+++ b/core/java/android/hardware/input/InputManagerGlobal.java
@@ -25,8 +25,8 @@ import android.hardware.BatteryState;
import android.hardware.SensorManager;
import android.hardware.input.InputManager.InputDeviceBatteryListener;
import android.hardware.input.InputManager.InputDeviceListener;
-import android.hardware.input.InputManager.KeyGestureEventHandler;
import android.hardware.input.InputManager.KeyEventActivityListener;
+import android.hardware.input.InputManager.KeyGestureEventHandler;
import android.hardware.input.InputManager.KeyGestureEventListener;
import android.hardware.input.InputManager.KeyboardBacklightListener;
import android.hardware.input.InputManager.OnTabletModeChangedListener;
@@ -49,6 +49,7 @@ import android.os.ServiceManager;
import android.os.VibrationEffect;
import android.os.Vibrator;
import android.os.VibratorManager;
+import android.util.IntArray;
import android.util.Log;
import android.util.SparseArray;
import android.view.Display;
@@ -132,13 +133,13 @@ public final class InputManagerGlobal {
@Nullable
private IKeyEventActivityListener mKeyEventActivityListener;
- private final Object mKeyGestureEventHandlerLock = new Object();
- @GuardedBy("mKeyGestureEventHandlerLock")
- @Nullable
- private ArrayList<KeyGestureEventHandler> mKeyGestureEventHandlers;
- @GuardedBy("mKeyGestureEventHandlerLock")
+ @GuardedBy("mKeyGesturesToHandlerMap")
@Nullable
private IKeyGestureHandler mKeyGestureHandler;
+ @GuardedBy("mKeyGesturesToHandlerMap")
+ private final SparseArray<KeyGestureEventHandler> mKeyGesturesToHandlerMap =
+ new SparseArray<>();
+
// InputDeviceSensorManager gets notified synchronously from the binder thread when input
// devices change, so it must be synchronized with the input device listeners.
@@ -1177,50 +1178,69 @@ public final class InputManagerGlobal {
private class LocalKeyGestureHandler extends IKeyGestureHandler.Stub {
@Override
- public boolean handleKeyGesture(@NonNull AidlKeyGestureEvent ev, IBinder focusedToken) {
- synchronized (mKeyGestureEventHandlerLock) {
- if (mKeyGestureEventHandlers == null) {
- return false;
- }
- final int numHandlers = mKeyGestureEventHandlers.size();
- final KeyGestureEvent event = new KeyGestureEvent(ev);
- for (int i = 0; i < numHandlers; i++) {
- KeyGestureEventHandler handler = mKeyGestureEventHandlers.get(i);
- if (handler.handleKeyGestureEvent(event, focusedToken)) {
- return true;
- }
+ public void handleKeyGesture(@NonNull AidlKeyGestureEvent ev, IBinder focusedToken) {
+ synchronized (mKeyGesturesToHandlerMap) {
+ KeyGestureEventHandler handler = mKeyGesturesToHandlerMap.get(ev.gestureType);
+ if (handler == null) {
+ Log.w(TAG, "Key gesture event " + ev.gestureType
+ + " occurred without a registered handler!");
+ return;
}
+ handler.handleKeyGestureEvent(new KeyGestureEvent(ev), focusedToken);
}
- return false;
}
}
/**
- * @see InputManager#registerKeyGestureEventHandler(KeyGestureEventHandler)
+ * @see InputManager#registerKeyGestureEventHandler(List, KeyGestureEventHandler)
*/
@RequiresPermission(Manifest.permission.MANAGE_KEY_GESTURES)
- void registerKeyGestureEventHandler(@NonNull KeyGestureEventHandler handler)
- throws IllegalArgumentException {
+ void registerKeyGestureEventHandler(List<Integer> keyGesturesToHandle,
+ @NonNull KeyGestureEventHandler handler) throws IllegalArgumentException {
+ Objects.requireNonNull(keyGesturesToHandle, "List of gestures should not be null");
Objects.requireNonNull(handler, "handler should not be null");
- synchronized (mKeyGestureEventHandlerLock) {
- if (mKeyGestureHandler == null) {
- mKeyGestureEventHandlers = new ArrayList<>();
- mKeyGestureHandler = new LocalKeyGestureHandler();
+ if (keyGesturesToHandle.isEmpty()) {
+ throw new IllegalArgumentException("No key gestures provided!");
+ }
- try {
- mIm.registerKeyGestureHandler(mKeyGestureHandler);
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ synchronized (mKeyGesturesToHandlerMap) {
+ IntArray newKeyGestures = new IntArray(
+ keyGesturesToHandle.size() + mKeyGesturesToHandlerMap.size());
+
+ // Check if the handler already exists
+ for (int i = 0; i < mKeyGesturesToHandlerMap.size(); i++) {
+ KeyGestureEventHandler h = mKeyGesturesToHandlerMap.valueAt(i);
+ if (h == handler) {
+ throw new IllegalArgumentException("Handler has already been registered!");
}
+ newKeyGestures.add(mKeyGesturesToHandlerMap.keyAt(i));
}
- final int numHandlers = mKeyGestureEventHandlers.size();
- for (int i = 0; i < numHandlers; i++) {
- if (mKeyGestureEventHandlers.get(i) == handler) {
- throw new IllegalArgumentException("Handler has already been registered!");
+
+ // Check if any of the key gestures are already handled by existing handlers
+ for (int gesture : keyGesturesToHandle) {
+ if (mKeyGesturesToHandlerMap.contains(gesture)) {
+ throw new IllegalArgumentException("Key gesture " + gesture
+ + " is already registered by another handler!");
+ }
+ newKeyGestures.add(gesture);
+ }
+
+ try {
+ // If handler was already registered for this process, we need to unregister and
+ // re-register it for the new set of gestures
+ if (mKeyGestureHandler != null) {
+ mIm.unregisterKeyGestureHandler(mKeyGestureHandler);
+ } else {
+ mKeyGestureHandler = new LocalKeyGestureHandler();
+ }
+ mIm.registerKeyGestureHandler(newKeyGestures.toArray(), mKeyGestureHandler);
+ for (int gesture : keyGesturesToHandle) {
+ mKeyGesturesToHandlerMap.put(gesture, handler);
}
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
}
- mKeyGestureEventHandlers.add(handler);
}
}
@@ -1231,18 +1251,21 @@ public final class InputManagerGlobal {
void unregisterKeyGestureEventHandler(@NonNull KeyGestureEventHandler handler) {
Objects.requireNonNull(handler, "handler should not be null");
- synchronized (mKeyGestureEventHandlerLock) {
- if (mKeyGestureEventHandlers == null) {
+ synchronized (mKeyGesturesToHandlerMap) {
+ if (mKeyGestureHandler == null) {
return;
}
- mKeyGestureEventHandlers.removeIf(existingHandler -> existingHandler == handler);
- if (mKeyGestureEventHandlers.isEmpty()) {
+ for (int i = mKeyGesturesToHandlerMap.size() - 1; i >= 0; i--) {
+ if (mKeyGesturesToHandlerMap.valueAt(i) == handler) {
+ mKeyGesturesToHandlerMap.removeAt(i);
+ }
+ }
+ if (mKeyGesturesToHandlerMap.size() == 0) {
try {
mIm.unregisterKeyGestureHandler(mKeyGestureHandler);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
- mKeyGestureEventHandlers = null;
mKeyGestureHandler = null;
}
}
diff --git a/core/java/android/net/VpnManager.java b/core/java/android/net/VpnManager.java
index c50bc569de72..4ef293a90a80 100644
--- a/core/java/android/net/VpnManager.java
+++ b/core/java/android/net/VpnManager.java
@@ -20,6 +20,7 @@ import static android.annotation.SystemApi.Client.MODULE_LIBRARIES;
import static com.android.internal.util.Preconditions.checkNotNull;
+import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -32,6 +33,7 @@ import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
+import android.net.platform.flags.Flags;
import android.os.RemoteException;
import com.android.internal.net.LegacyVpnInfo;
@@ -85,13 +87,33 @@ public class VpnManager {
public static final int TYPE_VPN_LEGACY = 3;
/**
- * An VPN created by OEM code through other means than {@link VpnService} or {@link VpnManager}.
+ * A VPN created by OEM code through other means than {@link VpnService} or {@link VpnManager}.
* @hide
*/
@SystemApi(client = MODULE_LIBRARIES)
public static final int TYPE_VPN_OEM = 4;
/**
+ * A VPN created by OEM code using {@link VpnService}, and which OEM code desires to
+ * differentiate from other VPN types. The core networking stack will treat this VPN type
+ * similarly to {@link #TYPE_VPN_SERVICE}.
+ * @hide
+ */
+ @FlaggedApi(Flags.FLAG_VPN_TYPE_OEM_SERVICE_AND_LEGACY)
+ @SystemApi(client = MODULE_LIBRARIES)
+ public static final int TYPE_VPN_OEM_SERVICE = 5;
+
+ /**
+ * A VPN created by OEM code using the legacy VPN mechanisms, and which OEM code desires to
+ * differentiate from other VPN types. The core networking stack will treat this VPN type
+ * similarly to {@link #TYPE_VPN_LEGACY}.
+ * @hide
+ */
+ @FlaggedApi(Flags.FLAG_VPN_TYPE_OEM_SERVICE_AND_LEGACY)
+ @SystemApi(client = MODULE_LIBRARIES)
+ public static final int TYPE_VPN_OEM_LEGACY = 6;
+
+ /**
* Channel for VPN notifications.
* @hide
*/
@@ -308,7 +330,7 @@ public class VpnManager {
/** @hide */
@IntDef(value = {TYPE_VPN_NONE, TYPE_VPN_SERVICE, TYPE_VPN_PLATFORM, TYPE_VPN_LEGACY,
- TYPE_VPN_OEM})
+ TYPE_VPN_OEM, TYPE_VPN_OEM_SERVICE, TYPE_VPN_OEM_LEGACY})
@Retention(RetentionPolicy.SOURCE)
public @interface VpnType {}
diff --git a/core/java/android/net/flags.aconfig b/core/java/android/net/flags.aconfig
index 8d12b76e23ff..519729bc1c88 100644
--- a/core/java/android/net/flags.aconfig
+++ b/core/java/android/net/flags.aconfig
@@ -37,3 +37,11 @@ flag {
description: "Flag for MDNS quality, reliability and performance improvement in 25Q2"
bug: "373270045"
}
+
+flag {
+ name: "vpn_type_oem_service_and_legacy"
+ namespace: "android_core_networking"
+ is_exported: false
+ description: "Flags the TYPE_VPN_OEM_SERVICE and TYPE_VPN_OEM_LEGACY VpnManager API constants"
+ bug: "389829981"
+}
diff --git a/core/java/android/os/CombinedMessageQueue/MessageQueue.java b/core/java/android/os/CombinedMessageQueue/MessageQueue.java
index c3ec96d17437..c21959b16fbb 100644
--- a/core/java/android/os/CombinedMessageQueue/MessageQueue.java
+++ b/core/java/android/os/CombinedMessageQueue/MessageQueue.java
@@ -144,6 +144,12 @@ public final class MessageQueue {
return;
}
+ // Holdback study.
+ if (Flags.messageQueueForceLegacy()) {
+ sIsProcessAllowedToUseConcurrent = false;
+ return;
+ }
+
if (Flags.forceConcurrentMessageQueue()) {
// b/379472827: Robolectric tests use reflection to access MessageQueue.mMessages.
// This is a hack to allow Robolectric tests to use the legacy implementation.
diff --git a/core/java/android/os/flags.aconfig b/core/java/android/os/flags.aconfig
index 0150d171d51c..b52a454ea956 100644
--- a/core/java/android/os/flags.aconfig
+++ b/core/java/android/os/flags.aconfig
@@ -4,6 +4,15 @@ container: "system"
# keep-sorted start block=yes newline_separated=yes
flag {
+ # Holdback study for concurrent MessageQueue.
+ # Do not promote beyond trunkfood.
+ namespace: "system_performance"
+ name: "message_queue_force_legacy"
+ description: "Whether to holdback concurrent MessageQueue (force legacy)."
+ bug: "336880969"
+}
+
+flag {
name: "adpf_gpu_report_actual_work_duration"
is_exported: true
namespace: "game"
diff --git a/core/java/android/os/image/flags/trade_in_mode_flags.aconfig b/core/java/android/os/image/flags/trade_in_mode_flags.aconfig
index e2e56ef70d62..c1adfe50db9e 100644
--- a/core/java/android/os/image/flags/trade_in_mode_flags.aconfig
+++ b/core/java/android/os/image/flags/trade_in_mode_flags.aconfig
@@ -9,3 +9,12 @@ flag {
bug: "332683751"
is_fixed_read_only: true
}
+
+flag {
+ name: "trade_in_mode_2025q4"
+ is_exported: true
+ namespace: "phoenix"
+ description: "Enable Trade-in Mode 2025Q4 changes"
+ bug: "397154502"
+ is_fixed_read_only: true
+}
diff --git a/core/java/android/util/ArrayMap.java b/core/java/android/util/ArrayMap.java
index 7ee0ff15c5ad..c59907937d6a 100644
--- a/core/java/android/util/ArrayMap.java
+++ b/core/java/android/util/ArrayMap.java
@@ -129,7 +129,7 @@ public final class ArrayMap<K, V> implements Map<K, V> {
return ContainerHelpers.binarySearch(hashes, N, hash);
} catch (ArrayIndexOutOfBoundsException e) {
if (CONCURRENT_MODIFICATION_EXCEPTIONS) {
- throw new ConcurrentModificationException();
+ throw new ConcurrentModificationException(e);
} else {
throw e; // the cache is poisoned at this point, there's not much we can do
}
diff --git a/core/java/android/view/InsetsController.java b/core/java/android/view/InsetsController.java
index 7e9dfe6d972a..4c578fb93600 100644
--- a/core/java/android/view/InsetsController.java
+++ b/core/java/android/view/InsetsController.java
@@ -2039,8 +2039,8 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation
} else if (Flags.refactorInsetsController()) {
if ((typesToReport & ime()) != 0 && mImeSourceConsumer != null) {
InsetsSourceControl control = mImeSourceConsumer.getControl();
- if (control != null && control.getLeash() == null) {
- // If the IME was requested twice, and we didn't receive the controls
+ if (control == null || control.getLeash() == null) {
+ // If the IME was requested to show twice, and we didn't receive the controls
// yet, this request will not continue. It should be cancelled here, as
// it would time out otherwise.
ImeTracker.forLogging().onCancelled(statsToken,
diff --git a/core/java/android/view/WindowManagerGlobal.java b/core/java/android/view/WindowManagerGlobal.java
index 624216776f42..b97f28da7559 100644
--- a/core/java/android/view/WindowManagerGlobal.java
+++ b/core/java/android/view/WindowManagerGlobal.java
@@ -329,13 +329,17 @@ public final class WindowManagerGlobal {
/**
* Adds a listener that will be notified whenever {@link #getWindowViews()} changes. The
- * current value is provided immediately. If it was registered previously then this is ano op.
+ * current value is provided immediately using the provided {@link Executor}. If this
+ * {@link Consumer} was registered previously, then this is a no op.
*/
public void addWindowViewsListener(@NonNull Executor executor,
@NonNull Consumer<List<View>> consumer) {
synchronized (mLock) {
+ if (mWindowViewsListenerGroup.isConsumerPresent(consumer)) {
+ return;
+ }
mWindowViewsListenerGroup.addListener(executor, consumer);
- mWindowViewsListenerGroup.accept(getWindowViews());
+ executor.execute(() -> consumer.accept(getWindowViews()));
}
}
diff --git a/core/java/android/view/inspector/WindowInspector.java b/core/java/android/view/inspector/WindowInspector.java
index 3ebca3c9d9b6..f0cc01133e07 100644
--- a/core/java/android/view/inspector/WindowInspector.java
+++ b/core/java/android/view/inspector/WindowInspector.java
@@ -42,8 +42,9 @@ public final class WindowInspector {
}
/**
- * Adds a listener that is notified whenever the list of global window views changes. If a
- * {@link Consumer} is already registered this method is a no op.
+ * Adds a listener that is notified whenever the value of {@link #getGlobalWindowViews()}
+ * changes. The current value is provided immediately using the provided {@link Executor}.
+ * If this {@link Consumer} is already registered, then this method is a no op.
* @see #getGlobalWindowViews()
*/
@FlaggedApi(android.view.flags.Flags.FLAG_ROOT_VIEW_CHANGED_LISTENER)
diff --git a/core/java/android/view/translation/ListenerGroup.java b/core/java/android/view/translation/ListenerGroup.java
index bf506815f841..5c70805042fa 100644
--- a/core/java/android/view/translation/ListenerGroup.java
+++ b/core/java/android/view/translation/ListenerGroup.java
@@ -48,7 +48,7 @@ public class ListenerGroup<T> {
* is a no op.
*/
public void addListener(@NonNull Executor executor, @NonNull Consumer<T> consumer) {
- if (isContained(consumer)) {
+ if (isConsumerPresent(consumer)) {
return;
}
mListeners.add(new ListenerWrapper<>(executor, consumer));
@@ -69,7 +69,7 @@ public class ListenerGroup<T> {
* Returns {@code true} if the {@link Consumer} is present in the list, {@code false}
* otherwise.
*/
- private boolean isContained(Consumer<T> consumer) {
+ public boolean isConsumerPresent(Consumer<T> consumer) {
return computeIndex(consumer) > -1;
}
diff --git a/core/java/android/widget/DatePickerCalendarDelegate.java b/core/java/android/widget/DatePickerCalendarDelegate.java
index 536b81f77174..4f7eef007a68 100644
--- a/core/java/android/widget/DatePickerCalendarDelegate.java
+++ b/core/java/android/widget/DatePickerCalendarDelegate.java
@@ -34,6 +34,7 @@ import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.DayPickerView.OnDaySelectedListener;
import android.widget.YearPickerView.OnYearSelectedListener;
@@ -76,10 +77,6 @@ class DatePickerCalendarDelegate extends DatePicker.AbstractDatePickerDelegate {
private DayPickerView mDayPickerView;
private YearPickerView mYearPickerView;
- // Accessibility strings.
- private String mSelectDay;
- private String mSelectYear;
-
private int mCurrentView = UNINITIALIZED;
private final Calendar mTempDate;
@@ -118,8 +115,15 @@ class DatePickerCalendarDelegate extends DatePicker.AbstractDatePickerDelegate {
final ViewGroup header = mContainer.findViewById(R.id.date_picker_header);
mHeaderYear = header.findViewById(R.id.date_picker_header_year);
mHeaderYear.setOnClickListener(mOnHeaderClickListener);
+ mHeaderYear.setAccessibilityDelegate(
+ new ClickActionDelegate(context, R.string.select_year));
+ mHeaderYear.setAccessibilityLiveRegion(View.ACCESSIBILITY_LIVE_REGION_POLITE);
+
mHeaderMonthDay = header.findViewById(R.id.date_picker_header_date);
mHeaderMonthDay.setOnClickListener(mOnHeaderClickListener);
+ mHeaderMonthDay.setAccessibilityDelegate(
+ new ClickActionDelegate(context, R.string.select_day));
+ mHeaderMonthDay.setAccessibilityLiveRegion(View.ACCESSIBILITY_LIVE_REGION_POLITE);
// For the sake of backwards compatibility, attempt to extract the text
// color from the header month text appearance. If it's set, we'll let
@@ -170,10 +174,6 @@ class DatePickerCalendarDelegate extends DatePicker.AbstractDatePickerDelegate {
mYearPickerView.setYear(mCurrentDate.get(Calendar.YEAR));
mYearPickerView.setOnYearSelectedListener(mOnYearSelectedListener);
- // Set up content descriptions.
- mSelectDay = res.getString(R.string.select_day);
- mSelectYear = res.getString(R.string.select_year);
-
// Initialize for current locale. This also initializes the date, so no
// need to call onDateChanged.
onLocaleChanged(mCurrentLocale);
@@ -230,6 +230,22 @@ class DatePickerCalendarDelegate extends DatePicker.AbstractDatePickerDelegate {
return srcRgb | (dstAlpha << 24);
}
+ private static class ClickActionDelegate extends View.AccessibilityDelegate {
+ private final AccessibilityNodeInfo.AccessibilityAction mClickAction;
+
+ ClickActionDelegate(Context context, int resId) {
+ mClickAction = new AccessibilityNodeInfo.AccessibilityAction(
+ AccessibilityNodeInfo.ACTION_CLICK, context.getString(resId));
+ }
+
+ @Override
+ public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfo(host, info);
+
+ info.addAction(mClickAction);
+ }
+ }
+
/**
* Listener called when the user selects a day in the day picker view.
*/
@@ -310,10 +326,10 @@ class DatePickerCalendarDelegate extends DatePicker.AbstractDatePickerDelegate {
mYearFormat = DateFormat.getInstanceForSkeleton("y", locale);
// Update the header text.
- onCurrentDateChanged(false);
+ onCurrentDateChanged();
}
- private void onCurrentDateChanged(boolean announce) {
+ private void onCurrentDateChanged() {
if (mHeaderYear == null) {
// Abort, we haven't initialized yet. This method will get called
// again later after everything has been set up.
@@ -325,11 +341,6 @@ class DatePickerCalendarDelegate extends DatePicker.AbstractDatePickerDelegate {
final String monthDay = mMonthDayFormat.format(mCurrentDate.getTime());
mHeaderMonthDay.setText(monthDay);
-
- // TODO: This should use live regions.
- if (announce) {
- mAnimator.announceForAccessibility(getFormattedCurrentDate());
- }
}
private void setCurrentView(final int viewIndex) {
@@ -343,8 +354,6 @@ class DatePickerCalendarDelegate extends DatePicker.AbstractDatePickerDelegate {
mAnimator.setDisplayedChild(VIEW_MONTH_DAY);
mCurrentView = viewIndex;
}
-
- mAnimator.announceForAccessibility(mSelectDay);
break;
case VIEW_YEAR:
final int year = mCurrentDate.get(Calendar.YEAR);
@@ -364,7 +373,6 @@ class DatePickerCalendarDelegate extends DatePicker.AbstractDatePickerDelegate {
mCurrentView = viewIndex;
}
- mAnimator.announceForAccessibility(mSelectYear);
break;
}
}
@@ -409,7 +417,7 @@ class DatePickerCalendarDelegate extends DatePicker.AbstractDatePickerDelegate {
mDayPickerView.setDate(mCurrentDate.getTimeInMillis());
mYearPickerView.setYear(year);
- onCurrentDateChanged(fromUser);
+ onCurrentDateChanged();
if (fromUser) {
tryVibrate();
@@ -564,7 +572,7 @@ class DatePickerCalendarDelegate extends DatePicker.AbstractDatePickerDelegate {
mMinDate.setTimeInMillis(ss.getMinDate());
mMaxDate.setTimeInMillis(ss.getMaxDate());
- onCurrentDateChanged(false);
+ onCurrentDateChanged();
final int currentView = ss.getCurrentView();
setCurrentView(currentView);
diff --git a/core/java/android/widget/TimePickerClockDelegate.java b/core/java/android/widget/TimePickerClockDelegate.java
index a453c2818566..ebb0f99e988b 100644
--- a/core/java/android/widget/TimePickerClockDelegate.java
+++ b/core/java/android/widget/TimePickerClockDelegate.java
@@ -121,12 +121,8 @@ class TimePickerClockDelegate extends TimePicker.AbstractTimePickerDelegate {
// Localization data.
private boolean mHourFormatShowLeadingZero;
- private boolean mHourFormatStartsAtZero;
-
- // Most recent time announcement values for accessibility.
- private CharSequence mLastAnnouncedText;
- private boolean mLastAnnouncedIsHour;
+ private boolean mHourFormatStartsAtZero;
public TimePickerClockDelegate(TimePicker delegator, Context context, AttributeSet attrs,
int defStyleAttr, int defStyleRes) {
super(delegator, context);
@@ -155,6 +151,7 @@ class TimePickerClockDelegate extends TimePicker.AbstractTimePickerDelegate {
mHourView.setOnDigitEnteredListener(mDigitEnteredListener);
mHourView.setAccessibilityDelegate(
new ClickActionDelegate(context, R.string.select_hours));
+ mHourView.setAccessibilityLiveRegion(View.ACCESSIBILITY_LIVE_REGION_POLITE);
mSeparatorView = (TextView) mainView.findViewById(R.id.separator);
mMinuteView = (NumericTextView) mainView.findViewById(R.id.minutes);
mMinuteView.setOnClickListener(mClickListener);
@@ -162,6 +159,7 @@ class TimePickerClockDelegate extends TimePicker.AbstractTimePickerDelegate {
mMinuteView.setOnDigitEnteredListener(mDigitEnteredListener);
mMinuteView.setAccessibilityDelegate(
new ClickActionDelegate(context, R.string.select_minutes));
+ mMinuteView.setAccessibilityLiveRegion(View.ACCESSIBILITY_LIVE_REGION_POLITE);
mMinuteView.setRange(0, 59);
// Set up AM/PM labels.
@@ -435,7 +433,7 @@ class TimePickerClockDelegate extends TimePicker.AbstractTimePickerDelegate {
private void updateRadialPicker(int index) {
mRadialTimePickerView.initialize(mCurrentHour, mCurrentMinute, mIs24Hour);
- setCurrentItemShowing(index, false, true);
+ setCurrentItemShowing(index, false);
}
private void updateHeaderAmPm() {
@@ -786,18 +784,10 @@ class TimePickerClockDelegate extends TimePicker.AbstractTimePickerDelegate {
private void updateHeaderHour(int hourOfDay, boolean announce) {
final int localizedHour = getLocalizedHour(hourOfDay);
mHourView.setValue(localizedHour);
-
- if (announce) {
- tryAnnounceForAccessibility(mHourView.getText(), true);
- }
}
private void updateHeaderMinute(int minuteOfHour, boolean announce) {
mMinuteView.setValue(minuteOfHour);
-
- if (announce) {
- tryAnnounceForAccessibility(mMinuteView.getText(), false);
- }
}
/**
@@ -876,31 +866,12 @@ class TimePickerClockDelegate extends TimePicker.AbstractTimePickerDelegate {
return -1;
}
- private void tryAnnounceForAccessibility(CharSequence text, boolean isHour) {
- if (mLastAnnouncedIsHour != isHour || !text.equals(mLastAnnouncedText)) {
- // TODO: Find a better solution, potentially live regions?
- mDelegator.announceForAccessibility(text);
- mLastAnnouncedText = text;
- mLastAnnouncedIsHour = isHour;
- }
- }
-
/**
* Show either Hours or Minutes.
*/
- private void setCurrentItemShowing(int index, boolean animateCircle, boolean announce) {
+ private void setCurrentItemShowing(int index, boolean animateCircle) {
mRadialTimePickerView.setCurrentItemShowing(index, animateCircle);
- if (index == HOUR_INDEX) {
- if (announce) {
- mDelegator.announceForAccessibility(mSelectHours);
- }
- } else {
- if (announce) {
- mDelegator.announceForAccessibility(mSelectMinutes);
- }
- }
-
mHourView.setActivated(index == HOUR_INDEX);
mMinuteView.setActivated(index == MINUTE_INDEX);
}
@@ -930,10 +901,7 @@ class TimePickerClockDelegate extends TimePicker.AbstractTimePickerDelegate {
final boolean isTransition = mAllowAutoAdvance && autoAdvance;
setHourInternal(newValue, FROM_RADIAL_PICKER, !isTransition, true);
if (isTransition) {
- setCurrentItemShowing(MINUTE_INDEX, true, false);
-
- final int localizedHour = getLocalizedHour(newValue);
- mDelegator.announceForAccessibility(localizedHour + ". " + mSelectMinutes);
+ setCurrentItemShowing(MINUTE_INDEX, true);
}
break;
case RadialTimePickerView.MINUTES:
@@ -1030,10 +998,10 @@ class TimePickerClockDelegate extends TimePicker.AbstractTimePickerDelegate {
setAmOrPm(PM);
break;
case R.id.hours:
- setCurrentItemShowing(HOUR_INDEX, true, true);
+ setCurrentItemShowing(HOUR_INDEX, true);
break;
case R.id.minutes:
- setCurrentItemShowing(MINUTE_INDEX, true, true);
+ setCurrentItemShowing(MINUTE_INDEX, true);
break;
default:
// Failed to handle this click, don't vibrate.
@@ -1058,10 +1026,10 @@ class TimePickerClockDelegate extends TimePicker.AbstractTimePickerDelegate {
setAmOrPm(PM);
break;
case R.id.hours:
- setCurrentItemShowing(HOUR_INDEX, true, true);
+ setCurrentItemShowing(HOUR_INDEX, true);
break;
case R.id.minutes:
- setCurrentItemShowing(MINUTE_INDEX, true, true);
+ setCurrentItemShowing(MINUTE_INDEX, true);
break;
default:
// Failed to handle this click, don't vibrate.
diff --git a/core/java/android/window/DesktopModeFlags.java b/core/java/android/window/DesktopModeFlags.java
index 703274dd708b..4bd0d97a54b0 100644
--- a/core/java/android/window/DesktopModeFlags.java
+++ b/core/java/android/window/DesktopModeFlags.java
@@ -44,7 +44,7 @@ public enum DesktopModeFlags {
// All desktop mode related flags to be overridden by developer option toggle will be added here
// go/keep-sorted start
DISABLE_DESKTOP_LAUNCH_PARAMS_OUTSIDE_DESKTOP_BUG_FIX(
- Flags::disableDesktopLaunchParamsOutsideDesktopBugFix, false),
+ Flags::disableDesktopLaunchParamsOutsideDesktopBugFix, true),
DISABLE_NON_RESIZABLE_APP_SNAP_RESIZE(Flags::disableNonResizableAppSnapResizing, true),
ENABLE_ACCESSIBLE_CUSTOM_HEADERS(Flags::enableAccessibleCustomHeaders, true),
ENABLE_APP_HEADER_WITH_TASK_DENSITY(Flags::enableAppHeaderWithTaskDensity, true),
@@ -103,7 +103,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_DRAG_RESIZE_SET_UP_IN_BG_THREAD(Flags::enableDragResizeSetUpInBgThread, true),
ENABLE_DRAG_TO_DESKTOP_INCOMING_TRANSITIONS_BUGFIX(
Flags::enableDragToDesktopIncomingTransitionsBugfix, false),
ENABLE_FULLY_IMMERSIVE_IN_DESKTOP(Flags::enableFullyImmersiveInDesktop, true),
@@ -115,11 +115,12 @@ public enum DesktopModeFlags {
ENABLE_OPAQUE_BACKGROUND_FOR_TRANSPARENT_WINDOWS(
Flags::enableOpaqueBackgroundForTransparentWindows, true),
ENABLE_QUICKSWITCH_DESKTOP_SPLIT_BUGFIX(Flags::enableQuickswitchDesktopSplitBugfix, true),
+ ENABLE_REQUEST_FULLSCREEN_BUGFIX(Flags::enableRequestFullscreenBugfix, false),
ENABLE_RESIZING_METRICS(Flags::enableResizingMetrics, true),
ENABLE_RESTORE_TO_PREVIOUS_SIZE_FROM_DESKTOP_IMMERSIVE(
Flags::enableRestoreToPreviousSizeFromDesktopImmersive, true),
ENABLE_SHELL_INITIAL_BOUNDS_REGRESSION_BUG_FIX(
- Flags::enableShellInitialBoundsRegressionBugFix, false),
+ Flags::enableShellInitialBoundsRegressionBugFix, true),
ENABLE_START_LAUNCH_TRANSITION_FROM_TASKBAR_BUGFIX(
Flags::enableStartLaunchTransitionFromTaskbarBugfix, true),
ENABLE_TASKBAR_OVERFLOW(Flags::enableTaskbarOverflow, false),
diff --git a/core/java/android/window/flags/lse_desktop_experience.aconfig b/core/java/android/window/flags/lse_desktop_experience.aconfig
index e4142a171669..0d87b73a5e03 100644
--- a/core/java/android/window/flags/lse_desktop_experience.aconfig
+++ b/core/java/android/window/flags/lse_desktop_experience.aconfig
@@ -971,6 +971,16 @@ flag {
}
flag {
+ name: "enable_request_fullscreen_bugfix"
+ namespace: "lse_desktop_experience"
+ description: "Fixes split to fullscreen restoration using the Activity#requestFullscreenMode API"
+ bug: "402973271"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+flag {
name: "enable_dynamic_radius_computation_bugfix"
namespace: "lse_desktop_experience"
description: "Enables bugfix to compute the corner/shadow radius of desktop windows dynamically with the current window context."
@@ -986,3 +996,13 @@ flag {
description: "Enables the home to be shown behind the desktop."
bug: "375644149"
}
+
+flag {
+ name: "enable_desktop_ime_bugfix"
+ namespace: "lse_desktop_experience"
+ description: "Enables bugfix to handle IME interactions in desktop windowing."
+ bug: "388570293"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
diff --git a/core/java/android/window/flags/responsible_apis.aconfig b/core/java/android/window/flags/responsible_apis.aconfig
index 36219812c002..7039add0b179 100644
--- a/core/java/android/window/flags/responsible_apis.aconfig
+++ b/core/java/android/window/flags/responsible_apis.aconfig
@@ -88,3 +88,10 @@ flag {
description: "Clear the allowlist duration when clearAllowBgActivityStarts is called"
bug: "322159724"
}
+
+flag {
+ name: "bal_additional_logging"
+ namespace: "responsible_apis"
+ description: "Enable additional logging."
+ bug: "403398176"
+}
diff --git a/core/java/com/android/internal/app/AssistUtils.java b/core/java/com/android/internal/app/AssistUtils.java
index 4261a0f14767..dd9cf9d7718e 100644
--- a/core/java/com/android/internal/app/AssistUtils.java
+++ b/core/java/com/android/internal/app/AssistUtils.java
@@ -61,6 +61,8 @@ public class AssistUtils {
public static final int INVOCATION_TYPE_ASSIST_BUTTON = 7;
/** value for INVOCATION_TYPE_KEY: long press on nav handle */
public static final int INVOCATION_TYPE_NAV_HANDLE_LONG_PRESS = 8;
+ /** value for INVOCATION_TYPE_KEY: sysui launcher */
+ public static final int INVOCATION_TYPE_LAUNCHER_SYSTEM_SHORTCUT = 9;
private final Context mContext;
private final IVoiceInteractionManagerService mVoiceInteractionManagerService;
diff --git a/core/java/com/android/internal/app/MediaRouteDialogPresenter.java b/core/java/com/android/internal/app/MediaRouteDialogPresenter.java
index 5628b7ed9d15..76b289416076 100644
--- a/core/java/com/android/internal/app/MediaRouteDialogPresenter.java
+++ b/core/java/com/android/internal/app/MediaRouteDialogPresenter.java
@@ -86,10 +86,7 @@ public abstract class MediaRouteDialogPresenter {
public static Dialog createDialog(Context context, int routeTypes,
View.OnClickListener extendedSettingsClickListener, int theme,
boolean showProgressBarWhenEmpty) {
- final MediaRouter router = context.getSystemService(MediaRouter.class);
-
- MediaRouter.RouteInfo route = router.getSelectedRoute();
- if (route.isDefault() || !route.matchesTypes(routeTypes)) {
+ if (shouldShowChooserDialog(context, routeTypes)) {
final MediaRouteChooserDialog d = new MediaRouteChooserDialog(context, theme,
showProgressBarWhenEmpty);
d.setRouteTypes(routeTypes);
@@ -99,4 +96,11 @@ public abstract class MediaRouteDialogPresenter {
return new MediaRouteControllerDialog(context, theme);
}
}
+
+ /** Whether we should show the chooser dialog or the controller dialog.. */
+ public static boolean shouldShowChooserDialog(Context context, int routeTypes) {
+ final MediaRouter router = context.getSystemService(MediaRouter.class);
+ MediaRouter.RouteInfo route = router.getSelectedRoute();
+ return route.isDefault() || !route.matchesTypes(routeTypes);
+ }
}
diff --git a/core/java/com/android/internal/statusbar/DisableStates.aidl b/core/java/com/android/internal/statusbar/DisableStates.aidl
new file mode 100644
index 000000000000..fd9882f0f7c2
--- /dev/null
+++ b/core/java/com/android/internal/statusbar/DisableStates.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.statusbar;
+
+parcelable DisableStates;
diff --git a/core/java/com/android/internal/statusbar/DisableStates.java b/core/java/com/android/internal/statusbar/DisableStates.java
new file mode 100644
index 000000000000..ca2fd6c03558
--- /dev/null
+++ b/core/java/com/android/internal/statusbar/DisableStates.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.statusbar;
+
+import android.app.StatusBarManager.Disable2Flags;
+import android.app.StatusBarManager.DisableFlags;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.Pair;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Holds display ids with their disable flags.
+ */
+public class DisableStates implements Parcelable {
+
+ /**
+ * A map of display IDs (integers) with corresponding disable flags.
+ */
+ public Map<Integer, Pair<@DisableFlags Integer, @Disable2Flags Integer>> displaysWithStates;
+
+ /**
+ * Whether the disable state change should be animated.
+ */
+ public boolean animate;
+
+ public DisableStates(
+ Map<Integer, Pair<@DisableFlags Integer, @Disable2Flags Integer>> displaysWithStates,
+ boolean animate) {
+ this.displaysWithStates = displaysWithStates;
+ this.animate = animate;
+ }
+
+ public DisableStates(
+ Map<Integer, Pair<@DisableFlags Integer, @Disable2Flags Integer>> displaysWithStates) {
+ this(displaysWithStates, true);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(displaysWithStates.size()); // Write the size of the map
+ for (Map.Entry<Integer, Pair<Integer, Integer>> entry : displaysWithStates.entrySet()) {
+ dest.writeInt(entry.getKey());
+ dest.writeInt(entry.getValue().first);
+ dest.writeInt(entry.getValue().second);
+ }
+ dest.writeBoolean(animate);
+ }
+
+ /**
+ * Used to make this class parcelable.
+ */
+ public static final Parcelable.Creator<DisableStates> CREATOR = new Parcelable.Creator<>() {
+ @Override
+ public DisableStates createFromParcel(Parcel source) {
+ int size = source.readInt(); // Read the size of the map
+ Map<Integer, Pair<Integer, Integer>> displaysWithStates = new HashMap<>(size);
+ for (int i = 0; i < size; i++) {
+ int key = source.readInt();
+ int first = source.readInt();
+ int second = source.readInt();
+ displaysWithStates.put(key, new Pair<>(first, second));
+ }
+ final boolean animate = source.readBoolean();
+ return new DisableStates(displaysWithStates, animate);
+ }
+
+ @Override
+ public DisableStates[] newArray(int size) {
+ return new DisableStates[size];
+ }
+ };
+}
+
diff --git a/core/java/com/android/internal/statusbar/IStatusBar.aidl b/core/java/com/android/internal/statusbar/IStatusBar.aidl
index 5a180d7358dd..ce9b036f2fd7 100644
--- a/core/java/com/android/internal/statusbar/IStatusBar.aidl
+++ b/core/java/com/android/internal/statusbar/IStatusBar.aidl
@@ -32,6 +32,7 @@ import android.os.UserHandle;
import android.view.KeyEvent;
import android.service.notification.StatusBarNotification;
+import com.android.internal.statusbar.DisableStates;
import com.android.internal.statusbar.IAddTileResultCallback;
import com.android.internal.statusbar.IUndoMediaTransferCallback;
import com.android.internal.statusbar.LetterboxDetails;
@@ -44,6 +45,7 @@ oneway interface IStatusBar
void setIcon(String slot, in StatusBarIcon icon);
void removeIcon(String slot);
void disable(int displayId, int state1, int state2);
+ void disableForAllDisplays(in DisableStates disableStates);
void animateExpandNotificationsPanel();
void animateExpandSettingsPanel(String subPanel);
void animateCollapsePanels();
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 8fbd10c25995..d62538b6d1ed 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/CoreDocument.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/CoreDocument.java
@@ -64,16 +64,16 @@ public class CoreDocument implements Serializable {
private static final boolean DEBUG = false;
// Semantic version
- public static final int MAJOR_VERSION = 1;
- public static final int MINOR_VERSION = 0;
+ public static final int MAJOR_VERSION = 0;
+ public static final int MINOR_VERSION = 4;
public static final int PATCH_VERSION = 0;
// Internal version level
- public static final int DOCUMENT_API_LEVEL = 5;
+ public static final int DOCUMENT_API_LEVEL = 4;
// We also keep a more fine-grained BUILD number, exposed as
// ID_API_LEVEL = DOCUMENT_API_LEVEL + BUILD
- static final float BUILD = 0.0f;
+ static final float BUILD = 0.8f;
private static final boolean UPDATE_VARIABLES_BEFORE_LAYOUT = false;
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 a86b62e2caa3..eb7399afd2b7 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/RemoteComposeBuffer.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/RemoteComposeBuffer.java
@@ -1333,7 +1333,7 @@ public class RemoteComposeBuffer {
* @return the nan id of float
*/
public float reserveFloatVariable() {
- int id = mRemoteComposeState.nextId();
+ int id = mRemoteComposeState.cacheFloat(0f);
return Utils.asNan(id);
}
@@ -2510,8 +2510,8 @@ public class RemoteComposeBuffer {
* Add a debug message
*
* @param textId text id
- * @param value
- * @param flags
+ * @param value value
+ * @param flags flags
*/
public void addDebugMessage(int textId, float value, int flags) {
DebugMessage.apply(mBuffer, textId, value, flags);
diff --git a/core/jni/Android.bp b/core/jni/Android.bp
index bfa0aa9638a9..7ed73d7668b9 100644
--- a/core/jni/Android.bp
+++ b/core/jni/Android.bp
@@ -210,7 +210,6 @@ cc_library_shared_for_libandroid_runtime {
"android_media_AudioAttributes.cpp",
"android_media_AudioProductStrategies.cpp",
"android_media_AudioVolumeGroups.cpp",
- "android_media_AudioVolumeGroupCallback.cpp",
"android_media_DeviceCallback.cpp",
"android_media_MediaMetricsJNI.cpp",
"android_media_MicrophoneInfo.cpp",
@@ -311,6 +310,7 @@ cc_library_shared_for_libandroid_runtime {
"audioflinger-aidl-cpp",
"audiopolicy-types-aidl-cpp",
"spatializer-aidl-cpp",
+ "volumegroupcallback-aidl-cpp",
"av-types-aidl-cpp",
"android.hardware.camera.device@3.2",
"camera_platform_flags_c_lib",
diff --git a/core/jni/AndroidRuntime.cpp b/core/jni/AndroidRuntime.cpp
index b2b826391e1d..1ff07745e904 100644
--- a/core/jni/AndroidRuntime.cpp
+++ b/core/jni/AndroidRuntime.cpp
@@ -101,7 +101,6 @@ extern int register_android_media_AudioTrack(JNIEnv *env);
extern int register_android_media_AudioAttributes(JNIEnv *env);
extern int register_android_media_AudioProductStrategies(JNIEnv *env);
extern int register_android_media_AudioVolumeGroups(JNIEnv *env);
-extern int register_android_media_AudioVolumeGroupChangeHandler(JNIEnv *env);
extern int register_android_media_ImageReader(JNIEnv *env);
extern int register_android_media_ImageWriter(JNIEnv *env);
extern int register_android_media_MicrophoneInfo(JNIEnv *env);
@@ -1660,7 +1659,6 @@ static const RegJNIRec gRegJNI[] = {
REG_JNI(register_android_media_AudioAttributes),
REG_JNI(register_android_media_AudioProductStrategies),
REG_JNI(register_android_media_AudioVolumeGroups),
- REG_JNI(register_android_media_AudioVolumeGroupChangeHandler),
REG_JNI(register_android_media_ImageReader),
REG_JNI(register_android_media_ImageWriter),
REG_JNI(register_android_media_MediaMetrics),
diff --git a/core/jni/android_media_AudioSystem.cpp b/core/jni/android_media_AudioSystem.cpp
index b679688959b1..1bbf811dc373 100644
--- a/core/jni/android_media_AudioSystem.cpp
+++ b/core/jni/android_media_AudioSystem.cpp
@@ -20,16 +20,17 @@
#include <atomic>
#define LOG_TAG "AudioSystem-JNI"
+#include <android-base/properties.h>
#include <android/binder_ibinder_jni.h>
#include <android/binder_libbinder.h>
#include <android/media/AudioVibratorInfo.h>
+#include <android/media/INativeAudioVolumeGroupCallback.h>
#include <android/media/INativeSpatializerCallback.h>
#include <android/media/ISpatializer.h>
#include <android/media/audio/common/AudioConfigBase.h>
#include <android_media_audiopolicy.h>
#include <android_os_Parcel.h>
#include <audiomanager/AudioManager.h>
-#include <android-base/properties.h>
#include <binder/IBinder.h>
#include <jni.h>
#include <media/AidlConversion.h>
@@ -41,14 +42,14 @@
#include <nativehelper/ScopedLocalRef.h>
#include <nativehelper/ScopedPrimitiveArray.h>
#include <nativehelper/jni_macros.h>
+#include <sys/system_properties.h>
#include <system/audio.h>
#include <system/audio_policy.h>
-#include <sys/system_properties.h>
#include <utils/Log.h>
+#include <memory>
#include <optional>
#include <sstream>
-#include <memory>
#include <vector>
#include "android_media_AudioAttributes.h"
@@ -59,8 +60,8 @@
#include "android_media_AudioFormat.h"
#include "android_media_AudioMixerAttributes.h"
#include "android_media_AudioProfile.h"
-#include "android_media_MicrophoneInfo.h"
#include "android_media_JNIUtils.h"
+#include "android_media_MicrophoneInfo.h"
#include "android_util_Binder.h"
#include "core_jni_helpers.h"
@@ -3442,6 +3443,21 @@ static void android_media_AudioSystem_triggerSystemPropertyUpdate(JNIEnv *env,
}
}
+static int android_media_AudioSystem_registerAudioVolumeGroupCallback(
+ JNIEnv *env, jobject thiz, jobject jIAudioVolumeGroupCallback) {
+ sp<media::INativeAudioVolumeGroupCallback> nIAudioVolumeGroupCallback =
+ interface_cast<media::INativeAudioVolumeGroupCallback>(
+ ibinderForJavaObject(env, jIAudioVolumeGroupCallback));
+ return AudioSystem::addAudioVolumeGroupCallback(nIAudioVolumeGroupCallback);
+}
+
+static int android_media_AudioSystem_unregisterAudioVolumeGroupCallback(
+ JNIEnv *env, jobject thiz, jobject jIAudioVolumeGroupCallback) {
+ sp<media::INativeAudioVolumeGroupCallback> nIAudioVolumeGroupCallback =
+ interface_cast<media::INativeAudioVolumeGroupCallback>(
+ ibinderForJavaObject(env, jIAudioVolumeGroupCallback));
+ return AudioSystem::removeAudioVolumeGroupCallback(nIAudioVolumeGroupCallback);
+}
// ----------------------------------------------------------------------------
@@ -3612,6 +3628,12 @@ static const JNINativeMethod gMethods[] = {
MAKE_JNI_NATIVE_METHOD("clearPreferredMixerAttributes",
"(Landroid/media/AudioAttributes;II)I",
android_media_AudioSystem_clearPreferredMixerAttributes),
+ MAKE_JNI_NATIVE_METHOD("registerAudioVolumeGroupCallback",
+ "(Landroid/media/INativeAudioVolumeGroupCallback;)I",
+ android_media_AudioSystem_registerAudioVolumeGroupCallback),
+ MAKE_JNI_NATIVE_METHOD("unregisterAudioVolumeGroupCallback",
+ "(Landroid/media/INativeAudioVolumeGroupCallback;)I",
+ android_media_AudioSystem_unregisterAudioVolumeGroupCallback),
MAKE_AUDIO_SYSTEM_METHOD(supportsBluetoothVariableLatency),
MAKE_AUDIO_SYSTEM_METHOD(setBluetoothVariableLatencyEnabled),
MAKE_AUDIO_SYSTEM_METHOD(isBluetoothVariableLatencyEnabled),
diff --git a/core/jni/android_media_AudioVolumeGroupCallback.cpp b/core/jni/android_media_AudioVolumeGroupCallback.cpp
deleted file mode 100644
index d130a4bc68fa..000000000000
--- a/core/jni/android_media_AudioVolumeGroupCallback.cpp
+++ /dev/null
@@ -1,176 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open 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.
- */
-#undef ANDROID_UTILS_REF_BASE_DISABLE_IMPLICIT_CONSTRUCTION // TODO:remove this and fix code
-
-//#define LOG_NDEBUG 0
-
-#define LOG_TAG "AudioVolumeGroupCallback-JNI"
-
-#include <utils/Log.h>
-#include <nativehelper/JNIHelp.h>
-#include "core_jni_helpers.h"
-
-#include "android_media_AudioVolumeGroupCallback.h"
-
-
-// ----------------------------------------------------------------------------
-using namespace android;
-
-static const char* const kAudioVolumeGroupChangeHandlerClassPathName =
- "android/media/audiopolicy/AudioVolumeGroupChangeHandler";
-
-static struct {
- jfieldID mJniCallback;
-} gAudioVolumeGroupChangeHandlerFields;
-
-static struct {
- jmethodID postEventFromNative;
-} gAudioVolumeGroupChangeHandlerMethods;
-
-static Mutex gLock;
-
-JNIAudioVolumeGroupCallback::JNIAudioVolumeGroupCallback(JNIEnv* env,
- jobject thiz,
- jobject weak_thiz)
-{
- jclass clazz = env->GetObjectClass(thiz);
- if (clazz == NULL) {
- ALOGE("Can't find class %s", kAudioVolumeGroupChangeHandlerClassPathName);
- return;
- }
- mClass = (jclass)env->NewGlobalRef(clazz);
-
- // We use a weak reference so the AudioVolumeGroupChangeHandler object can be garbage collected.
- // The reference is only used as a proxy for callbacks.
- mObject = env->NewGlobalRef(weak_thiz);
-}
-
-JNIAudioVolumeGroupCallback::~JNIAudioVolumeGroupCallback()
-{
- // remove global references
- JNIEnv *env = AndroidRuntime::getJNIEnv();
- if (env == NULL) {
- return;
- }
- env->DeleteGlobalRef(mObject);
- env->DeleteGlobalRef(mClass);
-}
-
-void JNIAudioVolumeGroupCallback::onAudioVolumeGroupChanged(volume_group_t group, int flags)
-{
- JNIEnv *env = AndroidRuntime::getJNIEnv();
- if (env == NULL) {
- return;
- }
- ALOGV("%s volume group id %d", __FUNCTION__, group);
- env->CallStaticVoidMethod(mClass,
- gAudioVolumeGroupChangeHandlerMethods.postEventFromNative,
- mObject,
- AUDIOVOLUMEGROUP_EVENT_VOLUME_CHANGED, group, flags, NULL);
- if (env->ExceptionCheck()) {
- ALOGW("An exception occurred while notifying an event.");
- env->ExceptionClear();
- }
-}
-
-void JNIAudioVolumeGroupCallback::onServiceDied()
-{
- JNIEnv *env = AndroidRuntime::getJNIEnv();
- if (env == NULL) {
- return;
- }
- env->CallStaticVoidMethod(mClass,
- gAudioVolumeGroupChangeHandlerMethods.postEventFromNative,
- mObject,
- AUDIOVOLUMEGROUP_EVENT_SERVICE_DIED, 0, 0, NULL);
- if (env->ExceptionCheck()) {
- ALOGW("An exception occurred while notifying an event.");
- env->ExceptionClear();
- }
-}
-
-static
-sp<JNIAudioVolumeGroupCallback> setJniCallback(JNIEnv* env,
- jobject thiz,
- const sp<JNIAudioVolumeGroupCallback>& callback)
-{
- Mutex::Autolock l(gLock);
- sp<JNIAudioVolumeGroupCallback> old = (JNIAudioVolumeGroupCallback*)env->GetLongField(
- thiz, gAudioVolumeGroupChangeHandlerFields.mJniCallback);
- if (callback.get()) {
- callback->incStrong((void*)setJniCallback);
- }
- if (old != 0) {
- old->decStrong((void*)setJniCallback);
- }
- env->SetLongField(thiz, gAudioVolumeGroupChangeHandlerFields.mJniCallback,
- (jlong)callback.get());
- return old;
-}
-
-static void
-android_media_AudioVolumeGroupChangeHandler_eventHandlerSetup(JNIEnv *env,
- jobject thiz,
- jobject weak_this)
-{
- ALOGV("%s", __FUNCTION__);
- sp<JNIAudioVolumeGroupCallback> callback =
- new JNIAudioVolumeGroupCallback(env, thiz, weak_this);
-
- if (AudioSystem::addAudioVolumeGroupCallback(callback) == NO_ERROR) {
- setJniCallback(env, thiz, callback);
- }
-}
-
-static void
-android_media_AudioVolumeGroupChangeHandler_eventHandlerFinalize(JNIEnv *env, jobject thiz)
-{
- ALOGV("%s", __FUNCTION__);
- sp<JNIAudioVolumeGroupCallback> callback = setJniCallback(env, thiz, 0);
- if (callback != 0) {
- AudioSystem::removeAudioVolumeGroupCallback(callback);
- }
-}
-
-/*
- * JNI registration.
- */
-static const JNINativeMethod gMethods[] = {
- {"native_setup", "(Ljava/lang/Object;)V",
- (void *)android_media_AudioVolumeGroupChangeHandler_eventHandlerSetup},
- {"native_finalize", "()V",
- (void *)android_media_AudioVolumeGroupChangeHandler_eventHandlerFinalize},
-};
-
-int register_android_media_AudioVolumeGroupChangeHandler(JNIEnv *env)
-{
- jclass audioVolumeGroupChangeHandlerClass =
- FindClassOrDie(env, kAudioVolumeGroupChangeHandlerClassPathName);
- gAudioVolumeGroupChangeHandlerMethods.postEventFromNative =
- GetStaticMethodIDOrDie(env, audioVolumeGroupChangeHandlerClass, "postEventFromNative",
- "(Ljava/lang/Object;IIILjava/lang/Object;)V");
-
- gAudioVolumeGroupChangeHandlerFields.mJniCallback =
- GetFieldIDOrDie(env, audioVolumeGroupChangeHandlerClass, "mJniCallback", "J");
-
- env->DeleteLocalRef(audioVolumeGroupChangeHandlerClass);
-
- return RegisterMethodsOrDie(env,
- kAudioVolumeGroupChangeHandlerClassPathName,
- gMethods,
- NELEM(gMethods));
-}
-
diff --git a/core/jni/android_media_AudioVolumeGroupCallback.h b/core/jni/android_media_AudioVolumeGroupCallback.h
deleted file mode 100644
index de06549621b9..000000000000
--- a/core/jni/android_media_AudioVolumeGroupCallback.h
+++ /dev/null
@@ -1,44 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open 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.
- */
-
-#pragma once
-
-#include <system/audio.h>
-#include <media/AudioSystem.h>
-
-namespace android {
-
-// keep in sync with AudioManager.AudioVolumeGroupChangeHandler.java
-#define AUDIOVOLUMEGROUP_EVENT_VOLUME_CHANGED 1000
-#define AUDIOVOLUMEGROUP_EVENT_SERVICE_DIED 1001
-
-class JNIAudioVolumeGroupCallback: public AudioSystem::AudioVolumeGroupCallback
-{
-public:
- JNIAudioVolumeGroupCallback(JNIEnv* env, jobject thiz, jobject weak_thiz);
- ~JNIAudioVolumeGroupCallback();
-
- void onAudioVolumeGroupChanged(volume_group_t group, int flags) override;
- void onServiceDied() override;
-
-private:
- void sendEvent(int event);
-
- jclass mClass; /**< Reference to AudioVolumeGroupChangeHandler class. */
- jobject mObject; /**< Weak ref to AudioVolumeGroupChangeHandler object to call on. */
-};
-
-} // namespace android
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 57c89b91cff7..201f46025286 100644
--- a/core/res/res/layout/notification_2025_template_collapsed_base.xml
+++ b/core/res/res/layout/notification_2025_template_collapsed_base.xml
@@ -74,6 +74,7 @@
android:id="@+id/notification_headerless_view_column"
android:layout_width="0px"
android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
android:layout_weight="1"
android:layout_marginVertical="@dimen/notification_2025_reduced_margin"
android:orientation="vertical"
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 de82f9feb512..e599de12097a 100644
--- a/core/res/res/layout/notification_2025_template_collapsed_media.xml
+++ b/core/res/res/layout/notification_2025_template_collapsed_media.xml
@@ -76,6 +76,7 @@
android:id="@+id/notification_headerless_view_column"
android:layout_width="0px"
android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
android:layout_weight="1"
android:layout_marginVertical="@dimen/notification_2025_reduced_margin"
android:orientation="vertical"
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index e47adc90fc7a..1a74fe6719e3 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -1219,6 +1219,8 @@
6 - Lock if keyguard enabled or go to sleep (doze)
7 - Dream if possible or go to sleep (doze)
8 - Go to glanceable hub or dream if possible, or sleep if neither is available (doze)
+ 9 - Go to dream if device is not dreaming, stop dream if device is dreaming, or sleep if
+ neither is available (doze)
-->
<integer name="config_shortPressOnPowerBehavior">1</integer>
diff --git a/core/res/res/values/config_telephony.xml b/core/res/res/values/config_telephony.xml
index ef6b9188532e..849ca2882889 100644
--- a/core/res/res/values/config_telephony.xml
+++ b/core/res/res/values/config_telephony.xml
@@ -86,7 +86,7 @@
CarrierConfigManager#KEY_AUTO_DATA_SWITCH_RAT_SIGNAL_SCORE_STRING_ARRAY.
If 0, the device always switch to the higher score SIM.
If < 0, the network type and signal strength based auto switch is disabled. -->
- <integer name="auto_data_switch_score_tolerance">4000</integer>
+ <integer name="auto_data_switch_score_tolerance">7000</integer>
<java-symbol type="integer" name="auto_data_switch_score_tolerance" />
<!-- Boolean indicating whether the Iwlan data service supports persistence of iwlan ipsec
@@ -261,7 +261,9 @@
to identify providers that should be ignored if the carrier config
carrier_supported_satellite_services_per_provider_bundle does not support them.
-->
- <string-array name="config_satellite_providers" translatable="false"></string-array>
+ <string-array name="config_satellite_providers" translatable="false">
+ <item>"310830"</item>
+ </string-array>
<java-symbol type="array" name="config_satellite_providers" />
<!-- The identifier of the satellite's SIM profile. The identifier is composed of MCC and MNC
diff --git a/core/tests/FileSystemUtilsTest/OWNERS b/core/tests/FileSystemUtilsTest/OWNERS
new file mode 100644
index 000000000000..74eeacfeb973
--- /dev/null
+++ b/core/tests/FileSystemUtilsTest/OWNERS
@@ -0,0 +1,2 @@
+waghpawan@google.com
+kaleshsingh@google.com
diff --git a/core/tests/FileSystemUtilsTest/src/com/android/internal/content/FileSystemUtilsTest.java b/core/tests/FileSystemUtilsTest/src/com/android/internal/content/FileSystemUtilsTest.java
index 208d74e49afe..dbfd3e8ccdaa 100644
--- a/core/tests/FileSystemUtilsTest/src/com/android/internal/content/FileSystemUtilsTest.java
+++ b/core/tests/FileSystemUtilsTest/src/com/android/internal/content/FileSystemUtilsTest.java
@@ -38,6 +38,8 @@ public class FileSystemUtilsTest extends BaseHostJUnit4Test {
private static final String PAGE_SIZE_COMPAT_ENABLED_BY_PLATFORM =
"app_with_4kb_elf_no_override.apk";
+ private static final int DEVICE_WAIT_TIMEOUT = 120000;
+
@Test
@AppModeFull
public void runPunchedApp_embeddedNativeLibs() throws DeviceNotAvailableException {
@@ -98,8 +100,20 @@ public class FileSystemUtilsTest extends BaseHostJUnit4Test {
@AppModeFull
public void runAppWith4KbLib_compatByAlignmentChecks()
throws DeviceNotAvailableException, TargetSetupError {
+ // make sure that device is available for UI test
+ prepareDevice();
// This test is expected to fail since compat is disabled in manifest
runPageSizeCompatTest(PAGE_SIZE_COMPAT_ENABLED_BY_PLATFORM,
"testPageSizeCompat_compatByAlignmentChecks");
}
+
+ private void prepareDevice() throws DeviceNotAvailableException {
+ // Verify that device is online before running test and enable root
+ getDevice().waitForDeviceAvailable(DEVICE_WAIT_TIMEOUT);
+ getDevice().enableAdbRoot();
+ getDevice().waitForDeviceAvailable(DEVICE_WAIT_TIMEOUT);
+
+ getDevice().executeShellCommand("input keyevent KEYCODE_WAKEUP");
+ getDevice().executeShellCommand("wm dismiss-keyguard");
+ }
}
diff --git a/core/tests/coretests/src/android/content/IntentTest.java b/core/tests/coretests/src/android/content/IntentTest.java
index fa1948d9786c..1dbe7f5f245b 100644
--- a/core/tests/coretests/src/android/content/IntentTest.java
+++ b/core/tests/coretests/src/android/content/IntentTest.java
@@ -22,6 +22,7 @@ import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
+import android.net.Uri;
import android.os.Binder;
import android.os.Bundle;
import android.os.IBinder;
@@ -32,14 +33,21 @@ import android.platform.test.flag.junit.CheckFlagsRule;
import android.platform.test.flag.junit.DeviceFlagsValueProvider;
import android.security.Flags;
import android.util.ArraySet;
+import android.util.Xml;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
+import com.android.internal.util.XmlUtils;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
+
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@@ -277,4 +285,40 @@ public class IntentTest {
assertThat(b2.getBundle("bundle").getClassLoader()).isEqualTo(cl);
}
+ @Test
+ @RequiresFlagsEnabled(android.content.flags.Flags.FLAG_INTENT_SAVE_TO_XML_PACKAGE)
+ public void testSaveToXmlAndRestore() throws Exception {
+ // Create an intent and set fields.
+ Intent original = new Intent();
+ original.setAction(Intent.ACTION_MAIN);
+ original.setComponent(ComponentName.createRelative("com.intent.test", "IntentTest"));
+ original.setData(Uri.parse("content://path/to/file.txt"));
+ original.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
+ original.setIdentifier("unique_identifier");
+ original.setPackage("com.intent.test");
+ original.addCategory(Intent.CATEGORY_LAUNCHER);
+ original.putExtra("Name", "Some really important data");
+
+ String tag = "intent";
+
+ // Write to xml.
+ ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+ TypedXmlSerializer serializer = Xml.resolveSerializer(byteArrayOutputStream);
+ serializer.startDocument(null, true);
+ serializer.startTag(null, tag);
+ original.saveToXml(serializer);
+ serializer.endTag(null, tag);
+ serializer.endDocument();
+
+ // Restore from xml.
+ ByteArrayInputStream byteArrayInputStream =
+ new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
+ TypedXmlPullParser parser = Xml.resolvePullParser(byteArrayInputStream);
+ XmlUtils.beginDocument(parser, tag);
+ Intent restored = Intent.restoreFromXml(parser);
+
+ // Verify that the restored intent passed filterEquals on the original.
+ assertTrue(original.filterEquals(restored));
+ }
+
}
diff --git a/core/tests/coretests/src/android/content/pm/RegisteredServicesCacheUnitTest.java b/core/tests/coretests/src/android/content/pm/RegisteredServicesCacheUnitTest.java
index 8349659517c5..b63fcdc8362f 100644
--- a/core/tests/coretests/src/android/content/pm/RegisteredServicesCacheUnitTest.java
+++ b/core/tests/coretests/src/android/content/pm/RegisteredServicesCacheUnitTest.java
@@ -68,6 +68,7 @@ import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
/**
* Unit tests for {@link android.content.pm.RegisteredServicesCache}
@@ -84,8 +85,8 @@ public class RegisteredServicesCacheUnitTest {
@Rule
public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
- private final ResolveInfo mResolveInfo1 = new ResolveInfo();
- private final ResolveInfo mResolveInfo2 = new ResolveInfo();
+ private final TestResolveInfo mResolveInfo1 = new TestResolveInfo();
+ private final TestResolveInfo mResolveInfo2 = new TestResolveInfo();
private final TestServiceType mTestServiceType1 = new TestServiceType("t1", "value1");
private final TestServiceType mTestServiceType2 = new TestServiceType("t2", "value2");
@Mock
@@ -195,13 +196,13 @@ public class RegisteredServicesCacheUnitTest {
reset(testServicesCache);
- testServicesCache.clearServicesForQuerying();
int u1uid = UserHandle.getUid(U1, UID1);
assertThat(u1uid).isNotEqualTo(UID1);
final RegisteredServicesCache.ServiceInfo<TestServiceType> serviceInfo2 = newServiceInfo(
mTestServiceType1, u1uid, mResolveInfo1.serviceInfo.getComponentName(),
1000L /* lastUpdateTime */);
+ mResolveInfo1.setResolveInfoId(U1);
testServicesCache.addServiceForQuerying(U1, mResolveInfo1, serviceInfo2);
testServicesCache.getAllServices(U1);
@@ -286,7 +287,7 @@ public class RegisteredServicesCacheUnitTest {
}
@Test
- public void testClearServiceInfoCachesAfterTimeout() throws Exception {
+ public void testClearServiceInfoCachesForSingleUserAfterTimeout() throws Exception {
PackageInfo packageInfo1 = createPackageInfo(1000L /* lastUpdateTime */);
when(mMockPackageManager.getPackageInfoAsUser(eq(mResolveInfo1.serviceInfo.packageName),
anyInt(), eq(U0))).thenReturn(packageInfo1);
@@ -316,6 +317,58 @@ public class RegisteredServicesCacheUnitTest {
verify(testServicesCache, times(1)).parseServiceInfo(eq(mResolveInfo1), eq(1000L));
}
+ @Test
+ public void testClearServiceInfoCachesForMultiUserAfterTimeout() throws Exception {
+ PackageInfo packageInfo1 = createPackageInfo(1000L /* lastUpdateTime */);
+ when(mMockPackageManager.getPackageInfoAsUser(eq(mResolveInfo1.serviceInfo.packageName),
+ anyInt(), eq(U0))).thenReturn(packageInfo1);
+ PackageInfo packageInfo2 = createPackageInfo(2000L /* lastUpdateTime */);
+ when(mMockPackageManager.getPackageInfoAsUser(eq(mResolveInfo2.serviceInfo.packageName),
+ anyInt(), eq(U1))).thenReturn(packageInfo2);
+
+ TestRegisteredServicesCache testServicesCache = spy(
+ new TestRegisteredServicesCache(mMockInjector, null /* serializerAndParser */));
+ final RegisteredServicesCache.ServiceInfo<TestServiceType> serviceInfo1 = newServiceInfo(
+ mTestServiceType1, UID1, mResolveInfo1.serviceInfo.getComponentName(),
+ 1000L /* lastUpdateTime */);
+ testServicesCache.addServiceForQuerying(U0, mResolveInfo1, serviceInfo1);
+
+ int u1uid = UserHandle.getUid(U1, UID1);
+ final RegisteredServicesCache.ServiceInfo<TestServiceType> serviceInfo2 = newServiceInfo(
+ mTestServiceType2, u1uid, mResolveInfo2.serviceInfo.getComponentName(),
+ 2000L /* lastUpdateTime */);
+ testServicesCache.addServiceForQuerying(U1, mResolveInfo2, serviceInfo2);
+
+ // Don't invoke run on the Runnable for U0 user, and it will not clear the service info of
+ // U0 user. Invoke run on the Runnable for U1 user, and it will just clear the service info
+ // of U1 user.
+ doAnswer(invocation -> {
+ Message message = invocation.getArgument(0);
+ if (!message.obj.equals(Integer.valueOf(U0))) {
+ message.getCallback().run();
+ }
+ return true;
+ }).when(mMockBackgroundHandler).sendMessageAtTime(any(Message.class), anyLong());
+
+ // It will generate the service info of U0 user into cache.
+ testServicesCache.getAllServices(U0);
+ verify(testServicesCache, times(1)).parseServiceInfo(eq(mResolveInfo1), eq(1000L));
+ // It will generate the service info of U1 user into cache.
+ testServicesCache.getAllServices(U1);
+ verify(testServicesCache, times(1)).parseServiceInfo(eq(mResolveInfo2), eq(2000L));
+ verify(mMockBackgroundHandler, times(2)).sendMessageAtTime(any(Message.class), anyLong());
+
+ reset(testServicesCache);
+
+ testServicesCache.invalidateCache(U0);
+ testServicesCache.getAllServices(U0);
+ verify(testServicesCache, never()).parseServiceInfo(eq(mResolveInfo1), eq(1000L));
+
+ testServicesCache.invalidateCache(U1);
+ testServicesCache.getAllServices(U1);
+ verify(testServicesCache, times(1)).parseServiceInfo(eq(mResolveInfo2), eq(2000L));
+ }
+
private static RegisteredServicesCache.ServiceInfo<TestServiceType> newServiceInfo(
TestServiceType type, int uid, ComponentName componentName, long lastUpdateTime) {
final ComponentInfo info = new ComponentInfo();
@@ -324,7 +377,7 @@ public class RegisteredServicesCacheUnitTest {
return new RegisteredServicesCache.ServiceInfo<>(type, info, componentName, lastUpdateTime);
}
- private void addServiceInfoIntoResolveInfo(ResolveInfo resolveInfo, String packageName,
+ private void addServiceInfoIntoResolveInfo(TestResolveInfo resolveInfo, String packageName,
String serviceName) {
final ServiceInfo serviceInfo = new ServiceInfo();
serviceInfo.packageName = packageName;
@@ -345,7 +398,7 @@ public class RegisteredServicesCacheUnitTest {
static final String SERVICE_INTERFACE = "RegisteredServicesCacheUnitTest";
static final String SERVICE_META_DATA = "RegisteredServicesCacheUnitTest";
static final String ATTRIBUTES_NAME = "test";
- private SparseArray<Map<ResolveInfo, ServiceInfo<TestServiceType>>> mServices =
+ private SparseArray<Map<TestResolveInfo, ServiceInfo<TestServiceType>>> mServices =
new SparseArray<>();
public TestRegisteredServicesCache(Injector<TestServiceType> injector,
@@ -362,14 +415,14 @@ public class RegisteredServicesCacheUnitTest {
@Override
protected List<ResolveInfo> queryIntentServices(int userId) {
- Map<ResolveInfo, ServiceInfo<TestServiceType>> map = mServices.get(userId,
- new HashMap<ResolveInfo, ServiceInfo<TestServiceType>>());
+ Map<TestResolveInfo, ServiceInfo<TestServiceType>> map = mServices.get(userId,
+ new HashMap<TestResolveInfo, ServiceInfo<TestServiceType>>());
return new ArrayList<>(map.keySet());
}
- void addServiceForQuerying(int userId, ResolveInfo resolveInfo,
+ void addServiceForQuerying(int userId, TestResolveInfo resolveInfo,
ServiceInfo<TestServiceType> serviceInfo) {
- Map<ResolveInfo, ServiceInfo<TestServiceType>> map = mServices.get(userId);
+ Map<TestResolveInfo, ServiceInfo<TestServiceType>> map = mServices.get(userId);
if (map == null) {
map = new HashMap<>();
mServices.put(userId, map);
@@ -377,16 +430,12 @@ public class RegisteredServicesCacheUnitTest {
map.put(resolveInfo, serviceInfo);
}
- void clearServicesForQuerying() {
- mServices.clear();
- }
-
@Override
protected ServiceInfo<TestServiceType> parseServiceInfo(ResolveInfo resolveInfo,
long lastUpdateTime) throws XmlPullParserException, IOException {
int size = mServices.size();
for (int i = 0; i < size; i++) {
- Map<ResolveInfo, ServiceInfo<TestServiceType>> map = mServices.valueAt(i);
+ Map<TestResolveInfo, ServiceInfo<TestServiceType>> map = mServices.valueAt(i);
ServiceInfo<TestServiceType> serviceInfo = map.get(resolveInfo);
if (serviceInfo != null) {
return serviceInfo;
@@ -400,4 +449,20 @@ public class RegisteredServicesCacheUnitTest {
super.onUserRemoved(userId);
}
}
+
+ /**
+ * Create different hash code with the same {@link android.content.pm.ResolveInfo} for testing.
+ */
+ public static class TestResolveInfo extends ResolveInfo {
+ int mResolveInfoId = 0;
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mResolveInfoId, serviceInfo);
+ }
+
+ public void setResolveInfoId(int resolveInfoId) {
+ mResolveInfoId = resolveInfoId;
+ }
+ }
}
diff --git a/core/tests/coretests/src/com/android/internal/app/MediaRouteDialogPresenterTest.kt b/core/tests/coretests/src/com/android/internal/app/MediaRouteDialogPresenterTest.kt
new file mode 100644
index 000000000000..e80d3a6e625f
--- /dev/null
+++ b/core/tests/coretests/src/com/android/internal/app/MediaRouteDialogPresenterTest.kt
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.app
+
+import android.content.Context
+import android.media.MediaRouter
+import android.testing.TestableLooper.RunWithLooper
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.stub
+
+@SmallTest
+@RunWithLooper(setAsMainLooper = true)
+@RunWith(AndroidJUnit4::class)
+class MediaRouteDialogPresenterTest {
+ private var selectedRoute: MediaRouter.RouteInfo = mock()
+ private var mediaRouter: MediaRouter = mock<MediaRouter> {
+ on { selectedRoute } doReturn selectedRoute
+ }
+ private var context: Context = mock<Context> {
+ on { getSystemServiceName(MediaRouter::class.java) } doReturn Context.MEDIA_ROUTER_SERVICE
+ on { getSystemService(MediaRouter::class.java) } doReturn mediaRouter
+ }
+
+ @Test
+ fun shouldShowChooserDialog_routeNotDefault_returnsFalse() {
+ selectedRoute.stub {
+ on { isDefault } doReturn false
+ on { matchesTypes(anyInt()) } doReturn true
+ }
+
+ assertThat(MediaRouteDialogPresenter.shouldShowChooserDialog(
+ context, MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY))
+ .isEqualTo(false)
+ }
+
+ @Test
+ fun shouldShowChooserDialog_routeDefault_returnsTrue() {
+ selectedRoute.stub {
+ on { isDefault } doReturn true
+ on { matchesTypes(anyInt()) } doReturn true
+ }
+
+ assertThat(MediaRouteDialogPresenter.shouldShowChooserDialog(
+ context, MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY))
+ .isEqualTo(true)
+ }
+
+ @Test
+ fun shouldShowChooserDialog_routeNotMatch_returnsTrue() {
+ selectedRoute.stub {
+ on { isDefault } doReturn false
+ on { matchesTypes(anyInt()) } doReturn false
+ }
+
+ assertThat(MediaRouteDialogPresenter.shouldShowChooserDialog(
+ context, MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY))
+ .isEqualTo(true)
+ }
+
+ @Test
+ fun shouldShowChooserDialog_routeDefaultAndNotMatch_returnsTrue() {
+ selectedRoute.stub {
+ on { isDefault } doReturn true
+ on { matchesTypes(anyInt()) } doReturn false
+ }
+
+ assertThat(MediaRouteDialogPresenter.shouldShowChooserDialog(
+ context, MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY))
+ .isEqualTo(true)
+ }
+} \ No newline at end of file
diff --git a/core/tests/coretests/src/com/android/internal/statusbar/DisableStatesTest.java b/core/tests/coretests/src/com/android/internal/statusbar/DisableStatesTest.java
new file mode 100644
index 000000000000..5b82696b81c3
--- /dev/null
+++ b/core/tests/coretests/src/com/android/internal/statusbar/DisableStatesTest.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.statusbar;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+import android.os.Parcel;
+import android.util.Pair;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.HashMap;
+import java.util.Map;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class DisableStatesTest {
+
+ @Test
+ public void testParcelable() {
+ Map<Integer, Pair<Integer, Integer>> displaysWithStates = new HashMap<>();
+ displaysWithStates.put(1, new Pair<>(10, 20));
+ displaysWithStates.put(2, new Pair<>(30, 40));
+ boolean animate = true;
+ DisableStates original = new DisableStates(displaysWithStates, animate);
+
+ Parcel parcel = Parcel.obtain();
+ original.writeToParcel(parcel, 0);
+ parcel.setDataPosition(0);
+ DisableStates restored = DisableStates.CREATOR.createFromParcel(parcel);
+
+ assertNotNull(restored);
+ assertEquals(original.displaysWithStates.size(), restored.displaysWithStates.size());
+ for (Map.Entry<Integer, Pair<Integer, Integer>> entry :
+ original.displaysWithStates.entrySet()) {
+ int displayId = entry.getKey();
+ Pair<Integer, Integer> originalDisplayStates = entry.getValue();
+ Pair<Integer, Integer> restoredDisplayStates = restored.displaysWithStates.get(
+ displayId);
+ assertEquals(originalDisplayStates.first, restoredDisplayStates.first);
+ assertEquals(originalDisplayStates.second, restoredDisplayStates.second);
+ }
+ assertEquals(original.animate, restored.animate);
+ }
+}
diff --git a/graphics/java/android/graphics/Typeface.java b/graphics/java/android/graphics/Typeface.java
index d1aca34c7b8d..39cd4a89aae6 100644
--- a/graphics/java/android/graphics/Typeface.java
+++ b/graphics/java/android/graphics/Typeface.java
@@ -80,6 +80,7 @@ import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
+import java.util.function.BiConsumer;
/**
* The Typeface class specifies the typeface and intrinsic style of a font.
@@ -1550,14 +1551,21 @@ public class Typeface {
setDefault(defaults.get(0));
ArrayList<Typeface> oldGenerics = new ArrayList<>();
- oldGenerics.add(sSystemFontMap.get("sans-serif"));
- sSystemFontMap.put("sans-serif", genericFamilies.get(0));
+ BiConsumer<Typeface, String> swapTypeface = (typeface, key) -> {
+ oldGenerics.add(sSystemFontMap.get(key));
+ sSystemFontMap.put(key, typeface);
+ };
- oldGenerics.add(sSystemFontMap.get("serif"));
- sSystemFontMap.put("serif", genericFamilies.get(1));
+ Typeface sansSerif = genericFamilies.get(0);
+ swapTypeface.accept(sansSerif, "sans-serif");
+ swapTypeface.accept(Typeface.create(sansSerif, 100, false), "sans-serif-thin");
+ swapTypeface.accept(Typeface.create(sansSerif, 300, false), "sans-serif-light");
+ swapTypeface.accept(Typeface.create(sansSerif, 500, false), "sans-serif-medium");
+ swapTypeface.accept(Typeface.create(sansSerif, 700, false), "sans-serif-bold");
+ swapTypeface.accept(Typeface.create(sansSerif, 900, false), "sans-serif-black");
- oldGenerics.add(sSystemFontMap.get("monospace"));
- sSystemFontMap.put("monospace", genericFamilies.get(2));
+ swapTypeface.accept(genericFamilies.get(1), "serif");
+ swapTypeface.accept(genericFamilies.get(2), "monospace");
return new Pair<>(oldDefaults, oldGenerics);
}
diff --git a/libs/WindowManager/Shell/res/drawable/desktop_mode_ic_handle_menu_restart.xml b/libs/WindowManager/Shell/res/drawable/desktop_mode_ic_handle_menu_restart.xml
new file mode 100644
index 000000000000..d407884d3fcf
--- /dev/null
+++ b/libs/WindowManager/Shell/res/drawable/desktop_mode_ic_handle_menu_restart.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.
+ -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="20dp"
+ android:height="20dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="#1C1C14"
+ android:pathData="M6,13c0,-1.65 0.67,-3.15 1.76,-4.24L6.34,7.34C4.9,8.79 4,10.79 4,13c0,4.08 3.05,7.44 7,7.93v-2.02C8.17,18.43 6,15.97 6,13z"/>
+ <path
+ android:fillColor="#1C1C14"
+ android:pathData="M20,13c0,-4.42 -3.58,-8 -8,-8c-0.06,0 -0.12,0.01 -0.18,0.01v0l1.09,-1.09L11.5,2.5L8,6l3.5,3.5l1.41,-1.41l-1.08,-1.08C11.89,7.01 11.95,7 12,7c3.31,0 6,2.69 6,6c0,2.97 -2.17,5.43 -5,5.91v2.02C16.95,20.44 20,17.08 20,13z"/>
+</vector>
diff --git a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml
index bfaa40771894..30acf1ac6eda 100644
--- a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml
+++ b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml
@@ -163,6 +163,13 @@
android:text="@string/change_aspect_ratio_text"
android:src="@drawable/desktop_mode_ic_handle_menu_change_aspect_ratio"
style="@style/DesktopModeHandleMenuActionButton"/>
+
+ <com.android.wm.shell.windowdecor.HandleMenuActionButton
+ android:id="@+id/handle_menu_restart_button"
+ android:contentDescription="@string/handle_menu_restart_text"
+ android:text="@string/handle_menu_restart_text"
+ android:src="@drawable/desktop_mode_ic_handle_menu_restart"
+ style="@style/DesktopModeHandleMenuActionButton"/>
</LinearLayout>
<LinearLayout
@@ -186,14 +193,13 @@
<ImageButton
android:id="@+id/open_by_default_button"
- android:layout_width="20dp"
- android:layout_height="20dp"
android:layout_gravity="end|center_vertical"
- android:layout_marginStart="8dp"
- android:layout_marginEnd="16dp"
+ android:paddingStart="12dp"
+ android:paddingEnd="16dp"
android:contentDescription="@string/open_by_default_settings_text"
android:src="@drawable/desktop_mode_ic_handle_menu_open_by_default_settings"
- android:tint="@androidprv:color/materialColorOnSurface"/>
+ android:tint="@androidprv:color/materialColorOnSurface"
+ style="@style/DesktopModeHandleMenuWindowingButton"/>
</LinearLayout>
</LinearLayout>
diff --git a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu_action_button.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu_action_button.xml
index 35e7de0e7c1e..0e5843f3e592 100644
--- a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu_action_button.xml
+++ b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu_action_button.xml
@@ -14,18 +14,7 @@
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto"
- xmlns:tools="http://schemas.android.com/tools"
- android:id="@+id/action_button"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:gravity="start|center_vertical"
- android:paddingHorizontal="16dp"
- android:importantForAccessibility="yes"
- android:orientation="horizontal"
- android:background="?android:attr/selectableItemBackground">
-
+<merge xmlns:android="http://schemas.android.com/apk/res/android">
<ImageView
android:id="@+id/image"
android:importantForAccessibility="no"
@@ -35,4 +24,4 @@
android:id="@+id/label"
android:importantForAccessibility="no"
style="@style/DesktopModeHandleMenuActionButtonTextView"/>
-</LinearLayout>
+</merge>
diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml
index e1bf6638a9b2..733f3bb8d6d0 100644
--- a/libs/WindowManager/Shell/res/values/dimen.xml
+++ b/libs/WindowManager/Shell/res/values/dimen.xml
@@ -294,6 +294,8 @@
<dimen name="bubble_bar_expanded_view_drop_target_padding_top">60dp</dimen>
<dimen name="bubble_bar_expanded_view_drop_target_padding_bottom">24dp</dimen>
<dimen name="bubble_bar_expanded_view_drop_target_padding_horizontal">48dp</dimen>
+ <dimen name="bubble_bar_drop_target_width">84dp</dimen>
+ <dimen name="bubble_bar_drop_target_height">48dp</dimen>
<!-- Width of the box around bottom center of the screen where drag only leads to dismiss -->
<dimen name="bubble_bar_dismiss_zone_width">192dp</dimen>
<!-- Height of the box around bottom center of the screen where drag only leads to dismiss -->
@@ -532,10 +534,10 @@
pill elevation. -->
<dimen name="desktop_mode_handle_menu_width">218dp</dimen>
- <!-- The maximum height of the handle menu in desktop mode. Three pills at 52dp each plus
- additional actions pill 208dp plus 2dp spacing between them plus 4dp top padding
- plus 2dp bottom padding: 52*3 + 52*4 + (4-1)*2 + 4 + 2 = 376 -->
- <dimen name="desktop_mode_handle_menu_height">376dp</dimen>
+ <!-- The maximum height of the handle menu in desktop mode. Three pills at 52dp each,
+ additional actions pill 260dp, plus 2dp spacing between them plus 4dp top padding.
+ 52*3 + 52*5 + (5-1)*2 + 4 = 428 -->
+ <dimen name="desktop_mode_handle_menu_height">428dp</dimen>
<!-- The elevation set on the handle menu pills. -->
<dimen name="desktop_mode_handle_menu_pill_elevation">1dp</dimen>
@@ -564,6 +566,9 @@
<!-- The height of the handle menu's "Change aspect ratio" pill in desktop mode. -->
<dimen name="desktop_mode_handle_menu_change_aspect_ratio_height">52dp</dimen>
+ <!-- The height of the handle menu's "Optimize View" pill in desktop mode. -->
+ <dimen name="desktop_mode_handle_menu_restart_button_height">52dp</dimen>
+
<!-- The margin between pills of the handle menu in desktop mode. -->
<dimen name="desktop_mode_handle_menu_pill_spacing_margin">2dp</dimen>
diff --git a/libs/WindowManager/Shell/res/values/strings.xml b/libs/WindowManager/Shell/res/values/strings.xml
index 5ef83826840b..1fd4704f7814 100644
--- a/libs/WindowManager/Shell/res/values/strings.xml
+++ b/libs/WindowManager/Shell/res/values/strings.xml
@@ -314,6 +314,8 @@
<string name="manage_windows_text">Manage Windows</string>
<!-- Accessibility text for the handle menu change aspect ratio button [CHAR LIMIT=NONE] -->
<string name="change_aspect_ratio_text">Change aspect ratio</string>
+ <!-- Accessibility text for the handle menu restart button [CHAR LIMIT=NONE] -->
+ <string name="handle_menu_restart_text">Optimize View</string>
<!-- Accessibility text for the handle menu close button [CHAR LIMIT=NONE] -->
<string name="close_text">Close</string>
<!-- Accessibility text for the handle menu close menu button [CHAR LIMIT=NONE] -->
diff --git a/libs/WindowManager/Shell/res/values/styles.xml b/libs/WindowManager/Shell/res/values/styles.xml
index 5f1db83d7acb..08cda7b94a78 100644
--- a/libs/WindowManager/Shell/res/values/styles.xml
+++ b/libs/WindowManager/Shell/res/values/styles.xml
@@ -45,7 +45,13 @@
<item name="android:layout_height">52dp</item>
<item name="android:textColor">@androidprv:color/materialColorOnSurface</item>
<item name="android:drawableTint">@androidprv:color/materialColorOnSurface</item>
- <item name="android:importantForAccessibility">no</item>
+ <item name="android:importantForAccessibility">yes</item>
+ <item name="android:gravity">start|center_vertical</item>
+ <item name="android:paddingHorizontal">16dp</item>
+ <item name="android:clickable">true</item>
+ <item name="android:focusable">true</item>
+ <item name="android:orientation">horizontal</item>
+ <item name="android:background">?android:attr/selectableItemBackground</item>
</style>
<style name="DesktopModeHandleMenuActionButtonImage">
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/BubbleDropTargetBoundsProvider.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/BubbleDropTargetBoundsProvider.kt
index 9bee11a92430..84e0fbe96de2 100644
--- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/BubbleDropTargetBoundsProvider.kt
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/BubbleDropTargetBoundsProvider.kt
@@ -26,4 +26,9 @@ interface BubbleDropTargetBoundsProvider {
* Get bubble bar expanded view visual drop target bounds on screen
*/
fun getBubbleBarExpandedViewDropTargetBounds(onLeft: Boolean): Rect
+
+ /**
+ * Get the bar visual drop target bounds on screen
+ */
+ fun getBarDropTargetBounds(onLeft: Boolean): Rect
} \ No newline at end of file
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 ed5e0c608675..e5a4cd034e72 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
@@ -278,6 +278,9 @@ public class DesktopModeStatus {
if (!canEnterDesktopMode(context)) {
return false;
}
+ if (!enforceDeviceRestrictions()) {
+ return true;
+ }
if (display.getType() == Display.TYPE_INTERNAL) {
return canInternalDisplayHostDesktops(context);
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
index 5d59af940da0..81cf031994a2 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
@@ -508,7 +508,11 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont
}
mShouldStartOnNextMoveEvent = false;
} else {
- mShouldStartOnNextMoveEvent = true;
+ if (predictiveBackDelayWmTransition()) {
+ onGestureStarted(touchX, touchY, swipeEdge);
+ } else {
+ mShouldStartOnNextMoveEvent = true;
+ }
}
}
} else if (keyAction == MotionEvent.ACTION_MOVE) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java
index 03d6b0a8075d..0b45b086e13c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java
@@ -106,6 +106,8 @@ public class BubblePositioner implements BubbleDropTargetBoundsProvider {
private int mBarExpViewDropTargetPaddingTop;
private int mBarExpViewDropTargetPaddingBottom;
private int mBarExpViewDropTargetPaddingHorizontal;
+ private int mBarDropTargetWidth;
+ private int mBarDropTargetHeight;
private PointF mRestingStackPosition;
@@ -181,6 +183,8 @@ public class BubblePositioner implements BubbleDropTargetBoundsProvider {
R.dimen.bubble_bar_expanded_view_drop_target_padding_bottom);
mBarExpViewDropTargetPaddingHorizontal = res.getDimensionPixelSize(
R.dimen.bubble_bar_expanded_view_drop_target_padding_horizontal);
+ mBarDropTargetWidth = res.getDimensionPixelSize(R.dimen.bubble_bar_drop_target_width);
+ mBarDropTargetHeight = res.getDimensionPixelSize(R.dimen.bubble_bar_drop_target_height);
if (mShowingInBubbleBar) {
mExpandedViewLargeScreenWidth = mExpandedViewBubbleBarWidth;
@@ -1003,4 +1007,20 @@ public class BubblePositioner implements BubbleDropTargetBoundsProvider {
);
return bounds;
}
+
+ @NonNull
+ @Override
+ public Rect getBarDropTargetBounds(boolean onLeft) {
+ Rect bounds = getBubbleBarExpandedViewDropTargetBounds(onLeft);
+ bounds.top = getBubbleBarTopOnScreen();
+ bounds.bottom = bounds.top + mBarDropTargetHeight;
+ if (onLeft) {
+ // Keep the left edge from expanded view
+ bounds.right = bounds.left + mBarDropTargetWidth;
+ } else {
+ // Keep the right edge from expanded view
+ bounds.left = bounds.right - mBarDropTargetWidth;
+ }
+ return bounds;
+ }
}
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 728975e8ef9f..6f7d7a486453 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
@@ -649,7 +649,7 @@ public class BubbleTransitions {
@Override
public void continueCollapse() {
mBubble.cleanupTaskView();
- if (mTaskLeash == null || !mTaskLeash.isValid()) return;
+ if (mTaskLeash == null || !mTaskLeash.isValid() || !mRootLeash.isValid()) return;
SurfaceControl.Transaction t = new SurfaceControl.Transaction();
t.reparent(mTaskLeash, mRootLeash);
t.apply();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipBoundsAlgorithm.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipBoundsAlgorithm.java
index 04e8d8dee520..5d603d6c087d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipBoundsAlgorithm.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipBoundsAlgorithm.java
@@ -221,8 +221,11 @@ public class PipBoundsAlgorithm {
+ " than destination(%s)", sourceRectHint, destinationBounds);
return false;
}
- if (!PictureInPictureParams.isSameAspectRatio(sourceRectHint,
- new Rational(destinationBounds.width(), destinationBounds.height()))) {
+ // We use the aspect ratio of source rect hint to check against destination bounds
+ // here to avoid upscaling error.
+ final Rational srcAspectRatio = new Rational(
+ sourceRectHint.width(), sourceRectHint.height());
+ if (!PictureInPictureParams.isSameAspectRatio(destinationBounds, srcAspectRatio)) {
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"isSourceRectHintValidForEnterPip=false, hint(%s) does not match"
+ " destination(%s) aspect ratio", sourceRectHint, destinationBounds);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipDesktopState.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipDesktopState.java
deleted file mode 100644
index 1128fb2259b2..000000000000
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipDesktopState.java
+++ /dev/null
@@ -1,122 +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.wm.shell.common.pip;
-
-import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
-import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
-
-import android.window.DesktopExperienceFlags;
-import android.window.DesktopModeFlags;
-import android.window.DisplayAreaInfo;
-
-import com.android.wm.shell.Flags;
-import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
-import com.android.wm.shell.desktopmode.DesktopUserRepositories;
-import com.android.wm.shell.desktopmode.DragToDesktopTransitionHandler;
-
-import java.util.Optional;
-
-/** Helper class for PiP on Desktop Mode. */
-public class PipDesktopState {
- private final PipDisplayLayoutState mPipDisplayLayoutState;
- private final Optional<DesktopUserRepositories> mDesktopUserRepositoriesOptional;
- private final Optional<DragToDesktopTransitionHandler> mDragToDesktopTransitionHandlerOptional;
- private final RootTaskDisplayAreaOrganizer mRootTaskDisplayAreaOrganizer;
-
- public PipDesktopState(PipDisplayLayoutState pipDisplayLayoutState,
- Optional<DesktopUserRepositories> desktopUserRepositoriesOptional,
- Optional<DragToDesktopTransitionHandler> dragToDesktopTransitionHandlerOptional,
- RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer) {
- mPipDisplayLayoutState = pipDisplayLayoutState;
- mDesktopUserRepositoriesOptional = desktopUserRepositoriesOptional;
- mDragToDesktopTransitionHandlerOptional = dragToDesktopTransitionHandlerOptional;
- mRootTaskDisplayAreaOrganizer = rootTaskDisplayAreaOrganizer;
- }
-
- /**
- * Returns whether PiP in Desktop Windowing is enabled by checking the following:
- * - PiP in Desktop Windowing flag is enabled
- * - DesktopUserRepositories is injected
- * - DragToDesktopTransitionHandler is injected
- */
- public boolean isDesktopWindowingPipEnabled() {
- return DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PIP.isTrue()
- && mDesktopUserRepositoriesOptional.isPresent()
- && mDragToDesktopTransitionHandlerOptional.isPresent();
- }
-
- /**
- * Returns whether PiP in Connected Displays is enabled by checking the following:
- * - PiP in Connected Displays flag is enabled
- * - PiP2 flag is enabled
- */
- public boolean isConnectedDisplaysPipEnabled() {
- return DesktopExperienceFlags.ENABLE_CONNECTED_DISPLAYS_PIP.isTrue() && Flags.enablePip2();
- }
-
- /** Returns whether the display with the PiP task is in freeform windowing mode. */
- private boolean isDisplayInFreeform() {
- final DisplayAreaInfo tdaInfo = mRootTaskDisplayAreaOrganizer.getDisplayAreaInfo(
- mPipDisplayLayoutState.getDisplayId());
- if (tdaInfo != null) {
- return tdaInfo.configuration.windowConfiguration.getWindowingMode()
- == WINDOWING_MODE_FREEFORM;
- }
- return false;
- }
-
- /** Returns whether PiP is active in a display that is in active Desktop Mode session. */
- public boolean isPipInDesktopMode() {
- // Early return if PiP in Desktop Windowing is not supported.
- if (!isDesktopWindowingPipEnabled()) {
- return false;
- }
- final int displayId = mPipDisplayLayoutState.getDisplayId();
- return mDesktopUserRepositoriesOptional.get().getCurrent().isAnyDeskActive(displayId);
- }
-
- /**
- * The windowing mode to restore to when resizing out of PIP direction.
- * Defaults to undefined and can be overridden to restore to an alternate windowing mode.
- */
- public int getOutPipWindowingMode() {
- // If we are exiting PiP while the device is in Desktop mode (the task should expand to
- // freeform windowing mode):
- // 1) If the display windowing mode is freeform, set windowing mode to UNDEFINED so it will
- // resolve the windowing mode to the display's windowing mode.
- // 2) If the display windowing mode is not FREEFORM, set windowing mode to FREEFORM.
- if (isPipInDesktopMode()) {
- if (isDisplayInFreeform()) {
- return WINDOWING_MODE_UNDEFINED;
- } else {
- return WINDOWING_MODE_FREEFORM;
- }
- }
-
- // By default, or if the task is going to fullscreen, reset the windowing mode to undefined.
- return WINDOWING_MODE_UNDEFINED;
- }
-
- /** Returns whether there is a drag-to-desktop transition in progress. */
- public boolean isDragToDesktopInProgress() {
- // Early return if PiP in Desktop Windowing is not supported.
- if (!isDesktopWindowingPipEnabled()) {
- return false;
- }
- return mDragToDesktopTransitionHandlerOptional.get().getInProgress();
- }
-}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipDesktopState.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipDesktopState.kt
new file mode 100644
index 000000000000..55bde8906b63
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipDesktopState.kt
@@ -0,0 +1,96 @@
+/*
+ * 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.pip
+
+import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
+import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED
+import android.window.DesktopExperienceFlags
+import android.window.DesktopModeFlags
+import com.android.wm.shell.Flags
+import com.android.wm.shell.RootTaskDisplayAreaOrganizer
+import com.android.wm.shell.desktopmode.DesktopUserRepositories
+import com.android.wm.shell.desktopmode.DragToDesktopTransitionHandler
+import java.util.Optional
+
+/** Helper class for PiP on Desktop Mode. */
+class PipDesktopState(
+ private val pipDisplayLayoutState: PipDisplayLayoutState,
+ private val desktopUserRepositoriesOptional: Optional<DesktopUserRepositories>,
+ private val dragToDesktopTransitionHandlerOptional: Optional<DragToDesktopTransitionHandler>,
+ private val rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer
+) {
+ /**
+ * Returns whether PiP in Desktop Windowing is enabled by checking the following:
+ * - PiP in Desktop Windowing flag is enabled
+ * - DesktopUserRepositories is present
+ * - DragToDesktopTransitionHandler is present
+ */
+ fun isDesktopWindowingPipEnabled(): Boolean =
+ DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PIP.isTrue &&
+ desktopUserRepositoriesOptional.isPresent &&
+ dragToDesktopTransitionHandlerOptional.isPresent
+
+ /**
+ * Returns whether PiP in Connected Displays is enabled by checking the following:
+ * - PiP in Connected Displays flag is enabled
+ * - PiP2 flag is enabled
+ */
+ fun isConnectedDisplaysPipEnabled(): Boolean =
+ DesktopExperienceFlags.ENABLE_CONNECTED_DISPLAYS_PIP.isTrue && Flags.enablePip2()
+
+ /** Returns whether the display with the PiP task is in freeform windowing mode. */
+ private fun isDisplayInFreeform(): Boolean {
+ val tdaInfo = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(
+ pipDisplayLayoutState.displayId
+ )
+
+ return tdaInfo?.configuration?.windowConfiguration?.windowingMode == WINDOWING_MODE_FREEFORM
+ }
+
+ /** Returns whether PiP is active in a display that is in active Desktop Mode session. */
+ fun isPipInDesktopMode(): Boolean {
+ if (!isDesktopWindowingPipEnabled()) {
+ return false
+ }
+
+ val displayId = pipDisplayLayoutState.displayId
+ return desktopUserRepositoriesOptional.get().current.isAnyDeskActive(displayId)
+ }
+
+ /** Returns the windowing mode to restore to when resizing out of PIP direction. */
+ // TODO(b/403345629): Update this for Multi-Desktop.
+ fun getOutPipWindowingMode(): Int {
+ // If we are exiting PiP while the device is in Desktop mode, the task should expand to
+ // freeform windowing mode.
+ // 1) If the display windowing mode is freeform, set windowing mode to UNDEFINED so it will
+ // resolve the windowing mode to the display's windowing mode.
+ // 2) If the display windowing mode is not FREEFORM, set windowing mode to FREEFORM.
+ if (isPipInDesktopMode()) {
+ return if (isDisplayInFreeform()) {
+ WINDOWING_MODE_UNDEFINED
+ } else {
+ WINDOWING_MODE_FREEFORM
+ }
+ }
+
+ // By default, or if the task is going to fullscreen, reset the windowing mode to undefined.
+ return WINDOWING_MODE_UNDEFINED
+ }
+
+ /** Returns whether there is a drag-to-desktop transition in progress. */
+ fun isDragToDesktopInProgress(): Boolean =
+ isDesktopWindowingPipEnabled() && dragToDesktopTransitionHandlerOptional.get().inProgress
+}
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
index 9fa162164e0e..d9a66e1d64b2 100644
--- 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
@@ -57,6 +57,11 @@ public class FlexParallaxSpec implements ParallaxSpec {
* @return 0f = no dim applied. 1f = full black.
*/
public float getDimValue(int position, DividerSnapAlgorithm snapAlgorithm) {
+ // On tablets, apps don't go offscreen, so only dim for dismissal.
+ if (!snapAlgorithm.areOffscreenRatiosSupported()) {
+ return ParallaxSpec.super.getDimValue(position, snapAlgorithm);
+ }
+
int startDismissPos = snapAlgorithm.getDismissStartTarget().getPosition();
int firstTargetPos = snapAlgorithm.getFirstSplitTarget().getPosition();
int middleTargetPos = snapAlgorithm.getMiddleTarget().getPosition();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java
index 4413c8715c0d..d5f4a3885dbb 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java
@@ -112,6 +112,12 @@ public class CompatUIController implements OnDisplaysChangedListener,
new SparseArray<>(0);
/**
+ * {@link SparseArray} that maps task ids to {@link CompatUIInfo}.
+ */
+ private final SparseArray<CompatUIInfo> mTaskIdToCompatUIInfoMap =
+ new SparseArray<>(0);
+
+ /**
* {@link Set} of task ids for which we need to display a restart confirmation dialog
*/
private Set<Integer> mSetOfTaskIdsShowingRestartDialog = new HashSet<>();
@@ -261,7 +267,11 @@ public class CompatUIController implements OnDisplaysChangedListener,
private void handleDisplayCompatShowRestartDialog(
CompatUIRequests.DisplayCompatShowRestartDialog request) {
- onRestartButtonClicked(new Pair<>(request.getTaskInfo(), request.getTaskListener()));
+ final CompatUIInfo compatUIInfo = mTaskIdToCompatUIInfoMap.get(request.getTaskId());
+ if (compatUIInfo == null) {
+ return;
+ }
+ onRestartButtonClicked(new Pair<>(compatUIInfo.getTaskInfo(), compatUIInfo.getListener()));
}
/**
@@ -273,6 +283,11 @@ public class CompatUIController implements OnDisplaysChangedListener,
public void onCompatInfoChanged(@NonNull CompatUIInfo compatUIInfo) {
final TaskInfo taskInfo = compatUIInfo.getTaskInfo();
final ShellTaskOrganizer.TaskListener taskListener = compatUIInfo.getListener();
+ if (taskListener == null) {
+ mTaskIdToCompatUIInfoMap.delete(taskInfo.taskId);
+ } else {
+ mTaskIdToCompatUIInfoMap.put(taskInfo.taskId, compatUIInfo);
+ }
final boolean isInDisplayCompatMode =
taskInfo.appCompatTaskInfo.isRestartMenuEnabledForDisplayMove();
if (taskInfo != null && !taskInfo.appCompatTaskInfo.isTopActivityInSizeCompat()
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/impl/CompatUIRequests.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/impl/CompatUIRequests.kt
index da4fc99491dc..b7af596ee0ae 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/impl/CompatUIRequests.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/impl/CompatUIRequests.kt
@@ -16,8 +16,6 @@
package com.android.wm.shell.compatui.impl
-import android.app.TaskInfo
-import com.android.wm.shell.ShellTaskOrganizer
import com.android.wm.shell.compatui.api.CompatUIRequest
internal const val DISPLAY_COMPAT_SHOW_RESTART_DIALOG = 0
@@ -27,7 +25,6 @@ internal const val DISPLAY_COMPAT_SHOW_RESTART_DIALOG = 0
*/
sealed class CompatUIRequests(override val requestId: Int) : CompatUIRequest {
/** Sent when the restart handle menu is clicked, and a restart dialog is requested. */
- data class DisplayCompatShowRestartDialog(val taskInfo: TaskInfo,
- val taskListener: ShellTaskOrganizer.TaskListener) :
+ data class DisplayCompatShowRestartDialog(val taskId: Int) :
CompatUIRequests(DISPLAY_COMPAT_SHOW_RESTART_DIALOG)
}
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 2cf671b0f446..0edab006cb66 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
@@ -79,6 +79,7 @@ import com.android.wm.shell.common.SyncTransactionQueue;
import com.android.wm.shell.common.TaskStackListenerImpl;
import com.android.wm.shell.common.UserProfileContexts;
import com.android.wm.shell.common.split.SplitState;
+import com.android.wm.shell.compatui.api.CompatUIHandler;
import com.android.wm.shell.compatui.letterbox.LetterboxCommandHandler;
import com.android.wm.shell.compatui.letterbox.LetterboxTransitionObserver;
import com.android.wm.shell.crashhandling.ShellCrashHandler;
@@ -1044,7 +1045,8 @@ public abstract class WMShellModule {
RecentsTransitionHandler recentsTransitionHandler,
DesktopModeCompatPolicy desktopModeCompatPolicy,
DesktopTilingDecorViewModel desktopTilingDecorViewModel,
- MultiDisplayDragMoveIndicatorController multiDisplayDragMoveIndicatorController
+ MultiDisplayDragMoveIndicatorController multiDisplayDragMoveIndicatorController,
+ Optional<CompatUIHandler> compatUI
) {
if (!DesktopModeStatus.canEnterDesktopModeOrShowAppHandle(context)) {
return Optional.empty();
@@ -1062,7 +1064,7 @@ public abstract class WMShellModule {
activityOrientationChangeHandler, focusTransitionObserver, desktopModeEventLogger,
desktopModeUiEventLogger, taskResourceLoader, recentsTransitionHandler,
desktopModeCompatPolicy, desktopTilingDecorViewModel,
- multiDisplayDragMoveIndicatorController));
+ multiDisplayDragMoveIndicatorController, compatUI.orElse(null)));
}
@WMSingleton
@@ -1213,7 +1215,8 @@ public abstract class WMShellModule {
ShellTaskOrganizer shellTaskOrganizer,
TaskStackListenerImpl taskStackListener,
ToggleResizeDesktopTaskTransitionHandler toggleResizeDesktopTaskTransitionHandler,
- @DynamicOverride DesktopUserRepositories desktopUserRepositories) {
+ @DynamicOverride DesktopUserRepositories desktopUserRepositories,
+ DisplayController displayController) {
if (DesktopModeStatus.canEnterDesktopMode(context)) {
return Optional.of(
new DesktopActivityOrientationChangeHandler(
@@ -1222,7 +1225,8 @@ public abstract class WMShellModule {
shellTaskOrganizer,
taskStackListener,
toggleResizeDesktopTaskTransitionHandler,
- desktopUserRepositories));
+ desktopUserRepositories,
+ displayController));
}
return Optional.empty();
}
@@ -1341,7 +1345,9 @@ public abstract class WMShellModule {
Context context,
ShellInit shellInit,
@ShellMainThread CoroutineScope mainScope,
+ ShellController shellController,
DisplayController displayController,
+ RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer,
Optional<DesktopUserRepositories> desktopUserRepositories,
Optional<DesktopTasksController> desktopTasksController,
Optional<DesktopDisplayModeController> desktopDisplayModeController,
@@ -1355,7 +1361,9 @@ public abstract class WMShellModule {
context,
shellInit,
mainScope,
+ shellController,
displayController,
+ rootTaskDisplayAreaOrganizer,
desktopRepositoryInitializer,
desktopUserRepositories.get(),
desktopTasksController.get(),
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopActivityOrientationChangeHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopActivityOrientationChangeHandler.kt
index b8f4bb8d8323..39ce5d9023a6 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopActivityOrientationChangeHandler.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopActivityOrientationChangeHandler.kt
@@ -23,10 +23,10 @@ import android.content.pm.ActivityInfo.ScreenOrientation
import android.content.res.Configuration.ORIENTATION_LANDSCAPE
import android.content.res.Configuration.ORIENTATION_PORTRAIT
import android.graphics.Rect
-import android.util.Size
import android.window.WindowContainerTransaction
import com.android.window.flags.Flags
import com.android.wm.shell.ShellTaskOrganizer
+import com.android.wm.shell.common.DisplayController
import com.android.wm.shell.common.TaskStackListenerCallback
import com.android.wm.shell.common.TaskStackListenerImpl
import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
@@ -40,6 +40,7 @@ class DesktopActivityOrientationChangeHandler(
private val taskStackListener: TaskStackListenerImpl,
private val resizeHandler: ToggleResizeDesktopTaskTransitionHandler,
private val desktopUserRepositories: DesktopUserRepositories,
+ private val displayController: DisplayController,
) {
init {
@@ -101,12 +102,24 @@ class DesktopActivityOrientationChangeHandler(
orientation == ORIENTATION_LANDSCAPE &&
ActivityInfo.isFixedOrientationPortrait(requestedOrientation)
) {
+ val displayLayout = displayController.getDisplayLayout(task.displayId) ?: return
+ val captionInsets =
+ task.configuration.windowConfiguration.appBounds?.let {
+ it.top - task.configuration.windowConfiguration.bounds.top
+ } ?: 0
+ val newOrientationBounds =
+ calculateInitialBounds(
+ displayLayout = displayLayout,
+ taskInfo = task,
+ captionInsets = captionInsets,
+ requestedScreenOrientation = requestedOrientation,
+ )
- val finalSize = Size(taskHeight, taskWidth)
// Use the center x as the resizing anchor point.
- val left = taskBounds.centerX() - finalSize.width / 2
- val right = left + finalSize.width
- val finalBounds = Rect(left, taskBounds.top, right, taskBounds.top + finalSize.height)
+ val left = taskBounds.centerX() - newOrientationBounds.width() / 2
+ val right = left + newOrientationBounds.width()
+ val finalBounds =
+ Rect(left, taskBounds.top, right, taskBounds.top + newOrientationBounds.height())
val wct = WindowContainerTransaction().setBounds(task.token, finalBounds)
resizeHandler.startTransition(wct)
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 683b74392fa6..3b98f8123b46 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
@@ -17,16 +17,20 @@
package com.android.wm.shell.desktopmode
import android.content.Context
+import android.view.Display
import android.view.Display.DEFAULT_DISPLAY
import android.window.DesktopExperienceFlags
import com.android.internal.protolog.ProtoLog
+import com.android.wm.shell.RootTaskDisplayAreaOrganizer
import com.android.wm.shell.common.DisplayController
import com.android.wm.shell.common.DisplayController.OnDisplaysChangedListener
import com.android.wm.shell.desktopmode.multidesks.OnDeskRemovedListener
import com.android.wm.shell.desktopmode.persistence.DesktopRepositoryInitializer
import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE
import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
+import com.android.wm.shell.sysui.ShellController
import com.android.wm.shell.sysui.ShellInit
+import com.android.wm.shell.sysui.UserChangeListener
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
@@ -36,7 +40,9 @@ class DesktopDisplayEventHandler(
private val context: Context,
shellInit: ShellInit,
private val mainScope: CoroutineScope,
+ private val shellController: ShellController,
private val displayController: DisplayController,
+ private val rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer,
private val desktopRepositoryInitializer: DesktopRepositoryInitializer,
private val desktopUserRepositories: DesktopUserRepositories,
private val desktopTasksController: DesktopTasksController,
@@ -53,8 +59,17 @@ class DesktopDisplayEventHandler(
private fun onInit() {
displayController.addDisplayWindowListener(this)
- if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue()) {
+ if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
desktopTasksController.onDeskRemovedListener = this
+
+ shellController.addUserChangeListener(
+ object : UserChangeListener {
+ override fun onUserChanged(newUserId: Int, userContext: Context) {
+ val displayIds = rootTaskDisplayAreaOrganizer.displayIds
+ createDefaultDesksIfNeeded(displayIds.toSet())
+ }
+ }
+ )
}
}
@@ -63,23 +78,7 @@ class DesktopDisplayEventHandler(
desktopDisplayModeController.refreshDisplayWindowingMode()
}
- if (!supportsDesks(displayId)) {
- logV("Display #$displayId does not support desks")
- return
- }
-
- mainScope.launch {
- desktopRepositoryInitializer.isInitialized.collect { initialized ->
- if (!initialized) return@collect
- if (desktopRepository.getNumberOfDesks(displayId) == 0) {
- logV("Creating new desk in new display#$displayId")
- // TODO: b/393978539 - consider activating the desk on creation when
- // applicable, such as for connected displays.
- desktopTasksController.createDesk(displayId)
- }
- cancel()
- }
- }
+ createDefaultDesksIfNeeded(displayIds = setOf(displayId))
}
override fun onDisplayRemoved(displayId: Int) {
@@ -93,8 +92,34 @@ class DesktopDisplayEventHandler(
override fun onDeskRemoved(lastDisplayId: Int, deskId: Int) {
val remainingDesks = desktopRepository.getNumberOfDesks(lastDisplayId)
if (remainingDesks == 0) {
- logV("All desks removed from display#$lastDisplayId, creating empty desk")
- desktopTasksController.createDesk(lastDisplayId)
+ logV("All desks removed from display#$lastDisplayId")
+ createDefaultDesksIfNeeded(setOf(lastDisplayId))
+ }
+ }
+
+ private fun createDefaultDesksIfNeeded(displayIds: Set<Int>) {
+ if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) return
+ logV("createDefaultDesksIfNeeded displays=%s", displayIds)
+ mainScope.launch {
+ desktopRepositoryInitializer.isInitialized.collect { initialized ->
+ if (!initialized) return@collect
+ displayIds
+ .filter { displayId -> displayId != Display.INVALID_DISPLAY }
+ .filter { displayId -> supportsDesks(displayId) }
+ .filter { displayId -> desktopRepository.getNumberOfDesks(displayId) == 0 }
+ .also { displaysNeedingDesk ->
+ logV(
+ "createDefaultDesksIfNeeded creating default desks in displays=%s",
+ displaysNeedingDesk,
+ )
+ }
+ .forEach { displayId ->
+ // TODO: b/393978539 - consider activating the desk on creation when
+ // applicable, such as for connected displays.
+ desktopTasksController.createDesk(displayId)
+ }
+ cancel()
+ }
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeKeyGestureHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeKeyGestureHandler.kt
index 1ea545f3ab67..19507c17bc95 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeKeyGestureHandler.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeKeyGestureHandler.kt
@@ -23,10 +23,7 @@ import android.hardware.input.InputManager
import android.hardware.input.InputManager.KeyGestureEventHandler
import android.hardware.input.KeyGestureEvent
import android.os.IBinder
-import android.window.DesktopModeFlags
-import com.android.hardware.input.Flags.manageKeyGestures
import com.android.internal.protolog.ProtoLog
-import com.android.window.flags.Flags.enableMoveToNextDisplayShortcut
import com.android.wm.shell.ShellTaskOrganizer
import com.android.wm.shell.common.DisplayController
import com.android.wm.shell.common.ShellExecutor
@@ -51,16 +48,20 @@ class DesktopModeKeyGestureHandler(
) : KeyGestureEventHandler {
init {
- inputManager.registerKeyGestureEventHandler(this)
+ if (desktopTasksController.isPresent && desktopModeWindowDecorViewModel.isPresent) {
+ val supportedGestures =
+ listOf(
+ KeyGestureEvent.KEY_GESTURE_TYPE_MOVE_TO_NEXT_DISPLAY,
+ KeyGestureEvent.KEY_GESTURE_TYPE_SNAP_LEFT_FREEFORM_WINDOW,
+ KeyGestureEvent.KEY_GESTURE_TYPE_SNAP_RIGHT_FREEFORM_WINDOW,
+ KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_MAXIMIZE_FREEFORM_WINDOW,
+ KeyGestureEvent.KEY_GESTURE_TYPE_MINIMIZE_FREEFORM_WINDOW,
+ )
+ inputManager.registerKeyGestureEventHandler(supportedGestures, this)
+ }
}
- override fun handleKeyGestureEvent(event: KeyGestureEvent, focusedToken: IBinder?): Boolean {
- if (
- !desktopTasksController.isPresent ||
- !desktopModeWindowDecorViewModel.isPresent
- ) {
- return false
- }
+ override fun handleKeyGestureEvent(event: KeyGestureEvent, focusedToken: IBinder?) {
when (event.keyGestureType) {
KeyGestureEvent.KEY_GESTURE_TYPE_MOVE_TO_NEXT_DISPLAY -> {
logV("Key gesture MOVE_TO_NEXT_DISPLAY is handled")
@@ -69,7 +70,6 @@ class DesktopModeKeyGestureHandler(
desktopTasksController.get().moveToNextDisplay(it.taskId)
}
}
- return true
}
KeyGestureEvent.KEY_GESTURE_TYPE_SNAP_LEFT_FREEFORM_WINDOW -> {
logV("Key gesture SNAP_LEFT_FREEFORM_WINDOW is handled")
@@ -85,7 +85,6 @@ class DesktopModeKeyGestureHandler(
)
}
}
- return true
}
KeyGestureEvent.KEY_GESTURE_TYPE_SNAP_RIGHT_FREEFORM_WINDOW -> {
logV("Key gesture SNAP_RIGHT_FREEFORM_WINDOW is handled")
@@ -101,7 +100,6 @@ class DesktopModeKeyGestureHandler(
)
}
}
- return true
}
KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_MAXIMIZE_FREEFORM_WINDOW -> {
logV("Key gesture TOGGLE_MAXIMIZE_FREEFORM_WINDOW is handled")
@@ -120,7 +118,6 @@ class DesktopModeKeyGestureHandler(
)
}
}
- return true
}
KeyGestureEvent.KEY_GESTURE_TYPE_MINIMIZE_FREEFORM_WINDOW -> {
logV("Key gesture MINIMIZE_FREEFORM_WINDOW is handled")
@@ -129,9 +126,7 @@ class DesktopModeKeyGestureHandler(
desktopTasksController.get().minimizeTask(it, MinimizeReason.KEY_GESTURE)
}
}
- return true
}
- else -> return false
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt
index a8b0bafee724..3c44fe8061aa 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt
@@ -69,6 +69,7 @@ fun calculateInitialBounds(
taskInfo: RunningTaskInfo,
scale: Float = DESKTOP_MODE_INITIAL_BOUNDS_SCALE,
captionInsets: Int = 0,
+ requestedScreenOrientation: Int? = null,
): Rect {
val screenBounds = Rect(0, 0, displayLayout.width(), displayLayout.height())
val appAspectRatio = calculateAspectRatio(taskInfo)
@@ -85,12 +86,13 @@ fun calculateInitialBounds(
}
val topActivityInfo =
taskInfo.topActivityInfo ?: return positionInScreen(idealSize, stableBounds)
+ val screenOrientation = requestedScreenOrientation ?: topActivityInfo.screenOrientation
val initialSize: Size =
when (taskInfo.configuration.orientation) {
ORIENTATION_LANDSCAPE -> {
if (taskInfo.canChangeAspectRatio) {
- if (isFixedOrientationPortrait(topActivityInfo.screenOrientation)) {
+ if (isFixedOrientationPortrait(screenOrientation)) {
// For portrait resizeable activities, respect apps fullscreen width but
// apply ideal size height.
Size(
@@ -104,14 +106,20 @@ fun calculateInitialBounds(
} else {
// If activity is unresizeable, regardless of orientation, calculate maximum
// size (within the ideal size) maintaining original aspect ratio.
- maximizeSizeGivenAspectRatio(taskInfo, idealSize, appAspectRatio, captionInsets)
+ maximizeSizeGivenAspectRatio(
+ taskInfo,
+ idealSize,
+ appAspectRatio,
+ captionInsets,
+ screenOrientation,
+ )
}
}
ORIENTATION_PORTRAIT -> {
val customPortraitWidthForLandscapeApp =
screenBounds.width() - (DESKTOP_MODE_LANDSCAPE_APP_PADDING * 2)
if (taskInfo.canChangeAspectRatio) {
- if (isFixedOrientationLandscape(topActivityInfo.screenOrientation)) {
+ if (isFixedOrientationLandscape(screenOrientation)) {
// For landscape resizeable activities, respect apps fullscreen height and
// apply custom app width.
Size(
@@ -123,7 +131,7 @@ fun calculateInitialBounds(
idealSize
}
} else {
- if (isFixedOrientationLandscape(topActivityInfo.screenOrientation)) {
+ if (isFixedOrientationLandscape(screenOrientation)) {
// For landscape unresizeable activities, apply custom app width to ideal
// size and calculate maximum size with this area while maintaining original
// aspect ratio.
@@ -132,6 +140,7 @@ fun calculateInitialBounds(
Size(customPortraitWidthForLandscapeApp, idealSize.height),
appAspectRatio,
captionInsets,
+ screenOrientation,
)
} else {
// For portrait unresizeable activities, calculate maximum size (within the
@@ -141,6 +150,7 @@ fun calculateInitialBounds(
idealSize,
appAspectRatio,
captionInsets,
+ screenOrientation,
)
}
}
@@ -190,13 +200,16 @@ fun maximizeSizeGivenAspectRatio(
targetArea: Size,
aspectRatio: Float,
captionInsets: Int = 0,
+ requestedScreenOrientation: Int? = null,
): Size {
val targetHeight = targetArea.height - captionInsets
val targetWidth = targetArea.width
val finalHeight: Int
val finalWidth: Int
// Get orientation either through top activity or task's orientation
- if (taskInfo.hasPortraitTopActivity()) {
+ val screenOrientation =
+ requestedScreenOrientation ?: taskInfo.topActivityInfo?.screenOrientation
+ if (taskInfo.hasPortraitTopActivity(screenOrientation)) {
val tempWidth = ceil(targetHeight / aspectRatio).toInt()
if (tempWidth <= targetWidth) {
finalHeight = targetHeight
@@ -354,9 +367,8 @@ fun centerInArea(desiredSize: Size, areaBounds: Rect, leftStart: Int, topStart:
return Rect(newLeft, newTop, newRight, newBottom)
}
-private fun TaskInfo.hasPortraitTopActivity(): Boolean {
- val topActivityScreenOrientation =
- topActivityInfo?.screenOrientation ?: SCREEN_ORIENTATION_UNSPECIFIED
+private fun TaskInfo.hasPortraitTopActivity(screenOrientation: Int?): Boolean {
+ val topActivityScreenOrientation = screenOrientation ?: SCREEN_ORIENTATION_UNSPECIFIED
val appBounds = configuration.windowConfiguration.appBounds
return when {
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 093e8ef8bc0e..5849d4af4e7e 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
@@ -465,6 +465,30 @@ class DesktopTasksController(
return isFreeformDisplay
}
+ /** Called when the recents transition that started while in desktop is finishing. */
+ fun onRecentsInDesktopAnimationFinishing(
+ transition: IBinder,
+ finishWct: WindowContainerTransaction,
+ returnToApp: Boolean,
+ ) {
+ if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) return
+ logV("onRecentsInDesktopAnimationFinishing returnToApp=%b", returnToApp)
+ if (returnToApp) return
+ // Home/Recents only exists in the default display.
+ val activeDesk = taskRepository.getActiveDeskId(DEFAULT_DISPLAY) ?: return
+ // Not going back to the active desk, deactivate it.
+ val runOnTransitStart =
+ performDesktopExitCleanUp(
+ wct = finishWct,
+ deskId = activeDesk,
+ displayId = DEFAULT_DISPLAY,
+ willExitDesktop = true,
+ shouldEndUpAtHome = true,
+ fromRecentsTransition = true,
+ )
+ runOnTransitStart?.invoke(transition)
+ }
+
/** Creates a new desk in the given display. */
fun createDesk(displayId: Int) {
if (displayId == Display.INVALID_DISPLAY) {
@@ -835,20 +859,22 @@ class DesktopTasksController(
val requestRes = transitions.dispatchRequest(Binder(), requestInfo, /* skip= */ null)
wct.merge(requestRes.second, true)
- desktopPipTransitionObserver.get().addPendingPipTransition(
- DesktopPipTransitionObserver.PendingPipTransition(
- token = freeformTaskTransitionStarter.startPipTransition(wct),
- taskId = taskInfo.taskId,
- onSuccess = {
- onDesktopTaskEnteredPip(
- taskId = taskId,
- deskId = deskId,
- displayId = taskInfo.displayId,
- taskIsLastVisibleTaskBeforePip = isLastTask,
- )
- },
+ desktopPipTransitionObserver
+ .get()
+ .addPendingPipTransition(
+ DesktopPipTransitionObserver.PendingPipTransition(
+ token = freeformTaskTransitionStarter.startPipTransition(wct),
+ taskId = taskInfo.taskId,
+ onSuccess = {
+ onDesktopTaskEnteredPip(
+ taskId = taskId,
+ deskId = deskId,
+ displayId = taskInfo.displayId,
+ taskIsLastVisibleTaskBeforePip = isLastTask,
+ )
+ },
+ )
)
- )
} else {
snapEventHandler.removeTaskIfTiled(displayId, taskId)
val willExitDesktop = willExitDesktop(taskId, displayId, forceExitDesktop = false)
@@ -1208,6 +1234,7 @@ class DesktopTasksController(
pendingIntentBackgroundActivityStartMode =
ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS
launchBounds = bounds
+ launchDisplayId = displayId
if (DesktopModeFlags.ENABLE_SHELL_INITIAL_BOUNDS_REGRESSION_BUG_FIX.isTrue) {
// Sets launch bounds size as flexible so core can recalculate.
flexibleLaunchSize = true
@@ -1937,16 +1964,23 @@ class DesktopTasksController(
displayId: Int,
willExitDesktop: Boolean,
shouldEndUpAtHome: Boolean = true,
+ fromRecentsTransition: Boolean = false,
): RunOnTransitStart? {
if (!willExitDesktop) return null
desktopModeEnterExitTransitionListener?.onExitDesktopModeTransitionStarted(
FULLSCREEN_ANIMATION_DURATION
)
- removeWallpaperActivity(wct, displayId)
- if (shouldEndUpAtHome) {
- // If the transition should end up with user going to home, launch home with a pending
- // intent.
- addLaunchHomePendingIntent(wct, displayId)
+ // No need to clean up the wallpaper / reorder home when coming from a recents transition.
+ if (
+ !fromRecentsTransition ||
+ !DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue
+ ) {
+ removeWallpaperActivity(wct, displayId)
+ if (shouldEndUpAtHome) {
+ // If the transition should end up with user going to home, launch home with a
+ // pending intent.
+ addLaunchHomePendingIntent(wct, displayId)
+ }
}
return prepareDeskDeactivationIfNeeded(wct, deskId)
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/VisualIndicatorViewContainer.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/VisualIndicatorViewContainer.kt
index 23562388b3e5..5e4122ba14ec 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/VisualIndicatorViewContainer.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/VisualIndicatorViewContainer.kt
@@ -18,6 +18,7 @@ package com.android.wm.shell.desktopmode
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
+import android.animation.AnimatorSet
import android.animation.RectEvaluator
import android.animation.ValueAnimator
import android.app.ActivityManager
@@ -32,6 +33,8 @@ import android.view.View
import android.view.WindowManager
import android.view.WindowlessWindowManager
import android.view.animation.DecelerateInterpolator
+import android.widget.FrameLayout
+import androidx.core.animation.doOnEnd
import com.android.internal.annotations.VisibleForTesting
import com.android.window.flags.Flags
import com.android.wm.shell.R
@@ -42,6 +45,7 @@ import com.android.wm.shell.common.SyncTransactionQueue
import com.android.wm.shell.desktopmode.DesktopModeVisualIndicator.IndicatorType
import com.android.wm.shell.shared.annotations.ShellDesktopThread
import com.android.wm.shell.shared.annotations.ShellMainThread
+import com.android.wm.shell.shared.bubbles.BubbleAnythingFlagHelper
import com.android.wm.shell.shared.bubbles.BubbleDropTargetBoundsProvider
import com.android.wm.shell.windowdecor.WindowDecoration.SurfaceControlViewHostFactory
import com.android.wm.shell.windowdecor.tiling.SnapEventHandler
@@ -64,6 +68,8 @@ constructor(
private val snapEventHandler: SnapEventHandler,
) {
@VisibleForTesting var indicatorView: View? = null
+ // Optional extra indicator showing the outline of the bubble bar
+ private var barIndicatorView: View? = null
private var indicatorViewHost: SurfaceControlViewHost? = null
// Below variables and the SyncTransactionQueue are the only variables that should
// be accessed from shell main thread. Everything else should be used exclusively
@@ -93,7 +99,12 @@ constructor(
screenWidth = metrics.widthPixels
screenHeight = metrics.heightPixels
}
- indicatorView = View(context)
+ indicatorView =
+ if (BubbleAnythingFlagHelper.enableBubbleToFullscreen()) {
+ FrameLayout(context)
+ } else {
+ View(context)
+ }
val leash =
indicatorBuilder
.setName("Desktop Mode Visual Indicator")
@@ -183,23 +194,50 @@ constructor(
)
} else {
val animStartType = IndicatorType.valueOf(currentType.name)
- val animator =
- indicatorView?.let {
- VisualIndicatorAnimator.animateIndicatorType(
- it,
- layout,
- animStartType,
- newType,
- bubbleBoundsProvider,
- taskInfo.displayId,
- snapEventHandler,
- )
- } ?: return@execute
+ val indicator = indicatorView ?: return@execute
+ var animator: Animator =
+ VisualIndicatorAnimator.animateIndicatorType(
+ indicator,
+ layout,
+ animStartType,
+ newType,
+ bubbleBoundsProvider,
+ taskInfo.displayId,
+ snapEventHandler,
+ )
+ if (BubbleAnythingFlagHelper.enableBubbleToFullscreen()) {
+ if (currentType.isBubbleType() || newType.isBubbleType()) {
+ animator = addBarIndicatorAnimation(animator, currentType, newType)
+ }
+ }
animator.start()
}
}
}
+ private fun addBarIndicatorAnimation(
+ visualIndicatorAnimator: Animator,
+ currentType: IndicatorType,
+ newType: IndicatorType,
+ ): Animator {
+ if (newType.isBubbleType()) {
+ getOrCreateBubbleBarIndicator(newType)?.let { bar ->
+ return AnimatorSet().apply {
+ playTogether(visualIndicatorAnimator, fadeBarIndicatorIn(bar))
+ }
+ }
+ }
+ if (currentType.isBubbleType()) {
+ barIndicatorView?.let { bar ->
+ barIndicatorView = null
+ return AnimatorSet().apply {
+ playTogether(visualIndicatorAnimator, fadeBarIndicatorOut(bar))
+ }
+ }
+ }
+ return visualIndicatorAnimator
+ }
+
/**
* Fade indicator in as provided type.
*
@@ -223,17 +261,20 @@ constructor(
snapEventHandler: SnapEventHandler,
) {
desktopExecutor.assertCurrentThread()
- indicatorView?.let {
- it.setBackgroundResource(R.drawable.desktop_windowing_transition_background)
- val animator =
+ indicatorView?.let { indicator ->
+ indicator.setBackgroundResource(R.drawable.desktop_windowing_transition_background)
+ var animator: Animator =
VisualIndicatorAnimator.fadeBoundsIn(
- it,
+ indicator,
type,
layout,
bubbleBoundsProvider,
displayId,
snapEventHandler,
)
+ if (BubbleAnythingFlagHelper.enableBubbleToFullscreen()) {
+ animator = addBarIndicatorAnimation(animator, IndicatorType.NO_INDICATOR, type)
+ }
animator.start()
}
}
@@ -259,7 +300,7 @@ constructor(
desktopExecutor.execute {
indicatorView?.let {
val animStartType = IndicatorType.valueOf(currentType.name)
- val animator =
+ var animator: Animator =
VisualIndicatorAnimator.fadeBoundsOut(
it,
animStartType,
@@ -268,6 +309,10 @@ constructor(
displayId,
snapEventHandler,
)
+ if (BubbleAnythingFlagHelper.enableBubbleToFullscreen()) {
+ animator =
+ addBarIndicatorAnimation(animator, currentType, IndicatorType.NO_INDICATOR)
+ }
animator.addListener(
object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
@@ -302,6 +347,38 @@ constructor(
isReleased = true
}
+ private fun getOrCreateBubbleBarIndicator(type: IndicatorType): View? {
+ val container = indicatorView as? FrameLayout ?: return null
+ val onLeft = type == IndicatorType.TO_BUBBLE_LEFT_INDICATOR
+ val bounds = bubbleBoundsProvider?.getBarDropTargetBounds(onLeft) ?: return null
+ val lp = FrameLayout.LayoutParams(bounds.width(), bounds.height())
+ lp.leftMargin = bounds.left
+ lp.topMargin = bounds.top
+ if (barIndicatorView == null) {
+ val indicator = View(container.context)
+ indicator.setBackgroundResource(R.drawable.desktop_windowing_transition_background)
+ container.addView(indicator, lp)
+ barIndicatorView = indicator
+ } else {
+ barIndicatorView?.layoutParams = lp
+ }
+ return barIndicatorView
+ }
+
+ private fun fadeBarIndicatorIn(barIndicator: View): Animator {
+ // Use layout bounds as the end bounds in case the view has not been laid out yet
+ val lp = barIndicator.layoutParams
+ val endBounds = Rect(0, 0, lp.width, lp.height)
+ return VisualIndicatorAnimator.fadeBoundsIn(barIndicator, endBounds)
+ }
+
+ private fun fadeBarIndicatorOut(barIndicator: View): Animator {
+ val startBounds = Rect(0, 0, barIndicator.width, barIndicator.height)
+ val barAnimator = VisualIndicatorAnimator.fadeBoundsOut(barIndicator, startBounds)
+ barAnimator.doOnEnd { (indicatorView as? FrameLayout)?.removeView(barIndicator) }
+ return barAnimator
+ }
+
/**
* Animator for Desktop Mode transitions which supports bounds and alpha animation. Functions
* should only be called from the desktop executor.
@@ -383,9 +460,13 @@ constructor(
displayId,
snapEventHandler,
)
+ return fadeBoundsIn(view, endBounds)
+ }
+
+ @ShellDesktopThread
+ fun fadeBoundsIn(view: View, endBounds: Rect): VisualIndicatorAnimator {
val startBounds = getMinBounds(endBounds)
view.background.bounds = startBounds
-
val animator = VisualIndicatorAnimator(view, startBounds, endBounds)
animator.interpolator = DecelerateInterpolator()
setupIndicatorAnimation(animator, AlphaAnimType.ALPHA_FADE_IN_ANIM)
@@ -409,6 +490,11 @@ constructor(
displayId,
snapEventHandler,
)
+ return fadeBoundsOut(view, startBounds)
+ }
+
+ @ShellDesktopThread
+ fun fadeBoundsOut(view: View, startBounds: Rect): VisualIndicatorAnimator {
val endBounds = getMinBounds(startBounds)
view.background.bounds = startBounds
val animator = VisualIndicatorAnimator(view, startBounds, endBounds)
@@ -571,4 +657,9 @@ constructor(
}
}
}
+
+ private fun IndicatorType.isBubbleType(): Boolean {
+ return this == IndicatorType.TO_BUBBLE_LEFT_INDICATOR ||
+ this == IndicatorType.TO_BUBBLE_RIGHT_INDICATOR
+ }
}
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 9dec96933ee5..454419c805c1 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
@@ -18,9 +18,12 @@ package com.android.wm.shell.desktopmode.multidesks
import android.os.IBinder
/** Represents shell-started transitions involving desks. */
-sealed class DeskTransition {
+sealed interface DeskTransition {
/** The transition token. */
- abstract val token: IBinder
+ val token: IBinder
+
+ /** Returns a copy of this desk transition with a new transition token. */
+ fun copyWithToken(token: IBinder): DeskTransition
/** A transition to remove a desk and its tasks from a display. */
data class RemoveDesk(
@@ -29,11 +32,15 @@ sealed class DeskTransition {
val deskId: Int,
val tasks: Set<Int>,
val onDeskRemovedListener: OnDeskRemovedListener?,
- ) : DeskTransition()
+ ) : DeskTransition {
+ override fun copyWithToken(token: IBinder): DeskTransition = copy(token)
+ }
/** A transition to activate a desk in its display. */
data class ActivateDesk(override val token: IBinder, val displayId: Int, val deskId: Int) :
- DeskTransition()
+ DeskTransition {
+ override fun copyWithToken(token: IBinder): DeskTransition = copy(token)
+ }
/** A transition to activate a desk by moving an outside task to it. */
data class ActiveDeskWithTask(
@@ -41,8 +48,12 @@ sealed class DeskTransition {
val displayId: Int,
val deskId: Int,
val enterTaskId: Int,
- ) : DeskTransition()
+ ) : DeskTransition {
+ override fun copyWithToken(token: IBinder): DeskTransition = copy(token)
+ }
/** A transition to deactivate a desk. */
- data class DeactivateDesk(override val token: IBinder, val deskId: Int) : DeskTransition()
+ data class DeactivateDesk(override val token: IBinder, val deskId: Int) : DeskTransition {
+ override fun copyWithToken(token: IBinder): DeskTransition = copy(token)
+ }
}
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 b521b2e8c942..588b5c350330 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
@@ -39,6 +39,7 @@ class DesksTransitionObserver(
val transitions = deskTransitions[transition.token] ?: mutableSetOf()
transitions += transition
deskTransitions[transition.token] = transitions
+ logD("Added pending desk transition: %s", transition)
}
/**
@@ -51,6 +52,43 @@ class DesksTransitionObserver(
deskTransitions.forEach { deskTransition -> handleDeskTransition(info, deskTransition) }
}
+ /**
+ * Called when a transition is merged with another transition, which may include transitions not
+ * tracked by this observer.
+ */
+ fun onTransitionMerged(merged: IBinder, playing: IBinder) {
+ if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) return
+ val transitions = deskTransitions.remove(merged) ?: return
+ deskTransitions[playing] =
+ transitions
+ .map { deskTransition -> deskTransition.copyWithToken(token = playing) }
+ .toMutableSet()
+ }
+
+ /**
+ * Called when any transition finishes, which may include transitions not tracked by this
+ * observer.
+ *
+ * Most [DeskTransition]s are not handled here because [onTransitionReady] handles them and
+ * removes them from the map. However, there can be cases where the transition was added after
+ * [onTransitionReady] had already been called and they need to be handled here, such as the
+ * swipe-to-home recents transition when there is no book-end transition.
+ */
+ fun onTransitionFinished(transition: IBinder) {
+ if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) return
+ val deskTransitions = deskTransitions.remove(transition) ?: return
+ deskTransitions.forEach { deskTransition ->
+ if (deskTransition is DeskTransition.DeactivateDesk) {
+ handleDeactivateDeskTransition(null, deskTransition)
+ } else {
+ logW(
+ "Unexpected desk transition finished without being handled: %s",
+ deskTransition,
+ )
+ }
+ }
+ }
+
private fun handleDeskTransition(info: TransitionInfo, deskTransition: DeskTransition) {
logD("Desk transition ready: %s", deskTransition)
val desktopRepository = desktopUserRepositories.current
@@ -102,41 +140,54 @@ 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)
+ is DeskTransition.DeactivateDesk -> handleDeactivateDeskTransition(info, deskTransition)
+ }
+ }
+
+ private fun handleDeactivateDeskTransition(
+ info: TransitionInfo?,
+ deskTransition: DeskTransition.DeactivateDesk,
+ ) {
+ logD("handleDeactivateDeskTransition: %s", deskTransition)
+ val desktopRepository = desktopUserRepositories.current
+ var deskChangeFound = false
+
+ val changes = info?.changes ?: emptyList()
+ for (change in changes) {
+ val isDeskChange = desksOrganizer.isDeskChange(change, deskTransition.deskId)
+ if (isDeskChange) {
+ deskChangeFound = 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 (!deskChangeFound) {
+ 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 fun logW(msg: String, vararg arguments: Any?) {
+ ProtoLog.w(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/freeform/FreeformTaskTransitionObserver.java b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java
index 0bf2ea61b0a4..e7492f17835a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java
@@ -207,6 +207,7 @@ public class FreeformTaskTransitionObserver implements Transitions.TransitionObs
@Override
public void onTransitionMerged(@NonNull IBinder merged, @NonNull IBinder playing) {
+ mDesksTransitionObserver.ifPresent(o -> o.onTransitionMerged(merged, playing));
if (DesktopModeFlags.ENABLE_FULLY_IMMERSIVE_IN_DESKTOP.isTrue()) {
// TODO(b/367268953): Remove when DesktopTaskListener is introduced.
mDesktopImmersiveController.ifPresent(h -> h.onTransitionMerged(merged, playing));
@@ -232,6 +233,7 @@ public class FreeformTaskTransitionObserver implements Transitions.TransitionObs
@Override
public void onTransitionFinished(@NonNull IBinder transition, boolean aborted) {
+ mDesksTransitionObserver.ifPresent(o -> o.onTransitionFinished(transition));
if (DesktopModeFlags.ENABLE_FULLY_IMMERSIVE_IN_DESKTOP.isTrue()) {
// TODO(b/367268953): Remove when DesktopTaskListener is introduced.
mDesktopImmersiveController.ifPresent(h -> h.onTransitionFinished(transition, aborted));
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java
index 4e341ac9b7eb..0e974ef9083b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java
@@ -31,6 +31,8 @@ import android.content.pm.ActivityInfo;
import android.content.res.Configuration;
import android.graphics.Rect;
import android.os.Bundle;
+import android.os.Debug;
+import android.util.Log;
import android.view.SurfaceControl;
import android.window.DesktopExperienceFlags;
import android.window.DisplayAreaInfo;
@@ -369,7 +371,13 @@ public class PipController implements ConfigurationChangeListener,
mPipBoundsAlgorithm.applySnapFraction(toBounds, snapFraction);
mPipBoundsState.setBounds(toBounds);
}
- t.setBounds(mPipTransitionState.getPipTaskToken(), mPipBoundsState.getBounds());
+ if (mPipTransitionState.getPipTaskToken() == null) {
+ Log.wtf(TAG, "PipController.onDisplayChange no PiP task token"
+ + " state=" + mPipTransitionState.getState()
+ + " callers=\n" + Debug.getCallers(4, " "));
+ } else {
+ t.setBounds(mPipTransitionState.getPipTaskToken(), mPipBoundsState.getBounds());
+ }
// Update the size spec in PipBoundsState afterwards.
mPipBoundsState.updateMinMaxSize(mPipBoundsState.getAspectRatio());
}
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 3e03e001c49b..8e10f15a36cc 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
@@ -1135,6 +1135,7 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler,
if (openingLeafCount > 0) {
appearedTargets = new RemoteAnimationTarget[openingLeafCount];
}
+ boolean onlyOpeningPausedTasks = true;
int nextTargetIdx = 0;
for (int i = 0; i < openingTasks.size(); ++i) {
final TransitionInfo.Change change = openingTasks.get(i);
@@ -1188,6 +1189,7 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler,
" opening new leaf taskId=%d wasClosing=%b",
target.taskId, wasClosing);
mOpeningTasks.add(new TaskState(change, target.leash));
+ onlyOpeningPausedTasks = false;
} else {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
" opening new taskId=%d", change.getTaskInfo().taskId);
@@ -1196,10 +1198,17 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler,
// is only animating the leafs.
startT.show(change.getLeash());
mOpeningTasks.add(new TaskState(change, null));
+ onlyOpeningPausedTasks = false;
}
}
didMergeThings = true;
- mState = STATE_NEW_TASK;
+ if (!onlyOpeningPausedTasks) {
+ // If we are only opening paused leaf tasks, then we aren't actually quick
+ // switching or launching a new task from overview, and if Launcher requests to
+ // finish(toHome=false) as a response to the pausing tasks being opened again,
+ // we should allow that to be considered returningToApp
+ mState = STATE_NEW_TASK;
+ }
}
if (mPausingTasks.isEmpty()) {
// The pausing tasks may be removed by the incoming closing tasks.
@@ -1368,8 +1377,9 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler,
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
"[%d] RecentsController.finishInner: toHome=%b userLeave=%b "
- + "willFinishToHome=%b state=%d reason=%s",
- mInstanceId, toHome, sendUserLeaveHint, mWillFinishToHome, mState, reason);
+ + "willFinishToHome=%b state=%d hasPausingTasks=%b reason=%s",
+ mInstanceId, toHome, sendUserLeaveHint, mWillFinishToHome, mState,
+ mPausingTasks != null, reason);
final SurfaceControl.Transaction t = mFinishTransaction;
final WindowContainerTransaction wct = new WindowContainerTransaction();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java
index a0fb62508cc1..e19ad23b2125 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java
@@ -394,6 +394,9 @@ public class DefaultMixedHandler implements MixedTransitionHandler,
if (mixed.mType == MixedTransition.TYPE_RECENTS_DURING_SPLIT) {
((RecentsMixedTransition) mixed).onAnimateRecentsDuringSplitFinishing(
returnToApp, finishWct, finishT);
+ } else if (mixed.mType == MixedTransition.TYPE_RECENTS_DURING_DESKTOP) {
+ ((RecentsMixedTransition) mixed).onAnimateRecentsDuringDesktopFinishing(
+ returnToApp, finishWct);
}
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RecentsMixedTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RecentsMixedTransition.java
index 1e926c57ca61..cfefc1f1ac66 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RecentsMixedTransition.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RecentsMixedTransition.java
@@ -198,6 +198,15 @@ class RecentsMixedTransition extends DefaultMixedHandler.MixedTransition {
}
}
+ /**
+ * Called when the recents animation during desktop is about to finish.
+ */
+ void onAnimateRecentsDuringDesktopFinishing(boolean returnToApp,
+ @NonNull WindowContainerTransaction finishWct) {
+ mDesktopTasksController.onRecentsInDesktopAnimationFinishing(mTransition, finishWct,
+ returnToApp);
+ }
+
@Override
void mergeAnimation(
@NonNull IBinder transition, @NonNull TransitionInfo info,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
index 003ef1d453fc..4f49ebcd2e83 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
@@ -602,6 +602,11 @@ public class Transitions implements RemoteCallable<Transitions>,
// Just in case there is a race with another animation (eg. recents finish()).
// Changes are visible->visible so it's a problem if it isn't visible.
t.show(leash);
+ // If there is a transient launch followed by a launch of one of the pausing tasks,
+ // we may end up with TRANSIT_TO_BACK followed by a CHANGE (w/ flag MOVE_TO_TOP),
+ // but since we are hiding the leash in the finish transaction above, we should also
+ // update the finish transaction here to reflect the change in visibility
+ finishT.show(leash);
}
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CarWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CarWindowDecoration.java
index 3182745d813e..f6acca95916f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CarWindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CarWindowDecoration.java
@@ -27,6 +27,7 @@ import android.view.InsetsState;
import android.view.SurfaceControl;
import android.view.View;
import android.view.WindowInsets;
+import android.window.DesktopModeFlags;
import android.window.WindowContainerTransaction;
import androidx.annotation.NonNull;
@@ -88,6 +89,9 @@ public class CarWindowDecoration extends WindowDecoration<WindowDecorLinearLayou
updateRelayoutParams(mRelayoutParams, taskInfo, isCaptionVisible);
relayout(mRelayoutParams, startT, finishT, wct, mRootView, mResult);
+ if (DesktopModeFlags.ENABLE_DESKTOP_APP_HANDLE_ANIMATION.isTrue()) {
+ setCaptionVisibility(isCaptionVisible);
+ }
// After this line, mTaskInfo is up-to-date and should be used instead of taskInfo
mBgExecutor.execute(() -> mTaskOrganizer.applyTransaction(wct));
@@ -102,6 +106,15 @@ public class CarWindowDecoration extends WindowDecoration<WindowDecorLinearLayou
}
}
+ private void setCaptionVisibility(boolean visible) {
+ if (mRootView == null) {
+ return;
+ }
+ final int v = visible ? View.VISIBLE : View.GONE;
+ final View captionView = mRootView.findViewById(getCaptionViewId());
+ captionView.setVisibility(v);
+ }
+
@Override
@NonNull
Rect calculateValidDragArea() {
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 800faca830f4..07b385bdb045 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
@@ -108,6 +108,8 @@ import com.android.wm.shell.common.MultiInstanceHelper;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.SyncTransactionQueue;
import com.android.wm.shell.compatui.CompatUIController;
+import com.android.wm.shell.compatui.api.CompatUIHandler;
+import com.android.wm.shell.compatui.impl.CompatUIRequests;
import com.android.wm.shell.desktopmode.DesktopActivityOrientationChangeHandler;
import com.android.wm.shell.desktopmode.DesktopImmersiveController;
import com.android.wm.shell.desktopmode.DesktopModeEventLogger;
@@ -266,6 +268,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel,
private final DesktopTilingDecorViewModel mDesktopTilingDecorViewModel;
private final MultiDisplayDragMoveIndicatorController mMultiDisplayDragMoveIndicatorController;
private final LatencyTracker mLatencyTracker;
+ private final CompatUIHandler mCompatUI;
public DesktopModeWindowDecorViewModel(
Context context,
@@ -306,7 +309,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel,
RecentsTransitionHandler recentsTransitionHandler,
DesktopModeCompatPolicy desktopModeCompatPolicy,
DesktopTilingDecorViewModel desktopTilingDecorViewModel,
- MultiDisplayDragMoveIndicatorController multiDisplayDragMoveIndicatorController) {
+ MultiDisplayDragMoveIndicatorController multiDisplayDragMoveIndicatorController,
+ CompatUIHandler compatUI) {
this(
context,
shellExecutor,
@@ -353,7 +357,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel,
recentsTransitionHandler,
desktopModeCompatPolicy,
desktopTilingDecorViewModel,
- multiDisplayDragMoveIndicatorController);
+ multiDisplayDragMoveIndicatorController,
+ compatUI);
}
@VisibleForTesting
@@ -403,7 +408,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel,
RecentsTransitionHandler recentsTransitionHandler,
DesktopModeCompatPolicy desktopModeCompatPolicy,
DesktopTilingDecorViewModel desktopTilingDecorViewModel,
- MultiDisplayDragMoveIndicatorController multiDisplayDragMoveIndicatorController) {
+ MultiDisplayDragMoveIndicatorController multiDisplayDragMoveIndicatorController,
+ CompatUIHandler compatUI) {
mContext = context;
mMainExecutor = shellExecutor;
mMainHandler = mainHandler;
@@ -444,6 +450,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel,
mActivityOrientationChangeHandler = activityOrientationChangeHandler;
mAssistContentRequester = assistContentRequester;
mWindowDecorViewHostSupplier = windowDecorViewHostSupplier;
+ mCompatUI = compatUI;
mOnDisplayChangingListener = (displayId, fromRotation, toRotation, displayAreaInfo, t) -> {
DesktopModeWindowDecoration decoration;
RunningTaskInfo taskInfo;
@@ -1864,6 +1871,11 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel,
CompatUIController.launchUserAspectRatioSettings(mContext, taskInfo);
return Unit.INSTANCE;
});
+ windowDecoration.setOnRestartClickListener(() -> {
+ mCompatUI.sendCompatUIRequest(new CompatUIRequests.DisplayCompatShowRestartDialog(
+ taskInfo.taskId));
+ return Unit.INSTANCE;
+ });
windowDecoration.setOnMaximizeHoverListener(() -> {
if (!windowDecoration.isMaximizeMenuActive()) {
mDesktopModeUiEventLogger.log(taskInfo,
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 e8019e47e374..49c380a1a736 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
@@ -163,6 +163,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
private Function0<Unit> mOnNewWindowClickListener;
private Function0<Unit> mOnManageWindowsClickListener;
private Function0<Unit> mOnChangeAspectRatioClickListener;
+ private Function0<Unit> mOnRestartClickListener;
private Function0<Unit> mOnMaximizeHoverListener;
private DragPositioningCallback mDragPositioningCallback;
private DragResizeInputListener mDragResizeListener;
@@ -408,6 +409,11 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
mOnChangeAspectRatioClickListener = listener;
}
+ /** Registers a listener to be called when the aspect ratio action is triggered. */
+ void setOnRestartClickListener(Function0<Unit> listener) {
+ mOnRestartClickListener = listener;
+ }
+
/** Registers a listener to be called when the maximize header button is hovered. */
void setOnMaximizeHoverListener(Function0<Unit> listener) {
mOnMaximizeHoverListener = listener;
@@ -863,7 +869,8 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
if (!isAppHandle(mWindowDecorViewHolder)) return;
asAppHandle(mWindowDecorViewHolder).bindData(new AppHandleViewHolder.HandleData(
mTaskInfo, determineHandlePosition(), mResult.mCaptionWidth,
- mResult.mCaptionHeight, isCaptionVisible()
+ mResult.mCaptionHeight, /* showInputLayer= */ isCaptionVisible(),
+ /* isCaptionVisible= */ isCaptionVisible()
));
}
@@ -876,7 +883,8 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
inFullImmersive,
hasGlobalFocus,
/* maximizeHoverEnabled= */ canOpenMaximizeMenu(
- /* animatingTaskResizeOrReposition= */ false)
+ /* animatingTaskResizeOrReposition= */ false),
+ isCaptionVisible()
));
}
@@ -1459,6 +1467,8 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
&& mMinimumInstancesFound;
final boolean shouldShowChangeAspectRatioButton = HandleMenu.Companion
.shouldShowChangeAspectRatioButton(mTaskInfo);
+ final boolean shouldShowRestartButton = HandleMenu.Companion
+ .shouldShowRestartButton(mTaskInfo);
final boolean inDesktopImmersive = mDesktopUserRepositories.getProfile(mTaskInfo.userId)
.isTaskInFullImmersiveState(mTaskInfo.taskId);
final boolean isBrowserApp = isBrowserApp();
@@ -1475,6 +1485,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
shouldShowManageWindowsButton,
shouldShowChangeAspectRatioButton,
isDesktopModeSupportedOnDisplay(mContext, mDisplay),
+ shouldShowRestartButton,
isBrowserApp,
isBrowserApp ? getAppLink() : getBrowserLink(),
mResult.mCaptionWidth,
@@ -1511,6 +1522,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
}
return Unit.INSTANCE;
},
+ /* onRestartClickListener= */ mOnRestartClickListener,
/* onCloseMenuClickListener= */ () -> {
closeHandleMenu();
return Unit.INSTANCE;
@@ -1866,7 +1878,8 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
DesktopModeUtils.isTaskMaximized(mTaskInfo, mDisplayController),
inFullImmersive,
isFocused(),
- /* maximizeHoverEnabled= */ canOpenMaximizeMenu(animatingTaskResizeOrReposition)));
+ /* maximizeHoverEnabled= */ canOpenMaximizeMenu(animatingTaskResizeOrReposition),
+ isCaptionVisible()));
}
/**
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt
index 9cc64ac9c276..f64b0d8695d2 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt
@@ -37,7 +37,6 @@ import android.view.WindowInsets.Type.systemBars
import android.view.WindowManager
import android.widget.ImageButton
import android.widget.ImageView
-import android.widget.LinearLayout
import android.widget.Space
import android.window.DesktopModeFlags
import android.window.SurfaceSyncGroup
@@ -95,6 +94,7 @@ class HandleMenu(
private val shouldShowManageWindowsButton: Boolean,
private val shouldShowChangeAspectRatioButton: Boolean,
private val shouldShowDesktopModeButton: Boolean,
+ private val shouldShowRestartButton: Boolean,
private val isBrowserApp: Boolean,
private val openInAppOrBrowserIntent: Intent?,
private val captionWidth: Int,
@@ -139,7 +139,8 @@ class HandleMenu(
private val shouldShowMoreActionsPill: Boolean
get() = SHOULD_SHOW_SCREENSHOT_BUTTON || shouldShowNewWindowButton ||
- shouldShowManageWindowsButton || shouldShowChangeAspectRatioButton
+ shouldShowManageWindowsButton || shouldShowChangeAspectRatioButton ||
+ shouldShowRestartButton
private var loadAppInfoJob: Job? = null
@@ -157,6 +158,7 @@ class HandleMenu(
onChangeAspectRatioClickListener: () -> Unit,
openInAppOrBrowserClickListener: (Intent) -> Unit,
onOpenByDefaultClickListener: () -> Unit,
+ onRestartClickListener: () -> Unit,
onCloseMenuClickListener: () -> Unit,
onOutsideTouchListener: () -> Unit,
forceShowSystemBars: Boolean = false,
@@ -176,6 +178,7 @@ class HandleMenu(
onChangeAspectRatioClickListener = onChangeAspectRatioClickListener,
openInAppOrBrowserClickListener = openInAppOrBrowserClickListener,
onOpenByDefaultClickListener = onOpenByDefaultClickListener,
+ onRestartClickListener = onRestartClickListener,
onCloseMenuClickListener = onCloseMenuClickListener,
onOutsideTouchListener = onOutsideTouchListener,
forceShowSystemBars = forceShowSystemBars,
@@ -198,6 +201,7 @@ class HandleMenu(
onChangeAspectRatioClickListener: () -> Unit,
openInAppOrBrowserClickListener: (Intent) -> Unit,
onOpenByDefaultClickListener: () -> Unit,
+ onRestartClickListener: () -> Unit,
onCloseMenuClickListener: () -> Unit,
onOutsideTouchListener: () -> Unit,
forceShowSystemBars: Boolean = false,
@@ -212,6 +216,7 @@ class HandleMenu(
shouldShowManageWindowsButton = shouldShowManageWindowsButton,
shouldShowChangeAspectRatioButton = shouldShowChangeAspectRatioButton,
shouldShowDesktopModeButton = shouldShowDesktopModeButton,
+ shouldShowRestartButton = shouldShowRestartButton,
isBrowserApp = isBrowserApp
).apply {
bind(taskInfo, shouldShowMoreActionsPill)
@@ -225,6 +230,7 @@ class HandleMenu(
this.onOpenInAppOrBrowserClickListener = {
openInAppOrBrowserClickListener.invoke(openInAppOrBrowserIntent!!)
}
+ this.onRestartClickListener = onRestartClickListener
this.onOpenByDefaultClickListener = onOpenByDefaultClickListener
this.onCloseMenuClickListener = onCloseMenuClickListener
this.onOutsideTouchListener = onOutsideTouchListener
@@ -431,6 +437,10 @@ class HandleMenu(
R.dimen.desktop_mode_handle_menu_change_aspect_ratio_height
)
}
+ if (!shouldShowRestartButton) {
+ menuHeight -= loadDimensionPixelSize(
+ R.dimen.desktop_mode_handle_menu_restart_button_height)
+ }
if (!shouldShowMoreActionsPill) {
menuHeight -= pillTopMargin
}
@@ -473,6 +483,7 @@ class HandleMenu(
private val shouldShowManageWindowsButton: Boolean,
private val shouldShowChangeAspectRatioButton: Boolean,
private val shouldShowDesktopModeButton: Boolean,
+ private val shouldShowRestartButton: Boolean,
private val isBrowserApp: Boolean
) {
val rootView = LayoutInflater.from(context)
@@ -501,6 +512,12 @@ class HandleMenu(
t = iconButtondrawableBaseInset,
b = iconButtondrawableBaseInset, l = 0, r = iconButtondrawableShiftInset
)
+ private val iconButtonDrawableInsetStart
+ get() =
+ if (context.isRtl) iconButtonDrawableInsetsRight else iconButtonDrawableInsetsLeft
+ private val iconButtonDrawableInsetEnd
+ get() =
+ if (context.isRtl) iconButtonDrawableInsetsLeft else iconButtonDrawableInsetsRight
// App Info Pill.
private val appInfoPill = rootView.requireViewById<View>(R.id.app_info_pill)
@@ -544,14 +561,15 @@ class HandleMenu(
.requireViewById<HandleMenuActionButton>(R.id.manage_windows_button)
private val changeAspectRatioBtn = moreActionsPill
.requireViewById<HandleMenuActionButton>(R.id.change_aspect_ratio_button)
+ private val restartBtn = moreActionsPill
+ .requireViewById<HandleMenuActionButton>(R.id.handle_menu_restart_button)
// Open in Browser/App Pill.
private val openInAppOrBrowserPill = rootView.requireViewById<View>(
R.id.open_in_app_or_browser_pill
)
- private val openInAppOrBrowserBtn = openInAppOrBrowserPill.requireViewById<View>(
- R.id.open_in_app_or_browser_button
- )
+ private val openInAppOrBrowserBtn = openInAppOrBrowserPill
+ .requireViewById<HandleMenuActionButton>(R.id.open_in_app_or_browser_button)
private val openByDefaultBtn = openInAppOrBrowserPill.requireViewById<ImageButton>(
R.id.open_by_default_button
)
@@ -570,6 +588,7 @@ class HandleMenu(
var onChangeAspectRatioClickListener: (() -> Unit)? = null
var onOpenInAppOrBrowserClickListener: (() -> Unit)? = null
var onOpenByDefaultClickListener: (() -> Unit)? = null
+ var onRestartClickListener: (() -> Unit)? = null
var onCloseMenuClickListener: (() -> Unit)? = null
var onOutsideTouchListener: (() -> Unit)? = null
@@ -586,6 +605,7 @@ class HandleMenu(
newWindowBtn.setOnClickListener { onNewWindowClickListener?.invoke() }
manageWindowBtn.setOnClickListener { onManageWindowsClickListener?.invoke() }
changeAspectRatioBtn.setOnClickListener { onChangeAspectRatioClickListener?.invoke() }
+ restartBtn.setOnClickListener { onRestartClickListener?.invoke() }
rootView.setOnTouchListener { _, event ->
if (event.actionMasked == ACTION_OUTSIDE) {
@@ -758,20 +778,16 @@ class HandleMenu(
floatingBtn.isEnabled = !taskInfo.isPinned
floatingBtn.imageTintList = style.windowingButtonColor
desktopBtn.isGone = !shouldShowDesktopModeButton
+ desktopBtnSpace.isGone = !shouldShowDesktopModeButton
desktopBtn.isSelected = taskInfo.isFreeform
desktopBtn.isEnabled = !taskInfo.isFreeform
desktopBtn.imageTintList = style.windowingButtonColor
- val startInsets = if (context.isRtl) iconButtonDrawableInsetsRight
- else iconButtonDrawableInsetsLeft
- val endInsets = if (context.isRtl) iconButtonDrawableInsetsLeft
- else iconButtonDrawableInsetsRight
-
fullscreenBtn.apply {
background = createBackgroundDrawable(
color = style.textColor,
cornerRadius = iconButtonRippleRadius,
- drawableInsets = startInsets
+ drawableInsets = iconButtonDrawableInsetStart
)
}
@@ -795,7 +811,7 @@ class HandleMenu(
background = createBackgroundDrawable(
color = style.textColor,
cornerRadius = iconButtonRippleRadius,
- drawableInsets = endInsets
+ drawableInsets = iconButtonDrawableInsetEnd
)
}
}
@@ -808,20 +824,16 @@ class HandleMenu(
newWindowBtn to shouldShowNewWindowButton,
manageWindowBtn to shouldShowManageWindowsButton,
changeAspectRatioBtn to shouldShowChangeAspectRatioButton,
- ).forEach {
- val button = it.first
- val shouldShow = it.second
-
- val buttonRoot = button.requireViewById<LinearLayout>(R.id.action_button)
- val label = buttonRoot.requireViewById<MarqueedTextView>(R.id.label)
- val image = buttonRoot.requireViewById<ImageView>(R.id.image)
-
- button.isGone = !shouldShow
- label.apply {
- setTextColor(style.textColor)
- startMarquee()
+ restartBtn to shouldShowRestartButton,
+ ).forEach { (button, shouldShow) ->
+ button.apply {
+ isGone = !shouldShow
+ textView.apply {
+ setTextColor(style.textColor)
+ startMarquee()
+ }
+ iconView.imageTintList = ColorStateList.valueOf(style.textColor)
}
- image.imageTintList = ColorStateList.valueOf(style.textColor)
}
}
@@ -837,20 +849,24 @@ class HandleMenu(
getString(R.string.open_in_browser_text)
}
- val buttonRoot = openInAppOrBrowserBtn.requireViewById<LinearLayout>(R.id.action_button)
- val label = openInAppOrBrowserBtn.requireViewById<MarqueedTextView>(R.id.label)
- val image = openInAppOrBrowserBtn.requireViewById<ImageView>(R.id.image)
- openInAppOrBrowserBtn.contentDescription = btnText
- buttonRoot.contentDescription = btnText
- label.apply {
- text = btnText
- setTextColor(style.textColor)
- startMarquee()
+ openInAppOrBrowserBtn.apply {
+ contentDescription = btnText
+ textView.apply {
+ text = btnText
+ setTextColor(style.textColor)
+ startMarquee()
+ }
+ iconView.imageTintList = ColorStateList.valueOf(style.textColor)
}
- image.imageTintList = ColorStateList.valueOf(style.textColor)
- openByDefaultBtn.isGone = isBrowserApp
- openByDefaultBtn.imageTintList = ColorStateList.valueOf(style.textColor)
+ openByDefaultBtn.apply {
+ isGone = isBrowserApp
+ imageTintList = ColorStateList.valueOf(style.textColor)
+ background = createBackgroundDrawable(
+ color = style.textColor,
+ cornerRadius = iconButtonRippleRadius,
+ drawableInsets = iconButtonDrawableInsetEnd)
+ }
}
private fun getString(@StringRes resId: Int): String = context.resources.getString(resId)
@@ -873,6 +889,13 @@ class HandleMenu(
fun shouldShowChangeAspectRatioButton(taskInfo: RunningTaskInfo): Boolean =
taskInfo.appCompatTaskInfo.eligibleForUserAspectRatioButton() &&
taskInfo.windowingMode == WindowConfiguration.WINDOWING_MODE_FULLSCREEN
+
+ /**
+ * Returns whether the restart button should be shown for the task. It usually means that
+ * the task has moved to a different display.
+ */
+ fun shouldShowRestartButton(taskInfo: RunningTaskInfo): Boolean =
+ taskInfo.appCompatTaskInfo.isRestartMenuEnabledForDisplayMove
}
}
@@ -891,6 +914,7 @@ interface HandleMenuFactory {
shouldShowManageWindowsButton: Boolean,
shouldShowChangeAspectRatioButton: Boolean,
shouldShowDesktopModeButton: Boolean,
+ shouldShowRestartButton: Boolean,
isBrowserApp: Boolean,
openInAppOrBrowserIntent: Intent?,
captionWidth: Int,
@@ -915,6 +939,7 @@ object DefaultHandleMenuFactory : HandleMenuFactory {
shouldShowManageWindowsButton: Boolean,
shouldShowChangeAspectRatioButton: Boolean,
shouldShowDesktopModeButton: Boolean,
+ shouldShowRestartButton: Boolean,
isBrowserApp: Boolean,
openInAppOrBrowserIntent: Intent?,
captionWidth: Int,
@@ -935,6 +960,7 @@ object DefaultHandleMenuFactory : HandleMenuFactory {
shouldShowManageWindowsButton,
shouldShowChangeAspectRatioButton,
shouldShowDesktopModeButton,
+ shouldShowRestartButton,
isBrowserApp,
openInAppOrBrowserIntent,
captionWidth,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuActionButton.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuActionButton.kt
index a723a7a4ac20..7aba54eef899 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuActionButton.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuActionButton.kt
@@ -39,20 +39,18 @@ class HandleMenuActionButton @JvmOverloads constructor(
defStyleAttr: Int = 0
) : LinearLayout(context, attrs, defStyleAttr) {
- private val rootElement: LinearLayout
- private val iconView: ImageView
- private val textView: MarqueedTextView
+ val iconView: ImageView
+ val textView: MarqueedTextView
init {
- val view = LayoutInflater.from(context).inflate(
+ LayoutInflater.from(context).inflate(
R.layout.desktop_mode_window_decor_handle_menu_action_button, this, true)
- rootElement = findViewById(R.id.action_button)
iconView = findViewById(R.id.image)
textView = findViewById(R.id.label)
context.withStyledAttributes(attrs, R.styleable.HandleMenuActionButton) {
+ contentDescription = getString(R.styleable.HandleMenuActionButton_android_text)
textView.text = getString(R.styleable.HandleMenuActionButton_android_text)
- rootElement.contentDescription = getString(R.styleable.HandleMenuActionButton_android_text)
textView.setTextColor(getColor(R.styleable.HandleMenuActionButton_android_textColor, 0))
iconView.setImageResource(getResourceId(
R.styleable.HandleMenuActionButton_android_src, 0))
@@ -62,15 +60,6 @@ class HandleMenuActionButton @JvmOverloads constructor(
}
/**
- * Sets a listener to be invoked when this view is clicked.
- *
- * @param l the [OnClickListener] that receives click events.
- */
- override fun setOnClickListener(l: OnClickListener?) {
- rootElement.setOnClickListener(l)
- }
-
- /**
* Sets the text color for the text inside the button.
*
* @param color the color to set for the text, as a color integer.
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 6fd963f4203d..6a9b366dfb97 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
@@ -48,6 +48,7 @@ import android.view.View;
import android.view.WindowManager;
import android.view.WindowlessWindowManager;
import android.window.DesktopExperienceFlags;
+import android.window.DesktopModeFlags;
import android.window.SurfaceSyncGroup;
import android.window.TaskConstants;
import android.window.WindowContainerToken;
@@ -652,7 +653,9 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer>
*/
private void updateCaptionVisibility(View rootView, @NonNull RelayoutParams params) {
mIsCaptionVisible = params.mIsCaptionVisible;
- setCaptionVisibility(rootView, mIsCaptionVisible);
+ if (!DesktopModeFlags.ENABLE_DESKTOP_APP_HANDLE_ANIMATION.isTrue()) {
+ setCaptionVisibility(rootView, mIsCaptionVisible);
+ }
}
void setTaskDragResizer(TaskDragResizer taskDragResizer) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHandleAnimator.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHandleAnimator.kt
new file mode 100644
index 000000000000..f0a85306d177
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHandleAnimator.kt
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.windowdecor
+
+import android.animation.ObjectAnimator
+import android.view.View
+import android.view.View.Visibility
+import android.view.animation.PathInterpolator
+import android.widget.ImageButton
+import androidx.core.animation.doOnEnd
+import com.android.wm.shell.shared.animation.Interpolators
+
+/**
+ * Animates the Desktop View's app handle.
+ */
+class AppHandleAnimator(
+ private val appHandleView: View,
+ private val captionHandle: ImageButton,
+) {
+ companion object {
+ // Constants for animating the whole caption
+ private const val APP_HANDLE_ALPHA_FADE_IN_ANIMATION_DURATION_MS: Long = 275L
+ private const val APP_HANDLE_ALPHA_FADE_OUT_ANIMATION_DURATION_MS: Long = 340
+ private val APP_HANDLE_ANIMATION_INTERPOLATOR = PathInterpolator(
+ 0.4f,
+ 0f,
+ 0.2f,
+ 1f
+ )
+
+ // Constants for animating the caption's handle
+ private const val HANDLE_ANIMATION_DURATION: Long = 100
+ private val HANDLE_ANIMATION_INTERPOLATOR = Interpolators.FAST_OUT_SLOW_IN
+ }
+
+ private var animator: ObjectAnimator? = null
+
+ /** Animates the given caption view to the given visibility after a visibility change. */
+ fun animateVisibilityChange(@Visibility visible: Int) {
+ when (visible) {
+ View.VISIBLE -> animateShowAppHandle()
+ else -> animateHideAppHandle()
+ }
+ }
+
+ /** Animate appearance/disappearance of caption's handle. */
+ fun animateCaptionHandleAlpha(startValue: Float, endValue: Float) {
+ cancel()
+ animator = ObjectAnimator.ofFloat(captionHandle, View.ALPHA, startValue, endValue).apply {
+ duration = HANDLE_ANIMATION_DURATION
+ interpolator = HANDLE_ANIMATION_INTERPOLATOR
+ start()
+ }
+ }
+
+ private fun animateShowAppHandle() {
+ cancel()
+ appHandleView.alpha = 0f
+ appHandleView.visibility = View.VISIBLE
+ animator = ObjectAnimator.ofFloat(appHandleView, View.ALPHA, 1f).apply {
+ duration = APP_HANDLE_ALPHA_FADE_IN_ANIMATION_DURATION_MS
+ interpolator = APP_HANDLE_ANIMATION_INTERPOLATOR
+ start()
+ }
+ }
+
+ private fun animateHideAppHandle() {
+ cancel()
+ animator = ObjectAnimator.ofFloat(appHandleView, View.ALPHA, 0f).apply {
+ duration = APP_HANDLE_ALPHA_FADE_OUT_ANIMATION_DURATION_MS
+ interpolator = APP_HANDLE_ANIMATION_INTERPOLATOR
+ doOnEnd {
+ appHandleView.visibility = View.GONE
+ }
+ start()
+ }
+ }
+
+ /**
+ * Cancels any active animations.
+ */
+ fun cancel() {
+ animator?.removeAllListeners()
+ animator?.cancel()
+ animator = null
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHandleViewHolder.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHandleViewHolder.kt
index 0985587a330e..9d16be59ba34 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHandleViewHolder.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHandleViewHolder.kt
@@ -15,7 +15,6 @@
*/
package com.android.wm.shell.windowdecor.viewholder
-import android.animation.ObjectAnimator
import android.app.ActivityManager.RunningTaskInfo
import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
import android.content.res.ColorStateList
@@ -40,8 +39,8 @@ import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.Accessibilit
import com.android.internal.policy.SystemBarUtils
import com.android.window.flags.Flags
import com.android.wm.shell.R
-import com.android.wm.shell.shared.animation.Interpolators
import com.android.wm.shell.shared.bubbles.BubbleAnythingFlagHelper
+import com.android.wm.shell.windowdecor.AppHandleAnimator
import com.android.wm.shell.windowdecor.WindowManagerWrapper
import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalSystemViewContainer
@@ -57,22 +56,20 @@ class AppHandleViewHolder(
private val handler: Handler
) : WindowDecorationViewHolder<AppHandleViewHolder.HandleData>(rootView) {
- companion object {
- private const val CAPTION_HANDLE_ANIMATION_DURATION: Long = 100
- }
-
data class HandleData(
val taskInfo: RunningTaskInfo,
val position: Point,
val width: Int,
val height: Int,
- val showInputLayer: Boolean
+ val showInputLayer: Boolean,
+ val isCaptionVisible: Boolean,
) : Data()
private lateinit var taskInfo: RunningTaskInfo
private val captionView: View = rootView.requireViewById(R.id.desktop_mode_caption)
private val captionHandle: ImageButton = rootView.requireViewById(R.id.caption_handle)
private val inputManager = context.getSystemService(InputManager::class.java)
+ private val animator: AppHandleAnimator = AppHandleAnimator(rootView, captionHandle)
private var statusBarInputLayerExists = false
// An invisible View that takes up the same coordinates as captionHandle but is layered
@@ -101,7 +98,14 @@ class AppHandleViewHolder(
}
override fun bindData(data: HandleData) {
- bindData(data.taskInfo, data.position, data.width, data.height, data.showInputLayer)
+ bindData(
+ data.taskInfo,
+ data.position,
+ data.width,
+ data.height,
+ data.showInputLayer,
+ data.isCaptionVisible
+ )
}
private fun bindData(
@@ -109,8 +113,10 @@ class AppHandleViewHolder(
position: Point,
width: Int,
height: Int,
- showInputLayer: Boolean
+ showInputLayer: Boolean,
+ isCaptionVisible: Boolean
) {
+ setVisibility(isCaptionVisible)
captionHandle.imageTintList = ColorStateList.valueOf(getCaptionHandleBarColor(taskInfo))
this.taskInfo = taskInfo
// If handle is not in status bar region(i.e., bottom stage in vertical split),
@@ -131,11 +137,11 @@ class AppHandleViewHolder(
}
override fun onHandleMenuOpened() {
- animateCaptionHandleAlpha(startValue = 1f, endValue = 0f)
+ animator.animateCaptionHandleAlpha(startValue = 1f, endValue = 0f)
}
override fun onHandleMenuClosed() {
- animateCaptionHandleAlpha(startValue = 0f, endValue = 1f)
+ animator.animateCaptionHandleAlpha(startValue = 0f, endValue = 1f)
}
private fun createStatusBarInputLayer(handlePosition: Point,
@@ -239,6 +245,17 @@ class AppHandleViewHolder(
}
}
+ private fun setVisibility(visible: Boolean) {
+ val v = if (visible) View.VISIBLE else View.GONE
+ if (
+ captionView.visibility == v ||
+ !DesktopModeFlags.ENABLE_DESKTOP_APP_HANDLE_ANIMATION.isTrue()
+ ) {
+ return
+ }
+ animator.animateVisibilityChange(v)
+ }
+
private fun getCaptionHandleBarColor(taskInfo: RunningTaskInfo): Int {
return if (shouldUseLightCaptionColors(taskInfo)) {
context.getColor(R.color.desktop_mode_caption_handle_bar_light)
@@ -264,18 +281,10 @@ class AppHandleViewHolder(
} ?: false
}
- /** Animate appearance/disappearance of caption handle as the handle menu is animated. */
- private fun animateCaptionHandleAlpha(startValue: Float, endValue: Float) {
- val animator =
- ObjectAnimator.ofFloat(captionHandle, View.ALPHA, startValue, endValue).apply {
- duration = CAPTION_HANDLE_ANIMATION_DURATION
- interpolator = Interpolators.FAST_OUT_SLOW_IN
- }
- animator.start()
+ override fun close() {
+ animator.cancel()
}
- override fun close() {}
-
/** Factory class for creating [AppHandleViewHolder] objects. */
class Factory {
/**
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt
index 30712b55bdfa..0e2698d0b6fa 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt
@@ -83,6 +83,7 @@ class AppHeaderViewHolder(
val inFullImmersiveState: Boolean,
val hasGlobalFocus: Boolean,
val enableMaximizeLongClick: Boolean,
+ val isCaptionVisible: Boolean,
) : Data()
private val decorThemeUtil = DecorThemeUtil(context)
@@ -264,7 +265,8 @@ class AppHeaderViewHolder(
data.isTaskMaximized,
data.inFullImmersiveState,
data.hasGlobalFocus,
- data.enableMaximizeLongClick
+ data.enableMaximizeLongClick,
+ data.isCaptionVisible,
)
}
@@ -306,6 +308,7 @@ class AppHeaderViewHolder(
inFullImmersiveState: Boolean,
hasGlobalFocus: Boolean,
enableMaximizeLongClick: Boolean,
+ isCaptionVisible: Boolean,
) {
if (DesktopModeFlags.ENABLE_THEMED_APP_HEADERS.isTrue()) {
bindDataWithThemedHeaders(
@@ -314,13 +317,21 @@ class AppHeaderViewHolder(
inFullImmersiveState,
hasGlobalFocus,
enableMaximizeLongClick,
+ isCaptionVisible,
)
} else {
- bindDataLegacy(taskInfo, hasGlobalFocus)
+ bindDataLegacy(taskInfo, hasGlobalFocus, isCaptionVisible)
}
}
- private fun bindDataLegacy(taskInfo: RunningTaskInfo, hasGlobalFocus: Boolean) {
+ private fun bindDataLegacy(
+ taskInfo: RunningTaskInfo,
+ hasGlobalFocus: Boolean,
+ isCaptionVisible: Boolean,
+ ) {
+ if (DesktopModeFlags.ENABLE_DESKTOP_APP_HANDLE_ANIMATION.isTrue()) {
+ setCaptionVisibility(isCaptionVisible)
+ }
captionView.setBackgroundColor(getCaptionBackgroundColor(taskInfo, hasGlobalFocus))
val color = getAppNameAndButtonColor(taskInfo, hasGlobalFocus)
val alpha = Color.alpha(color)
@@ -359,10 +370,15 @@ class AppHeaderViewHolder(
inFullImmersiveState: Boolean,
hasGlobalFocus: Boolean,
enableMaximizeLongClick: Boolean,
+ isCaptionVisible: Boolean,
) {
val header = fillHeaderInfo(taskInfo, hasGlobalFocus)
val headerStyle = getHeaderStyle(header)
+ if (DesktopModeFlags.ENABLE_DESKTOP_APP_HANDLE_ANIMATION.isTrue()) {
+ setCaptionVisibility(isCaptionVisible)
+ }
+
// Caption Background
when (headerStyle.background) {
is HeaderStyle.Background.Opaque -> {
@@ -464,6 +480,11 @@ class AppHeaderViewHolder(
}
}
+ private fun setCaptionVisibility(visible: Boolean) {
+ val v = if (visible) View.VISIBLE else View.GONE
+ captionView.visibility = v
+ }
+
override fun onHandleMenuOpened() {}
override fun onHandleMenuClosed() {}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PipDesktopStateTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PipDesktopStateTest.java
deleted file mode 100644
index 25dbc64f83de..000000000000
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PipDesktopStateTest.java
+++ /dev/null
@@ -1,184 +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.wm.shell.common.pip;
-
-import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
-import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
-import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
-
-import static com.android.window.flags.Flags.FLAG_ENABLE_CONNECTED_DISPLAYS_PIP;
-import static com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_PIP;
-import static com.android.wm.shell.Flags.FLAG_ENABLE_PIP2;
-
-import static junit.framework.Assert.assertEquals;
-import static junit.framework.Assert.assertFalse;
-import static junit.framework.Assert.assertTrue;
-
-import static org.mockito.Mockito.when;
-
-import android.app.ActivityManager;
-import android.platform.test.annotations.EnableFlags;
-import android.testing.AndroidTestingRunner;
-import android.testing.TestableLooper;
-import android.window.DisplayAreaInfo;
-import android.window.WindowContainerToken;
-
-import androidx.test.filters.SmallTest;
-
-import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
-import com.android.wm.shell.desktopmode.DesktopRepository;
-import com.android.wm.shell.desktopmode.DesktopUserRepositories;
-import com.android.wm.shell.desktopmode.DragToDesktopTransitionHandler;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.Mockito;
-import org.mockito.MockitoAnnotations;
-
-import java.util.Optional;
-
-/**
- * Unit test against {@link PipDesktopState}.
- */
-@SmallTest
-@TestableLooper.RunWithLooper
-@RunWith(AndroidTestingRunner.class)
-@EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PIP)
-public class PipDesktopStateTest {
- @Mock private PipDisplayLayoutState mMockPipDisplayLayoutState;
- @Mock private Optional<DesktopUserRepositories> mMockDesktopUserRepositoriesOptional;
- @Mock private DesktopUserRepositories mMockDesktopUserRepositories;
- @Mock private DesktopRepository mMockDesktopRepository;
- @Mock
- private Optional<DragToDesktopTransitionHandler> mMockDragToDesktopTransitionHandlerOptional;
- @Mock private DragToDesktopTransitionHandler mMockDragToDesktopTransitionHandler;
-
- @Mock private RootTaskDisplayAreaOrganizer mMockRootTaskDisplayAreaOrganizer;
- @Mock private ActivityManager.RunningTaskInfo mMockTaskInfo;
-
- private static final int DISPLAY_ID = 1;
- private DisplayAreaInfo mDefaultTda;
- private PipDesktopState mPipDesktopState;
-
- @Before
- public void setUp() {
- MockitoAnnotations.initMocks(this);
- when(mMockDesktopUserRepositoriesOptional.get()).thenReturn(mMockDesktopUserRepositories);
- when(mMockDesktopUserRepositories.getCurrent()).thenReturn(mMockDesktopRepository);
- when(mMockDesktopUserRepositoriesOptional.isPresent()).thenReturn(true);
-
- when(mMockDragToDesktopTransitionHandlerOptional.get()).thenReturn(
- mMockDragToDesktopTransitionHandler);
- when(mMockDragToDesktopTransitionHandlerOptional.isPresent()).thenReturn(true);
-
- when(mMockTaskInfo.getDisplayId()).thenReturn(DISPLAY_ID);
- when(mMockPipDisplayLayoutState.getDisplayId()).thenReturn(DISPLAY_ID);
-
- mDefaultTda = new DisplayAreaInfo(Mockito.mock(WindowContainerToken.class), DISPLAY_ID, 0);
- when(mMockRootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DISPLAY_ID)).thenReturn(
- mDefaultTda);
-
- mPipDesktopState = new PipDesktopState(mMockPipDisplayLayoutState,
- mMockDesktopUserRepositoriesOptional,
- mMockDragToDesktopTransitionHandlerOptional,
- mMockRootTaskDisplayAreaOrganizer);
- }
-
- @Test
- public void isDesktopWindowingPipEnabled_returnsTrue() {
- assertTrue(mPipDesktopState.isDesktopWindowingPipEnabled());
- }
-
- @Test
- public void isDesktopWindowingPipEnabled_desktopRepositoryEmpty_returnsFalse() {
- when(mMockDesktopUserRepositoriesOptional.isPresent()).thenReturn(false);
-
- assertFalse(mPipDesktopState.isDesktopWindowingPipEnabled());
- }
-
- @Test
- public void isDesktopWindowingPipEnabled_dragToDesktopTransitionHandlerEmpty_returnsFalse() {
- when(mMockDragToDesktopTransitionHandlerOptional.isPresent()).thenReturn(false);
-
- assertFalse(mPipDesktopState.isDesktopWindowingPipEnabled());
- }
-
- @Test
- @EnableFlags({
- FLAG_ENABLE_CONNECTED_DISPLAYS_PIP, FLAG_ENABLE_PIP2
- })
- public void isConnectedDisplaysPipEnabled_returnsTrue() {
- assertTrue(mPipDesktopState.isConnectedDisplaysPipEnabled());
- }
-
- @Test
- public void isPipInDesktopMode_anyDeskActive_returnsTrue() {
- when(mMockDesktopRepository.isAnyDeskActive(DISPLAY_ID)).thenReturn(true);
-
- assertTrue(mPipDesktopState.isPipInDesktopMode());
- }
-
- @Test
- public void isPipInDesktopMode_noDeskActive_returnsFalse() {
- when(mMockDesktopRepository.isAnyDeskActive(DISPLAY_ID)).thenReturn(false);
-
- assertFalse(mPipDesktopState.isPipInDesktopMode());
- }
-
- @Test
- public void getOutPipWindowingMode_exitToDesktop_displayFreeform_returnsUndefined() {
- when(mMockDesktopRepository.isAnyDeskActive(DISPLAY_ID)).thenReturn(true);
- setDisplayWindowingMode(WINDOWING_MODE_FREEFORM);
-
- assertEquals(WINDOWING_MODE_UNDEFINED, mPipDesktopState.getOutPipWindowingMode());
- }
-
- @Test
- public void getOutPipWindowingMode_exitToDesktop_displayFullscreen_returnsFreeform() {
- when(mMockDesktopRepository.isAnyDeskActive(DISPLAY_ID)).thenReturn(true);
- setDisplayWindowingMode(WINDOWING_MODE_FULLSCREEN);
-
- assertEquals(WINDOWING_MODE_FREEFORM, mPipDesktopState.getOutPipWindowingMode());
- }
-
- @Test
- public void getOutPipWindowingMode_exitToFullscreen_displayFullscreen_returnsUndefined() {
- setDisplayWindowingMode(WINDOWING_MODE_FULLSCREEN);
-
- assertEquals(WINDOWING_MODE_UNDEFINED, mPipDesktopState.getOutPipWindowingMode());
- }
-
- @Test
- public void isDragToDesktopInProgress_inProgress_returnsTrue() {
- when(mMockDragToDesktopTransitionHandler.getInProgress()).thenReturn(true);
-
- assertTrue(mPipDesktopState.isDragToDesktopInProgress());
- }
-
- @Test
- public void isDragToDesktopInProgress_notInProgress_returnsFalse() {
- when(mMockDragToDesktopTransitionHandler.getInProgress()).thenReturn(false);
-
- assertFalse(mPipDesktopState.isDragToDesktopInProgress());
- }
-
- private void setDisplayWindowingMode(int windowingMode) {
- mDefaultTda.configuration.windowConfiguration.setWindowingMode(windowingMode);
- }
-}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PipDesktopStateTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PipDesktopStateTest.kt
new file mode 100644
index 000000000000..2c50cd9d0c81
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PipDesktopStateTest.kt
@@ -0,0 +1,153 @@
+/*
+ * 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.pip
+
+import android.app.ActivityManager
+import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
+import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN
+import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED
+import android.platform.test.annotations.EnableFlags
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper.RunWithLooper
+import android.window.DisplayAreaInfo
+import android.window.WindowContainerToken
+import androidx.test.filters.SmallTest
+import com.android.window.flags.Flags.FLAG_ENABLE_CONNECTED_DISPLAYS_PIP
+import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_PIP
+import com.android.wm.shell.Flags.FLAG_ENABLE_PIP2
+import com.android.wm.shell.RootTaskDisplayAreaOrganizer
+import com.android.wm.shell.ShellTestCase
+import com.android.wm.shell.desktopmode.DesktopRepository
+import com.android.wm.shell.desktopmode.DesktopUserRepositories
+import com.android.wm.shell.desktopmode.DragToDesktopTransitionHandler
+import com.google.common.truth.Truth.assertThat
+import java.util.Optional
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
+
+/**
+ * Unit test against [PipDesktopState].
+ */
+@SmallTest
+@RunWithLooper
+@RunWith(AndroidTestingRunner::class)
+@EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PIP)
+class PipDesktopStateTest : ShellTestCase() {
+ private val mockPipDisplayLayoutState = mock<PipDisplayLayoutState>()
+ private val mockDesktopUserRepositories = mock<DesktopUserRepositories>()
+ private val mockDesktopRepository = mock<DesktopRepository>()
+ private val mockDragToDesktopTransitionHandler = mock<DragToDesktopTransitionHandler>()
+ private val mockRootTaskDisplayAreaOrganizer = mock<RootTaskDisplayAreaOrganizer>()
+ private val mockTaskInfo = mock<ActivityManager.RunningTaskInfo>()
+ private lateinit var defaultTda: DisplayAreaInfo
+ private lateinit var pipDesktopState: PipDesktopState
+
+ @Before
+ fun setUp() {
+ whenever(mockDesktopUserRepositories.current).thenReturn(mockDesktopRepository)
+ whenever(mockTaskInfo.getDisplayId()).thenReturn(DISPLAY_ID)
+ whenever(mockPipDisplayLayoutState.displayId).thenReturn(DISPLAY_ID)
+
+ defaultTda = DisplayAreaInfo(mock<WindowContainerToken>(), DISPLAY_ID, /* featureId = */ 0)
+ whenever(mockRootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DISPLAY_ID)).thenReturn(
+ defaultTda
+ )
+
+ pipDesktopState =
+ PipDesktopState(
+ mockPipDisplayLayoutState,
+ Optional.of(mockDesktopUserRepositories),
+ Optional.of(mockDragToDesktopTransitionHandler),
+ mockRootTaskDisplayAreaOrganizer
+ )
+ }
+
+ @Test
+ fun isDesktopWindowingPipEnabled_returnsTrue() {
+ assertThat(pipDesktopState.isDesktopWindowingPipEnabled()).isTrue()
+ }
+
+ @Test
+ @EnableFlags(
+ FLAG_ENABLE_CONNECTED_DISPLAYS_PIP,
+ FLAG_ENABLE_PIP2
+ )
+ fun isConnectedDisplaysPipEnabled_returnsTrue() {
+ assertThat(pipDesktopState.isConnectedDisplaysPipEnabled()).isTrue()
+ }
+
+ @Test
+ fun isPipInDesktopMode_anyDeskActive_returnsTrue() {
+ whenever(mockDesktopRepository.isAnyDeskActive(DISPLAY_ID)).thenReturn(true)
+
+ assertThat(pipDesktopState.isPipInDesktopMode()).isTrue()
+ }
+
+ @Test
+ fun isPipInDesktopMode_noDeskActive_returnsFalse() {
+ whenever(mockDesktopRepository.isAnyDeskActive(DISPLAY_ID)).thenReturn(false)
+
+ assertThat(pipDesktopState.isPipInDesktopMode()).isFalse()
+ }
+
+ @Test
+ fun outPipWindowingMode_exitToDesktop_displayFreeform_returnsUndefined() {
+ whenever(mockDesktopRepository.isAnyDeskActive(DISPLAY_ID)).thenReturn(true)
+ setDisplayWindowingMode(WINDOWING_MODE_FREEFORM)
+
+ assertThat(pipDesktopState.getOutPipWindowingMode()).isEqualTo(WINDOWING_MODE_UNDEFINED)
+ }
+
+ @Test
+ fun outPipWindowingMode_exitToDesktop_displayFullscreen_returnsFreeform() {
+ whenever(mockDesktopRepository.isAnyDeskActive(DISPLAY_ID)).thenReturn(true)
+ setDisplayWindowingMode(WINDOWING_MODE_FULLSCREEN)
+
+ assertThat(pipDesktopState.getOutPipWindowingMode()).isEqualTo(WINDOWING_MODE_FREEFORM)
+ }
+
+ @Test
+ fun outPipWindowingMode_exitToFullscreen_displayFullscreen_returnsUndefined() {
+ setDisplayWindowingMode(WINDOWING_MODE_FULLSCREEN)
+
+ assertThat(pipDesktopState.getOutPipWindowingMode()).isEqualTo(WINDOWING_MODE_UNDEFINED)
+ }
+
+ @Test
+ fun isDragToDesktopInProgress_inProgress_returnsTrue() {
+ whenever(mockDragToDesktopTransitionHandler.inProgress).thenReturn(true)
+
+ assertThat(pipDesktopState.isDragToDesktopInProgress()).isTrue()
+ }
+
+ @Test
+ fun isDragToDesktopInProgress_notInProgress_returnsFalse() {
+ whenever(mockDragToDesktopTransitionHandler.inProgress).thenReturn(false)
+
+ assertThat(pipDesktopState.isDragToDesktopInProgress()).isFalse()
+ }
+
+ private fun setDisplayWindowingMode(windowingMode: Int) {
+ defaultTda.configuration.windowConfiguration.windowingMode = windowingMode
+ }
+
+ companion object {
+ private const val DISPLAY_ID = 1
+ }
+}
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
index 22a85fc49a4b..9f2534eb2662 100644
--- 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
@@ -71,6 +71,7 @@ public class FlexParallaxSpecTests {
when(mockSnapAlgorithm.getMiddleTarget()).thenReturn(mockMiddleTarget);
when(mockSnapAlgorithm.getLastSplitTarget()).thenReturn(mockLastTarget);
when(mockSnapAlgorithm.getDismissEndTarget()).thenReturn(mockEndEdge);
+ when(mockSnapAlgorithm.areOffscreenRatiosSupported()).thenReturn(true);
when(mockStartEdge.getPosition()).thenReturn(0);
when(mockFirstTarget.getPosition()).thenReturn(250);
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java
index 597e4a55ed0e..9035df28aa7c 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java
@@ -680,7 +680,8 @@ public class CompatUIControllerTest extends ShellTestCase {
// Create transparent task
final TaskInfo taskInfo1 = createTaskInfo(DISPLAY_ID, newTaskId, /* hasSizeCompat= */ true,
- /* isVisible */ true, /* isFocused */ true, /* isTopActivityTransparent */ true);
+ /* isVisible */ true, /* isFocused */ true, /* isTopActivityTransparent */ true,
+ /* isRestartMenuEnabledForDisplayMove */ true);
// Simulate new task being shown
mController.updateActiveTaskInfo(taskInfo1);
@@ -742,32 +743,38 @@ public class CompatUIControllerTest extends ShellTestCase {
@Test
@RequiresFlagsDisabled(Flags.FLAG_APP_COMPAT_UI_FRAMEWORK)
public void testSendCompatUIRequest_createRestartDialog() {
- TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ false);
- doReturn(true).when(mMockRestartDialogLayout)
- .needsToBeRecreated(any(TaskInfo.class),
- any(ShellTaskOrganizer.TaskListener.class));
+ final TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true,
+ /* isVisible */ true, /* isFocused */ true, /* isTopActivityTransparent */ false,
+ /* isRestartMenuEnabledForDisplayMove */ true);
doReturn(true).when(mCompatUIConfiguration).isRestartDialogEnabled();
doReturn(true).when(mCompatUIConfiguration).shouldShowRestartDialogAgain(eq(taskInfo));
- mController.sendCompatUIRequest(new CompatUIRequests.DisplayCompatShowRestartDialog(
- taskInfo, mMockTaskListener));
+ mController.onCompatInfoChanged(new CompatUIInfo(taskInfo, mMockTaskListener));
verify(mController).createRestartDialogWindowManager(any(), eq(taskInfo),
eq(mMockTaskListener));
+ verify(mMockRestartDialogLayout).setRequestRestartDialog(false);
+
+ mController.sendCompatUIRequest(
+ new CompatUIRequests.DisplayCompatShowRestartDialog(taskInfo.taskId));
+ verify(mMockRestartDialogLayout).setRequestRestartDialog(true);
}
private static TaskInfo createTaskInfo(int displayId, int taskId, boolean hasSizeCompat) {
return createTaskInfo(displayId, taskId, hasSizeCompat, /* isVisible */ false,
- /* isFocused */ false, /* isTopActivityTransparent */ false);
+ /* isFocused */ false, /* isTopActivityTransparent */ false,
+ /* isRestartMenuEnabledForDisplayMove */ false);
}
private static TaskInfo createTaskInfo(int displayId, int taskId, boolean hasSizeCompat,
boolean isVisible, boolean isFocused) {
return createTaskInfo(displayId, taskId, hasSizeCompat,
- isVisible, isFocused, /* isTopActivityTransparent */ false);
+ isVisible, isFocused, /* isTopActivityTransparent */ false,
+ /* isRestartMenuEnabledForDisplayMove */ false);
}
private static TaskInfo createTaskInfo(int displayId, int taskId, boolean hasSizeCompat,
- boolean isVisible, boolean isFocused, boolean isTopActivityTransparent) {
+ boolean isVisible, boolean isFocused, boolean isTopActivityTransparent,
+ boolean isRestartMenuEnabledForDisplayMove) {
RunningTaskInfo taskInfo = new RunningTaskInfo();
taskInfo.taskId = taskId;
taskInfo.displayId = displayId;
@@ -777,6 +784,8 @@ public class CompatUIControllerTest extends ShellTestCase {
taskInfo.isTopActivityTransparent = isTopActivityTransparent;
taskInfo.appCompatTaskInfo.setLetterboxEducationEnabled(true);
taskInfo.appCompatTaskInfo.setTopActivityLetterboxed(true);
+ taskInfo.appCompatTaskInfo.setRestartMenuEnabledForDisplayMove(
+ isRestartMenuEnabledForDisplayMove);
return taskInfo;
}
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopActivityOrientationChangeHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopActivityOrientationChangeHandlerTest.kt
index d58f8a34c98e..94fe03084989 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopActivityOrientationChangeHandlerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopActivityOrientationChangeHandlerTest.kt
@@ -37,6 +37,8 @@ import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE
import com.android.window.flags.Flags.FLAG_RESPECT_ORIENTATION_CHANGE_FOR_UNRESIZEABLE
import com.android.wm.shell.ShellTaskOrganizer
import com.android.wm.shell.ShellTestCase
+import com.android.wm.shell.common.DisplayController
+import com.android.wm.shell.common.DisplayLayout
import com.android.wm.shell.common.ShellExecutor
import com.android.wm.shell.common.TaskStackListenerImpl
import com.android.wm.shell.desktopmode.DesktopTestHelpers.createFreeformTask
@@ -96,12 +98,15 @@ class DesktopActivityOrientationChangeHandlerTest : ShellTestCase() {
@Mock lateinit var repositoryInitializer: DesktopRepositoryInitializer
@Mock lateinit var userManager: UserManager
@Mock lateinit var shellController: ShellController
+ @Mock lateinit var displayController: DisplayController
+ @Mock lateinit var displayLayout: DisplayLayout
private lateinit var mockitoSession: StaticMockitoSession
private lateinit var handler: DesktopActivityOrientationChangeHandler
private lateinit var shellInit: ShellInit
private lateinit var userRepositories: DesktopUserRepositories
private lateinit var testScope: CoroutineScope
+
// Mock running tasks are registered here so we can get the list from mock shell task organizer.
private val runningTasks = mutableListOf<RunningTaskInfo>()
@@ -131,6 +136,7 @@ class DesktopActivityOrientationChangeHandlerTest : ShellTestCase() {
whenever(transitions.startTransition(anyInt(), any(), isNull())).thenAnswer { Binder() }
whenever(runBlocking { persistentRepository.readDesktop(any(), any()) })
.thenReturn(Desktop.getDefaultInstance())
+ whenever(displayController.getDisplayLayout(anyInt())).thenReturn(displayLayout)
handler =
DesktopActivityOrientationChangeHandler(
@@ -140,6 +146,7 @@ class DesktopActivityOrientationChangeHandlerTest : ShellTestCase() {
taskStackListener,
resizeTransitionHandler,
userRepositories,
+ displayController,
)
shellInit.init()
@@ -171,6 +178,7 @@ class DesktopActivityOrientationChangeHandlerTest : ShellTestCase() {
taskStackListener,
resizeTransitionHandler,
userRepositories,
+ displayController,
)
verify(shellInit, never())
@@ -251,6 +259,11 @@ class DesktopActivityOrientationChangeHandlerTest : ShellTestCase() {
val oldBounds = task.configuration.windowConfiguration.bounds
val newTask =
setUpFreeformTask(isResizeable = false, orientation = SCREEN_ORIENTATION_LANDSCAPE)
+ whenever(displayLayout.height()).thenReturn(800)
+ whenever(displayLayout.width()).thenReturn(2000)
+ whenever(displayLayout.getStableBounds(any())).thenAnswer { i ->
+ (i.arguments.first() as Rect).set(Rect(0, 0, 2000, 800))
+ }
handler.handleActivityOrientationChange(task, newTask)
@@ -279,6 +292,11 @@ class DesktopActivityOrientationChangeHandlerTest : ShellTestCase() {
bounds = oldBounds,
)
val newTask = setUpFreeformTask(isResizeable = false, bounds = oldBounds)
+ whenever(displayLayout.height()).thenReturn(2000)
+ whenever(displayLayout.width()).thenReturn(800)
+ whenever(displayLayout.getStableBounds(any())).thenAnswer { i ->
+ (i.arguments.first() as Rect).set(Rect(0, 0, 800, 2000))
+ }
handler.handleActivityOrientationChange(task, newTask)
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandlerTest.kt
index 2aebcdcc3bf5..9268db60aa51 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandlerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandlerTest.kt
@@ -24,13 +24,16 @@ import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession
import com.android.dx.mockito.inline.extended.ExtendedMockito.never
import com.android.dx.mockito.inline.extended.StaticMockitoSession
import com.android.window.flags.Flags
+import com.android.wm.shell.RootTaskDisplayAreaOrganizer
import com.android.wm.shell.ShellTestCase
import com.android.wm.shell.common.DisplayController
import com.android.wm.shell.common.DisplayController.OnDisplaysChangedListener
import com.android.wm.shell.common.ShellExecutor
import com.android.wm.shell.desktopmode.persistence.DesktopRepositoryInitializer
import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
+import com.android.wm.shell.sysui.ShellController
import com.android.wm.shell.sysui.ShellInit
+import com.android.wm.shell.sysui.UserChangeListener
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.MutableStateFlow
@@ -46,6 +49,7 @@ import org.mockito.Mockito.spy
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
import org.mockito.kotlin.argumentCaptor
+import org.mockito.kotlin.clearInvocations
import org.mockito.kotlin.whenever
import org.mockito.quality.Strictness
@@ -60,6 +64,8 @@ import org.mockito.quality.Strictness
class DesktopDisplayEventHandlerTest : ShellTestCase() {
@Mock lateinit var testExecutor: ShellExecutor
@Mock lateinit var displayController: DisplayController
+ @Mock private lateinit var mockShellController: ShellController
+ @Mock private lateinit var mockRootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer
@Mock private lateinit var mockDesktopUserRepositories: DesktopUserRepositories
@Mock private lateinit var mockDesktopRepository: DesktopRepository
@Mock private lateinit var mockDesktopTasksController: DesktopTasksController
@@ -89,7 +95,9 @@ class DesktopDisplayEventHandlerTest : ShellTestCase() {
context,
shellInit,
testScope.backgroundScope,
+ mockShellController,
displayController,
+ mockRootTaskDisplayAreaOrganizer,
desktopRepositoryInitializer,
mockDesktopUserRepositories,
mockDesktopTasksController,
@@ -107,6 +115,7 @@ class DesktopDisplayEventHandlerTest : ShellTestCase() {
}
@Test
+ @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
fun testDisplayAdded_supportsDesks_desktopRepositoryInitialized_createsDesk() =
testScope.runTest {
whenever(DesktopModeStatus.canEnterDesktopMode(context)).thenReturn(true)
@@ -119,6 +128,7 @@ class DesktopDisplayEventHandlerTest : ShellTestCase() {
}
@Test
+ @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
fun testDisplayAdded_supportsDesks_desktopRepositoryNotInitialized_doesNotCreateDesk() =
testScope.runTest {
whenever(DesktopModeStatus.canEnterDesktopMode(context)).thenReturn(true)
@@ -130,6 +140,7 @@ class DesktopDisplayEventHandlerTest : ShellTestCase() {
}
@Test
+ @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
fun testDisplayAdded_supportsDesks_desktopRepositoryInitializedTwice_createsDeskOnce() =
testScope.runTest {
whenever(DesktopModeStatus.canEnterDesktopMode(context)).thenReturn(true)
@@ -143,6 +154,7 @@ class DesktopDisplayEventHandlerTest : ShellTestCase() {
}
@Test
+ @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
fun testDisplayAdded_supportsDesks_desktopRepositoryInitialized_deskExists_doesNotCreateDesk() =
testScope.runTest {
whenever(DesktopModeStatus.canEnterDesktopMode(context)).thenReturn(true)
@@ -156,33 +168,71 @@ class DesktopDisplayEventHandlerTest : ShellTestCase() {
}
@Test
- fun testDisplayAdded_cannotEnterDesktopMode_doesNotCreateDesk() {
- whenever(DesktopModeStatus.canEnterDesktopMode(context)).thenReturn(false)
+ @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun testDisplayAdded_cannotEnterDesktopMode_doesNotCreateDesk() =
+ testScope.runTest {
+ whenever(DesktopModeStatus.canEnterDesktopMode(context)).thenReturn(false)
+ desktopRepositoryInitializer.initialize(mockDesktopUserRepositories)
- onDisplaysChangedListenerCaptor.lastValue.onDisplayAdded(DEFAULT_DISPLAY)
+ onDisplaysChangedListenerCaptor.lastValue.onDisplayAdded(DEFAULT_DISPLAY)
+ runCurrent()
- verify(mockDesktopTasksController, never()).createDesk(DEFAULT_DISPLAY)
- }
+ verify(mockDesktopTasksController, never()).createDesk(DEFAULT_DISPLAY)
+ }
@Test
@EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
- fun testDeskRemoved_noDesksRemain_createsDesk() {
- whenever(mockDesktopRepository.getNumberOfDesks(DEFAULT_DISPLAY)).thenReturn(0)
+ fun testDeskRemoved_noDesksRemain_createsDesk() =
+ testScope.runTest {
+ whenever(DesktopModeStatus.canEnterDesktopMode(context)).thenReturn(true)
+ whenever(mockDesktopRepository.getNumberOfDesks(DEFAULT_DISPLAY)).thenReturn(0)
+ desktopRepositoryInitializer.initialize(mockDesktopUserRepositories)
- handler.onDeskRemoved(DEFAULT_DISPLAY, deskId = 1)
+ handler.onDeskRemoved(DEFAULT_DISPLAY, deskId = 1)
+ runCurrent()
- verify(mockDesktopTasksController).createDesk(DEFAULT_DISPLAY)
- }
+ verify(mockDesktopTasksController).createDesk(DEFAULT_DISPLAY)
+ }
@Test
@EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
- fun testDeskRemoved_desksRemain_doesNotCreateDesk() {
- whenever(mockDesktopRepository.getNumberOfDesks(DEFAULT_DISPLAY)).thenReturn(1)
+ fun testDeskRemoved_desksRemain_doesNotCreateDesk() =
+ testScope.runTest {
+ whenever(DesktopModeStatus.canEnterDesktopMode(context)).thenReturn(true)
+ whenever(mockDesktopRepository.getNumberOfDesks(DEFAULT_DISPLAY)).thenReturn(1)
+ desktopRepositoryInitializer.initialize(mockDesktopUserRepositories)
+
+ handler.onDeskRemoved(DEFAULT_DISPLAY, deskId = 1)
+ runCurrent()
- handler.onDeskRemoved(DEFAULT_DISPLAY, deskId = 1)
+ verify(mockDesktopTasksController, never()).createDesk(DEFAULT_DISPLAY)
+ }
- verify(mockDesktopTasksController, never()).createDesk(DEFAULT_DISPLAY)
- }
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun testUserChanged_createsDeskWhenNeeded() =
+ testScope.runTest {
+ whenever(DesktopModeStatus.canEnterDesktopMode(context)).thenReturn(true)
+ val userChangeListenerCaptor = argumentCaptor<UserChangeListener>()
+ verify(mockShellController).addUserChangeListener(userChangeListenerCaptor.capture())
+ whenever(mockDesktopRepository.getNumberOfDesks(displayId = 2)).thenReturn(0)
+ whenever(mockDesktopRepository.getNumberOfDesks(displayId = 3)).thenReturn(0)
+ whenever(mockDesktopRepository.getNumberOfDesks(displayId = 4)).thenReturn(1)
+ whenever(mockRootTaskDisplayAreaOrganizer.displayIds).thenReturn(intArrayOf(2, 3, 4))
+ desktopRepositoryInitializer.initialize(mockDesktopUserRepositories)
+ handler.onDisplayAdded(displayId = 2)
+ handler.onDisplayAdded(displayId = 3)
+ handler.onDisplayAdded(displayId = 4)
+ runCurrent()
+
+ clearInvocations(mockDesktopTasksController)
+ userChangeListenerCaptor.lastValue.onUserChanged(1, context)
+ runCurrent()
+
+ verify(mockDesktopTasksController).createDesk(displayId = 2)
+ verify(mockDesktopTasksController).createDesk(displayId = 3)
+ verify(mockDesktopTasksController, never()).createDesk(displayId = 4)
+ }
@Test
fun testConnectExternalDisplay() {
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeKeyGestureHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeKeyGestureHandlerTest.kt
index d510570e8839..e40da5e8498d 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeKeyGestureHandlerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeKeyGestureHandlerTest.kt
@@ -50,7 +50,6 @@ import com.android.wm.shell.desktopmode.common.ToggleTaskSizeInteraction
import com.android.wm.shell.sysui.ShellInit
import com.android.wm.shell.transition.FocusTransitionObserver
import com.android.wm.shell.windowdecor.DesktopModeWindowDecorViewModel
-import com.google.common.truth.Truth.assertThat
import java.util.Optional
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -120,11 +119,11 @@ class DesktopModeKeyGestureHandlerTest : ShellTestCase() {
whenever(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)).thenReturn(tda)
doAnswer {
- keyGestureEventHandler = (it.arguments[0] as KeyGestureEventHandler)
+ keyGestureEventHandler = (it.arguments[1] as KeyGestureEventHandler)
null
}
.whenever(inputManager)
- .registerKeyGestureEventHandler(any())
+ .registerKeyGestureEventHandler(any(), any())
shellInit.init()
desktopModeKeyGestureHandler =
@@ -176,10 +175,9 @@ class DesktopModeKeyGestureHandlerTest : ShellTestCase() {
.setKeycodes(intArrayOf(KeyEvent.KEYCODE_D))
.setModifierState(KeyEvent.META_META_ON or KeyEvent.META_CTRL_ON)
.build()
- val result = keyGestureEventHandler.handleKeyGestureEvent(event, null)
+ keyGestureEventHandler.handleKeyGestureEvent(event, null)
testExecutor.flushAll()
- assertThat(result).isTrue()
verify(desktopTasksController).moveToNextDisplay(task.taskId)
}
@@ -197,10 +195,9 @@ class DesktopModeKeyGestureHandlerTest : ShellTestCase() {
.setKeycodes(intArrayOf(KeyEvent.KEYCODE_LEFT_BRACKET))
.setModifierState(KeyEvent.META_META_ON)
.build()
- val result = keyGestureEventHandler.handleKeyGestureEvent(event, null)
+ keyGestureEventHandler.handleKeyGestureEvent(event, null)
testExecutor.flushAll()
- assertThat(result).isTrue()
verify(desktopModeWindowDecorViewModel)
.onSnapResize(
task.taskId,
@@ -224,10 +221,9 @@ class DesktopModeKeyGestureHandlerTest : ShellTestCase() {
.setKeycodes(intArrayOf(KeyEvent.KEYCODE_RIGHT_BRACKET))
.setModifierState(KeyEvent.META_META_ON)
.build()
- val result = keyGestureEventHandler.handleKeyGestureEvent(event, null)
+ keyGestureEventHandler.handleKeyGestureEvent(event, null)
testExecutor.flushAll()
- assertThat(result).isTrue()
verify(desktopModeWindowDecorViewModel)
.onSnapResize(
task.taskId,
@@ -251,10 +247,9 @@ class DesktopModeKeyGestureHandlerTest : ShellTestCase() {
.setKeycodes(intArrayOf(KeyEvent.KEYCODE_EQUALS))
.setModifierState(KeyEvent.META_META_ON)
.build()
- val result = keyGestureEventHandler.handleKeyGestureEvent(event, null)
+ keyGestureEventHandler.handleKeyGestureEvent(event, null)
testExecutor.flushAll()
- assertThat(result).isTrue()
verify(desktopTasksController)
.toggleDesktopTaskSize(
task,
@@ -280,10 +275,9 @@ class DesktopModeKeyGestureHandlerTest : ShellTestCase() {
.setKeycodes(intArrayOf(KeyEvent.KEYCODE_MINUS))
.setModifierState(KeyEvent.META_META_ON)
.build()
- val result = keyGestureEventHandler.handleKeyGestureEvent(event, null)
+ keyGestureEventHandler.handleKeyGestureEvent(event, null)
testExecutor.flushAll()
- assertThat(result).isTrue()
verify(desktopTasksController).minimizeTask(task, MinimizeReason.KEY_GESTURE)
}
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 71e46bcb0698..b577667d8279 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
@@ -1526,6 +1526,37 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
}
@Test
+ fun launchIntent_taskInDesktopMode_onSecondaryDisplay_transitionStarted() {
+ setUpLandscapeDisplay()
+ taskRepository.addDesk(SECOND_DISPLAY, deskId = 2)
+ val intent = Intent().setComponent(homeComponentName)
+ whenever(
+ desktopMixedTransitionHandler.startLaunchTransition(
+ eq(TRANSIT_OPEN),
+ any(),
+ anyOrNull(),
+ anyOrNull(),
+ anyOrNull(),
+ )
+ )
+ .thenReturn(Binder())
+
+ controller.startLaunchIntentTransition(intent, Bundle.EMPTY, SECOND_DISPLAY)
+
+ val wct = getLatestDesktopMixedTaskWct(type = TRANSIT_OPEN)
+ // We expect two actions: open the app and start the desk
+ assertThat(wct.hierarchyOps).hasSize(2)
+ val hOps0 = wct.hierarchyOps[0]
+ val hOps1 = wct.hierarchyOps[1]
+ assertThat(hOps0.type).isEqualTo(HIERARCHY_OP_TYPE_PENDING_INTENT)
+ val activityOptions0 = ActivityOptions.fromBundle(hOps0.launchOptions)
+ assertThat(activityOptions0.launchDisplayId).isEqualTo(SECOND_DISPLAY)
+ assertThat(hOps1.type).isEqualTo(HIERARCHY_OP_TYPE_PENDING_INTENT)
+ val activityOptions1 = ActivityOptions.fromBundle(hOps1.launchOptions)
+ assertThat(activityOptions1.launchDisplayId).isEqualTo(SECOND_DISPLAY)
+ }
+
+ @Test
@EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
fun addMoveToDeskTaskChanges_landscapeDevice_userFullscreenOverride_defaultPortraitBounds() {
setUpLandscapeDisplay()
@@ -7615,6 +7646,106 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
assertNull(latestWct.hierarchyOps.find { op -> op.container == wallpaperToken.asBinder() })
}
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun onRecentsInDesktopAnimationFinishing_returningToApp_noDeskDeactivation() {
+ val deskId = 0
+ taskRepository.setActiveDesk(DEFAULT_DISPLAY, deskId)
+
+ val transition = Binder()
+ val finishWct = WindowContainerTransaction()
+ controller.onRecentsInDesktopAnimationFinishing(
+ transition = transition,
+ finishWct = finishWct,
+ returnToApp = true,
+ )
+
+ verify(desksOrganizer, never()).deactivateDesk(finishWct, deskId)
+ verify(desksTransitionsObserver, never())
+ .addPendingTransition(
+ argThat { t -> t.token == transition && t is DeskTransition.DeactivateDesk }
+ )
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun onRecentsInDesktopAnimationFinishing_noActiveDesk_noDeskDeactivation() {
+ val deskId = 0
+ taskRepository.setDeskInactive(deskId)
+
+ val transition = Binder()
+ val finishWct = WindowContainerTransaction()
+ controller.onRecentsInDesktopAnimationFinishing(
+ transition = transition,
+ finishWct = finishWct,
+ returnToApp = false,
+ )
+
+ verify(desksOrganizer, never()).deactivateDesk(finishWct, deskId)
+ verify(desksTransitionsObserver, never())
+ .addPendingTransition(
+ argThat { t -> t.token == transition && t is DeskTransition.DeactivateDesk }
+ )
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun onRecentsInDesktopAnimationFinishing_activeDesk_notReturningToDesk_deactivatesDesk() {
+ val deskId = 0
+ taskRepository.setActiveDesk(DEFAULT_DISPLAY, deskId)
+
+ val transition = Binder()
+ val finishWct = WindowContainerTransaction()
+ controller.onRecentsInDesktopAnimationFinishing(
+ transition = transition,
+ finishWct = finishWct,
+ returnToApp = false,
+ )
+
+ verify(desksOrganizer).deactivateDesk(finishWct, deskId)
+ verify(desksTransitionsObserver)
+ .addPendingTransition(DeskTransition.DeactivateDesk(transition, deskId))
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun onRecentsInDesktopAnimationFinishing_activeDesk_notReturningToDesk_notifiesDesktopExit() {
+ val deskId = 0
+ taskRepository.setActiveDesk(DEFAULT_DISPLAY, deskId)
+
+ val transition = Binder()
+ val finishWct = WindowContainerTransaction()
+ controller.onRecentsInDesktopAnimationFinishing(
+ transition = transition,
+ finishWct = finishWct,
+ returnToApp = false,
+ )
+
+ verify(desktopModeEnterExitTransitionListener).onExitDesktopModeTransitionStarted(any())
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun onRecentsInDesktopAnimationFinishing_activeDesk_notReturningToDesk_doesNotBringUpWallpaperOrHome() {
+ val deskId = 0
+ taskRepository.setActiveDesk(DEFAULT_DISPLAY, deskId)
+
+ val transition = Binder()
+ val finishWct = WindowContainerTransaction()
+ controller.onRecentsInDesktopAnimationFinishing(
+ transition = transition,
+ finishWct = finishWct,
+ returnToApp = false,
+ )
+
+ finishWct.assertWithoutHop { hop ->
+ hop.type == HIERARCHY_OP_TYPE_REORDER &&
+ hop.container == wallpaperToken.asBinder() &&
+ !hop.toTop
+ }
+ finishWct.assertWithoutHop { hop -> hop.type == HIERARCHY_OP_TYPE_PENDING_INTENT }
+ }
+
private class RunOnStartTransitionCallback : ((IBinder) -> Unit) {
var invocations = 0
private set
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/VisualIndicatorViewContainerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/VisualIndicatorViewContainerTest.kt
index 3983bfbb2080..75f8d9e819cb 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/VisualIndicatorViewContainerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/VisualIndicatorViewContainerTest.kt
@@ -16,6 +16,7 @@
package com.android.wm.shell.desktopmode
+import android.animation.AnimatorTestRule
import android.app.ActivityManager
import android.app.ActivityManager.RunningTaskInfo
import android.graphics.Rect
@@ -29,6 +30,7 @@ import android.view.Display.DEFAULT_DISPLAY
import android.view.SurfaceControl
import android.view.SurfaceControlViewHost
import android.view.View
+import android.widget.FrameLayout
import androidx.test.filters.SmallTest
import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE
import com.android.wm.shell.ShellTestCase
@@ -43,6 +45,7 @@ import com.android.wm.shell.windowdecor.tiling.SnapEventHandler
import com.google.common.truth.Truth.assertThat
import kotlin.test.Test
import org.junit.Before
+import org.junit.Rule
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.anyInt
import org.mockito.Mock
@@ -67,6 +70,9 @@ import org.mockito.kotlin.whenever
@RunWith(AndroidTestingRunner::class)
@EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
class VisualIndicatorViewContainerTest : ShellTestCase() {
+
+ @JvmField @Rule val animatorTestRule = AnimatorTestRule(this)
+
@Mock private lateinit var view: View
@Mock private lateinit var displayLayout: DisplayLayout
@Mock private lateinit var displayController: DisplayController
@@ -297,6 +303,95 @@ class VisualIndicatorViewContainerTest : ShellTestCase() {
verify(spyViewContainer, never()).fadeInIndicatorInternal(any(), any(), any(), any())
}
+ @Test
+ @EnableFlags(
+ com.android.wm.shell.Flags.FLAG_ENABLE_BUBBLE_TO_FULLSCREEN,
+ com.android.wm.shell.Flags.FLAG_ENABLE_CREATE_ANY_BUBBLE,
+ )
+ fun testCreateView_bubblesEnabled_indicatorIsFrameLayout() {
+ val spyViewContainer = setupSpyViewContainer()
+ assertThat(spyViewContainer.indicatorView).isInstanceOf(FrameLayout::class.java)
+ }
+
+ @Test
+ @EnableFlags(
+ com.android.wm.shell.Flags.FLAG_ENABLE_BUBBLE_TO_FULLSCREEN,
+ com.android.wm.shell.Flags.FLAG_ENABLE_CREATE_ANY_BUBBLE,
+ )
+ fun testFadeInOutBubbleIndicator_addAndRemoveBarIndicator() {
+ setUpBubbleBoundsProvider()
+ val spyViewContainer = setupSpyViewContainer()
+ spyViewContainer.fadeInIndicator(
+ displayLayout,
+ DesktopModeVisualIndicator.IndicatorType.TO_BUBBLE_RIGHT_INDICATOR,
+ DEFAULT_DISPLAY,
+ )
+ desktopExecutor.flushAll()
+ animatorTestRule.advanceTimeBy(200)
+ assertThat((spyViewContainer.indicatorView as FrameLayout).getChildAt(0)).isNotNull()
+
+ spyViewContainer.fadeOutIndicator(
+ displayLayout,
+ DesktopModeVisualIndicator.IndicatorType.TO_BUBBLE_RIGHT_INDICATOR,
+ finishCallback = null,
+ DEFAULT_DISPLAY,
+ snapEventHandler,
+ )
+ desktopExecutor.flushAll()
+ animatorTestRule.advanceTimeBy(250)
+ assertThat((spyViewContainer.indicatorView as FrameLayout).getChildAt(0)).isNull()
+ }
+
+ @Test
+ @EnableFlags(
+ com.android.wm.shell.Flags.FLAG_ENABLE_BUBBLE_TO_FULLSCREEN,
+ com.android.wm.shell.Flags.FLAG_ENABLE_CREATE_ANY_BUBBLE,
+ )
+ fun testTransitionIndicator_fullscreenToBubble_addBarIndicator() {
+ setUpBubbleBoundsProvider()
+ val spyViewContainer = setupSpyViewContainer()
+
+ spyViewContainer.transitionIndicator(
+ taskInfo,
+ displayController,
+ DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR,
+ DesktopModeVisualIndicator.IndicatorType.TO_BUBBLE_RIGHT_INDICATOR,
+ )
+ desktopExecutor.flushAll()
+ animatorTestRule.advanceTimeBy(200)
+
+ assertThat((spyViewContainer.indicatorView as FrameLayout).getChildAt(0)).isNotNull()
+ }
+
+ @Test
+ @EnableFlags(
+ com.android.wm.shell.Flags.FLAG_ENABLE_BUBBLE_TO_FULLSCREEN,
+ com.android.wm.shell.Flags.FLAG_ENABLE_CREATE_ANY_BUBBLE,
+ )
+ fun testTransitionIndicator_bubbleToFullscreen_removeBarIndicator() {
+ setUpBubbleBoundsProvider()
+ val spyViewContainer = setupSpyViewContainer()
+ spyViewContainer.fadeInIndicator(
+ displayLayout,
+ DesktopModeVisualIndicator.IndicatorType.TO_BUBBLE_RIGHT_INDICATOR,
+ DEFAULT_DISPLAY,
+ )
+ desktopExecutor.flushAll()
+ animatorTestRule.advanceTimeBy(200)
+ assertThat((spyViewContainer.indicatorView as FrameLayout).getChildAt(0)).isNotNull()
+
+ spyViewContainer.transitionIndicator(
+ taskInfo,
+ displayController,
+ DesktopModeVisualIndicator.IndicatorType.TO_BUBBLE_RIGHT_INDICATOR,
+ DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR,
+ )
+ desktopExecutor.flushAll()
+ animatorTestRule.advanceTimeBy(200)
+
+ assertThat((spyViewContainer.indicatorView as FrameLayout).getChildAt(0)).isNull()
+ }
+
private fun setupSpyViewContainer(): VisualIndicatorViewContainer {
val viewContainer =
VisualIndicatorViewContainer(
@@ -331,7 +426,22 @@ class VisualIndicatorViewContainerTest : ShellTestCase() {
.build()
}
+ private fun setUpBubbleBoundsProvider() {
+ bubbleDropTargetBoundsProvider =
+ object : BubbleDropTargetBoundsProvider {
+ override fun getBubbleBarExpandedViewDropTargetBounds(onLeft: Boolean): Rect {
+ return BUBBLE_INDICATOR_BOUNDS
+ }
+
+ override fun getBarDropTargetBounds(onLeft: Boolean): Rect {
+ return BAR_INDICATOR_BOUNDS
+ }
+ }
+ }
+
companion object {
private val DISPLAY_BOUNDS = Rect(0, 0, 1000, 1000)
+ private val BUBBLE_INDICATOR_BOUNDS = Rect(800, 200, 900, 900)
+ private val BAR_INDICATOR_BOUNDS = Rect(880, 950, 900, 960)
}
}
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 409ca57715fc..e55d7cbb73e1 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
@@ -264,6 +264,36 @@ class DesksTransitionObserverTest : ShellTestCase() {
@Test
@EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun onTransitionFinish_deactivateDesk_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.onTransitionFinished(transition)
+
+ assertThat(repository.getActiveDeskId(DEFAULT_DISPLAY)).isNull()
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun onTransitionMergedAndFinished_deactivateDesk_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)
+ val bookEndTransition = Binder()
+ observer.onTransitionMerged(merged = transition, playing = bookEndTransition)
+ observer.onTransitionFinished(bookEndTransition)
+
+ assertThat(repository.getActiveDeskId(DEFAULT_DISPLAY)).isNull()
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
fun onTransitionReady_twoPendingTransitions_handlesBoth() {
val transition = Binder()
// Active one desk and deactivate another in different displays, such as in some
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserverTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserverTest.java
index 714e5f486285..69a42164071b 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserverTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserverTest.java
@@ -373,6 +373,25 @@ public class FreeformTaskTransitionObserverTest extends ShellTestCase {
verify(mDesksTransitionObserver).onTransitionReady(transition, info);
}
+ @Test
+ public void onTransitionMerged_forwardsToDesksTransitionObserver() {
+ final IBinder merged = mock(IBinder.class);
+ final IBinder playing = mock(IBinder.class);
+
+ mTransitionObserver.onTransitionMerged(merged, playing);
+
+ verify(mDesksTransitionObserver).onTransitionMerged(merged, playing);
+ }
+
+ @Test
+ public void onTransitionFinished_forwardsToDesksTransitionObserver() {
+ final IBinder transition = mock(IBinder.class);
+
+ mTransitionObserver.onTransitionFinished(transition, /* aborted = */ false);
+
+ verify(mDesksTransitionObserver).onTransitionFinished(transition);
+ }
+
private static TransitionInfo.Change createChange(int mode, int taskId, int windowingMode) {
final ActivityManager.RunningTaskInfo taskInfo = new ActivityManager.RunningTaskInfo();
taskInfo.taskId = taskId;
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java
index 677330790bab..9849b1174d8e 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java
@@ -37,6 +37,7 @@ import static android.view.WindowManager.TRANSIT_TO_BACK;
import static android.view.WindowManager.TRANSIT_TO_FRONT;
import static android.window.TransitionInfo.FLAG_DISPLAY_HAS_ALERT_WINDOWS;
import static android.window.TransitionInfo.FLAG_IS_DISPLAY;
+import static android.window.TransitionInfo.FLAG_MOVED_TO_TOP;
import static android.window.TransitionInfo.FLAG_SYNC;
import static android.window.TransitionInfo.FLAG_TRANSLUCENT;
@@ -1742,6 +1743,53 @@ public class ShellTransitionTests extends ShellTestCase {
eq(R.styleable.WindowAnimation_activityCloseEnterAnimation), anyBoolean());
}
+ @Test
+ public void testTransientHideWithMoveToTop() {
+ Transitions transitions = createTestTransitions();
+ transitions.replaceDefaultHandlerForTest(mDefaultHandler);
+ final TransitionAnimation transitionAnimation = new TransitionAnimation(mContext, false,
+ Transitions.TAG);
+ spyOn(transitionAnimation);
+
+ // Prepare for a TO_BACK transition
+ final RunningTaskInfo taskInfo = createTaskInfo(1);
+ final IBinder closeTransition = new Binder();
+ final SurfaceControl.Transaction closeTransitionFinishT =
+ mock(SurfaceControl.Transaction.class);
+
+ // Start a TO_BACK transition
+ transitions.requestStartTransition(closeTransition,
+ new TransitionRequestInfo(TRANSIT_TO_BACK, null /* trigger */, null /* remote */));
+ TransitionInfo closeInfo = new TransitionInfoBuilder(TRANSIT_TO_BACK)
+ .addChange(TRANSIT_TO_BACK, taskInfo)
+ .build();
+ transitions.onTransitionReady(closeTransition, closeInfo, new StubTransaction(),
+ closeTransitionFinishT);
+
+ // Verify that the transition hides the task surface in the finish transaction
+ verify(closeTransitionFinishT).hide(any());
+
+ // Prepare for a CHANGE transition
+ final IBinder changeTransition = new Binder();
+ final SurfaceControl.Transaction changeTransitionFinishT =
+ mock(SurfaceControl.Transaction.class);
+
+ // Start a CHANGE transition w/ MOVE_TO_FRONT that is merged into the TO_BACK
+ mDefaultHandler.setShouldMerge(changeTransition);
+ transitions.requestStartTransition(changeTransition,
+ new TransitionRequestInfo(TRANSIT_OPEN, null /* trigger */, null /* remote */));
+ TransitionInfo changeInfo = new TransitionInfoBuilder(TRANSIT_OPEN)
+ .addChange(TRANSIT_CHANGE, FLAG_MOVED_TO_TOP, taskInfo)
+ .build();
+ transitions.onTransitionReady(changeTransition, changeInfo, new StubTransaction(),
+ changeTransitionFinishT);
+
+ // Verify that the transition shows the task surface in the finish transaction so that the
+ // when the original transition finishes, the finish transaction does not clobber the
+ // visibility of the merged transition
+ verify(changeTransitionFinishT).show(any());
+ }
+
class TestTransitionHandler implements Transitions.TransitionHandler {
ArrayList<Pair<IBinder, Transitions.TransitionFinishCallback>> mFinishes =
new ArrayList<>();
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 4c9c2f14d805..ed69d912c4ef 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
@@ -55,6 +55,7 @@ import com.android.wm.shell.common.DisplayLayout
import com.android.wm.shell.common.MultiDisplayDragMoveIndicatorController
import com.android.wm.shell.common.MultiInstanceHelper
import com.android.wm.shell.common.SyncTransactionQueue
+import com.android.wm.shell.compatui.api.CompatUIHandler
import com.android.wm.shell.desktopmode.DesktopActivityOrientationChangeHandler
import com.android.wm.shell.desktopmode.DesktopImmersiveController
import com.android.wm.shell.desktopmode.DesktopModeEventLogger
@@ -144,6 +145,7 @@ open class DesktopModeWindowDecorViewModelTestsBase : ShellTestCase() {
mock<DesktopActivityOrientationChangeHandler>()
protected val mockMultiDisplayDragMoveIndicatorController =
mock<MultiDisplayDragMoveIndicatorController>()
+ protected val mockCompatUIHandler = mock<CompatUIHandler>()
protected val mockInputManager = mock<InputManager>()
private val mockTaskPositionerFactory =
mock<DesktopModeWindowDecorViewModel.TaskPositionerFactory>()
@@ -243,6 +245,7 @@ open class DesktopModeWindowDecorViewModelTestsBase : ShellTestCase() {
desktopModeCompatPolicy,
mockTilingWindowDecoration,
mockMultiDisplayDragMoveIndicatorController,
+ mockCompatUIHandler,
)
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 f7b9c3352dea..77513adf0088 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
@@ -303,7 +303,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {
doReturn(mInsetsState).when(mMockDisplayController).getInsetsState(anyInt());
when(mMockHandleMenuFactory.create(any(), any(), any(), any(), any(), anyInt(), any(),
anyBoolean(), anyBoolean(), anyBoolean(), anyBoolean(), anyBoolean(), anyBoolean(),
- any(), anyInt(), anyInt(), anyInt(), anyInt()))
+ anyBoolean(), any(), anyInt(), anyInt(), anyInt(), anyInt()))
.thenReturn(mMockHandleMenu);
when(mMockMultiInstanceHelper.supportsMultiInstanceSplit(any(), anyInt()))
.thenReturn(false);
@@ -1450,6 +1450,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {
any(),
any(),
any(),
+ any(),
anyBoolean()
);
// Run runnable to set captured link to used
@@ -1487,6 +1488,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {
any(),
any(),
any(),
+ any(),
anyBoolean()
);
openInBrowserCaptor.getValue().invoke(new Intent(Intent.ACTION_MAIN, TEST_URI1));
@@ -1519,6 +1521,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {
any(),
any(),
any(),
+ any(),
anyBoolean()
);
@@ -1584,6 +1587,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {
any(),
any(),
any(),
+ any(),
closeClickListener.capture(),
any(),
anyBoolean()
@@ -1617,6 +1621,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {
any(),
any(),
any(),
+ any(),
/* forceShowSystemBars= */ eq(true)
);
}
@@ -1790,7 +1795,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {
private void verifyHandleMenuCreated(@Nullable Uri uri) {
verify(mMockHandleMenuFactory).create(any(), any(), any(), any(), any(), anyInt(),
any(), anyBoolean(), anyBoolean(), anyBoolean(), anyBoolean(), anyBoolean(),
- anyBoolean(), argThat(intent ->
+ anyBoolean(), anyBoolean(), argThat(intent ->
(uri == null && intent == null) || intent.getData().equals(uri)),
anyInt(), anyInt(), anyInt(), anyInt());
}
@@ -1916,6 +1921,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {
.setTaskDescriptionBuilder(taskDescriptionBuilder)
.setVisible(visible)
.build();
+ taskInfo.isVisibleRequested = visible;
taskInfo.realActivity = new ComponentName("com.android.wm.shell.windowdecor",
"DesktopModeWindowDecorationTests");
taskInfo.baseActivity = new ComponentName("com.android.wm.shell.windowdecor",
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt
index 2e46f6312d03..e4b897264883 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt
@@ -287,6 +287,7 @@ class HandleMenuTest : ShellTestCase() {
shouldShowManageWindowsButton = false,
shouldShowChangeAspectRatioButton = false,
shouldShowDesktopModeButton = true,
+ shouldShowRestartButton = true,
isBrowserApp = false,
null /* openInAppOrBrowserIntent */,
captionWidth = HANDLE_WIDTH,
@@ -304,6 +305,7 @@ class HandleMenuTest : ShellTestCase() {
onChangeAspectRatioClickListener = mock(),
openInAppOrBrowserClickListener = mock(),
onOpenByDefaultClickListener = mock(),
+ onRestartClickListener = mock(),
onCloseMenuClickListener = mock(),
onOutsideTouchListener = mock(),
forceShowSystemBars = forceShowSystemBars
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/viewholder/AppHandleViewHolderTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/viewholder/AppHandleViewHolderTest.kt
index bc4865a07f7f..2c3009cb8dc4 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/viewholder/AppHandleViewHolderTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/viewholder/AppHandleViewHolderTest.kt
@@ -78,7 +78,8 @@ class AppHandleViewHolderTest : ShellTestCase() {
position = captionPosition,
width = CAPTION_WIDTH,
height = CAPTION_HEIGHT,
- showInputLayer = false
+ showInputLayer = false,
+ isCaptionVisible = true
)
)
diff --git a/libs/androidfw/AssetManager2.cpp b/libs/androidfw/AssetManager2.cpp
index 714f6e41ff05..e09ab5fd1643 100644
--- a/libs/androidfw/AssetManager2.cpp
+++ b/libs/androidfw/AssetManager2.cpp
@@ -879,10 +879,10 @@ base::expected<FindEntryResult, NullOrIOError> AssetManager2::FindEntry(
// if we don't have a result yet
if (!final_result ||
// or this config is better before the locale than the existing result
- result->config.isBetterThanBeforeLocale(final_result->config, desired_config) ||
+ result->config.isBetterThanBeforeLocale(final_result->config, *desired_config) ||
// or the existing config isn't better before locale and this one specifies a locale
// whereas the existing one doesn't
- (!final_result->config.isBetterThanBeforeLocale(result->config, desired_config)
+ (!final_result->config.isBetterThanBeforeLocale(result->config, *desired_config)
&& has_locale && !final_has_locale)) {
final_result = result.value();
final_overlaid = overlaid;
diff --git a/libs/androidfw/CursorWindow.cpp b/libs/androidfw/CursorWindow.cpp
index a592749c5398..6e11d430c5ea 100644
--- a/libs/androidfw/CursorWindow.cpp
+++ b/libs/androidfw/CursorWindow.cpp
@@ -55,7 +55,7 @@ status_t CursorWindow::create(const String8 &name, size_t inflatedSize, CursorWi
window->mName = name;
window->mSize = std::min(kInlineSize, inflatedSize);
window->mInflatedSize = inflatedSize;
- window->mData = malloc(window->mSize);
+ window->mData = calloc(window->mSize, 1);
if (!window->mData) goto fail;
window->mReadOnly = false;
diff --git a/libs/androidfw/ResourceTypes.cpp b/libs/androidfw/ResourceTypes.cpp
index 8ecd6ba9b253..6ec605c2ced5 100644
--- a/libs/androidfw/ResourceTypes.cpp
+++ b/libs/androidfw/ResourceTypes.cpp
@@ -2615,16 +2615,14 @@ bool ResTable_config::isLocaleBetterThan(const ResTable_config& o,
}
bool ResTable_config::isBetterThanBeforeLocale(const ResTable_config& o,
- const ResTable_config* requested) const {
- if (requested) {
- if (imsi || o.imsi) {
- if ((mcc != o.mcc) && requested->mcc) {
- return (mcc);
- }
+ const ResTable_config& requested) const {
+ if (imsi || o.imsi) {
+ if ((mcc != o.mcc) && requested.mcc) {
+ return mcc;
+ }
- if ((mnc != o.mnc) && requested->mnc) {
- return (mnc);
- }
+ if ((mnc != o.mnc) && requested.mnc) {
+ return mnc;
}
}
return false;
diff --git a/libs/androidfw/include/androidfw/ResourceTypes.h b/libs/androidfw/include/androidfw/ResourceTypes.h
index 63b28da075cd..bd72d3741460 100644
--- a/libs/androidfw/include/androidfw/ResourceTypes.h
+++ b/libs/androidfw/include/androidfw/ResourceTypes.h
@@ -1416,7 +1416,10 @@ struct ResTable_config
// match the requested configuration at all.
bool isLocaleBetterThan(const ResTable_config& o, const ResTable_config* requested) const;
- bool isBetterThanBeforeLocale(const ResTable_config& o, const ResTable_config* requested) const;
+ // The first part of isBetterThan() that only compares the fields that are higher priority than
+ // the locale. Use it when you need to do custom locale matching to filter out the configs prior
+ // to that.
+ bool isBetterThanBeforeLocale(const ResTable_config& o, const ResTable_config& requested) const;
String8 toString() const;
diff --git a/libs/hwui/renderthread/ReliableSurface.cpp b/libs/hwui/renderthread/ReliableSurface.cpp
index 01e8010444c0..64d38b9ef466 100644
--- a/libs/hwui/renderthread/ReliableSurface.cpp
+++ b/libs/hwui/renderthread/ReliableSurface.cpp
@@ -149,25 +149,9 @@ ANativeWindowBuffer* ReliableSurface::acquireFallbackBuffer(int error) {
return AHardwareBuffer_to_ANativeWindowBuffer(mScratchBuffer.get());
}
- int width = -1;
- int result = mWindow->query(mWindow, NATIVE_WINDOW_DEFAULT_WIDTH, &width);
- if (result != OK || width < 0) {
- ALOGW("Failed to query window default width: %s (%d) value=%d", strerror(-result), result,
- width);
- width = 1;
- }
-
- int height = -1;
- result = mWindow->query(mWindow, NATIVE_WINDOW_DEFAULT_HEIGHT, &height);
- if (result != OK || height < 0) {
- ALOGW("Failed to query window default height: %s (%d) value=%d", strerror(-result), result,
- height);
- height = 1;
- }
-
AHardwareBuffer_Desc desc = AHardwareBuffer_Desc{
- .width = static_cast<uint32_t>(width),
- .height = static_cast<uint32_t>(height),
+ .width = 1,
+ .height = 1,
.layers = 1,
.format = mFormat,
.usage = mUsage,
@@ -176,9 +160,9 @@ ANativeWindowBuffer* ReliableSurface::acquireFallbackBuffer(int error) {
};
AHardwareBuffer* newBuffer;
- result = AHardwareBuffer_allocate(&desc, &newBuffer);
+ int result = AHardwareBuffer_allocate(&desc, &newBuffer);
- if (result != OK) {
+ if (result != NO_ERROR) {
// Allocate failed, that sucks
ALOGW("Failed to allocate scratch buffer, error=%d", result);
return nullptr;
diff --git a/libs/hwui/renderthread/VulkanManager.cpp b/libs/hwui/renderthread/VulkanManager.cpp
index a67aea466c1c..0cd9c53c830f 100644
--- a/libs/hwui/renderthread/VulkanManager.cpp
+++ b/libs/hwui/renderthread/VulkanManager.cpp
@@ -238,6 +238,7 @@ void VulkanManager::setupDevice(skgpu::VulkanExtensions& grExtensions,
for (uint32_t i = 0; i < queueCount; i++) {
queuePriorityProps[i].sType = VK_STRUCTURE_TYPE_QUEUE_FAMILY_GLOBAL_PRIORITY_PROPERTIES_EXT;
queuePriorityProps[i].pNext = nullptr;
+ queueProps[i].sType = VK_STRUCTURE_TYPE_QUEUE_FAMILY_PROPERTIES_2;
queueProps[i].pNext = &queuePriorityProps[i];
}
mGetPhysicalDeviceQueueFamilyProperties2(mPhysicalDevice, &queueCount, queueProps.get());
diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java
index 4aba491c291e..0a1bfd55e77f 100644
--- a/media/java/android/media/AudioManager.java
+++ b/media/java/android/media/AudioManager.java
@@ -19,14 +19,15 @@ package android.media;
import static android.companion.virtual.VirtualDeviceParams.DEVICE_POLICY_DEFAULT;
import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_AUDIO;
import static android.content.Context.DEVICE_ID_DEFAULT;
-import static android.media.audio.Flags.FLAG_UNIFY_ABSOLUTE_VOLUME_MANAGEMENT;
-import static android.media.audio.Flags.autoPublicVolumeApiHardening;
-import static android.media.audio.Flags.cacheGetStreamMinMaxVolume;
-import static android.media.audio.Flags.cacheGetStreamVolume;
import static android.media.audio.Flags.FLAG_DEPRECATE_STREAM_BT_SCO;
import static android.media.audio.Flags.FLAG_FOCUS_EXCLUSIVE_WITH_RECORDING;
import static android.media.audio.Flags.FLAG_FOCUS_FREEZE_TEST_API;
+import static android.media.audio.Flags.FLAG_REGISTER_VOLUME_CALLBACK_API_HARDENING;
import static android.media.audio.Flags.FLAG_SUPPORTED_DEVICE_TYPES_API;
+import static android.media.audio.Flags.FLAG_UNIFY_ABSOLUTE_VOLUME_MANAGEMENT;
+import static android.media.audio.Flags.autoPublicVolumeApiHardening;
+import static android.media.audio.Flags.cacheGetStreamMinMaxVolume;
+import static android.media.audio.Flags.cacheGetStreamVolume;
import static android.media.audiopolicy.Flags.FLAG_ENABLE_FADE_MANAGER_CONFIGURATION;
import android.Manifest;
@@ -65,7 +66,7 @@ import android.media.audiopolicy.AudioPolicy;
import android.media.audiopolicy.AudioPolicy.AudioPolicyFocusListener;
import android.media.audiopolicy.AudioProductStrategy;
import android.media.audiopolicy.AudioVolumeGroup;
-import android.media.audiopolicy.AudioVolumeGroupChangeHandler;
+import android.media.audiopolicy.IAudioVolumeChangeDispatcher;
import android.media.projection.MediaProjection;
import android.media.session.MediaController;
import android.media.session.MediaSession;
@@ -128,8 +129,6 @@ public class AudioManager {
private static final String TAG = "AudioManager";
private static final boolean DEBUG = false;
private static final AudioPortEventHandler sAudioPortEventHandler = new AudioPortEventHandler();
- private static final AudioVolumeGroupChangeHandler sAudioAudioVolumeGroupChangedHandler =
- new AudioVolumeGroupChangeHandler();
private static WeakReference<Context> sContext;
@@ -8761,9 +8760,13 @@ public class AudioManager {
}
}
+ //====================================================================
+ // Notification of volume group changes
/**
+ * Callback to receive updates on volume group changes, register using
+ * {@link AudioManager#registerVolumeGroupCallback(Executor, AudioVolumeCallback)}.
+ *
* @hide
- * Callback registered by client to be notified upon volume group change.
*/
@SystemApi
public abstract static class VolumeGroupCallback {
@@ -8774,34 +8777,69 @@ public class AudioManager {
public void onAudioVolumeGroupChanged(int group, int flags) {}
}
- /**
- * @hide
- * Register an audio volume group change listener.
- * @param callback the {@link VolumeGroupCallback} to register
- */
+ /**
+ * Register an audio volume group change listener.
+ *
+ * @param executor {@link Executor} to handle the callbacks
+ * @param callback the callback to receive the audio volume group changes
+ * @throws SecurityException if the caller doesn't have the required permission.
+ *
+ * @hide
+ */
@SystemApi
- public void registerVolumeGroupCallback(
- @NonNull Executor executor,
+ @FlaggedApi(FLAG_REGISTER_VOLUME_CALLBACK_API_HARDENING)
+ @RequiresPermission("Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED")
+ public void registerVolumeGroupCallback(@NonNull Executor executor,
@NonNull VolumeGroupCallback callback) {
- Preconditions.checkNotNull(executor, "executor must not be null");
- Preconditions.checkNotNull(callback, "volume group change cb must not be null");
- sAudioAudioVolumeGroupChangedHandler.init();
- // TODO: make use of executor
- sAudioAudioVolumeGroupChangedHandler.registerListener(callback);
+ mVolumeChangedListenerMgr.addListener(executor, callback, "registerVolumeGroupCallback",
+ () -> new AudioVolumeChangeDispatcherStub());
}
- /**
- * @hide
- * Unregister an audio volume group change listener.
- * @param callback the {@link VolumeGroupCallback} to unregister
- */
+ /**
+ * Unregister an audio volume group change listener.
+ *
+ * @param callback the {@link VolumeGroupCallback} to unregister
+ *
+ * @hide
+ */
@SystemApi
- public void unregisterVolumeGroupCallback(
- @NonNull VolumeGroupCallback callback) {
- Preconditions.checkNotNull(callback, "volume group change cb must not be null");
- sAudioAudioVolumeGroupChangedHandler.unregisterListener(callback);
+ @FlaggedApi(FLAG_REGISTER_VOLUME_CALLBACK_API_HARDENING)
+ @RequiresPermission("Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED")
+ public void unregisterVolumeGroupCallback(@NonNull VolumeGroupCallback callback) {
+ mVolumeChangedListenerMgr.removeListener(callback, "unregisterVolumeGroupCallback");
+ }
+
+ /**
+ * Manages the VolumeGroupCallback listeners and the AudioVolumeChangeDispatcherStub
+ */
+ private final CallbackUtil.LazyListenerManager<VolumeGroupCallback> mVolumeChangedListenerMgr =
+ new CallbackUtil.LazyListenerManager();
+
+ final class AudioVolumeChangeDispatcherStub extends IAudioVolumeChangeDispatcher.Stub
+ implements CallbackUtil.DispatcherStub {
+
+ @Override
+ public void register(boolean register) {
+ try {
+ if (register) {
+ getService().registerAudioVolumeCallback(this);
+ } else {
+ getService().unregisterAudioVolumeCallback(this);
+ }
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
+ public void onAudioVolumeGroupChanged(int group, int flags) {
+ mVolumeChangedListenerMgr.callListeners((listener) ->
+ listener.onAudioVolumeGroupChanged(group, flags));
+ }
}
+ //====================================================================
+
/**
* Return if an asset contains haptic channels or not.
*
diff --git a/media/java/android/media/AudioSystem.java b/media/java/android/media/AudioSystem.java
index ad6f2e52fd97..4906cd3fb1e5 100644
--- a/media/java/android/media/AudioSystem.java
+++ b/media/java/android/media/AudioSystem.java
@@ -2732,4 +2732,25 @@ public class AudioSystem
* @hide
*/
public static native void triggerSystemPropertyUpdate(long handle);
+
+ /**
+ * Registers the given {@link INativeAudioVolumeGroupCallback} to native audioserver.
+ * @param callback to register
+ * @return {@link #SUCCESS} if successfully registered.
+ *
+ * @hide
+ */
+ public static native int registerAudioVolumeGroupCallback(
+ INativeAudioVolumeGroupCallback callback);
+
+ /**
+ * Unegisters the given {@link INativeAudioVolumeGroupCallback} from native audioserver
+ * previously registered via {@link #registerAudioVolumeGroupCallback}.
+ * @param callback to register
+ * @return {@link #SUCCESS} if successfully registered.
+ *
+ * @hide
+ */
+ public static native int unregisterAudioVolumeGroupCallback(
+ INativeAudioVolumeGroupCallback callback);
}
diff --git a/media/java/android/media/IAudioService.aidl b/media/java/android/media/IAudioService.aidl
index 8aadb418cf5a..c505bcee0332 100644
--- a/media/java/android/media/IAudioService.aidl
+++ b/media/java/android/media/IAudioService.aidl
@@ -65,6 +65,7 @@ import android.media.audiopolicy.AudioPolicyConfig;
import android.media.audiopolicy.AudioProductStrategy;
import android.media.audiopolicy.AudioVolumeGroup;
import android.media.audiopolicy.IAudioPolicyCallback;
+import android.media.audiopolicy.IAudioVolumeChangeDispatcher;
import android.media.projection.IMediaProjection;
import android.net.Uri;
import android.os.PersistableBundle;
@@ -446,6 +447,12 @@ interface IAudioService {
boolean isAudioServerRunning();
+ @EnforcePermission("MODIFY_AUDIO_SETTINGS_PRIVILEGED")
+ void registerAudioVolumeCallback(IAudioVolumeChangeDispatcher avc);
+
+ @EnforcePermission("MODIFY_AUDIO_SETTINGS_PRIVILEGED")
+ oneway void unregisterAudioVolumeCallback(IAudioVolumeChangeDispatcher avc);
+
int setUidDeviceAffinity(in IAudioPolicyCallback pcb, in int uid, in int[] deviceTypes,
in String[] deviceAddresses);
diff --git a/media/java/android/media/audiopolicy/AudioVolumeGroupChangeHandler.java b/media/java/android/media/audiopolicy/AudioVolumeGroupChangeHandler.java
deleted file mode 100644
index 022cfeeb4e43..000000000000
--- a/media/java/android/media/audiopolicy/AudioVolumeGroupChangeHandler.java
+++ /dev/null
@@ -1,166 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.media.audiopolicy;
-
-import android.annotation.NonNull;
-import android.media.AudioManager;
-import android.os.Handler;
-import android.os.HandlerThread;
-import android.os.Message;
-
-import com.android.internal.util.Preconditions;
-
-import java.lang.ref.WeakReference;
-import java.util.ArrayList;
-
-/**
- * The AudioVolumeGroupChangeHandler handles AudioManager.OnAudioVolumeGroupChangedListener
- * callbacks posted from JNI
- *
- * TODO: Make use of Executor of callbacks.
- * @hide
- */
-public class AudioVolumeGroupChangeHandler {
- private Handler mHandler;
- private HandlerThread mHandlerThread;
- private final ArrayList<AudioManager.VolumeGroupCallback> mListeners =
- new ArrayList<AudioManager.VolumeGroupCallback>();
-
- private static final String TAG = "AudioVolumeGroupChangeHandler";
-
- private static final int AUDIOVOLUMEGROUP_EVENT_VOLUME_CHANGED = 1000;
- private static final int AUDIOVOLUMEGROUP_EVENT_NEW_LISTENER = 4;
-
- /**
- * Accessed by native methods: JNI Callback context.
- */
- @SuppressWarnings("unused")
- private long mJniCallback;
-
- /**
- * Initialization
- */
- public void init() {
- synchronized (this) {
- if (mHandler != null) {
- return;
- }
- // create a new thread for our new event handler
- mHandlerThread = new HandlerThread(TAG);
- mHandlerThread.start();
-
- if (mHandlerThread.getLooper() == null) {
- mHandler = null;
- return;
- }
- mHandler = new Handler(mHandlerThread.getLooper()) {
- @Override
- public void handleMessage(Message msg) {
- ArrayList<AudioManager.VolumeGroupCallback> listeners;
- synchronized (this) {
- if (msg.what == AUDIOVOLUMEGROUP_EVENT_NEW_LISTENER) {
- listeners =
- new ArrayList<AudioManager.VolumeGroupCallback>();
- if (mListeners.contains(msg.obj)) {
- listeners.add(
- (AudioManager.VolumeGroupCallback) msg.obj);
- }
- } else {
- listeners = (ArrayList<AudioManager.VolumeGroupCallback>)
- mListeners.clone();
- }
- }
- if (listeners.isEmpty()) {
- return;
- }
-
- switch (msg.what) {
- case AUDIOVOLUMEGROUP_EVENT_VOLUME_CHANGED:
- for (int i = 0; i < listeners.size(); i++) {
- listeners.get(i).onAudioVolumeGroupChanged((int) msg.arg1,
- (int) msg.arg2);
- }
- break;
-
- default:
- break;
- }
- }
- };
- native_setup(new WeakReference<AudioVolumeGroupChangeHandler>(this));
- }
- }
-
- private native void native_setup(Object moduleThis);
-
- @Override
- protected void finalize() {
- native_finalize();
- if (mHandlerThread.isAlive()) {
- mHandlerThread.quit();
- }
- }
- private native void native_finalize();
-
- /**
- * @param cb the {@link AudioManager.VolumeGroupCallback} to register
- */
- public void registerListener(@NonNull AudioManager.VolumeGroupCallback cb) {
- Preconditions.checkNotNull(cb, "volume group callback shall not be null");
- synchronized (this) {
- mListeners.add(cb);
- }
- if (mHandler != null) {
- Message m = mHandler.obtainMessage(
- AUDIOVOLUMEGROUP_EVENT_NEW_LISTENER, 0, 0, cb);
- mHandler.sendMessage(m);
- }
- }
-
- /**
- * @param cb the {@link AudioManager.VolumeGroupCallback} to unregister
- */
- public void unregisterListener(@NonNull AudioManager.VolumeGroupCallback cb) {
- Preconditions.checkNotNull(cb, "volume group callback shall not be null");
- synchronized (this) {
- mListeners.remove(cb);
- }
- }
-
- Handler handler() {
- return mHandler;
- }
-
- @SuppressWarnings("unused")
- private static void postEventFromNative(Object moduleRef,
- int what, int arg1, int arg2, Object obj) {
- AudioVolumeGroupChangeHandler eventHandler =
- (AudioVolumeGroupChangeHandler) ((WeakReference) moduleRef).get();
- if (eventHandler == null) {
- return;
- }
-
- if (eventHandler != null) {
- Handler handler = eventHandler.handler();
- if (handler != null) {
- Message m = handler.obtainMessage(what, arg1, arg2, obj);
- // Do not remove previous messages, as we would lose notification of group changes
- handler.sendMessage(m);
- }
- }
- }
-}
diff --git a/media/java/android/media/audiopolicy/IAudioVolumeChangeDispatcher.aidl b/media/java/android/media/audiopolicy/IAudioVolumeChangeDispatcher.aidl
new file mode 100644
index 000000000000..e6f9024cfd1e
--- /dev/null
+++ b/media/java/android/media/audiopolicy/IAudioVolumeChangeDispatcher.aidl
@@ -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.
+ */
+
+package android.media.audiopolicy;
+
+/**
+ * AIDL for the AudioService to signal audio volume groups changes
+ *
+ * {@hide}
+ */
+oneway interface IAudioVolumeChangeDispatcher {
+
+ /**
+ * Called when a volume group has been changed
+ * @param group id of the volume group that has changed.
+ * @param flags one or more flags to describe the volume change.
+ */
+ void onAudioVolumeGroupChanged(int group, int flags);
+}
diff --git a/media/java/android/media/quality/MediaQualityContract.java b/media/java/android/media/quality/MediaQualityContract.java
index fccdba8e727f..ece87a66556f 100644
--- a/media/java/android/media/quality/MediaQualityContract.java
+++ b/media/java/android/media/quality/MediaQualityContract.java
@@ -72,6 +72,43 @@ public class MediaQualityContract {
*/
public static final String LEVEL_OFF = "level_off";
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @StringDef(prefix = "COLOR_TEMP", value = {
+ COLOR_TEMP_USER,
+ COLOR_TEMP_COOL,
+ COLOR_TEMP_STANDARD,
+ COLOR_TEMP_WARM,
+ COLOR_TEMP_USER_HDR10PLUS,
+ COLOR_TEMP_COOL_HDR10PLUS,
+ COLOR_TEMP_STANDARD_HDR10PLUS,
+ COLOR_TEMP_WARM_HDR10PLUS,
+ COLOR_TEMP_FMMSDR,
+ COLOR_TEMP_FMMHDR,
+ })
+ public @interface ColorTempValue {}
+
+ /** @hide */
+ public static final String COLOR_TEMP_USER = "color_temp_user";
+ /** @hide */
+ public static final String COLOR_TEMP_COOL = "color_temp_cool";
+ /** @hide */
+ public static final String COLOR_TEMP_STANDARD = "color_temp_standard";
+ /** @hide */
+ public static final String COLOR_TEMP_WARM = "color_temp_warm";
+ /** @hide */
+ public static final String COLOR_TEMP_USER_HDR10PLUS = "color_temp_user_hdr10plus";
+ /** @hide */
+ public static final String COLOR_TEMP_COOL_HDR10PLUS = "color_temp_cool_hdr10plus";
+ /** @hide */
+ public static final String COLOR_TEMP_STANDARD_HDR10PLUS = "color_temp_standard_hdr10plus";
+ /** @hide */
+ public static final String COLOR_TEMP_WARM_HDR10PLUS = "color_temp_warm_hdr10plus";
+ /** @hide */
+ public static final String COLOR_TEMP_FMMSDR = "color_temp_fmmsdr";
+ /** @hide */
+ public static final String COLOR_TEMP_FMMHDR = "color_temp_fmmhdr";
+
/**
* @hide
@@ -82,7 +119,6 @@ public class MediaQualityContract {
String PARAMETER_NAME = "_name";
String PARAMETER_PACKAGE = "_package";
String PARAMETER_INPUT_ID = "_input_id";
- String VENDOR_PARAMETERS = "_vendor_parameters";
}
/**
diff --git a/media/tests/AudioPolicyTest/AndroidManifest.xml b/media/tests/AudioPolicyTest/AndroidManifest.xml
index 5c911b135a5d..466da7e66fbf 100644
--- a/media/tests/AudioPolicyTest/AndroidManifest.xml
+++ b/media/tests/AudioPolicyTest/AndroidManifest.xml
@@ -19,6 +19,7 @@
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
+ <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_ROUTING" />
<uses-permission android:name="android.permission.CHANGE_ACCESSIBILITY_VOLUME" />
diff --git a/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioVolumeGroupChangeHandlerTest.java b/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioVolumeGroupChangeHandlerTest.java
deleted file mode 100644
index 82394a2eb420..000000000000
--- a/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioVolumeGroupChangeHandlerTest.java
+++ /dev/null
@@ -1,211 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open 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.audiopolicytest;
-
-import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
-
-import static com.android.audiopolicytest.AudioVolumeTestUtil.DEFAULT_ATTRIBUTES;
-import static com.android.audiopolicytest.AudioVolumeTestUtil.incrementVolumeIndex;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertThrows;
-import static org.junit.Assert.assertTrue;
-
-import android.media.AudioAttributes;
-import android.media.AudioManager;
-import android.media.audiopolicy.AudioVolumeGroup;
-import android.media.audiopolicy.AudioVolumeGroupChangeHandler;
-import android.platform.test.annotations.Presubmit;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.util.ArrayList;
-import java.util.List;
-
-@Presubmit
-@RunWith(AndroidJUnit4.class)
-public class AudioVolumeGroupChangeHandlerTest {
- private static final String TAG = "AudioVolumeGroupChangeHandlerTest";
-
- @Rule
- public final AudioVolumesTestRule rule = new AudioVolumesTestRule();
-
- private AudioManager mAudioManager;
-
- @Before
- public void setUp() {
- mAudioManager = getApplicationContext().getSystemService(AudioManager.class);
- }
-
- @Test
- public void testRegisterInvalidCallback() {
- final AudioVolumeGroupChangeHandler audioAudioVolumeGroupChangedHandler =
- new AudioVolumeGroupChangeHandler();
-
- audioAudioVolumeGroupChangedHandler.init();
-
- assertThrows(NullPointerException.class, () -> {
- AudioManager.VolumeGroupCallback nullCb = null;
- audioAudioVolumeGroupChangedHandler.registerListener(nullCb);
- });
- }
-
- @Test
- public void testUnregisterInvalidCallback() {
- final AudioVolumeGroupChangeHandler audioAudioVolumeGroupChangedHandler =
- new AudioVolumeGroupChangeHandler();
-
- audioAudioVolumeGroupChangedHandler.init();
-
- final AudioVolumeGroupCallbackHelper cb = new AudioVolumeGroupCallbackHelper();
- audioAudioVolumeGroupChangedHandler.registerListener(cb);
-
- assertThrows(NullPointerException.class, () -> {
- AudioManager.VolumeGroupCallback nullCb = null;
- audioAudioVolumeGroupChangedHandler.unregisterListener(nullCb);
- });
- audioAudioVolumeGroupChangedHandler.unregisterListener(cb);
- }
-
- @Test
- public void testRegisterUnregisterCallback() {
- final AudioVolumeGroupChangeHandler audioAudioVolumeGroupChangedHandler =
- new AudioVolumeGroupChangeHandler();
-
- audioAudioVolumeGroupChangedHandler.init();
- final AudioVolumeGroupCallbackHelper validCb = new AudioVolumeGroupCallbackHelper();
-
- // Should not assert, otherwise test will fail
- audioAudioVolumeGroupChangedHandler.registerListener(validCb);
-
- // Should not assert, otherwise test will fail
- audioAudioVolumeGroupChangedHandler.unregisterListener(validCb);
- }
-
- @Test
- public void testCallbackReceived() {
- final AudioVolumeGroupChangeHandler audioAudioVolumeGroupChangedHandler =
- new AudioVolumeGroupChangeHandler();
-
- audioAudioVolumeGroupChangedHandler.init();
-
- final AudioVolumeGroupCallbackHelper validCb = new AudioVolumeGroupCallbackHelper();
- audioAudioVolumeGroupChangedHandler.registerListener(validCb);
-
- List<AudioVolumeGroup> audioVolumeGroups = mAudioManager.getAudioVolumeGroups();
- assertTrue(audioVolumeGroups.size() > 0);
-
- try {
- for (final AudioVolumeGroup audioVolumeGroup : audioVolumeGroups) {
- int volumeGroupId = audioVolumeGroup.getId();
-
- List<AudioAttributes> avgAttributes = audioVolumeGroup.getAudioAttributes();
- // Set the volume per attributes (if valid) and wait the callback
- if (avgAttributes.size() == 0 || avgAttributes.get(0).equals(DEFAULT_ATTRIBUTES)) {
- // Some volume groups may not have valid attributes, used for internal
- // volume management like patch/rerouting
- // so bailing out strategy retrieval from attributes
- continue;
- }
- final AudioAttributes aa = avgAttributes.get(0);
-
- int index = mAudioManager.getVolumeIndexForAttributes(aa);
- int indexMax = mAudioManager.getMaxVolumeIndexForAttributes(aa);
- int indexMin = mAudioManager.getMinVolumeIndexForAttributes(aa);
-
- final int indexForAa = incrementVolumeIndex(index, indexMin, indexMax);
-
- // Set the receiver to filter only the current group callback
- validCb.setExpectedVolumeGroup(volumeGroupId);
- mAudioManager.setVolumeIndexForAttributes(aa, indexForAa, 0/*flags*/);
- assertTrue(validCb.waitForExpectedVolumeGroupChanged(
- AudioVolumeGroupCallbackHelper.ASYNC_TIMEOUT_MS));
-
- final int readIndex = mAudioManager.getVolumeIndexForAttributes(aa);
- assertEquals(readIndex, indexForAa);
- }
- } finally {
- audioAudioVolumeGroupChangedHandler.unregisterListener(validCb);
- }
- }
-
- @Test
- public void testMultipleCallbackReceived() {
-
- final AudioVolumeGroupChangeHandler audioAudioVolumeGroupChangedHandler =
- new AudioVolumeGroupChangeHandler();
-
- audioAudioVolumeGroupChangedHandler.init();
-
- final int callbackCount = 10;
- final List<AudioVolumeGroupCallbackHelper> validCbs =
- new ArrayList<AudioVolumeGroupCallbackHelper>();
- for (int i = 0; i < callbackCount; i++) {
- validCbs.add(new AudioVolumeGroupCallbackHelper());
- }
- for (final AudioVolumeGroupCallbackHelper cb : validCbs) {
- audioAudioVolumeGroupChangedHandler.registerListener(cb);
- }
-
- List<AudioVolumeGroup> audioVolumeGroups = mAudioManager.getAudioVolumeGroups();
- assertTrue(audioVolumeGroups.size() > 0);
-
- try {
- for (final AudioVolumeGroup audioVolumeGroup : audioVolumeGroups) {
- int volumeGroupId = audioVolumeGroup.getId();
-
- List<AudioAttributes> avgAttributes = audioVolumeGroup.getAudioAttributes();
- // Set the volume per attributes (if valid) and wait the callback
- if (avgAttributes.size() == 0 || avgAttributes.get(0).equals(DEFAULT_ATTRIBUTES)) {
- // Some volume groups may not have valid attributes, used for internal
- // volume management like patch/rerouting
- // so bailing out strategy retrieval from attributes
- continue;
- }
- AudioAttributes aa = avgAttributes.get(0);
-
- int index = mAudioManager.getVolumeIndexForAttributes(aa);
- int indexMax = mAudioManager.getMaxVolumeIndexForAttributes(aa);
- int indexMin = mAudioManager.getMinVolumeIndexForAttributes(aa);
-
- final int indexForAa = incrementVolumeIndex(index, indexMin, indexMax);
-
- // Set the receiver to filter only the current group callback
- for (final AudioVolumeGroupCallbackHelper cb : validCbs) {
- cb.setExpectedVolumeGroup(volumeGroupId);
- }
- mAudioManager.setVolumeIndexForAttributes(aa, indexForAa, 0/*flags*/);
-
- for (final AudioVolumeGroupCallbackHelper cb : validCbs) {
- assertTrue(cb.waitForExpectedVolumeGroupChanged(
- AudioVolumeGroupCallbackHelper.ASYNC_TIMEOUT_MS));
- }
- int readIndex = mAudioManager.getVolumeIndexForAttributes(aa);
- assertEquals(readIndex, indexForAa);
- }
- } finally {
- for (final AudioVolumeGroupCallbackHelper cb : validCbs) {
- audioAudioVolumeGroupChangedHandler.unregisterListener(cb);
- }
- }
- }
-}
diff --git a/media/tests/projection/Android.bp b/media/tests/projection/Android.bp
index 94db2c02eb28..48621e4e2094 100644
--- a/media/tests/projection/Android.bp
+++ b/media/tests/projection/Android.bp
@@ -16,7 +16,6 @@ android_test {
name: "MediaProjectionTests",
srcs: ["**/*.java"],
-
libs: [
"android.test.base.stubs.system",
"android.test.mock.stubs.system",
@@ -30,6 +29,7 @@ android_test {
"frameworks-base-testutils",
"mockito-target-extended-minus-junit4",
"platform-test-annotations",
+ "cts-mediaprojection-common",
"testng",
"testables",
"truth",
@@ -42,7 +42,11 @@ android_test {
"libstaticjvmtiagent",
],
- test_suites: ["device-tests"],
+ data: [
+ ":CtsMediaProjectionTestCasesHelperApp",
+ ],
+
+ test_suites: ["general-tests"],
platform_apis: true,
diff --git a/media/tests/projection/AndroidManifest.xml b/media/tests/projection/AndroidManifest.xml
index 0c9760400ce0..514fb5f689c9 100644
--- a/media/tests/projection/AndroidManifest.xml
+++ b/media/tests/projection/AndroidManifest.xml
@@ -20,6 +20,8 @@
android:sharedUserId="com.android.uid.test">
<uses-permission android:name="android.permission.READ_COMPAT_CHANGE_CONFIG" />
<uses-permission android:name="android.permission.MANAGE_MEDIA_PROJECTION" />
+ <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
+ <uses-permission android:name="android.permission.READ_PHONE_STATE" />
<application android:debuggable="true"
android:testOnly="true">
diff --git a/media/tests/projection/AndroidTest.xml b/media/tests/projection/AndroidTest.xml
index f64930a0eb3f..99b42d1cd263 100644
--- a/media/tests/projection/AndroidTest.xml
+++ b/media/tests/projection/AndroidTest.xml
@@ -22,6 +22,15 @@
<option name="install-arg" value="-t" />
<option name="test-file-name" value="MediaProjectionTests.apk" />
</target_preparer>
+ <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+ <option name="cleanup-apks" value="true" />
+ <option name="force-install-mode" value="FULL"/>ss
+ <option name="test-file-name" value="CtsMediaProjectionTestCasesHelperApp.apk" />
+ </target_preparer>
+ <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
+ <option name="run-command" value="input keyevent KEYCODE_WAKEUP" />
+ <option name="run-command" value="wm dismiss-keyguard" />
+ </target_preparer>
<option name="test-tag" value="MediaProjectionTests" />
<test class="com.android.tradefed.testtype.AndroidJUnitTest">
diff --git a/media/tests/projection/src/android/media/projection/MediaProjectionStoppingTest.java b/media/tests/projection/src/android/media/projection/MediaProjectionStoppingTest.java
new file mode 100644
index 000000000000..0b84e01c4d02
--- /dev/null
+++ b/media/tests/projection/src/android/media/projection/MediaProjectionStoppingTest.java
@@ -0,0 +1,293 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.media.projection;
+
+import static com.android.compatibility.common.util.FeatureUtil.isAutomotive;
+import static com.android.compatibility.common.util.FeatureUtil.isTV;
+import static com.android.compatibility.common.util.FeatureUtil.isWatch;
+import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeFalse;
+import static org.junit.Assume.assumeTrue;
+
+import android.Manifest;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.media.cts.MediaProjectionRule;
+import android.os.UserHandle;
+import android.platform.test.annotations.RequiresFlagsDisabled;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.telecom.TelecomManager;
+import android.telephony.TelephonyCallback;
+import android.telephony.TelephonyManager;
+import android.util.Log;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.uiautomator.By;
+import androidx.test.uiautomator.UiDevice;
+import androidx.test.uiautomator.Until;
+
+import com.android.compatibility.common.util.ApiTest;
+import com.android.compatibility.common.util.FrameworkSpecificTest;
+import com.android.media.projection.flags.Flags;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Test {@link MediaProjection} stopping behavior.
+ *
+ * Run with:
+ * atest MediaProjectionTests:MediaProjectionStoppingTest
+ */
+@FrameworkSpecificTest
+public class MediaProjectionStoppingTest {
+ private static final String TAG = "MediaProjectionStoppingTest";
+ private static final int STOP_DIALOG_WAIT_TIMEOUT_MS = 5000;
+ private static final String CALL_HELPER_START_CALL = "start_call";
+ private static final String CALL_HELPER_STOP_CALL = "stop_call";
+ private static final String STOP_DIALOG_TITLE_RES_ID = "android:id/alertTitle";
+ private static final String STOP_DIALOG_CLOSE_BUTTON_RES_ID = "android:id/button2";
+
+ @Rule public MediaProjectionRule mMediaProjectionRule = new MediaProjectionRule();
+
+ private Context mContext;
+ private int mTimeoutMs;
+ private TelecomManager mTelecomManager;
+ private TelephonyManager mTelephonyManager;
+ private TestCallStateListener mTestCallStateListener;
+
+ @Before
+ public void setUp() throws InterruptedException {
+ mContext = InstrumentationRegistry.getInstrumentation().getContext();
+ runWithShellPermissionIdentity(
+ () -> {
+ mContext.getPackageManager()
+ .revokeRuntimePermission(
+ mContext.getPackageName(),
+ Manifest.permission.SYSTEM_ALERT_WINDOW,
+ new UserHandle(mContext.getUserId()));
+ });
+ mTimeoutMs = 1000;
+
+ mTestCallStateListener = new TestCallStateListener(mContext);
+ }
+
+ @After
+ public void cleanup() {
+ mTestCallStateListener.release();
+ }
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_STOP_MEDIA_PROJECTION_ON_CALL_END)
+ @ApiTest(apis = "android.media.projection.MediaProjection.Callback#onStop")
+ public void testMediaProjectionStop_callStartedAfterMediaProjection_doesNotStop()
+ throws Exception {
+ assumeTrue(mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELECOM));
+
+ mMediaProjectionRule.startMediaProjection();
+
+ CountDownLatch latch = new CountDownLatch(1);
+ mMediaProjectionRule.registerCallback(
+ new MediaProjection.Callback() {
+ @Override
+ public void onStop() {
+ latch.countDown();
+ }
+ });
+ mMediaProjectionRule.createVirtualDisplay();
+
+ try {
+ startPhoneCall();
+ } finally {
+ endPhoneCall();
+ }
+
+ assertWithMessage("MediaProjection should not be stopped on call end")
+ .that(latch.await(mTimeoutMs, TimeUnit.MILLISECONDS)).isFalse();
+ }
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_STOP_MEDIA_PROJECTION_ON_CALL_END)
+ @RequiresFlagsDisabled(Flags.FLAG_SHOW_STOP_DIALOG_POST_CALL_END)
+ @ApiTest(apis = "android.media.projection.MediaProjection.Callback#onStop")
+ public void
+ testMediaProjectionStop_callStartedBeforeMediaProjection_stopDialogFlagDisabled__shouldStop()
+ throws Exception {
+ assumeTrue(mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELECOM));
+ CountDownLatch latch = new CountDownLatch(1);
+ try {
+ startPhoneCall();
+
+ mMediaProjectionRule.startMediaProjection();
+
+ mMediaProjectionRule.registerCallback(
+ new MediaProjection.Callback() {
+ @Override
+ public void onStop() {
+ latch.countDown();
+ }
+ });
+ mMediaProjectionRule.createVirtualDisplay();
+
+ } finally {
+ endPhoneCall();
+ }
+
+ assertWithMessage("MediaProjection was not stopped after call end")
+ .that(latch.await(mTimeoutMs, TimeUnit.MILLISECONDS)).isTrue();
+ }
+
+ @Test
+ @RequiresFlagsEnabled({
+ Flags.FLAG_STOP_MEDIA_PROJECTION_ON_CALL_END,
+ Flags.FLAG_SHOW_STOP_DIALOG_POST_CALL_END
+ })
+ public void
+ callEnds_mediaProjectionStartedDuringCallAndIsActive_stopDialogFlagEnabled_showsStopDialog()
+ throws Exception {
+ // MediaProjection stop Dialog is only available on phones.
+ assumeFalse(isWatch());
+ assumeFalse(isAutomotive());
+ assumeFalse(isTV());
+
+ assumeTrue(mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELECOM));
+
+ try {
+ startPhoneCall();
+ mMediaProjectionRule.startMediaProjection();
+
+ mMediaProjectionRule.registerCallback(
+ new MediaProjection.Callback() {
+ @Override
+ public void onStop() {
+ fail(
+ "MediaProjection should not be stopped when"
+ + " FLAG_SHOW_STOP_DIALOG_POST_CALL_END is enabled");
+ }
+ });
+ mMediaProjectionRule.createVirtualDisplay();
+
+ } finally {
+ endPhoneCall();
+ }
+
+ UiDevice device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
+ boolean isDialogShown =
+ device.wait(
+ Until.hasObject(By.res(STOP_DIALOG_TITLE_RES_ID)),
+ STOP_DIALOG_WAIT_TIMEOUT_MS);
+ assertWithMessage("Stop dialog should be visible").that(isDialogShown).isTrue();
+
+ // Find and click the "Close" button
+ boolean hasCloseButton =
+ device.wait(
+ Until.hasObject(By.res(STOP_DIALOG_CLOSE_BUTTON_RES_ID)),
+ STOP_DIALOG_WAIT_TIMEOUT_MS);
+ if (hasCloseButton) {
+ device.findObject(By.res(STOP_DIALOG_CLOSE_BUTTON_RES_ID)).click();
+ Log.d(TAG, "Clicked on 'Close' button to dismiss the stop dialog.");
+ } else {
+ fail("Close button not found, unable to dismiss stop dialog.");
+ }
+ }
+
+ private void startPhoneCall() throws InterruptedException {
+ mTestCallStateListener.assertCallState(false);
+ mContext.startActivity(getCallHelperIntent(CALL_HELPER_START_CALL));
+ mTestCallStateListener.waitForNextCallState(true, mTimeoutMs, TimeUnit.MILLISECONDS);
+ }
+
+ private void endPhoneCall() throws InterruptedException {
+ mTestCallStateListener.assertCallState(true);
+ mContext.startActivity(getCallHelperIntent(CALL_HELPER_STOP_CALL));
+ mTestCallStateListener.waitForNextCallState(false, mTimeoutMs, TimeUnit.MILLISECONDS);
+ }
+
+ private Intent getCallHelperIntent(String action) {
+ return new Intent(action)
+ .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK)
+ .setComponent(
+ new ComponentName(
+ "android.media.projection.cts.helper",
+ "android.media.projection.cts.helper.CallHelperActivity"));
+ }
+
+ private static final class TestCallStateListener extends TelephonyCallback
+ implements TelephonyCallback.CallStateListener {
+ private final BlockingQueue<Boolean> mCallStates = new LinkedBlockingQueue<>();
+ private final TelecomManager mTelecomManager;
+ private final TelephonyManager mTelephonyManager;
+
+ private TestCallStateListener(Context context) throws InterruptedException {
+ mTelecomManager = context.getSystemService(TelecomManager.class);
+ mTelephonyManager = context.getSystemService(TelephonyManager.class);
+ mCallStates.offer(isInCall());
+
+ assertThat(mCallStates.take()).isFalse();
+
+ runWithShellPermissionIdentity(
+ () ->
+ mTelephonyManager.registerTelephonyCallback(
+ context.getMainExecutor(), this));
+ }
+
+ public void release() {
+ runWithShellPermissionIdentity(
+ () -> mTelephonyManager.unregisterTelephonyCallback(this));
+ }
+
+ @Override
+ public void onCallStateChanged(int state) {
+ mCallStates.offer(isInCall());
+ }
+
+ public void waitForNextCallState(boolean expectedCallState, long timeout, TimeUnit unit)
+ throws InterruptedException {
+ String message =
+ String.format(
+ "Call was not %s after timeout",
+ expectedCallState ? "started" : "ended");
+
+ boolean value;
+ do {
+ value = mCallStates.poll(timeout, unit);
+ } while (value != expectedCallState);
+ assertWithMessage(message).that(value).isEqualTo(expectedCallState);
+ }
+
+ private boolean isInCall() {
+ return runWithShellPermissionIdentity(mTelecomManager::isInCall);
+ }
+
+ public void assertCallState(boolean expected) {
+ assertWithMessage("Unexpected call state").that(isInCall()).isEqualTo(expected);
+ }
+ }
+}
diff --git a/packages/CredentialManager/wear/AndroidManifest.xml b/packages/CredentialManager/wear/AndroidManifest.xml
index b480ac30d2cb..c91bf13bf98e 100644
--- a/packages/CredentialManager/wear/AndroidManifest.xml
+++ b/packages/CredentialManager/wear/AndroidManifest.xml
@@ -32,7 +32,8 @@
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:label="@string/app_name"
- android:supportsRtl="true">
+ android:supportsRtl="true"
+ android:theme="@style/Theme.CredentialSelector">
<!-- Activity called by GMS has to be exactly:
com.android.credentialmanager.CredentialSelectorActivity -->
@@ -42,7 +43,8 @@
android:exported="true"
android:label="@string/app_name"
android:launchMode="singleTop"
- android:permission="android.permission.LAUNCH_CREDENTIAL_SELECTOR" />
+ android:permission="android.permission.LAUNCH_CREDENTIAL_SELECTOR"
+ android:theme="@style/Theme.CredentialSelector"/>
</application>
</manifest>
diff --git a/packages/CredentialManager/wear/res/values/themes.xml b/packages/CredentialManager/wear/res/values/themes.xml
new file mode 100644
index 000000000000..22329e9ff2ce
--- /dev/null
+++ b/packages/CredentialManager/wear/res/values/themes.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<resources>
+ <style name="Theme.CredentialSelector" parent="@*android:style/ThemeOverlay.DeviceDefault.Accent.DayNight">
+ <item name="android:windowContentOverlay">@null</item>
+ <item name="android:windowNoTitle">true</item>
+ <item name="android:windowBackground">@android:color/transparent</item>
+ <item name="android:windowIsTranslucent">true</item>
+ </style>
+</resources> \ No newline at end of file
diff --git a/packages/SettingsLib/AdaptiveIcon/Android.bp b/packages/SettingsLib/AdaptiveIcon/Android.bp
index 67b6fb5f2ed9..c9409e44c8ee 100644
--- a/packages/SettingsLib/AdaptiveIcon/Android.bp
+++ b/packages/SettingsLib/AdaptiveIcon/Android.bp
@@ -18,8 +18,9 @@ android_library {
resource_dirs: ["res"],
static_libs: [
- "androidx.annotation_annotation",
"SettingsLibTile",
+ "androidx.annotation_annotation",
+ "androidx.core_core",
],
min_sdk_version: "21",
diff --git a/packages/SettingsLib/AdaptiveIcon/src/com/android/settingslib/widget/AdaptiveIconShapeDrawable.java b/packages/SettingsLib/AdaptiveIcon/src/com/android/settingslib/widget/AdaptiveIconShapeDrawable.java
index 4d7610cf97b6..aa1d35afab4a 100644
--- a/packages/SettingsLib/AdaptiveIcon/src/com/android/settingslib/widget/AdaptiveIconShapeDrawable.java
+++ b/packages/SettingsLib/AdaptiveIcon/src/com/android/settingslib/widget/AdaptiveIconShapeDrawable.java
@@ -23,7 +23,8 @@ import android.graphics.drawable.AdaptiveIconDrawable;
import android.graphics.drawable.ShapeDrawable;
import android.graphics.drawable.shapes.PathShape;
import android.util.AttributeSet;
-import android.util.PathParser;
+
+import androidx.core.graphics.PathParser;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
diff --git a/packages/SettingsLib/AdaptiveIcon/src/com/android/settingslib/widget/AdaptiveOutlineDrawable.java b/packages/SettingsLib/AdaptiveIcon/src/com/android/settingslib/widget/AdaptiveOutlineDrawable.java
index 12c234ecd739..c7ca18a9c339 100644
--- a/packages/SettingsLib/AdaptiveIcon/src/com/android/settingslib/widget/AdaptiveOutlineDrawable.java
+++ b/packages/SettingsLib/AdaptiveIcon/src/com/android/settingslib/widget/AdaptiveOutlineDrawable.java
@@ -27,12 +27,12 @@ import android.graphics.Rect;
import android.graphics.drawable.AdaptiveIconDrawable;
import android.graphics.drawable.DrawableWrapper;
import android.os.RemoteException;
-import android.util.PathParser;
import android.view.IWindowManager;
import android.view.WindowManagerGlobal;
import androidx.annotation.IntDef;
import androidx.annotation.VisibleForTesting;
+import androidx.core.graphics.PathParser;
import com.android.settingslib.widget.adaptiveicon.R;
diff --git a/packages/SettingsLib/SettingsTheme/res/layout/settingslib_expressive_collapsable_textview.xml b/packages/SettingsLib/SettingsTheme/res/layout/settingslib_expressive_collapsable_textview.xml
index 7d7bec14ed78..cc55cacb9e5c 100644
--- a/packages/SettingsLib/SettingsTheme/res/layout/settingslib_expressive_collapsable_textview.xml
+++ b/packages/SettingsLib/SettingsTheme/res/layout/settingslib_expressive_collapsable_textview.xml
@@ -44,10 +44,11 @@
<com.android.settingslib.widget.LinkableTextView
android:id="@+id/settingslib_expressive_learn_more"
- android:layout_width="wrap_content"
+ android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@android:id/title"
app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
android:paddingTop="@dimen/settingslib_expressive_space_extrasmall6"
android:textAlignment="viewStart"
android:clickable="true"
diff --git a/packages/SettingsLib/SettingsTheme/src/com/android/settingslib/widget/CollapsableTextView.kt b/packages/SettingsLib/SettingsTheme/src/com/android/settingslib/widget/CollapsableTextView.kt
index 976711bdc5f3..007dc5143262 100644
--- a/packages/SettingsLib/SettingsTheme/src/com/android/settingslib/widget/CollapsableTextView.kt
+++ b/packages/SettingsLib/SettingsTheme/src/com/android/settingslib/widget/CollapsableTextView.kt
@@ -85,6 +85,7 @@ class CollapsableTextView @JvmOverloads constructor(
Gravity.CENTER_VERTICAL, Gravity.CENTER, Gravity.CENTER_HORIZONTAL -> {
centerHorizontally(titleTextView)
centerHorizontally(collapseButton)
+ centerHorizontally(learnMoreTextView)
}
}
isCollapsable = getBoolean(isCollapsableAttr, DEFAULT_COLLAPSABLE)
diff --git a/packages/SettingsLib/SettingsTheme/src/com/android/settingslib/widget/SettingsPreferenceGroupAdapter.kt b/packages/SettingsLib/SettingsTheme/src/com/android/settingslib/widget/SettingsPreferenceGroupAdapter.kt
index 2672787a0519..d1c88de3f399 100644
--- a/packages/SettingsLib/SettingsTheme/src/com/android/settingslib/widget/SettingsPreferenceGroupAdapter.kt
+++ b/packages/SettingsLib/SettingsTheme/src/com/android/settingslib/widget/SettingsPreferenceGroupAdapter.kt
@@ -71,18 +71,27 @@ open class SettingsPreferenceGroupAdapter(preferenceGroup: PreferenceGroup) :
override fun onPreferenceHierarchyChange(preference: Preference) {
super.onPreferenceHierarchyChange(preference)
- // Post after super class has posted their sync runnable to update preferences.
- mHandler.removeCallbacks(syncRunnable)
- mHandler.post(syncRunnable)
+ if (SettingsThemeHelper.isExpressiveTheme(preference.context)) {
+ // Post after super class has posted their sync runnable to update preferences.
+ mHandler.removeCallbacks(syncRunnable)
+ mHandler.post(syncRunnable)
+ }
}
@SuppressLint("RestrictedApi")
override fun onBindViewHolder(holder: PreferenceViewHolder, position: Int) {
super.onBindViewHolder(holder, position)
- updateBackground(holder, position)
+
+ if (SettingsThemeHelper.isExpressiveTheme(holder.itemView.context)) {
+ updateBackground(holder, position)
+ }
}
private fun updatePreferencesList() {
+ if (!SettingsThemeHelper.isExpressiveTheme(mPreferenceGroup.context)) {
+ return
+ }
+
val oldList = ArrayList(mRoundCornerMappingList)
mRoundCornerMappingList = ArrayList()
mappingPreferenceGroup(mRoundCornerMappingList, mPreferenceGroup)
diff --git a/packages/SettingsLib/Spa/build.gradle.kts b/packages/SettingsLib/Spa/build.gradle.kts
index 50408190b3ef..25406d794af9 100644
--- a/packages/SettingsLib/Spa/build.gradle.kts
+++ b/packages/SettingsLib/Spa/build.gradle.kts
@@ -28,7 +28,7 @@ val androidTop: String = File(rootDir, "../../../../..").canonicalPath
allprojects {
extra["androidTop"] = androidTop
- extra["jetpackComposeVersion"] = "1.8.0-beta02"
+ extra["jetpackComposeVersion"] = "1.8.0-rc01"
}
subprojects {
@@ -36,11 +36,11 @@ subprojects {
plugins.withType<AndroidBasePlugin> {
configure<BaseExtension> {
- compileSdkVersion(35)
+ compileSdkVersion(36)
defaultConfig {
minSdk = 21
- targetSdk = 35
+ targetSdk = 36
}
}
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt
index 8636524ed23c..b9a3a7f4b48f 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt
@@ -124,4 +124,6 @@ class GallerySpaEnvironment(context: Context) : SpaEnvironment(context) {
// For debugging
override val searchProviderAuthorities = "com.android.spa.gallery.search.provider"
+
+ override val isSpaExpressiveEnabled = true
}
diff --git a/packages/SettingsLib/Spa/gradle/libs.versions.toml b/packages/SettingsLib/Spa/gradle/libs.versions.toml
index b074f4b91ed0..d041eb011986 100644
--- a/packages/SettingsLib/Spa/gradle/libs.versions.toml
+++ b/packages/SettingsLib/Spa/gradle/libs.versions.toml
@@ -15,10 +15,10 @@
#
[versions]
-agp = "8.8.1"
+agp = "8.9.0"
dexmaker-mockito = "2.28.3"
jvm = "21"
-kotlin = "2.0.21"
+kotlin = "2.1.10"
truth = "1.4.4"
[libraries]
diff --git a/packages/SettingsLib/Spa/spa/build.gradle.kts b/packages/SettingsLib/Spa/spa/build.gradle.kts
index 7ce5b71f678e..57f5520fd659 100644
--- a/packages/SettingsLib/Spa/spa/build.gradle.kts
+++ b/packages/SettingsLib/Spa/spa/build.gradle.kts
@@ -52,18 +52,18 @@ android {
dependencies {
api(project(":SettingsLibColor"))
api("androidx.appcompat:appcompat:1.7.0")
- api("androidx.compose.material3:material3:1.4.0-alpha08")
- api("androidx.compose.material:material-icons-extended")
+ api("androidx.compose.material3:material3:1.4.0-alpha10")
+ api("androidx.compose.material:material-icons-extended:1.7.8")
api("androidx.compose.runtime:runtime-livedata:$jetpackComposeVersion")
api("androidx.compose.ui:ui-tooling-preview:$jetpackComposeVersion")
api("androidx.graphics:graphics-shapes-android:1.0.1")
api("androidx.lifecycle:lifecycle-livedata-ktx")
api("androidx.lifecycle:lifecycle-runtime-compose")
- api("androidx.navigation:navigation-compose:2.9.0-alpha06")
+ api("androidx.navigation:navigation-compose:2.9.0-alpha08")
api("com.github.PhilJay:MPAndroidChart:v3.1.0-alpha")
api("com.google.android.material:material:1.13.0-alpha08")
debugApi("androidx.compose.ui:ui-tooling:$jetpackComposeVersion")
- implementation("com.airbnb.android:lottie-compose:6.4.0")
+ implementation("com.airbnb.android:lottie-compose:6.5.2")
androidTestImplementation(project(":testutils"))
androidTestImplementation(libs.dexmaker.mockito)
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Category.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Category.kt
index f10f96afd389..395328f86047 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Category.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Category.kt
@@ -78,6 +78,8 @@ fun CategoryTitle(title: String) {
/**
* A container that is used to group similar items. A [Category] displays a [CategoryTitle] and
* visually separates groups of items.
+ *
+ * @param content The content of the category.
*/
@Composable
fun Category(
@@ -126,7 +128,8 @@ fun Category(
* be decided by the index.
* @param bottomPadding Optional. Bottom outside padding of the category.
* @param state Optional. State of LazyList.
- * @param content Optional. Content to be shown at the top of the category.
+ * @param footer Optional. Content to be shown at the bottom of the category.
+ * @param header Optional. Content to be shown at the top of the category.
*/
@Composable
fun LazyCategory(
@@ -136,7 +139,8 @@ fun LazyCategory(
title: ((Int) -> String?)? = null,
bottomPadding: Dp = SettingsDimension.paddingSmall,
state: LazyListState = rememberLazyListState(),
- content: @Composable () -> Unit,
+ footer: @Composable () -> Unit = {},
+ header: @Composable () -> Unit,
) {
Column(
Modifier.padding(
@@ -154,12 +158,14 @@ fun LazyCategory(
verticalArrangement = Arrangement.spacedBy(SettingsDimension.paddingTiny),
state = state,
) {
- item { CompositionLocalProvider(LocalIsInCategory provides true) { content() } }
+ item { CompositionLocalProvider(LocalIsInCategory provides true) { header() } }
items(count = list.size, key = key) {
title?.invoke(it)?.let { title -> CategoryTitle(title) }
CompositionLocalProvider(LocalIsInCategory provides true) { entry(it)() }
}
+
+ item { CompositionLocalProvider(LocalIsInCategory provides true) { footer() } }
}
}
}
@@ -189,3 +195,28 @@ private fun CategoryPreview() {
}
}
}
+
+@Preview
+@Composable
+private fun LazyCategoryPreview() {
+ SettingsTheme {
+ LazyCategory(
+ list = listOf(1, 2, 3),
+ entry = { key ->
+ @Composable {
+ Preference(
+ object : PreferenceModel {
+ override val title = key.toString()
+ }
+ )
+ }
+ },
+ footer = @Composable {
+ Footer("Footer")
+ },
+ header = @Composable {
+ Text("Header")
+ },
+ )
+ }
+}
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/ui/CategoryTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/ui/CategoryTest.kt
index 4b4a8c20b39e..7d199511044a 100644
--- a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/ui/CategoryTest.kt
+++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/ui/CategoryTest.kt
@@ -71,10 +71,17 @@ class CategoryTest {
}
@Test
- fun lazyCategory_content_displayed() {
+ fun lazyCategory_headerDisplayed() {
composeTestRule.setContent { TestLazyCategory() }
- composeTestRule.onNodeWithText("text").assertExists()
+ composeTestRule.onNodeWithText("Header").assertExists()
+ }
+
+ @Test
+ fun lazyCategory_footerDisplayed() {
+ composeTestRule.setContent { TestLazyCategory() }
+
+ composeTestRule.onNodeWithText("Footer").assertExists()
}
@Test
@@ -102,8 +109,8 @@ private fun TestLazyCategory() {
list = list,
entry = { index: Int -> @Composable { Preference(list[index]) } },
title = { index: Int -> if (index == 0) "LazyCategory $index" else null },
- ) {
- Text("text")
- }
+ footer = @Composable { Footer("Footer") },
+ header = @Composable { Text("Header") },
+ )
}
}
diff --git a/packages/SettingsLib/StatusBannerPreference/res/layout/settingslib_expressive_preference_statusbanner.xml b/packages/SettingsLib/StatusBannerPreference/res/layout/settingslib_expressive_preference_statusbanner.xml
index 083b862e8a5c..c778fb03e04f 100644
--- a/packages/SettingsLib/StatusBannerPreference/res/layout/settingslib_expressive_preference_statusbanner.xml
+++ b/packages/SettingsLib/StatusBannerPreference/res/layout/settingslib_expressive_preference_statusbanner.xml
@@ -55,6 +55,27 @@
android:layout_gravity="center"
android:scaleType="centerInside"/>
+ <com.google.android.material.progressindicator.CircularProgressIndicator
+ android:id="@+id/progress_indicator"
+ style="@style/Widget.Material3.CircularProgressIndicator"
+ android:layout_width="@dimen/settingslib_expressive_space_medium4"
+ android:layout_height="@dimen/settingslib_expressive_space_medium4"
+ android:layout_gravity="center"
+ android:scaleType="centerInside"
+ android:indeterminate="false"
+ android:max="100"
+ android:progress="0"
+ android:visibility="gone" />
+
+ <com.google.android.material.loadingindicator.LoadingIndicator
+ android:id="@+id/loading_indicator"
+ style="@style/Widget.Material3.LoadingIndicator"
+ android:layout_width="@dimen/settingslib_expressive_space_medium4"
+ android:layout_height="@dimen/settingslib_expressive_space_medium4"
+ android:layout_gravity="center"
+ android:scaleType="centerInside"
+ android:visibility="gone" />
+
</FrameLayout>
<LinearLayout
diff --git a/packages/SettingsLib/StatusBannerPreference/res/values/attrs.xml b/packages/SettingsLib/StatusBannerPreference/res/values/attrs.xml
index deda2586c2e0..bb9a5ad689cd 100644
--- a/packages/SettingsLib/StatusBannerPreference/res/values/attrs.xml
+++ b/packages/SettingsLib/StatusBannerPreference/res/values/attrs.xml
@@ -22,6 +22,8 @@
<enum name="medium" value="2"/>
<enum name="high" value="3"/>
<enum name="off" value="4"/>
+ <enum name="loading_determinate" value="5"/>
+ <enum name="loading_indeterminate" value="6"/>
</attr>
<attr name="buttonLevel" format="enum">
<enum name="generic" value="0"/>
diff --git a/packages/SettingsLib/StatusBannerPreference/src/com/android/settingslib/widget/StatusBannerPreference.kt b/packages/SettingsLib/StatusBannerPreference/src/com/android/settingslib/widget/StatusBannerPreference.kt
index eda281c07053..e6c6638f7de4 100644
--- a/packages/SettingsLib/StatusBannerPreference/src/com/android/settingslib/widget/StatusBannerPreference.kt
+++ b/packages/SettingsLib/StatusBannerPreference/src/com/android/settingslib/widget/StatusBannerPreference.kt
@@ -28,6 +28,7 @@ import androidx.preference.Preference
import androidx.preference.PreferenceViewHolder
import com.android.settingslib.widget.preference.statusbanner.R
import com.google.android.material.button.MaterialButton
+import com.google.android.material.progressindicator.CircularProgressIndicator
class StatusBannerPreference @JvmOverloads constructor(
context: Context,
@@ -41,7 +42,9 @@ class StatusBannerPreference @JvmOverloads constructor(
LOW,
MEDIUM,
HIGH,
- OFF
+ OFF,
+ LOADING_DETERMINATE, // The loading progress is set by the caller.
+ LOADING_INDETERMINATE // No loading progress. Just loading animation
}
var iconLevel: BannerStatus = BannerStatus.GENERIC
set(value) {
@@ -60,6 +63,8 @@ class StatusBannerPreference @JvmOverloads constructor(
}
private var listener: View.OnClickListener? = null
+ private var circularProgressIndicator: CircularProgressIndicator? = null
+
init {
layoutResource = R.layout.settingslib_expressive_preference_statusbanner
isSelectable = false
@@ -89,6 +94,8 @@ class StatusBannerPreference @JvmOverloads constructor(
2 -> BannerStatus.MEDIUM
3 -> BannerStatus.HIGH
4 -> BannerStatus.OFF
+ 5 -> BannerStatus.LOADING_DETERMINATE
+ 6 -> BannerStatus.LOADING_INDETERMINATE
else -> BannerStatus.GENERIC
}
@@ -102,7 +109,38 @@ class StatusBannerPreference @JvmOverloads constructor(
}
holder.findViewById(android.R.id.icon_frame)?.apply {
- visibility = if (icon != null) View.VISIBLE else View.GONE
+ visibility =
+ if (
+ icon != null || iconLevel == BannerStatus.LOADING_DETERMINATE ||
+ iconLevel == BannerStatus.LOADING_INDETERMINATE
+ )
+ View.VISIBLE
+ else View.GONE
+ }
+
+ holder.findViewById(android.R.id.icon)?.apply {
+ visibility =
+ if (iconLevel == BannerStatus.LOADING_DETERMINATE ||
+ iconLevel == BannerStatus.LOADING_INDETERMINATE)
+ View.GONE
+ else View.VISIBLE
+ }
+
+ circularProgressIndicator = holder.findViewById(R.id.progress_indicator)
+ as? CircularProgressIndicator
+
+ (circularProgressIndicator)?.apply {
+ visibility =
+ if (iconLevel == BannerStatus.LOADING_DETERMINATE)
+ View.VISIBLE
+ else View.GONE
+ }
+
+ holder.findViewById(R.id.loading_indicator)?.apply {
+ visibility =
+ if (iconLevel == BannerStatus.LOADING_INDETERMINATE)
+ View.VISIBLE
+ else View.GONE
}
(holder.findViewById(R.id.status_banner_button) as? MaterialButton)?.apply {
@@ -116,6 +154,10 @@ class StatusBannerPreference @JvmOverloads constructor(
}
}
+ fun getProgressIndicator(): CircularProgressIndicator? {
+ return circularProgressIndicator
+ }
+
/**
* Sets the text to be displayed in button.
*/
@@ -203,7 +245,7 @@ class StatusBannerPreference @JvmOverloads constructor(
R.drawable.settingslib_expressive_background_level_high
)
- // GENERIC and OFF are using the same background drawable.
+ // Using the same background drawable for other levels.
else -> ContextCompat.getDrawable(
context,
R.drawable.settingslib_expressive_background_generic
diff --git a/packages/SettingsLib/aconfig/settingslib.aconfig b/packages/SettingsLib/aconfig/settingslib.aconfig
index 349d13a29b05..90e5a010416c 100644
--- a/packages/SettingsLib/aconfig/settingslib.aconfig
+++ b/packages/SettingsLib/aconfig/settingslib.aconfig
@@ -37,16 +37,6 @@ flag {
}
flag {
- name: "enable_set_preferred_transport_for_le_audio_device"
- namespace: "bluetooth"
- description: "Enable setting preferred transport for Le Audio device"
- bug: "330581926"
- metadata {
- purpose: PURPOSE_BUGFIX
- }
-}
-
-flag {
name: "enable_determining_advanced_details_header_with_metadata"
namespace: "pixel_cross_device_control"
description: "Use metadata instead of device type to determine whether a bluetooth device should use advanced details header."
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/AmbientVolumeUiController.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/AmbientVolumeUiController.java
index e7ddc46093e3..f7da8a57410f 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/AmbientVolumeUiController.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/AmbientVolumeUiController.java
@@ -80,6 +80,7 @@ public class AmbientVolumeUiController implements
mLocalDataManager = new HearingDeviceLocalDataManager(context);
mLocalDataManager.setOnDeviceLocalDataChangeListener(this,
ThreadUtils.getBackgroundExecutor());
+ mLocalDataManager.start();
}
@VisibleForTesting
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java
index ae9ad958b287..33dcb051d194 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java
@@ -1068,18 +1068,42 @@ public class BluetoothUtils {
/** Get primary device Uri in broadcast. */
@NonNull
public static String getPrimaryGroupIdUriForBroadcast() {
+ // TODO: once API is stable, deprecate SettingsProvider solution
return "bluetooth_le_broadcast_fallback_active_group_id";
}
- /** Get primary device group id in broadcast. */
+ /** Get primary device group id in broadcast from SettingsProvider. */
@WorkerThread
public static int getPrimaryGroupIdForBroadcast(@NonNull ContentResolver contentResolver) {
+ // TODO: once API is stable, deprecate SettingsProvider solution
return Settings.Secure.getInt(
contentResolver,
getPrimaryGroupIdUriForBroadcast(),
BluetoothCsipSetCoordinator.GROUP_ID_INVALID);
}
+ /**
+ * Get primary device group id in broadcast.
+ *
+ * If Flags.adoptPrimaryGroupManagementApiV2 is enabled, get group id by API,
+ * Otherwise, still get value from SettingsProvider.
+ */
+ @WorkerThread
+ public static int getPrimaryGroupIdForBroadcast(@NonNull ContentResolver contentResolver,
+ @Nullable LocalBluetoothManager manager) {
+ if (Flags.adoptPrimaryGroupManagementApiV2()) {
+ LeAudioProfile leaProfile = manager == null ? null :
+ manager.getProfileManager().getLeAudioProfile();
+ if (leaProfile == null) {
+ Log.d(TAG, "getPrimaryGroupIdForBroadcast: profile is null");
+ return BluetoothCsipSetCoordinator.GROUP_ID_INVALID;
+ }
+ return leaProfile.getBroadcastToUnicastFallbackGroup();
+ } else {
+ return getPrimaryGroupIdForBroadcast(contentResolver);
+ }
+ }
+
/** Get develop option value for audio sharing preview. */
@WorkerThread
public static boolean getAudioSharingPreviewValue(@Nullable ContentResolver contentResolver) {
@@ -1101,7 +1125,7 @@ public class BluetoothUtils {
LocalBluetoothLeBroadcast broadcast =
localBtManager.getProfileManager().getLeAudioBroadcastProfile();
if (broadcast == null || !broadcast.isEnabled(null)) return null;
- int primaryGroupId = getPrimaryGroupIdForBroadcast(contentResolver);
+ int primaryGroupId = getPrimaryGroupIdForBroadcast(contentResolver, localBtManager);
if (primaryGroupId == BluetoothCsipSetCoordinator.GROUP_ID_INVALID) return null;
LocalBluetoothLeBroadcastAssistant assistant =
localBtManager.getProfileManager().getLeAudioBroadcastAssistantProfile();
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
index 011b2fc15807..edec2e427315 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
@@ -75,6 +75,7 @@ import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.Executor;
+import java.util.stream.IntStream;
import java.util.stream.Stream;
/**
@@ -154,8 +155,8 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice>
private boolean mIsLeAudioProfileConnectedFail = false;
private boolean mUnpairing;
@Nullable
- private final InputDevice mInputDevice;
- private final boolean mIsDeviceStylus;
+ private InputDevice mInputDevice;
+ private boolean mIsDeviceStylus;
// Group second device for Hearing Aid
private CachedBluetoothDevice mSubDevice;
@@ -313,8 +314,7 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice>
mLocalNapRoleConnected = true;
}
}
- if (Flags.enableSetPreferredTransportForLeAudioDevice()
- && profile instanceof HidProfile) {
+ if (profile instanceof HidProfile) {
updatePreferredTransport();
}
} else if (profile instanceof MapProfile
@@ -329,8 +329,7 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice>
mLocalNapRoleConnected = false;
}
- if (Flags.enableSetPreferredTransportForLeAudioDevice()
- && profile instanceof LeAudioProfile) {
+ if (profile instanceof LeAudioProfile) {
updatePreferredTransport();
}
@@ -762,11 +761,8 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice>
* {@link BluetoothDevice#BATTERY_LEVEL_UNKNOWN}
*/
public int getMinBatteryLevelWithMemberDevices() {
- return Stream.concat(Stream.of(this), mMemberDevices.stream())
- .mapToInt(cachedDevice -> cachedDevice.getBatteryLevel())
- .filter(batteryLevel -> batteryLevel > BluetoothDevice.BATTERY_LEVEL_UNKNOWN)
- .min()
- .orElse(BluetoothDevice.BATTERY_LEVEL_UNKNOWN);
+ return getMinBatteryLevels(Stream.concat(Stream.of(this), mMemberDevices.stream())
+ .mapToInt(CachedBluetoothDevice::getBatteryLevel));
}
/**
@@ -789,6 +785,13 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice>
: null;
}
+ private int getMinBatteryLevels(IntStream batteryLevels) {
+ return batteryLevels
+ .filter(battery -> battery > BluetoothDevice.BATTERY_LEVEL_UNKNOWN)
+ .min()
+ .orElse(BluetoothDevice.BATTERY_LEVEL_UNKNOWN);
+ }
+
void refresh() {
ListenableFuture<Void> future = ThreadUtils.getBackgroundExecutor().submit(() -> {
if (BluetoothUtils.isAdvancedDetailsHeader(mDevice)) {
@@ -1358,7 +1361,7 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice>
// Gets summary for the buds which are in the audio sharing.
int groupId = BluetoothUtils.getGroupId(this);
int primaryGroupId = BluetoothUtils.getPrimaryGroupIdForBroadcast(
- mContext.getContentResolver());
+ mContext.getContentResolver(), mBluetoothManager);
if ((primaryGroupId != BluetoothCsipSetCoordinator.GROUP_ID_INVALID)
? (groupId == primaryGroupId) : isActiveDevice(BluetoothProfile.LE_AUDIO)) {
// The buds are primary buds
@@ -1674,10 +1677,8 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice>
return null;
} else {
int overallBattery =
- Arrays.stream(new int[]{leftBattery, rightBattery, caseBattery})
- .filter(battery -> battery > BluetoothDevice.BATTERY_LEVEL_UNKNOWN)
- .min()
- .orElse(BluetoothDevice.BATTERY_LEVEL_UNKNOWN);
+ getMinBatteryLevels(
+ Arrays.stream(new int[]{leftBattery, rightBattery, caseBattery}));
Log.d(TAG, "Acquired battery info from metadata for untethered device "
+ mDevice.getAnonymizedAddress()
+ " left earbud battery: " + leftBattery
@@ -1711,10 +1712,75 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice>
@Nullable
private BatteryLevelsInfo getBatteryFromBluetoothService() {
- // TODO(b/397847825): Implement the logic to get battery from Bluetooth service.
- return null;
+ BatteryLevelsInfo batteryLevelsInfo;
+ if (isConnectedHearingAidDevice()) {
+ // If the device is hearing aid device, sides can be distinguished by HearingAidInfo.
+ batteryLevelsInfo = getBatteryOfHearingAidDeviceComponents();
+ if (batteryLevelsInfo != null) {
+ return batteryLevelsInfo;
+ }
+ }
+ if (isConnectedLeAudioDevice()) {
+ // If the device is LE Audio device, sides can be distinguished by LeAudioProfile.
+ batteryLevelsInfo = getBatteryOfLeAudioDeviceComponents();
+ if (batteryLevelsInfo != null) {
+ return batteryLevelsInfo;
+ }
+ }
+ int overallBattery = getMinBatteryLevelWithMemberDevices();
+ return overallBattery > BluetoothDevice.BATTERY_LEVEL_UNKNOWN
+ ? new BatteryLevelsInfo(
+ BluetoothDevice.BATTERY_LEVEL_UNKNOWN,
+ BluetoothDevice.BATTERY_LEVEL_UNKNOWN,
+ BluetoothDevice.BATTERY_LEVEL_UNKNOWN,
+ overallBattery)
+ : null;
}
+ @Nullable
+ private BatteryLevelsInfo getBatteryOfHearingAidDeviceComponents() {
+ if (getDeviceSide() == HearingAidInfo.DeviceSide.SIDE_LEFT_AND_RIGHT) {
+ return new BatteryLevelsInfo(
+ BluetoothDevice.BATTERY_LEVEL_UNKNOWN,
+ BluetoothDevice.BATTERY_LEVEL_UNKNOWN,
+ BluetoothDevice.BATTERY_LEVEL_UNKNOWN,
+ mDevice.getBatteryLevel());
+ }
+
+ int leftBattery = getHearingAidSideBattery(HearingAidInfo.DeviceSide.SIDE_LEFT);
+ int rightBattery = getHearingAidSideBattery(HearingAidInfo.DeviceSide.SIDE_RIGHT);
+ int overallBattery = getMinBatteryLevels(
+ Arrays.stream(new int[]{leftBattery, rightBattery}));
+
+ Log.d(TAG, "Acquired battery info from Bluetooth service for hearing aid device "
+ + mDevice.getAnonymizedAddress()
+ + " left battery: " + leftBattery
+ + " right battery: " + rightBattery
+ + " overall battery: " + overallBattery);
+ return overallBattery > BluetoothDevice.BATTERY_LEVEL_UNKNOWN
+ ? new BatteryLevelsInfo(
+ leftBattery,
+ rightBattery,
+ BluetoothDevice.BATTERY_LEVEL_UNKNOWN,
+ overallBattery)
+ : null;
+ }
+
+ private int getHearingAidSideBattery(int side) {
+ Optional<CachedBluetoothDevice> connectedHearingAidSide = getConnectedHearingAidSide(side);
+ return connectedHearingAidSide.isPresent()
+ ? connectedHearingAidSide
+ .map(CachedBluetoothDevice::getBatteryLevel)
+ .filter(batteryLevel -> batteryLevel > BluetoothDevice.BATTERY_LEVEL_UNKNOWN)
+ .orElse(BluetoothDevice.BATTERY_LEVEL_UNKNOWN)
+ : BluetoothDevice.BATTERY_LEVEL_UNKNOWN;
+ }
+
+ @Nullable
+ private BatteryLevelsInfo getBatteryOfLeAudioDeviceComponents() {
+ // TODO(b/397847825): Implement the logic to get battery of LE audio device components.
+ return null;
+ }
private CharSequence getTvBatterySummary(int mainBattery, int leftBattery, int rightBattery,
int lowBatteryColorRes) {
// Since there doesn't seem to be a way to use format strings to add the
@@ -1833,10 +1899,7 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice>
// Retrieve hearing aids (ASHA, HAP) individual side battery level
if (leftBattery == BluetoothDevice.BATTERY_LEVEL_UNKNOWN) {
- leftBattery = getConnectedHearingAidSide(HearingAidInfo.DeviceSide.SIDE_LEFT)
- .map(CachedBluetoothDevice::getBatteryLevel)
- .filter(batteryLevel -> batteryLevel > BluetoothDevice.BATTERY_LEVEL_UNKNOWN)
- .orElse(BluetoothDevice.BATTERY_LEVEL_UNKNOWN);
+ leftBattery = getHearingAidSideBattery(HearingAidInfo.DeviceSide.SIDE_LEFT);
}
return leftBattery;
@@ -1852,10 +1915,7 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice>
// Retrieve hearing aids (ASHA, HAP) individual side battery level
if (rightBattery == BluetoothDevice.BATTERY_LEVEL_UNKNOWN) {
- rightBattery = getConnectedHearingAidSide(HearingAidInfo.DeviceSide.SIDE_RIGHT)
- .map(CachedBluetoothDevice::getBatteryLevel)
- .filter(batteryLevel -> batteryLevel > BluetoothDevice.BATTERY_LEVEL_UNKNOWN)
- .orElse(BluetoothDevice.BATTERY_LEVEL_UNKNOWN);
+ rightBattery = getHearingAidSideBattery(HearingAidInfo.DeviceSide.SIDE_RIGHT);
}
return rightBattery;
@@ -2263,6 +2323,16 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice>
mBluetoothManager = bluetoothManager;
}
+ @VisibleForTesting
+ void setIsDeviceStylus(Boolean isDeviceStylus) {
+ mIsDeviceStylus = isDeviceStylus;
+ }
+
+ @VisibleForTesting
+ void setInputDevice(@Nullable InputDevice inputDevice) {
+ mInputDevice = inputDevice;
+ }
+
private boolean isAndroidAuto() {
try {
ParcelUuid[] uuids = mDevice.getUuids();
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java
index bf86911ee683..f2c013598cdc 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java
@@ -431,7 +431,10 @@ public class CsipDeviceManager {
for (BluetoothDevice device : sinksToSync) {
log("addMemberDevicesIntoMainDevice: sync audio sharing source to "
+ device.getAnonymizedAddress());
- assistant.addSource(device, metadata, /* isGroupOp= */ false);
+ if (assistant.getConnectionStatus(device)
+ == BluetoothProfile.STATE_CONNECTED) {
+ assistant.addSource(device, metadata, /* isGroupOp= */ false);
+ }
}
}
}
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 c4e724554c04..21d518a644a9 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
@@ -27,7 +27,6 @@ import android.content.IntentFilter
import android.database.ContentObserver
import android.os.Handler
import android.provider.Settings
-import com.android.settingslib.flags.Flags
import com.android.settingslib.notification.modes.ZenMode
import com.android.settingslib.notification.modes.ZenModesBackend
import java.time.Duration
@@ -35,6 +34,7 @@ import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.callbackFlow
@@ -72,7 +72,7 @@ class ZenModeRepositoryImpl(
private val notificationManager: NotificationManager,
private val backend: ZenModesBackend,
private val contentResolver: ContentResolver,
- val scope: CoroutineScope,
+ val applicationScope: CoroutineScope,
val backgroundCoroutineContext: CoroutineContext,
// This is nullable just to simplify testing, since SettingsLib doesn't have a good way
// to create a fake handler.
@@ -104,7 +104,7 @@ class ZenModeRepositoryImpl(
awaitClose { context.unregisterReceiver(receiver) }
}
.flowOn(backgroundCoroutineContext)
- .shareIn(started = SharingStarted.WhileSubscribed(), scope = scope)
+ .shareIn(started = SharingStarted.WhileSubscribed(), scope = applicationScope)
}
override val consolidatedNotificationPolicy: StateFlow<NotificationManager.Policy?> by lazy {
@@ -129,14 +129,11 @@ class ZenModeRepositoryImpl(
.map { mapper(it) }
.onStart { emit(mapper(null)) }
.flowOn(backgroundCoroutineContext)
- .stateIn(scope, SharingStarted.WhileSubscribed(), null)
+ .stateIn(applicationScope, SharingStarted.WhileSubscribed(), null)
private val zenConfigChanged by lazy {
if (android.app.Flags.modesUi()) {
callbackFlow {
- // emit an initial value
- trySend(Unit)
-
val observer =
object : ContentObserver(backgroundHandler) {
override fun onChange(selfChange: Boolean) {
@@ -163,16 +160,18 @@ class ZenModeRepositoryImpl(
}
}
- override val modes: Flow<List<ZenMode>> by lazy {
- if (android.app.Flags.modesUi()) {
+ override val modes: StateFlow<List<ZenMode>> =
+ if (android.app.Flags.modesUi())
zenConfigChanged
.map { backend.modes }
.distinctUntilChanged()
.flowOn(backgroundCoroutineContext)
- } else {
- flowOf(emptyList())
- }
- }
+ .stateIn(
+ scope = applicationScope,
+ started = SharingStarted.Eagerly,
+ initialValue = backend.modes,
+ )
+ else MutableStateFlow<List<ZenMode>>(emptyList())
/**
* Gets the current list of [ZenMode] instances according to the backend.
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java
index b7814127b716..8fc4aa81b53f 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java
@@ -936,15 +936,60 @@ public class BluetoothUtilsTest {
}
@Test
+ @EnableFlags(Flags.FLAG_ADOPT_PRIMARY_GROUP_MANAGEMENT_API_V2)
+ public void getSecondaryDeviceForBroadcast_adoptAPI_noSecondary_returnNull() {
+ when(mBroadcast.isEnabled(any())).thenReturn(true);
+ when(mLeAudioProfile.getBroadcastToUnicastFallbackGroup()).thenReturn(1);
+ when(mDeviceManager.findDevice(mBluetoothDevice)).thenReturn(mCachedBluetoothDevice);
+ when(mCachedBluetoothDevice.getDevice()).thenReturn(mBluetoothDevice);
+ when(mCachedBluetoothDevice.getGroupId()).thenReturn(1);
+ BluetoothLeBroadcastReceiveState state = mock(BluetoothLeBroadcastReceiveState.class);
+ when(mAssistant.getAllSources(mBluetoothDevice)).thenReturn(ImmutableList.of(state));
+ when(mAssistant.getAllConnectedDevices()).thenReturn(ImmutableList.of(mBluetoothDevice));
+
+ assertThat(
+ BluetoothUtils.getSecondaryDeviceForBroadcast(
+ mContext.getContentResolver(), mLocalBluetoothManager))
+ .isNull();
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ADOPT_PRIMARY_GROUP_MANAGEMENT_API_V2)
+ public void getSecondaryDeviceForBroadcast_adoptAPI_returnCorrectDevice() {
+ when(mBroadcast.isEnabled(any())).thenReturn(true);
+ when(mLeAudioProfile.getBroadcastToUnicastFallbackGroup()).thenReturn(1);
+ CachedBluetoothDevice cachedBluetoothDevice = mock(CachedBluetoothDevice.class);
+ BluetoothDevice bluetoothDevice = mock(BluetoothDevice.class);
+ when(cachedBluetoothDevice.getDevice()).thenReturn(bluetoothDevice);
+ when(cachedBluetoothDevice.getGroupId()).thenReturn(1);
+ when(mCachedBluetoothDevice.getDevice()).thenReturn(mBluetoothDevice);
+ when(mCachedBluetoothDevice.getGroupId()).thenReturn(2);
+ when(mDeviceManager.findDevice(bluetoothDevice)).thenReturn(cachedBluetoothDevice);
+ when(mDeviceManager.findDevice(mBluetoothDevice)).thenReturn(mCachedBluetoothDevice);
+ BluetoothLeBroadcastReceiveState state = mock(BluetoothLeBroadcastReceiveState.class);
+ List<Long> bisSyncState = new ArrayList<>();
+ bisSyncState.add(1L);
+ when(state.getBisSyncState()).thenReturn(bisSyncState);
+ when(mAssistant.getAllSources(any(BluetoothDevice.class)))
+ .thenReturn(ImmutableList.of(state));
+ when(mAssistant.getAllConnectedDevices())
+ .thenReturn(ImmutableList.of(mBluetoothDevice, bluetoothDevice));
+
+ assertThat(
+ BluetoothUtils.getSecondaryDeviceForBroadcast(
+ mContext.getContentResolver(), mLocalBluetoothManager))
+ .isEqualTo(mCachedBluetoothDevice);
+ }
+
+ @Test
+ @DisableFlags(Flags.FLAG_ADOPT_PRIMARY_GROUP_MANAGEMENT_API_V2)
public void getSecondaryDeviceForBroadcast_noSecondary_returnNull() {
Settings.Secure.putInt(
mContext.getContentResolver(),
BluetoothUtils.getPrimaryGroupIdUriForBroadcast(),
1);
when(mBroadcast.isEnabled(any())).thenReturn(true);
- CachedBluetoothDeviceManager deviceManager = mock(CachedBluetoothDeviceManager.class);
- when(mLocalBluetoothManager.getCachedDeviceManager()).thenReturn(deviceManager);
- when(deviceManager.findDevice(mBluetoothDevice)).thenReturn(mCachedBluetoothDevice);
+ when(mDeviceManager.findDevice(mBluetoothDevice)).thenReturn(mCachedBluetoothDevice);
when(mCachedBluetoothDevice.getDevice()).thenReturn(mBluetoothDevice);
when(mCachedBluetoothDevice.getGroupId()).thenReturn(1);
BluetoothLeBroadcastReceiveState state = mock(BluetoothLeBroadcastReceiveState.class);
@@ -958,6 +1003,7 @@ public class BluetoothUtilsTest {
}
@Test
+ @DisableFlags(Flags.FLAG_ADOPT_PRIMARY_GROUP_MANAGEMENT_API_V2)
public void getSecondaryDeviceForBroadcast_returnCorrectDevice() {
Settings.Secure.putInt(
mContext.getContentResolver(),
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 f57ee0c0930e..b4384b74ccbe 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
@@ -16,7 +16,6 @@
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;
@@ -42,6 +41,8 @@ import android.content.Context;
import android.graphics.drawable.BitmapDrawable;
import android.hardware.input.InputManager;
import android.media.AudioManager;
+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.Spannable;
@@ -138,7 +139,6 @@ public class CachedBluetoothDeviceTest {
public void setUp() {
MockitoAnnotations.initMocks(this);
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;
@@ -163,6 +163,7 @@ public class CachedBluetoothDeviceTest {
when(mHidProfile.getProfileId()).thenReturn(BluetoothProfile.HID_HOST);
when(mLocalBluetoothManager.getProfileManager()).thenReturn(mProfileManager);
when(mBroadcast.isEnabled(any())).thenReturn(false);
+ when(mProfileManager.getLeAudioProfile()).thenReturn(mLeAudioProfile);
when(mProfileManager.getLeAudioBroadcastProfile()).thenReturn(mBroadcast);
when(mProfileManager.getLeAudioBroadcastAssistantProfile()).thenReturn(mAssistant);
mCachedDevice = spy(new CachedBluetoothDevice(mContext, mProfileManager, mDevice));
@@ -2004,6 +2005,70 @@ public class CachedBluetoothDeviceTest {
}
@Test
+ @EnableFlags(com.android.settingslib.flags.Flags.FLAG_ADOPT_PRIMARY_GROUP_MANAGEMENT_API_V2)
+ public void getConnectionSummary_adoptAPI_isBroadcastPrimary_fallbackDevice_returnActive() {
+ when(mBroadcast.isEnabled(any())).thenReturn(true);
+ when(mCachedDevice.getDevice()).thenReturn(mDevice);
+ when(mLeAudioProfile.getBroadcastToUnicastFallbackGroup()).thenReturn(1);
+
+ 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);
+
+ assertThat(mCachedDevice.getConnectionSummary(false))
+ .isEqualTo(mContext.getString(R.string.bluetooth_active_no_battery_level));
+ }
+
+ @Test
+ @EnableFlags(com.android.settingslib.flags.Flags.FLAG_ADOPT_PRIMARY_GROUP_MANAGEMENT_API_V2)
+ public void getConnectionSummary_adoptAPI_isBroadcastPrimary_activeDevice_returnActive() {
+ when(mBroadcast.isEnabled(any())).thenReturn(true);
+ when(mCachedDevice.getDevice()).thenReturn(mDevice);
+ when(mLeAudioProfile.getBroadcastToUnicastFallbackGroup()).thenReturn(
+ BluetoothCsipSetCoordinator.GROUP_ID_INVALID);
+
+ 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
+ @EnableFlags(com.android.settingslib.flags.Flags.FLAG_ADOPT_PRIMARY_GROUP_MANAGEMENT_API_V2)
+ public void getConnectionSummary_adoptAPI_isBroadcastNotPrimary_returnActiveMedia() {
+ when(mBroadcast.isEnabled(any())).thenReturn(true);
+ when(mCachedDevice.getDevice()).thenReturn(mDevice);
+ when(mLeAudioProfile.getBroadcastToUnicastFallbackGroup()).thenReturn(1);
+
+ 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_active_media_only_no_battery_level));
+ }
+
+ @Test
+ @DisableFlags(com.android.settingslib.flags.Flags.FLAG_ADOPT_PRIMARY_GROUP_MANAGEMENT_API_V2)
public void getConnectionSummary_isBroadcastPrimary_fallbackDevice_returnActive() {
when(mBroadcast.isEnabled(any())).thenReturn(true);
when(mCachedDevice.getDevice()).thenReturn(mDevice);
@@ -2026,6 +2091,7 @@ public class CachedBluetoothDeviceTest {
}
@Test
+ @DisableFlags(com.android.settingslib.flags.Flags.FLAG_ADOPT_PRIMARY_GROUP_MANAGEMENT_API_V2)
public void getConnectionSummary_isBroadcastPrimary_activeDevice_returnActive() {
when(mBroadcast.isEnabled(any())).thenReturn(true);
when(mCachedDevice.getDevice()).thenReturn(mDevice);
@@ -2049,6 +2115,7 @@ public class CachedBluetoothDeviceTest {
}
@Test
+ @DisableFlags(com.android.settingslib.flags.Flags.FLAG_ADOPT_PRIMARY_GROUP_MANAGEMENT_API_V2)
public void getConnectionSummary_isBroadcastNotPrimary_returnActiveMedia() {
when(mBroadcast.isEnabled(any())).thenReturn(true);
when(mCachedDevice.getDevice()).thenReturn(mDevice);
@@ -2231,11 +2298,7 @@ public class CachedBluetoothDeviceTest {
"false".getBytes());
when(mDevice.getMetadata(BluetoothDevice.METADATA_MAIN_BATTERY)).thenReturn(
MAIN_BATTERY.getBytes());
- when(mContext.getSystemService(InputManager.class)).thenReturn(mInputManager);
- when(mInputManager.getInputDeviceIds()).thenReturn(new int[]{TEST_DEVICE_ID});
- when(mInputManager.getInputDeviceBluetoothAddress(TEST_DEVICE_ID)).thenReturn(
- DEVICE_ADDRESS);
- when(mInputManager.getInputDevice(TEST_DEVICE_ID)).thenReturn(mInputDevice);
+ mCachedDevice.setInputDevice(mInputDevice);
BatteryLevelsInfo batteryLevelsInfo = mCachedDevice.getBatteryLevelsInfo();
@@ -2253,10 +2316,9 @@ public class CachedBluetoothDeviceTest {
public void getBatteryLevelsInfo_stylusDeviceWithBattery_returnBatteryLevelsInfo() {
when(mDevice.getMetadata(BluetoothDevice.METADATA_IS_UNTETHERED_HEADSET)).thenReturn(
"false".getBytes());
- when(mDevice.getMetadata(BluetoothDevice.METADATA_DEVICE_TYPE)).thenReturn(
- BluetoothDevice.DEVICE_TYPE_STYLUS.getBytes());
when(mDevice.getMetadata(BluetoothDevice.METADATA_MAIN_BATTERY)).thenReturn(
MAIN_BATTERY.getBytes());
+ mCachedDevice.setIsDeviceStylus(true);
BatteryLevelsInfo batteryLevelsInfo = mCachedDevice.getBatteryLevelsInfo();
@@ -2270,6 +2332,31 @@ public class CachedBluetoothDeviceTest {
Integer.parseInt(MAIN_BATTERY));
}
+ @Test
+ public void getBatteryLevelsInfo_hearingAidDeviceWithBattery_returnBatteryLevelsInfo() {
+ when(mDevice.getMetadata(BluetoothDevice.METADATA_IS_UNTETHERED_HEADSET)).thenReturn(
+ "false".getBytes());
+ when(mProfileManager.getHearingAidProfile()).thenReturn(mHearingAidProfile);
+ updateProfileStatus(mHearingAidProfile, BluetoothProfile.STATE_CONNECTED);
+ mSubCachedDevice.setHearingAidInfo(getLeftAshaHearingAidInfo());
+ when(mSubCachedDevice.getBatteryLevel()).thenReturn(Integer.parseInt(TWS_BATTERY_LEFT));
+ updateSubDeviceProfileStatus(mHearingAidProfile, BluetoothProfile.STATE_CONNECTED);
+ mCachedDevice.setSubDevice(mSubCachedDevice);
+ mCachedDevice.setHearingAidInfo(getRightAshaHearingAidInfo());
+ when(mCachedDevice.getBatteryLevel()).thenReturn(Integer.parseInt(TWS_BATTERY_RIGHT));
+
+ BatteryLevelsInfo batteryLevelsInfo = mCachedDevice.getBatteryLevelsInfo();
+
+ assertThat(batteryLevelsInfo.getLeftBatteryLevel()).isEqualTo(
+ Integer.parseInt(TWS_BATTERY_LEFT));
+ assertThat(batteryLevelsInfo.getRightBatteryLevel()).isEqualTo(
+ Integer.parseInt(TWS_BATTERY_RIGHT));
+ assertThat(batteryLevelsInfo.getCaseBatteryLevel()).isEqualTo(
+ BluetoothDevice.BATTERY_LEVEL_UNKNOWN);
+ assertThat(batteryLevelsInfo.getOverallBatteryLevel()).isEqualTo(
+ Integer.parseInt(TWS_BATTERY_LEFT));
+ }
+
private void updateProfileStatus(LocalBluetoothProfile profile, int status) {
doReturn(status).when(profile).getConnectionStatus(mDevice);
mCachedDevice.onProfileStateChanged(profile, status);
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CsipDeviceManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CsipDeviceManagerTest.java
index fd14d1ff6786..4314982752c3 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CsipDeviceManagerTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CsipDeviceManagerTest.java
@@ -377,6 +377,7 @@ public class CsipDeviceManagerTest {
BluetoothLeBroadcastReceiveState.class);
when(state.getBisSyncState()).thenReturn(ImmutableList.of(1L));
when(mAssistant.getAllSources(mDevice2)).thenReturn(ImmutableList.of(state));
+ when(mAssistant.getConnectionStatus(mDevice1)).thenReturn(BluetoothAdapter.STATE_CONNECTED);
when(mContext.getSystemService(UserManager.class)).thenReturn(mUserManager);
when(mUserManager.isManagedProfile()).thenReturn(true);
@@ -407,6 +408,7 @@ public class CsipDeviceManagerTest {
BluetoothLeBroadcastReceiveState.class);
when(state.getBisSyncState()).thenReturn(ImmutableList.of(1L));
when(mAssistant.getAllSources(mDevice2)).thenReturn(ImmutableList.of(state));
+ when(mAssistant.getConnectionStatus(mDevice1)).thenReturn(BluetoothAdapter.STATE_CONNECTED);
assertThat(mCsipDeviceManager.addMemberDevicesIntoMainDevice(GROUP1, preferredDevice))
.isTrue();
@@ -474,6 +476,8 @@ public class CsipDeviceManagerTest {
BluetoothLeBroadcastReceiveState.class);
when(state.getBisSyncState()).thenReturn(ImmutableList.of(1L));
when(mAssistant.getAllSources(mDevice1)).thenReturn(ImmutableList.of(state));
+ when(mAssistant.getConnectionStatus(mDevice2)).thenReturn(BluetoothAdapter.STATE_CONNECTED);
+ when(mAssistant.getConnectionStatus(mDevice3)).thenReturn(BluetoothAdapter.STATE_CONNECTED);
assertThat(mCsipDeviceManager.addMemberDevicesIntoMainDevice(GROUP1, preferredDevice))
.isTrue();
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 b364368df473..ec7baf6bf081 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
@@ -75,10 +75,14 @@ class ZenModeRepositoryTest {
private val testScope: TestScope = TestScope()
+ private val initialModes = listOf(TestModeBuilder().setId("Built-in").build())
+
@Before
fun setup() {
MockitoAnnotations.initMocks(this)
+ `when`(zenModesBackend.modes).thenReturn(initialModes)
+
underTest =
ZenModeRepositoryImpl(
context,
@@ -151,8 +155,8 @@ class ZenModeRepositoryTest {
fun modesListEmitsOnSettingsChange() {
testScope.runTest {
val values = mutableListOf<List<ZenMode>>()
- val modes1 = listOf(TestModeBuilder().setId("One").build())
- `when`(zenModesBackend.modes).thenReturn(modes1)
+
+ // an initial list of modes is read when the stateflow is created
underTest.modes.onEach { values.add(it) }.launchIn(backgroundScope)
runCurrent()
@@ -172,7 +176,7 @@ class ZenModeRepositoryTest {
triggerZenModeSettingUpdate()
runCurrent()
- assertThat(values).containsExactly(modes1, modes2, modes3).inOrder()
+ assertThat(values).containsExactly(initialModes, modes2, modes3).inOrder()
}
}
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java
index ed11e12c32ff..2273b4f81eea 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java
@@ -54,6 +54,7 @@ import com.android.settingslib.devicestate.DeviceStateRotationLockSettingsManage
import java.io.FileNotFoundException;
import java.util.ArrayList;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
@@ -650,6 +651,10 @@ public class SettingsHelper {
* e.g. current locale "en-US,zh-CN" and backup locale "ja-JP,zh-Hans-CN,en-US" are merged to
* "en-US,zh-CN,ja-JP".
*
+ * - Same language codes and scripts are dropped.
+ * e.g. current locale "en-US, zh-Hans-TW" and backup locale "en-UK, en-GB, zh-Hans-HK" are
+ * merged to "en-US, zh-Hans-TW".
+ *
* - Unsupported locales are dropped.
* e.g. current locale "en-US" and backup locale "ja-JP,zh-CN" but the supported locales
* are "en-US,zh-CN", the merged locale list is "en-US,zh-CN".
@@ -683,13 +688,23 @@ public class SettingsHelper {
filtered.add(locale);
}
+ final HashSet<String> existingLanguageAndScript = new HashSet<>();
for (int i = 0; i < restore.size(); i++) {
final Locale restoredLocaleWithExtension = copyExtensionToTargetLocale(restoredLocale,
getFilteredLocale(restore.get(i), allLocales));
+
if (restoredLocaleWithExtension != null) {
- filtered.add(restoredLocaleWithExtension);
+ String language = restoredLocaleWithExtension.getLanguage();
+ String script = restoredLocaleWithExtension.getScript();
+
+ String restoredLanguageAndScript =
+ script == null ? language : language + "-" + script;
+ if (existingLanguageAndScript.add(restoredLanguageAndScript)) {
+ filtered.add(restoredLocaleWithExtension);
+ }
}
}
+
if (filtered.size() == current.size()) {
return current; // Nothing added to current locale list.
}
diff --git a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsHelperTest.java b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsHelperTest.java
index 40654b0e2f37..48c778542d66 100644
--- a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsHelperTest.java
+++ b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsHelperTest.java
@@ -388,7 +388,11 @@ public class SettingsHelperTest {
LocaleList.forLanguageTags("zh-Hant-TW"), // current
new String[] { "fa-Arab-AF-u-nu-latn", "zh-Hant-TW" })); // supported
-
+ assertEquals(LocaleList.forLanguageTags("en-US,zh-Hans-TW"),
+ SettingsHelper.resolveLocales(
+ LocaleList.forLanguageTags("en-UK,en-GB,zh-Hans-HK"), // restore
+ LocaleList.forLanguageTags("en-US,zh-Hans-TW"), // current
+ new String[] { "en-US,zh-Hans-TW,en-UK,en-GB,zh-Hans-HK" })); // supported
}
@Test
diff --git a/packages/SystemUI/aconfig/desktop_users_and_accounts.aconfig b/packages/SystemUI/aconfig/desktop_users_and_accounts.aconfig
new file mode 100644
index 000000000000..c7e9c9fbee2e
--- /dev/null
+++ b/packages/SystemUI/aconfig/desktop_users_and_accounts.aconfig
@@ -0,0 +1,9 @@
+package: "com.android.systemui"
+container: "system"
+
+flag {
+ name: "user_switcher_add_sign_out_option"
+ namespace: "desktop_users_and_accounts"
+ description: "Add a sign out option to the user switcher menu if sign out is possible"
+ bug: "381478261"
+} \ No newline at end of file
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index 1c37687a6eb8..7800a5059092 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -289,6 +289,15 @@ flag {
}
}
+flag {
+ name: "notification_skip_silent_updates"
+ namespace: "systemui"
+ description: "Do not notify HeadsUpManager for silent updates."
+ bug: "401068530"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
flag {
name: "scene_container"
@@ -533,6 +542,14 @@ flag {
}
flag {
+ name: "icon_refresh_2025"
+ namespace: "systemui"
+ description: "Build time flag for 2025 icon refresh"
+ bug: "391605373"
+ is_fixed_read_only: true
+}
+
+flag {
name: "promote_notifications_automatically"
namespace: "systemui"
description: "Flag to automatically turn certain notifications into promoted notifications so "
@@ -1856,6 +1873,14 @@ flag {
}
flag {
+ name: "shade_header_font_update"
+ namespace: "systemui"
+ description: "Updates the fonts of the shade header"
+ bug: "393609960"
+ is_fixed_read_only: true
+}
+
+flag {
name: "keyboard_shortcut_helper_shortcut_customizer"
namespace: "systemui"
description: "An implementation of shortcut customizations through shortcut helper."
@@ -1900,13 +1925,6 @@ flag {
}
flag {
- name: "spatial_model_launcher_pushback"
- namespace: "systemui"
- description: "Implement the depth push scaling effect on Launcher when users pull down shade."
- bug: "370562309"
-}
-
-flag {
name: "spatial_model_app_pushback"
namespace: "systemui"
description: "Implement the depth push scaling effect on the current app when users pull down shade."
@@ -2012,7 +2030,14 @@ flag {
flag {
name: "permission_helper_ui_rich_ongoing"
namespace: "systemui"
- description: "[RONs] Guards inline permission helper for demoting RONs"
+ description: "[RONs] Guards inline permission helper for demoting RONs [Guts/card version]"
+ bug: "379186372"
+}
+
+flag {
+ name: "permission_helper_inline_ui_rich_ongoing"
+ namespace: "systemui"
+ description: "[RONs] Guards inline permission helper for demoting RONs [Inline version]"
bug: "379186372"
}
@@ -2138,3 +2163,12 @@ flag {
}
}
+flag {
+ name: "keyguard_wm_reorder_atms_calls"
+ namespace: "systemui"
+ description: "Calls ATMS#setLockScreenShown before default display callbacks in case they're slow"
+ bug: "399693427"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityTransitionAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityTransitionAnimator.kt
index 5599db7689c2..1fb7901dcb59 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityTransitionAnimator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityTransitionAnimator.kt
@@ -751,7 +751,8 @@ constructor(
OriginTransition(createLongLivedRunner(controllerFactory, scope, forLaunch = true)),
"${cookie}_launchTransition",
)
- transitionRegister.register(launchFilter, launchRemoteTransition, includeTakeover = true)
+ // TODO(b/403529740): re-enable takeovers once we solve the Compose jank issues.
+ transitionRegister.register(launchFilter, launchRemoteTransition, includeTakeover = false)
// Cross-task close transitions should not use this animation, so we only register it for
// when the opening window is Launcher.
@@ -777,7 +778,8 @@ constructor(
),
"${cookie}_returnTransition",
)
- transitionRegister.register(returnFilter, returnRemoteTransition, includeTakeover = true)
+ // TODO(b/403529740): re-enable takeovers once we solve the Compose jank issues.
+ transitionRegister.register(returnFilter, returnRemoteTransition, includeTakeover = false)
longLivedTransitions[cookie] = Pair(launchRemoteTransition, returnRemoteTransition)
}
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/gesture/effect/OffsetOverscrollEffect.kt b/packages/SystemUI/compose/core/src/com/android/compose/gesture/effect/OffsetOverscrollEffect.kt
index 1bb8ae5019fb..07a571b94ce4 100644
--- a/packages/SystemUI/compose/core/src/com/android/compose/gesture/effect/OffsetOverscrollEffect.kt
+++ b/packages/SystemUI/compose/core/src/com/android/compose/gesture/effect/OffsetOverscrollEffect.kt
@@ -16,7 +16,6 @@
package com.android.compose.gesture.effect
-import androidx.annotation.VisibleForTesting
import androidx.compose.animation.core.AnimationSpec
import androidx.compose.foundation.OverscrollEffect
import androidx.compose.foundation.OverscrollFactory
@@ -94,7 +93,6 @@ class OffsetOverscrollEffect(animationScope: CoroutineScope, animationSpec: Anim
companion object {
private val MaxDistance = 400.dp
- @VisibleForTesting
fun computeOffset(density: Density, overscrollDistance: Float): Int {
val maxDistancePx = with(density) { MaxDistance.toPx() }
val progress = ProgressConverter.Default.convert(overscrollDistance / maxDistancePx)
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinInputDisplay.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinInputDisplay.kt
index 1f98cd8e07c0..90311ed93987 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinInputDisplay.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinInputDisplay.kt
@@ -35,7 +35,7 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.heightIn
+import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.layout.wrapContentSize
@@ -83,10 +83,7 @@ import kotlinx.coroutines.joinAll
import kotlinx.coroutines.launch
@Composable
-fun PinInputDisplay(
- viewModel: PinBouncerViewModel,
- modifier: Modifier = Modifier,
-) {
+fun PinInputDisplay(viewModel: PinBouncerViewModel, modifier: Modifier = Modifier) {
val hintedPinLength: Int? by viewModel.hintedPinLength.collectAsStateWithLifecycle()
val shapeAnimations = rememberShapeAnimations(viewModel.pinShapes)
@@ -173,7 +170,10 @@ private fun HintingPinInputDisplay(
LaunchedEffect(Unit) { playAnimation = true }
val dotColor = MaterialTheme.colorScheme.onSurfaceVariant
- Row(modifier = modifier.heightIn(min = shapeAnimations.shapeSize)) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = modifier.height(shapeAnimations.shapeSize),
+ ) {
pinEntryDrawable.forEachIndexed { index, drawable ->
// Key the loop by [index] and [drawable], so that updating a shape drawable at the same
// index will play the new animation (by remembering a new [atEnd]).
@@ -316,17 +316,15 @@ private fun SimArea(viewModel: PinBouncerViewModel) {
Box(modifier = Modifier.padding(bottom = 20.dp)) {
// If isLockedEsim is null, then we do not show anything.
if (isLockedEsim == true) {
- PlatformOutlinedButton(
- onClick = { viewModel.onDisableEsimButtonClicked() },
- ) {
+ PlatformOutlinedButton(onClick = { viewModel.onDisableEsimButtonClicked() }) {
Row(
horizontalArrangement = Arrangement.spacedBy(10.dp),
- verticalAlignment = Alignment.CenterVertically
+ verticalAlignment = Alignment.CenterVertically,
) {
Image(
painter = painterResource(id = R.drawable.ic_no_sim),
contentDescription = null,
- colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onSurface)
+ colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onSurface),
)
Text(
text = stringResource(R.string.disable_carrier_button_text),
@@ -339,15 +337,13 @@ private fun SimArea(viewModel: PinBouncerViewModel) {
Image(
painter = painterResource(id = R.drawable.ic_lockscreen_sim),
contentDescription = null,
- colorFilter = ColorFilter.tint(colorResource(id = R.color.background_protected))
+ colorFilter = ColorFilter.tint(colorResource(id = R.color.background_protected)),
)
}
}
}
-private class PinInputRow(
- val shapeAnimations: ShapeAnimations,
-) {
+private class PinInputRow(val shapeAnimations: ShapeAnimations) {
private val entries = mutableStateListOf<PinInputEntry>()
@Composable
@@ -359,10 +355,11 @@ private class PinInputRow(
contentAlignment = Alignment.Center,
) {
Row(
- modifier
- .heightIn(min = shapeAnimations.shapeSize)
- // Pins overflowing horizontally should still be shown as scrolling.
- .wrapContentSize(unbounded = true)
+ verticalAlignment = Alignment.CenterVertically,
+ modifier =
+ Modifier.height(shapeAnimations.shapeSize)
+ // Pins overflowing horizontally should still be shown as scrolling.
+ .wrapContentSize(unbounded = true),
) {
entries.forEach { entry -> key(entry.digit) { entry.Content() } }
}
@@ -439,10 +436,7 @@ private class PinInputRow(
}
}
-private class PinInputEntry(
- val digit: Digit,
- val shapeAnimations: ShapeAnimations,
-) {
+private class PinInputEntry(val digit: Digit, val shapeAnimations: ShapeAnimations) {
private val shape = shapeAnimations.getShapeToDot(digit.sequenceNumber)
// horizontal space occupied, used to shift contents as individual digits are animated in/out
private val entryWidth =
@@ -474,7 +468,7 @@ private class PinInputEntry(
suspend fun animateRemoval() = coroutineScope {
awaitAll(
async { entryWidth.animateTo(0.dp, shapeAnimations.inputShiftAnimationSpec) },
- async { shapeSize.animateTo(0.dp, shapeAnimations.deleteShapeSizeAnimationSpec) }
+ async { shapeSize.animateTo(0.dp, shapeAnimations.deleteShapeSizeAnimationSpec) },
)
}
@@ -505,7 +499,7 @@ private class PinInputEntry(
layout(animatedEntryWidth.roundToPx(), shapeHeight.roundToPx()) {
placeable.place(
((animatedEntryWidth - animatedShapeSize) / 2f).roundToPx(),
- ((shapeHeight - animatedShapeSize) / 2f).roundToPx()
+ ((shapeHeight - animatedShapeSize) / 2f).roundToPx(),
)
}
},
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/LockSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/LockSection.kt
index 0db2bb51c971..5fac6863e931 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/LockSection.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/LockSection.kt
@@ -33,6 +33,7 @@ import com.android.compose.animation.scene.ElementKey
import com.android.systemui.biometrics.AuthController
import com.android.systemui.customization.R as customR
import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.flags.FeatureFlagsClassic
import com.android.systemui.flags.Flags
import com.android.systemui.keyguard.ui.binder.DeviceEntryIconViewBinder
@@ -49,12 +50,14 @@ import com.android.systemui.res.R
import com.android.systemui.statusbar.VibratorHelper
import dagger.Lazy
import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
class LockSection
@Inject
constructor(
@Application private val applicationScope: CoroutineScope,
+ @Main private val mainDispatcher: CoroutineDispatcher,
private val windowManager: WindowManager,
private val authController: AuthController,
private val featureFlags: FeatureFlagsClassic,
@@ -80,6 +83,7 @@ constructor(
id = R.id.device_entry_icon_view
DeviceEntryIconViewBinder.bind(
applicationScope,
+ mainDispatcher,
this,
deviceEntryIconViewModel.get(),
deviceEntryForegroundViewModel.get(),
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationScrimFlingBehavior.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationScrimFlingBehavior.kt
new file mode 100644
index 000000000000..bc38ef0abb25
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationScrimFlingBehavior.kt
@@ -0,0 +1,73 @@
+/*
+ * 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.notifications.ui.composable
+
+import androidx.compose.animation.core.AnimationState
+import androidx.compose.animation.core.DecayAnimationSpec
+import androidx.compose.animation.core.animateDecay
+import androidx.compose.foundation.gestures.FlingBehavior
+import androidx.compose.foundation.gestures.ScrollScope
+import androidx.compose.ui.MotionDurationScale
+import com.android.systemui.scene.session.ui.composable.rememberSession
+import kotlinx.coroutines.CancellationException
+import kotlinx.coroutines.withContext
+import kotlin.math.abs
+
+/**
+ * Fork of [androidx.compose.foundation.gestures.DefaultFlingBehavior] to allow us to use it with
+ * [rememberSession].
+ */
+internal class NotificationScrimFlingBehavior(
+ private var flingDecay: DecayAnimationSpec<Float>,
+ private val motionDurationScale: MotionDurationScale = NotificationScrimMotionDurationScale
+) : FlingBehavior {
+ override suspend fun ScrollScope.performFling(initialVelocity: Float): Float {
+ // come up with the better threshold, but we need it since spline curve gives us NaNs
+ return withContext(motionDurationScale) {
+ if (abs(initialVelocity) > 1f) {
+ var velocityLeft = initialVelocity
+ var lastValue = 0f
+ val animationState =
+ AnimationState(
+ initialValue = 0f,
+ initialVelocity = initialVelocity,
+ )
+ try {
+ animationState.animateDecay(flingDecay) {
+ val delta = value - lastValue
+ val consumed = scrollBy(delta)
+ lastValue = value
+ velocityLeft = this.velocity
+ // avoid rounding errors and stop if anything is unconsumed
+ if (abs(delta - consumed) > 0.5f) this.cancelAnimation()
+ }
+ } catch (exception: CancellationException) {
+ velocityLeft = animationState.velocity
+ }
+ velocityLeft
+ } else {
+ initialVelocity
+ }
+ }
+ }
+}
+
+internal val NotificationScrimMotionDurationScale =
+ object : MotionDurationScale {
+ override val scaleFactor: Float
+ get() = 1f
+ } \ No newline at end of file
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt
index 09b8d178cc8e..79b346439d5d 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt
@@ -20,12 +20,13 @@ package com.android.systemui.notifications.ui.composable
import android.util.Log
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.AnimationVector1D
+import androidx.compose.animation.core.DecayAnimationSpec
import androidx.compose.animation.core.tween
+import androidx.compose.animation.splineBasedDecay
import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.Orientation
-import androidx.compose.foundation.gestures.ScrollableDefaults
import androidx.compose.foundation.gestures.animateScrollBy
import androidx.compose.foundation.gestures.rememberScrollableState
import androidx.compose.foundation.gestures.scrollBy
@@ -59,7 +60,6 @@ import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
-import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
@@ -97,6 +97,7 @@ import com.android.systemui.common.ui.compose.windowinsets.LocalScreenCornerRadi
import com.android.systemui.res.R
import com.android.systemui.scene.session.ui.composable.SaveableSession
import com.android.systemui.scene.session.ui.composable.rememberSession
+import com.android.systemui.scene.session.ui.composable.sessionCoroutineScope
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.shade.ui.composable.ShadeHeader
import com.android.systemui.statusbar.notification.stack.shared.model.AccessibilityScrollEvent
@@ -298,7 +299,7 @@ fun ContentScope.NotificationScrollingStack(
onEmptySpaceClick: (() -> Unit)? = null,
modifier: Modifier = Modifier,
) {
- val coroutineScope = rememberCoroutineScope()
+ val coroutineScope = shadeSession.sessionCoroutineScope()
val density = LocalDensity.current
val screenCornerRadius = LocalScreenCornerRadius.current
val scrimCornerRadius = dimensionResource(R.dimen.notification_scrim_corner_radius)
@@ -308,8 +309,6 @@ fun ContentScope.NotificationScrollingStack(
ScrollState(initial = 0)
}
val syntheticScroll = viewModel.syntheticScroll.collectAsStateWithLifecycle(0f)
- val isCurrentGestureOverscroll =
- viewModel.isCurrentGestureOverscroll.collectAsStateWithLifecycle(false)
val expansionFraction by viewModel.expandFraction.collectAsStateWithLifecycle(0f)
val shadeToQsFraction by viewModel.shadeToQsFraction.collectAsStateWithLifecycle(0f)
@@ -454,15 +453,15 @@ fun ContentScope.NotificationScrollingStack(
}
}
- val flingBehavior = ScrollableDefaults.flingBehavior()
val scrimNestedScrollConnection =
shadeSession.rememberSession(
scrimOffset,
- maxScrimTop,
minScrimTop,
- isCurrentGestureOverscroll,
- flingBehavior,
+ viewModel.isCurrentGestureOverscroll,
+ density,
) {
+ val flingSpec: DecayAnimationSpec<Float> = splineBasedDecay(density)
+ val flingBehavior = NotificationScrimFlingBehavior(flingSpec)
NotificationScrimNestedScrollConnection(
scrimOffset = { scrimOffset.value },
snapScrimOffset = { value -> coroutineScope.launch { scrimOffset.snapTo(value) } },
@@ -473,7 +472,7 @@ fun ContentScope.NotificationScrollingStack(
maxScrimOffset = 0f,
contentHeight = { stackHeight.intValue.toFloat() },
minVisibleScrimHeight = minVisibleScrimHeight,
- isCurrentGestureOverscroll = { isCurrentGestureOverscroll.value },
+ isCurrentGestureOverscroll = { viewModel.isCurrentGestureOverscroll },
flingBehavior = flingBehavior,
)
}
@@ -574,11 +573,11 @@ fun ContentScope.NotificationScrollingStack(
) {
Column(
modifier =
- Modifier.thenIf(supportNestedScrolling) {
+ Modifier.disableSwipesWhenScrolling(NestedScrollableBound.BottomRight)
+ .thenIf(supportNestedScrolling) {
Modifier.nestedScroll(scrimNestedScrollConnection)
}
.stackVerticalOverscroll(coroutineScope) { scrollState.canScrollForward }
- .disableSwipesWhenScrolling(NestedScrollableBound.BottomRight)
.verticalScroll(scrollState)
.padding(top = stackTopPadding, bottom = stackBottomPadding)
.fillMaxWidth()
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/session/ui/composable/Session.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/session/ui/composable/Session.kt
index 4eaacf31e23d..1750b11a998b 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/session/ui/composable/Session.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/session/ui/composable/Session.kt
@@ -17,10 +17,14 @@
package com.android.systemui.scene.session.ui.composable
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffectResult
+import androidx.compose.runtime.DisposableEffectScope
+import androidx.compose.runtime.RememberObserver
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.currentCompositeKeyHash
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.rememberCompositionContext
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.SaverScope
import androidx.compose.runtime.saveable.mapSaver
@@ -28,6 +32,10 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import com.android.systemui.scene.session.shared.SessionStorage
import com.android.systemui.util.kotlin.mapValuesNotNullTo
+import kotlin.coroutines.CoroutineContext
+import kotlin.coroutines.EmptyCoroutineContext
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
/**
* An explicit storage for remembering composable state outside of the lifetime of a composition.
@@ -89,6 +97,55 @@ fun <T> Session.rememberSession(vararg inputs: Any?, key: String? = null, init:
rememberSession(key, *inputs, init = init)
/**
+ * A side effect of composition that must be reversed or cleaned up if the [Session] ends.
+ *
+ * @see androidx.compose.runtime.DisposableEffect
+ */
+@Composable
+fun Session.SessionDisposableEffect(
+ vararg inputs: Any?,
+ key: String? = null,
+ effect: DisposableEffectScope.() -> DisposableEffectResult,
+) {
+ rememberSession(inputs, key) {
+ object : RememberObserver {
+
+ var onDispose: DisposableEffectResult? = null
+
+ override fun onAbandoned() {
+ // no-op
+ }
+
+ override fun onForgotten() {
+ onDispose?.dispose()
+ onDispose = null
+ }
+
+ override fun onRemembered() {
+ onDispose = DisposableEffectScope().effect()
+ }
+ }
+ }
+}
+
+/**
+ * Return a [CoroutineScope] bound to this [Session] using the optional [CoroutineContext] provided
+ * by [getContext]. [getContext] will only be called once and the same [CoroutineScope] instance
+ * will be returned for the duration of the [Session].
+ *
+ * @see androidx.compose.runtime.rememberCoroutineScope
+ */
+@Composable
+fun Session.sessionCoroutineScope(
+ getContext: () -> CoroutineContext = { EmptyCoroutineContext }
+): CoroutineScope {
+ val effectContext: CoroutineContext = rememberCompositionContext().effectCoroutineContext
+ val job = rememberSession { Job() }
+ SessionDisposableEffect { onDispose { job.cancel() } }
+ return rememberSession { CoroutineScope(effectContext + job + getContext()) }
+}
+
+/**
* An explicit storage for remembering composable state outside of the lifetime of a composition.
*
* Specifically, this allows easy conversion of standard [rememberSession] invocations to ones that
@@ -147,15 +204,10 @@ interface SaveableSession : Session {
* location in the composition tree.
*/
@Composable
-fun rememberSaveableSession(
- vararg inputs: Any?,
- key: String? = null,
-): SaveableSession =
+fun rememberSaveableSession(vararg inputs: Any?, key: String? = null): SaveableSession =
rememberSaveable(*inputs, SaveableSessionImpl.SessionSaver, key) { SaveableSessionImpl() }
-private class SessionImpl(
- private val storage: SessionStorage = SessionStorage(),
-) : Session {
+private class SessionImpl(private val storage: SessionStorage = SessionStorage()) : Session {
@Composable
override fun <T> rememberSession(key: String?, vararg inputs: Any?, init: () -> T): T {
val storage = storage.storage
@@ -169,16 +221,31 @@ private class SessionImpl(
}
if (finalKey !in storage) {
val value = init()
- SideEffect { storage[finalKey] = SessionStorage.StorageEntry(inputs, value) }
+ SideEffect {
+ storage[finalKey] = SessionStorage.StorageEntry(inputs, value)
+ if (value is RememberObserver) {
+ value.onRemembered()
+ }
+ }
return value
}
val entry = storage[finalKey]!!
if (!inputs.contentEquals(entry.keys)) {
val value = init()
- SideEffect { entry.stored = value }
+ SideEffect {
+ val oldValue = entry.stored
+ if (oldValue is RememberObserver) {
+ oldValue.onForgotten()
+ }
+ entry.stored = value
+ if (value is RememberObserver) {
+ value.onRemembered()
+ }
+ }
return value
}
- @Suppress("UNCHECKED_CAST") return entry.stored as T
+ @Suppress("UNCHECKED_CAST")
+ return entry.stored as T
}
}
@@ -228,7 +295,8 @@ private class SaveableSessionImpl(
}
return value
}
- @Suppress("UNCHECKED_CAST") return entry.stored as T
+ @Suppress("UNCHECKED_CAST")
+ return entry.stored as T
}
}
}
@@ -263,7 +331,7 @@ private class SaveableSessionImpl(
v?.let { StorageEntry.Unrestored(v) }
}
)
- }
+ },
)
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt
index 86c8fc34a63c..03debb6fa7ca 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt
@@ -43,6 +43,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
@@ -67,12 +68,15 @@ import com.android.compose.animation.scene.content.state.TransitionState
import com.android.compose.modifiers.thenIf
import com.android.compose.theme.colorAttr
import com.android.settingslib.Utils
+import com.android.systemui.Flags
import com.android.systemui.battery.BatteryMeterView
import com.android.systemui.battery.BatteryMeterViewController
import com.android.systemui.common.ui.compose.windowinsets.CutoutLocation
import com.android.systemui.common.ui.compose.windowinsets.LocalDisplayCutout
import com.android.systemui.common.ui.compose.windowinsets.LocalScreenCornerRadius
import com.android.systemui.compose.modifiers.sysuiResTag
+import com.android.systemui.kairos.ExperimentalKairosApi
+import com.android.systemui.kairos.buildSpec
import com.android.systemui.privacy.OngoingPrivacyChip
import com.android.systemui.res.R
import com.android.systemui.scene.shared.model.Scenes
@@ -86,8 +90,12 @@ import com.android.systemui.shade.ui.viewmodel.ShadeHeaderViewModel.HeaderChipHi
import com.android.systemui.statusbar.phone.StatusBarLocation
import com.android.systemui.statusbar.phone.StatusIconContainer
import com.android.systemui.statusbar.pipeline.mobile.ui.view.ModernShadeCarrierGroupMobileView
+import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModelKairosComposeWrapper
import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.ShadeCarrierGroupMobileIconViewModel
+import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.ShadeCarrierGroupMobileIconViewModelKairos
+import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.composeWrapper
import com.android.systemui.statusbar.policy.Clock
+import com.android.systemui.util.composable.kairos.ActivatedKairosSpec
object ShadeHeader {
object Elements {
@@ -285,6 +293,25 @@ fun ContentScope.OverlayShadeHeader(
viewModel: ShadeHeaderViewModel,
modifier: Modifier = Modifier,
) {
+ OverlayShadeHeaderPartialStateless(
+ viewModel,
+ viewModel.showClock,
+ modifier,
+ )
+}
+
+/**
+ * Ideally, we should have a stateless function for overlay shade header, which facilitates testing.
+ * However, it is cumbersome to implement such a stateless function, especially when some of the
+ * overlay shade header's children accept a view model as the param. Therefore, this function only
+ * break up the clock visibility. It is where "PartialStateless" comes from.
+ */
+@Composable
+fun ContentScope.OverlayShadeHeaderPartialStateless(
+ viewModel: ShadeHeaderViewModel,
+ showClock: Boolean,
+ modifier: Modifier = Modifier,
+) {
val horizontalPadding =
max(LocalScreenCornerRadius.current / 2f, Shade.Dimensions.HorizontalPadding)
@@ -300,7 +327,7 @@ fun ContentScope.OverlayShadeHeader(
modifier = Modifier.padding(horizontal = horizontalPadding),
) {
val chipHighlight = viewModel.notificationsChipHighlight
- if (viewModel.showClock) {
+ if (showClock) {
Clock(
onClick = viewModel::onClockClicked,
modifier = Modifier.padding(horizontal = 4.dp),
@@ -520,8 +547,14 @@ private fun BatteryIcon(
)
}
+@OptIn(ExperimentalKairosApi::class)
@Composable
private fun ShadeCarrierGroup(viewModel: ShadeHeaderViewModel, modifier: Modifier = Modifier) {
+ if (Flags.statusBarMobileIconKairos()) {
+ ShadeCarrierGroupKairos(viewModel, modifier)
+ return
+ }
+
Row(modifier = modifier, horizontalArrangement = Arrangement.spacedBy(5.dp)) {
for (subId in viewModel.mobileSubIds) {
AndroidView(
@@ -543,6 +576,49 @@ private fun ShadeCarrierGroup(viewModel: ShadeHeaderViewModel, modifier: Modifie
}
}
+@ExperimentalKairosApi
+@Composable
+private fun ShadeCarrierGroupKairos(
+ viewModel: ShadeHeaderViewModel,
+ modifier: Modifier = Modifier,
+) {
+ Row(modifier = modifier) {
+ ActivatedKairosSpec(
+ buildSpec = viewModel.mobileIconsViewModelKairos.get().composeWrapper(),
+ kairosNetwork = viewModel.kairosNetwork,
+ ) { iconsViewModel: MobileIconsViewModelKairosComposeWrapper ->
+ for ((subId, icon) in iconsViewModel.icons) {
+ Spacer(modifier = Modifier.width(5.dp))
+ val scope = rememberCoroutineScope()
+ AndroidView(
+ factory = { context ->
+ ModernShadeCarrierGroupMobileView.constructAndBind(
+ context = context,
+ logger = iconsViewModel.logger,
+ slot = "mobile_carrier_shade_group",
+ viewModel =
+ buildSpec {
+ ShadeCarrierGroupMobileIconViewModelKairos(
+ icon,
+ icon.iconInteractor,
+ )
+ },
+ scope = scope,
+ subscriptionId = subId,
+ location = StatusBarLocation.SHADE_CARRIER_GROUP,
+ kairosNetwork = viewModel.kairosNetwork,
+ )
+ .first
+ .also {
+ it.setOnClickListener { viewModel.onShadeCarrierGroupClicked() }
+ }
+ }
+ )
+ }
+ }
+ }
+}
+
@Composable
private fun ContentScope.StatusIcons(
viewModel: ShadeHeaderViewModel,
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockController.kt
index 96c3ac75587e..8744357a74c9 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockController.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockController.kt
@@ -144,23 +144,25 @@ class FlexClockController(private val clockCtx: ClockContext) : ClockController
listOf(
GSFAxes.WEIGHT.toClockAxis(
type = AxisType.Float,
- currentValue = 475f,
+ currentValue = 400f,
name = "Weight",
description = "Glyph Weight",
),
GSFAxes.WIDTH.toClockAxis(
type = AxisType.Float,
- currentValue = 85f,
+ currentValue = 80f,
name = "Width",
description = "Glyph Width",
),
GSFAxes.ROUND.toClockAxis(
type = AxisType.Boolean,
+ currentValue = 100f,
name = "Round",
description = "Glyph Roundness",
),
GSFAxes.SLANT.toClockAxis(
type = AxisType.Boolean,
+ currentValue = 0f,
name = "Slant",
description = "Glyph Slant",
),
diff --git a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerTest.java b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerTest.java
index 2845f6a2983a..e75f60736435 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerTest.java
@@ -507,8 +507,8 @@ public class KeyguardSecurityContainerTest extends SysuiTestCase {
0 /* flags */);
users.add(new UserRecord(info, null, false /* isGuest */, false /* isCurrent */,
false /* isAddUser */, false /* isRestricted */, true /* isSwitchToEnabled */,
- false /* isAddSupervisedUser */, null /* enforcedAdmin */,
- false /* isManageUsers */));
+ false /* isAddSupervisedUser */, false /* isSignOut */,
+ null /* enforcedAdmin */, false /* isManageUsers */));
}
return users;
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegateTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegateTest.java
index d118ace08b85..530aba4993a8 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegateTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegateTest.java
@@ -176,6 +176,8 @@ public class HearingDevicesDialogDelegateTest extends SysuiTestCase {
.thenReturn(SETTINGS_PACKAGE_NAME);
when(mDevice.getBondState()).thenReturn(BOND_BONDED);
when(mDevice.isConnected()).thenReturn(true);
+ when(mDevice.getAddress()).thenReturn(DEVICE_ADDRESS);
+ when(mDevice.getAnonymizedAddress()).thenReturn(DEVICE_ADDRESS);
when(mCachedDevice.getDevice()).thenReturn(mDevice);
when(mCachedDevice.getAddress()).thenReturn(DEVICE_ADDRESS);
when(mCachedDevice.getName()).thenReturn(DEVICE_NAME);
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractorTest.kt
index c1feca29906a..91ec1cbce8a4 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractorTest.kt
@@ -77,6 +77,7 @@ class PrimaryBouncerInteractorTest : SysuiTestCase() {
private lateinit var resources: TestableResources
private lateinit var trustRepository: FakeTrustRepository
private lateinit var testScope: TestScope
+ private val TEST_REASON = "reason"
@Before
fun setUp() {
@@ -118,7 +119,7 @@ class PrimaryBouncerInteractorTest : SysuiTestCase() {
mainHandler.setMode(FakeHandler.Mode.QUEUEING)
// WHEN bouncer show is requested
- underTest.show(true)
+ underTest.show(true, TEST_REASON)
// WHEN all queued messages are dispatched
mainHandler.dispatchQueuedMessages()
@@ -134,7 +135,7 @@ class PrimaryBouncerInteractorTest : SysuiTestCase() {
@Test
fun testShow_isScrimmed() {
- underTest.show(true)
+ underTest.show(true, TEST_REASON)
verify(repository).setKeyguardAuthenticatedBiometrics(null)
verify(repository).setPrimaryStartingToHide(false)
verify(repository).setPrimaryScrimmed(true)
@@ -162,7 +163,7 @@ class PrimaryBouncerInteractorTest : SysuiTestCase() {
@Test
fun testShowReturnsFalseWhenDelegateIsNotSet() {
whenever(bouncerView.delegate).thenReturn(null)
- assertThat(underTest.show(true)).isEqualTo(false)
+ assertThat(underTest.show(true, TEST_REASON)).isEqualTo(false)
}
@Test
@@ -171,7 +172,7 @@ class PrimaryBouncerInteractorTest : SysuiTestCase() {
whenever(keyguardSecurityModel.getSecurityMode(anyInt()))
.thenReturn(KeyguardSecurityModel.SecurityMode.SimPuk)
- underTest.show(true)
+ underTest.show(true, TEST_REASON)
verify(repository).setPrimaryShow(false)
verify(repository).setPrimaryShow(true)
}
@@ -352,7 +353,7 @@ class PrimaryBouncerInteractorTest : SysuiTestCase() {
whenever(faceAuthInteractor.canFaceAuthRun()).thenReturn(true)
// WHEN bouncer show is requested
- underTest.show(true)
+ underTest.show(true, TEST_REASON)
// THEN primary show & primary showing soon aren't updated immediately
verify(repository, never()).setPrimaryShow(true)
@@ -375,7 +376,7 @@ class PrimaryBouncerInteractorTest : SysuiTestCase() {
whenever(faceAuthInteractor.canFaceAuthRun()).thenReturn(false)
// WHEN bouncer show is requested
- underTest.show(true)
+ underTest.show(true, TEST_REASON)
// THEN primary show & primary showing soon are updated immediately
verify(repository).setPrimaryShow(true)
@@ -394,7 +395,7 @@ class PrimaryBouncerInteractorTest : SysuiTestCase() {
runCurrent()
// WHEN bouncer show is requested
- underTest.show(true)
+ underTest.show(true, TEST_REASON)
// THEN primary show & primary showing soon were scheduled to update
verify(repository, never()).setPrimaryShow(true)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorTest.kt
index 0718d0d32812..83fd4c258082 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorTest.kt
@@ -264,6 +264,29 @@ class KeyguardInteractorTest : SysuiTestCase() {
}
@Test
+ fun dismissAlpha_doesNotEmitWhenNotDismissible() =
+ testScope.runTest {
+ val dismissAlpha by collectValues(underTest.dismissAlpha)
+ assertThat(dismissAlpha[0]).isEqualTo(1f)
+ assertThat(dismissAlpha.size).isEqualTo(1)
+
+ keyguardTransitionRepository.sendTransitionSteps(from = AOD, to = LOCKSCREEN, testScope)
+
+ // User begins to swipe up when not dimissible, which would show bouncer
+ repository.setStatusBarState(StatusBarState.KEYGUARD)
+ repository.setKeyguardDismissible(false)
+ shadeRepository.setLegacyShadeExpansion(0.98f)
+
+ assertThat(dismissAlpha[0]).isEqualTo(1f)
+ assertThat(dismissAlpha.size).isEqualTo(1)
+
+ // Shade reset should not affect dismiss alpha when not dismissible
+ shadeRepository.setLegacyShadeExpansion(0f)
+ assertThat(dismissAlpha[0]).isEqualTo(1f)
+ assertThat(dismissAlpha.size).isEqualTo(1)
+ }
+
+ @Test
fun dismissAlpha_onGlanceableHub_doesNotEmitWhenShadeResets() =
testScope.runTest {
val dismissAlpha by collectValues(underTest.dismissAlpha)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardKeyEventInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardKeyEventInteractorTest.kt
index 6704d63395ad..582666561be2 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardKeyEventInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardKeyEventInteractorTest.kt
@@ -267,12 +267,20 @@ class KeyguardKeyEventInteractorTest : SysuiTestCase() {
// action down: does NOT collapse the shade
val actionDownMenuKeyEvent = KeyEvent(KeyEvent.ACTION_DOWN, keycode)
assertThat(underTest.dispatchKeyEvent(actionDownMenuKeyEvent)).isFalse()
- verify(statusBarKeyguardViewManager, never()).showPrimaryBouncer(any())
+ verify(statusBarKeyguardViewManager, never())
+ .showPrimaryBouncer(
+ any(),
+ eq("KeyguardKeyEventInteractor#collapseShadeLockedOrShowPrimaryBouncer"),
+ )
// action up: collapses the shade
val actionUpMenuKeyEvent = KeyEvent(KeyEvent.ACTION_UP, keycode)
assertThat(underTest.dispatchKeyEvent(actionUpMenuKeyEvent)).isTrue()
- verify(statusBarKeyguardViewManager).showPrimaryBouncer(eq(true))
+ verify(statusBarKeyguardViewManager)
+ .showPrimaryBouncer(
+ eq(true),
+ eq("KeyguardKeyEventInteractor#collapseShadeLockedOrShowPrimaryBouncer"),
+ )
}
private fun verifyActionsDoNothing(keycode: Int) {
@@ -280,12 +288,20 @@ class KeyguardKeyEventInteractorTest : SysuiTestCase() {
val actionDownMenuKeyEvent = KeyEvent(KeyEvent.ACTION_DOWN, keycode)
assertThat(underTest.dispatchKeyEvent(actionDownMenuKeyEvent)).isFalse()
verify(shadeController, never()).animateCollapseShadeForced()
- verify(statusBarKeyguardViewManager, never()).showPrimaryBouncer(any())
+ verify(statusBarKeyguardViewManager, never())
+ .showPrimaryBouncer(
+ any(),
+ eq("KeyguardKeyEventInteractor#collapseShadeLockedOrShowPrimaryBouncer"),
+ )
// action up: doesNothing
val actionUpMenuKeyEvent = KeyEvent(KeyEvent.ACTION_UP, keycode)
assertThat(underTest.dispatchKeyEvent(actionUpMenuKeyEvent)).isFalse()
verify(shadeController, never()).animateCollapseShadeForced()
- verify(statusBarKeyguardViewManager, never()).showPrimaryBouncer(any())
+ verify(statusBarKeyguardViewManager, never())
+ .showPrimaryBouncer(
+ any(),
+ eq("KeyguardKeyEventInteractor#collapseShadeLockedOrShowPrimaryBouncer"),
+ )
}
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractorTest.kt
index f0eedee48e57..4f351143c793 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractorTest.kt
@@ -340,6 +340,22 @@ class WindowManagerLockscreenVisibilityInteractorTest : SysuiTestCase() {
@Test
@EnableSceneContainer
+ fun surfaceBehindVisibility_whileSceneContainerNotVisible_alwaysTrue() =
+ testScope.runTest {
+ val isSurfaceBehindVisible by collectLastValue(underTest.value.surfaceBehindVisibility)
+ val currentScene by collectLastValue(kosmos.sceneInteractor.currentScene)
+ assertThat(currentScene).isEqualTo(Scenes.Lockscreen)
+ assertThat(isSurfaceBehindVisible).isFalse()
+
+ kosmos.sceneInteractor.setVisible(false, "test")
+ runCurrent()
+
+ assertThat(currentScene).isEqualTo(Scenes.Lockscreen)
+ assertThat(isSurfaceBehindVisible).isTrue()
+ }
+
+ @Test
+ @EnableSceneContainer
fun surfaceBehindVisibility_idleWhileLocked_alwaysFalse() =
testScope.runTest {
val isSurfaceBehindVisible by collectLastValue(underTest.value.surfaceBehindVisibility)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModelTest.kt
index e1323c166f6b..9aee4c97f214 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModelTest.kt
@@ -35,6 +35,7 @@ import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.statusbar.phone.statusBarKeyguardViewManager
import com.android.systemui.testKosmos
import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.eq
import com.google.common.collect.Range
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.test.runTest
@@ -56,7 +57,8 @@ class AlternateBouncerViewModelTest : SysuiTestCase() {
fun onTapped() =
testScope.runTest {
underTest.onTapped()
- verify(statusBarKeyguardViewManager).showPrimaryBouncer(any())
+ verify(statusBarKeyguardViewManager)
+ .showPrimaryBouncer(any(), eq("AlternateBouncerViewModel#onTapped"))
}
@Test
@@ -154,7 +156,7 @@ class AlternateBouncerViewModelTest : SysuiTestCase() {
private fun stepToAlternateBouncer(
value: Float,
- state: TransitionState = TransitionState.RUNNING
+ state: TransitionState = TransitionState.RUNNING,
): TransitionStep {
return step(
from = KeyguardState.LOCKSCREEN,
@@ -166,7 +168,7 @@ class AlternateBouncerViewModelTest : SysuiTestCase() {
private fun stepFromAlternateBouncer(
value: Float,
- state: TransitionState = TransitionState.RUNNING
+ state: TransitionState = TransitionState.RUNNING,
): TransitionStep {
return step(
from = KeyguardState.ALTERNATE_BOUNCER,
@@ -180,14 +182,14 @@ class AlternateBouncerViewModelTest : SysuiTestCase() {
from: KeyguardState,
to: KeyguardState,
value: Float,
- transitionState: TransitionState
+ transitionState: TransitionState,
): TransitionStep {
return TransitionStep(
from = from,
to = to,
value = value,
transitionState = transitionState,
- ownerName = "AlternateBouncerViewModelTest"
+ ownerName = "AlternateBouncerViewModelTest",
)
}
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/model/SysUiStateExtTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/model/SysUiStateExtTest.kt
index d1b552906fbb..5d4de02f9aaa 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/model/SysUiStateExtTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/model/SysUiStateExtTest.kt
@@ -16,7 +16,6 @@
package com.android.systemui.model
-import android.view.Display
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
@@ -35,7 +34,7 @@ class SysUiStateExtTest : SysuiTestCase() {
@Test
fun updateFlags() {
- underTest.updateFlags(Display.DEFAULT_DISPLAY, 1L to true, 2L to false, 4L to true)
+ underTest.updateFlags(1L to true, 2L to false, 4L to true)
assertThat(underTest.flags and 1L).isNotEqualTo(0L)
assertThat(underTest.flags and 2L).isEqualTo(0L)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/EditTileListStateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/EditTileListStateTest.kt
index 8b9ae9a0606d..2dd2f7c0f562 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/EditTileListStateTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/EditTileListStateTest.kt
@@ -23,6 +23,7 @@ import com.android.systemui.SysuiTestCase
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.qs.panels.shared.model.SizedTile
import com.android.systemui.qs.panels.shared.model.SizedTileImpl
+import com.android.systemui.qs.panels.ui.compose.selection.PlacementEvent
import com.android.systemui.qs.panels.ui.model.GridCell
import com.android.systemui.qs.panels.ui.model.TileGridCell
import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel
@@ -108,6 +109,76 @@ class EditTileListStateTest : SysuiTestCase() {
assertThat(underTest.tiles.toStrings()).doesNotContain(TestEditTiles[0].tile.tileSpec.spec)
}
+ @Test
+ fun targetIndexForPlacementToTileSpec_returnsCorrectIndex() {
+ val placementEvent =
+ PlacementEvent.PlaceToTileSpec(
+ movingSpec = TestEditTiles[0].tile.tileSpec,
+ targetSpec = TestEditTiles[3].tile.tileSpec,
+ )
+ val index = underTest.targetIndexForPlacement(placementEvent)
+
+ assertThat(index).isEqualTo(3)
+ }
+
+ @Test
+ fun targetIndexForPlacementToIndex_indexOutOfBounds_returnsCorrectIndex() {
+ val placementEventTooLow =
+ PlacementEvent.PlaceToIndex(
+ movingSpec = TestEditTiles[0].tile.tileSpec,
+ targetIndex = -1,
+ )
+ val index1 = underTest.targetIndexForPlacement(placementEventTooLow)
+
+ assertThat(index1).isEqualTo(0)
+
+ val placementEventTooHigh =
+ PlacementEvent.PlaceToIndex(
+ movingSpec = TestEditTiles[0].tile.tileSpec,
+ targetIndex = 10,
+ )
+ val index2 = underTest.targetIndexForPlacement(placementEventTooHigh)
+ assertThat(index2).isEqualTo(TestEditTiles.size)
+ }
+
+ @Test
+ fun targetIndexForPlacementToIndex_movingBack_returnsCorrectIndex() {
+ /**
+ * With the grid: [ a ] [ b ] [ c ] [ Large D ] [ e ] [ f ]
+ *
+ * Moving 'e' to the spacer at index 3 will result in the tilespec order: a, b, c, e, d, f
+ *
+ * 'e' is now at index 3
+ */
+ val placementEvent =
+ PlacementEvent.PlaceToIndex(
+ movingSpec = TestEditTiles[4].tile.tileSpec,
+ targetIndex = 3,
+ )
+ val index = underTest.targetIndexForPlacement(placementEvent)
+
+ assertThat(index).isEqualTo(3)
+ }
+
+ @Test
+ fun targetIndexForPlacementToIndex_movingForward_returnsCorrectIndex() {
+ /**
+ * With the grid: [ a ] [ b ] [ c ] [ Large D ] [ e ] [ f ]
+ *
+ * Moving '1' to the spacer at index 3 will result in the tilespec order: b, c, a, d, e, f
+ *
+ * 'a' is now at index 2
+ */
+ val placementEvent =
+ PlacementEvent.PlaceToIndex(
+ movingSpec = TestEditTiles[0].tile.tileSpec,
+ targetIndex = 3,
+ )
+ val index = underTest.targetIndexForPlacement(placementEvent)
+
+ assertThat(index).isEqualTo(2)
+ }
+
private fun List<GridCell>.toStrings(): List<String> {
return map {
if (it is TileGridCell) {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/selection/MutableSelectionStateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/selection/MutableSelectionStateTest.kt
index ab217a3f50ef..33ee3379c0d6 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/selection/MutableSelectionStateTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/selection/MutableSelectionStateTest.kt
@@ -19,8 +19,14 @@ package com.android.systemui.qs.panels.ui.compose.selection
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
+import com.android.systemui.qs.panels.ui.compose.selection.TileState.GreyedOut
+import com.android.systemui.qs.panels.ui.compose.selection.TileState.None
+import com.android.systemui.qs.panels.ui.compose.selection.TileState.Placeable
+import com.android.systemui.qs.panels.ui.compose.selection.TileState.Removable
+import com.android.systemui.qs.panels.ui.compose.selection.TileState.Selected
import com.android.systemui.qs.pipeline.shared.TileSpec
import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.junit.runner.RunWith
@@ -45,7 +51,104 @@ class MutableSelectionStateTest : SysuiTestCase() {
assertThat(underTest.selection).isEqualTo(newSpec)
}
+ @Test
+ fun placementModeEnabled_tapOnIndex_sendsCorrectPlacementEvent() {
+ // Tap while in placement mode
+ underTest.enterPlacementMode(TEST_SPEC)
+ underTest.onTap(2)
+
+ assertThat(underTest.placementEnabled).isFalse()
+ val event = underTest.placementEvent as PlacementEvent.PlaceToIndex
+ assertThat(event.movingSpec).isEqualTo(TEST_SPEC)
+ assertThat(event.targetIndex).isEqualTo(2)
+ }
+
+ @Test
+ fun placementModeDisabled_tapOnIndex_doesNotSendPlacementEvent() {
+ // Tap while placement mode is disabled
+ underTest.onTap(2)
+
+ assertThat(underTest.placementEnabled).isFalse()
+ assertThat(underTest.placementEvent).isNull()
+ }
+
+ @Test
+ fun placementModeEnabled_tapOnSelection_exitPlacementMode() {
+ // Tap while in placement mode
+ underTest.enterPlacementMode(TEST_SPEC)
+ underTest.onTap(TEST_SPEC)
+
+ assertThat(underTest.placementEnabled).isFalse()
+ assertThat(underTest.placementEvent).isNull()
+ }
+
+ @Test
+ fun placementModeEnabled_tapOnTileSpec_sendsCorrectPlacementEvent() {
+ // Tap while in placement mode
+ underTest.enterPlacementMode(TEST_SPEC)
+ underTest.onTap(TEST_SPEC_2)
+
+ assertThat(underTest.placementEnabled).isFalse()
+ val event = underTest.placementEvent as PlacementEvent.PlaceToTileSpec
+ assertThat(event.movingSpec).isEqualTo(TEST_SPEC)
+ assertThat(event.targetSpec).isEqualTo(TEST_SPEC_2)
+ }
+
+ @Test
+ fun placementModeDisabled_tapOnSelection_unselect() {
+ // Select the tile and tap on it
+ underTest.select(TEST_SPEC)
+ underTest.onTap(TEST_SPEC)
+
+ assertThat(underTest.placementEnabled).isFalse()
+ assertThat(underTest.selected).isFalse()
+ }
+
+ @Test
+ fun placementModeDisabled_tapOnTile_selects() {
+ // Select a tile but tap a second one
+ underTest.select(TEST_SPEC)
+ underTest.onTap(TEST_SPEC_2)
+
+ assertThat(underTest.placementEnabled).isFalse()
+ assertThat(underTest.selection).isEqualTo(TEST_SPEC_2)
+ }
+
+ @Test
+ fun tileStateFor_selectedTile_returnsSingleSelection() = runTest {
+ underTest.select(TEST_SPEC)
+
+ assertThat(underTest.tileStateFor(TEST_SPEC, None, canShowRemovalBadge = true))
+ .isEqualTo(Selected)
+ assertThat(underTest.tileStateFor(TEST_SPEC_2, None, canShowRemovalBadge = true))
+ .isEqualTo(Removable)
+ assertThat(underTest.tileStateFor(TEST_SPEC_3, None, canShowRemovalBadge = true))
+ .isEqualTo(Removable)
+ }
+
+ @Test
+ fun tileStateFor_placementMode_returnsSinglePlaceable() = runTest {
+ underTest.enterPlacementMode(TEST_SPEC)
+
+ assertThat(underTest.tileStateFor(TEST_SPEC, None, canShowRemovalBadge = true))
+ .isEqualTo(Placeable)
+ assertThat(underTest.tileStateFor(TEST_SPEC_2, None, canShowRemovalBadge = true))
+ .isEqualTo(GreyedOut)
+ assertThat(underTest.tileStateFor(TEST_SPEC_3, None, canShowRemovalBadge = true))
+ .isEqualTo(GreyedOut)
+ }
+
+ @Test
+ fun tileStateFor_nonRemovableTile_returnsNoneState() = runTest {
+ assertThat(underTest.tileStateFor(TEST_SPEC, None, canShowRemovalBadge = true))
+ .isEqualTo(Removable)
+ assertThat(underTest.tileStateFor(TEST_SPEC_2, None, canShowRemovalBadge = false))
+ .isEqualTo(None)
+ }
+
companion object {
private val TEST_SPEC = TileSpec.create("testSpec")
+ private val TEST_SPEC_2 = TileSpec.create("testSpec2")
+ private val TEST_SPEC_3 = TileSpec.create("testSpec3")
}
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/CastTileTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/CastTileTest.java
index 765c5749cd4b..d880aa604849 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/CastTileTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/CastTileTest.java
@@ -50,6 +50,7 @@ import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.qs.QSHost;
import com.android.systemui.qs.QsEventLogger;
import com.android.systemui.qs.logging.QSLogger;
+import com.android.systemui.qs.tiles.dialog.CastDetailsViewModel;
import com.android.systemui.shade.domain.interactor.FakeShadeDialogContextInteractor;
import com.android.systemui.shade.domain.interactor.ShadeDialogContextInteractor;
import com.android.systemui.statusbar.connectivity.IconState;
@@ -63,6 +64,7 @@ import com.android.systemui.statusbar.policy.HotspotController;
import com.android.systemui.statusbar.policy.KeyguardStateController;
import org.junit.After;
+import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -104,6 +106,8 @@ public class CastTileTest extends SysuiTestCase {
private DialogTransitionAnimator mDialogTransitionAnimator;
@Mock
private QsEventLogger mUiEventLogger;
+ @Mock
+ private CastDetailsViewModel.Factory mCastDetailsViewModelFactory;
private final TileJavaAdapter mJavaAdapter = new TileJavaAdapter();
private final FakeConnectivityRepository mConnectivityRepository =
@@ -517,6 +521,29 @@ public class CastTileTest extends SysuiTestCase {
assertTrue(mCastTile.getState().forceExpandIcon);
}
+ @Test
+ public void testDetailsViewUnavailableState_returnsNull() {
+ createAndStartTileNewImpl();
+ mTestableLooper.processAllMessages();
+
+ assertEquals(Tile.STATE_UNAVAILABLE, mCastTile.getState().state);
+ mCastTile.getDetailsViewModel(Assert::assertNull);
+ }
+
+ @Test
+ public void testDetailsViewAvailableState_returnsNotNull() {
+ createAndStartTileNewImpl();
+ CastDevice device = createConnectedCastDevice();
+ List<CastDevice> devices = new ArrayList<>();
+ devices.add(device);
+ when(mController.getCastDevices()).thenReturn(devices);
+ mConnectivityRepository.setWifiConnected(true);
+ mTestableLooper.processAllMessages();
+
+ assertEquals(Tile.STATE_ACTIVE, mCastTile.getState().state);
+ mCastTile.getDetailsViewModel(Assert::assertNotNull);
+ }
+
/**
* For simplicity, let this method still set the field even though that's kind of gross
*/
@@ -540,7 +567,8 @@ public class CastTileTest extends SysuiTestCase {
mConnectivityRepository,
mJavaAdapter,
mFeatureFlags,
- mShadeDialogContextInteractor
+ mShadeDialogContextInteractor,
+ mCastDetailsViewModelFactory
);
mCastTile.initialize();
@@ -584,7 +612,8 @@ public class CastTileTest extends SysuiTestCase {
mConnectivityRepository,
mJavaAdapter,
mFeatureFlags,
- mShadeDialogContextInteractor
+ mShadeDialogContextInteractor,
+ mCastDetailsViewModelFactory
);
mCastTile.initialize();
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/dialog/CastDetailsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/dialog/CastDetailsViewModelTest.kt
new file mode 100644
index 000000000000..468c3dc3be93
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/dialog/CastDetailsViewModelTest.kt
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles.dialog
+
+import android.content.Context
+import android.media.MediaRouter
+import android.provider.Settings
+import android.testing.TestableLooper.RunWithLooper
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.internal.app.MediaRouteDialogPresenter
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.qs.tiles.base.domain.actions.FakeQSTileIntentUserInputHandler
+import com.android.systemui.qs.tiles.base.domain.actions.intentInputs
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.stub
+
+@SmallTest
+@RunWithLooper(setAsMainLooper = true)
+@RunWith(AndroidJUnit4::class)
+class CastDetailsViewModelTest : SysuiTestCase() {
+ var inputHandler: FakeQSTileIntentUserInputHandler = FakeQSTileIntentUserInputHandler()
+ private var context: Context = mock()
+ private var mediaRouter: MediaRouter = mock()
+ private var selectedRoute: MediaRouter.RouteInfo = mock()
+
+ @Test
+ fun testClickOnSettingsButton() {
+ var viewModel = CastDetailsViewModel(inputHandler, context, MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY)
+
+ viewModel.clickOnSettingsButton()
+
+ assertThat(inputHandler.handledInputs).hasSize(1)
+ val intentInput = inputHandler.intentInputs.last()
+ assertThat(intentInput.expandable).isNull()
+ assertThat(intentInput.intent.action).isEqualTo(Settings.ACTION_CAST_SETTINGS)
+ }
+
+ @Test
+ fun testShouldShowChooserDialog() {
+ context.stub {
+ on { getSystemService(MediaRouter::class.java) } doReturn mediaRouter
+ }
+ mediaRouter.stub {
+ on { selectedRoute } doReturn selectedRoute
+ }
+
+ var viewModel =
+ CastDetailsViewModel(inputHandler, context, MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY)
+
+ assertThat(viewModel.shouldShowChooserDialog())
+ .isEqualTo(
+ MediaRouteDialogPresenter.shouldShowChooserDialog(
+ context,
+ MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY,
+ )
+ )
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
index 1743e056b65c..6d4fffdefb1b 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
@@ -22,7 +22,6 @@ import android.os.PowerManager
import android.platform.test.annotations.DisableFlags
import android.platform.test.annotations.EnableFlags
import android.provider.Settings
-import android.view.Display
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.compose.animation.scene.ObservableTransitionState
@@ -1213,15 +1212,15 @@ class SceneContainerStartableTest : SysuiTestCase() {
fakeSceneDataSource.pause()
sceneInteractor.changeScene(sceneKey, "reason")
runCurrent()
- verify(sysUiState, times(index)).commitUpdate(Display.DEFAULT_DISPLAY)
+ verify(sysUiState, times(index)).commitUpdate()
fakeSceneDataSource.unpause(expectedScene = sceneKey)
runCurrent()
- verify(sysUiState, times(index)).commitUpdate(Display.DEFAULT_DISPLAY)
+ verify(sysUiState, times(index)).commitUpdate()
transitionStateFlow.value = ObservableTransitionState.Idle(sceneKey)
runCurrent()
- verify(sysUiState, times(index + 1)).commitUpdate(Display.DEFAULT_DISPLAY)
+ verify(sysUiState, times(index + 1)).commitUpdate()
}
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ShadeControllerSceneImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ShadeControllerSceneImplTest.kt
index 32eec56af8ea..b376558371f3 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ShadeControllerSceneImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ShadeControllerSceneImplTest.kt
@@ -21,6 +21,7 @@ import androidx.test.filters.SmallTest
import com.android.compose.animation.scene.ObservableTransitionState
import com.android.compose.animation.scene.SceneKey
import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor
import com.android.systemui.flags.EnableSceneContainer
import com.android.systemui.flags.Flags
@@ -30,8 +31,10 @@ import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticati
import com.android.systemui.kosmos.testCase
import com.android.systemui.kosmos.testScope
import com.android.systemui.scene.domain.interactor.sceneInteractor
+import com.android.systemui.scene.shared.model.Overlays
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.shade.domain.interactor.ShadeInteractor
+import com.android.systemui.shade.domain.interactor.enableDualShade
import com.android.systemui.shade.domain.interactor.shadeInteractor
import com.android.systemui.statusbar.CommandQueue
import com.android.systemui.testKosmos
@@ -177,6 +180,40 @@ class ShadeControllerSceneImplTest : SysuiTestCase() {
verify(testRunnable, times(1)).run()
}
+ @Test
+ fun instantCollapseShade_notificationShadeHidden() =
+ testScope.runTest {
+ val currentOverlays by collectLastValue(sceneInteractor.currentOverlays)
+ // GIVEN the dual shade configuration with the notification shade overlay visible
+ kosmos.enableDualShade()
+ runCurrent()
+ sceneInteractor.showOverlay(Overlays.NotificationsShade, "test")
+ assertThat(currentOverlays).isEqualTo(setOf(Overlays.NotificationsShade))
+
+ // WHEN shade instantly collapses
+ underTest.instantCollapseShade()
+
+ // THEN overlay was hidden
+ assertThat(currentOverlays).isEmpty()
+ }
+
+ @Test
+ fun instantCollapseShade_qsShadeHidden() =
+ testScope.runTest {
+ val currentOverlays by collectLastValue(sceneInteractor.currentOverlays)
+ // GIVEN the dual shade configuration with the QS shade overlay visible
+ kosmos.enableDualShade()
+ runCurrent()
+ sceneInteractor.showOverlay(Overlays.QuickSettingsShade, "test")
+ assertThat(currentOverlays).isEqualTo(setOf(Overlays.QuickSettingsShade))
+
+ // WHEN shade instantly collapses
+ underTest.instantCollapseShade()
+
+ // THEN overlay was hidden
+ assertThat(currentOverlays).isEmpty()
+ }
+
private fun setScene(key: SceneKey) {
sceneInteractor.changeScene(key, "test")
sceneInteractor.setTransitionState(
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/carrier/ShadeCarrierGroupControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/carrier/ShadeCarrierGroupControllerTest.java
index cd55bb23c1fc..6f1150e5fd96 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/carrier/ShadeCarrierGroupControllerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/carrier/ShadeCarrierGroupControllerTest.java
@@ -46,6 +46,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import com.android.keyguard.CarrierTextManager;
+import com.android.systemui.kairos.KairosNetwork;
import com.android.systemui.log.core.FakeLogBuffer;
import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.statusbar.connectivity.IconState;
@@ -55,6 +56,7 @@ import com.android.systemui.statusbar.connectivity.SignalCallback;
import com.android.systemui.statusbar.connectivity.ui.MobileContextProvider;
import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags;
import com.android.systemui.statusbar.pipeline.mobile.ui.MobileUiAdapter;
+import com.android.systemui.statusbar.pipeline.mobile.ui.MobileUiAdapterKairos;
import com.android.systemui.statusbar.pipeline.mobile.ui.MobileViewLogger;
import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel;
import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.ShadeCarrierGroupMobileIconViewModel;
@@ -63,6 +65,7 @@ import com.android.systemui.util.kotlin.FlowProviderKt;
import com.android.systemui.utils.leaks.LeakCheckedTest;
import com.android.systemui.utils.os.FakeHandler;
+import kotlinx.coroutines.CoroutineScope;
import kotlinx.coroutines.flow.MutableStateFlow;
import org.junit.Before;
@@ -178,8 +181,10 @@ public class ShadeCarrierGroupControllerTest extends LeakCheckedTest {
mSlotIndexResolver,
mMobileUiAdapter,
mMobileContextProvider,
- mStatusBarPipelineFlags
- )
+ mStatusBarPipelineFlags,
+ mock(CoroutineScope.class),
+ mock(KairosNetwork.class),
+ () -> mock(MobileUiAdapterKairos.class))
.setShadeCarrierGroup(mShadeCarrierGroup)
.build();
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/CommandQueueTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/CommandQueueTest.java
index 70df82d95008..c26f18f5ab6d 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/CommandQueueTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/CommandQueueTest.java
@@ -36,7 +36,9 @@ import android.hardware.biometrics.IBiometricSysuiReceiver;
import android.hardware.biometrics.PromptInfo;
import android.hardware.fingerprint.IUdfpsRefreshRateRequestCallback;
import android.os.Bundle;
+import android.os.RemoteException;
import android.platform.test.annotations.EnableFlags;
+import android.util.Pair;
import android.view.KeyEvent;
import android.view.WindowInsets;
import android.view.WindowInsets.Type.InsetsType;
@@ -46,6 +48,7 @@ import android.view.WindowInsetsController.Behavior;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
+import com.android.internal.statusbar.DisableStates;
import com.android.internal.statusbar.LetterboxDetails;
import com.android.internal.statusbar.StatusBarIcon;
import com.android.internal.view.AppearanceRegion;
@@ -58,11 +61,14 @@ import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
+import java.util.HashMap;
+import java.util.Map;
+
@SmallTest
@RunWith(AndroidJUnit4.class)
public class CommandQueueTest extends SysuiTestCase {
- private static final LetterboxDetails[] TEST_LETTERBOX_DETAILS = new LetterboxDetails[] {
+ private static final LetterboxDetails[] TEST_LETTERBOX_DETAILS = new LetterboxDetails[]{
new LetterboxDetails(
/* letterboxInnerBounds= */ new Rect(100, 0, 200, 500),
/* letterboxFullBounds= */ new Rect(0, 0, 500, 100),
@@ -119,6 +125,27 @@ public class CommandQueueTest extends SysuiTestCase {
}
@Test
+ public void testDisableForAllDisplays() throws RemoteException {
+ int state1 = 14;
+ int state2 = 42;
+ int secondaryDisplayState1 = 16;
+ int secondaryDisplayState2 = 44;
+ Map<Integer, Pair<Integer, Integer>> displaysWithStates = new HashMap<>();
+ displaysWithStates.put(DEFAULT_DISPLAY, new Pair<>(state1, state2)); // Example values
+ displaysWithStates.put(SECONDARY_DISPLAY,
+ new Pair<>(secondaryDisplayState1, secondaryDisplayState2)); // Example values
+ DisableStates expectedDisableStates = new DisableStates(displaysWithStates, true);
+
+ mCommandQueue.disableForAllDisplays(expectedDisableStates);
+ waitForIdleSync();
+
+ verify(mCallbacks).disable(eq(DEFAULT_DISPLAY), eq(state1), eq(state2), eq(true));
+ verify(mCallbacks).disable(eq(SECONDARY_DISPLAY), eq(secondaryDisplayState1),
+ eq(secondaryDisplayState2), eq(true));
+ }
+
+
+ @Test
public void testExpandNotifications() {
mCommandQueue.animateExpandNotificationsPanel();
waitForIdleSync();
@@ -475,7 +502,8 @@ public class CommandQueueTest extends SysuiTestCase {
final long requestId = 10;
mCommandQueue.showAuthenticationDialog(promptInfo, receiver, sensorIds,
- credentialAllowed, requireConfirmation, userId, operationId, packageName, requestId);
+ credentialAllowed, requireConfirmation, userId, operationId, packageName,
+ requestId);
waitForIdleSync();
verify(mCallbacks).showAuthenticationDialog(eq(promptInfo), eq(receiver), eq(sensorIds),
eq(credentialAllowed), eq(requireConfirmation), eq(userId), eq(operationId),
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationGroupingUtilTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationGroupingUtilTest.kt
index e04162bf990a..18f25cd4838b 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationGroupingUtilTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationGroupingUtilTest.kt
@@ -16,24 +16,35 @@
package com.android.systemui.statusbar
+import android.app.Notification
+import android.platform.test.annotations.EnableFlags
import android.platform.test.flag.junit.FlagsParameterization
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
+import com.android.systemui.res.R
+import com.android.systemui.statusbar.notification.collection.BundleEntry
+import com.android.systemui.statusbar.notification.collection.EntryAdapterFactory
+import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
import com.android.systemui.statusbar.notification.row.NotificationTestHelper
+import com.android.systemui.statusbar.notification.row.entryAdapterFactory
import com.android.systemui.statusbar.notification.shared.NotificationBundleUi
+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.Mockito.mock
+import org.mockito.Mockito.`when`
import platform.test.runner.parameterized.ParameterizedAndroidJunit4
import platform.test.runner.parameterized.Parameters
@SmallTest
@RunWith(ParameterizedAndroidJunit4::class)
class NotificationGroupingUtilTest(flags: FlagsParameterization) : SysuiTestCase() {
-
+ private val kosmos = testKosmos()
private lateinit var underTest: NotificationGroupingUtil
+ private val factory: EntryAdapterFactory = kosmos.entryAdapterFactory
private lateinit var testHelper: NotificationTestHelper
companion object {
@@ -60,4 +71,55 @@ class NotificationGroupingUtilTest(flags: FlagsParameterization) : SysuiTestCase
underTest = NotificationGroupingUtil(row)
assertThat(underTest.showsTime(row)).isTrue()
}
+
+ @Test
+ fun iconExtractor_extractsSbn_notification() {
+ val row = testHelper.createRow()
+
+ underTest = NotificationGroupingUtil(row)
+
+ assertThat(NotificationGroupingUtil.ICON_EXTRACTOR.extractData(row)).isInstanceOf(
+ Notification::class.java)
+ }
+
+ @Test
+ @EnableFlags(NotificationBundleUi.FLAG_NAME)
+ fun iconExtractor_noException_bundle() {
+ val row = mock(ExpandableNotificationRow::class.java)
+ val be = BundleEntry("promotions")
+ `when`(row.entryAdapter).thenReturn(factory.create(be))
+
+ underTest = NotificationGroupingUtil(row)
+
+ assertThat(NotificationGroupingUtil.ICON_EXTRACTOR.extractData(row)).isNull()
+ }
+
+ @Test
+ fun iconComparator_sameNotificationIcon() {
+ val n1 = NotificationGroupingUtil.ICON_EXTRACTOR.extractData(testHelper.createRow())
+ val n2 = NotificationGroupingUtil.ICON_EXTRACTOR.extractData(testHelper.createRow())
+
+ assertThat(NotificationGroupingUtil.IconComparator().hasSameIcon(n1, n2)).isTrue()
+ }
+
+ @Test
+ fun iconComparator_differentNotificationIcon() {
+ val notif = Notification.Builder(mContext, "").setSmallIcon(R.drawable.ic_menu).build()
+ val n1 = NotificationGroupingUtil.ICON_EXTRACTOR.extractData(testHelper.createRow(notif))
+ val n2 = NotificationGroupingUtil.ICON_EXTRACTOR.extractData(testHelper.createRow())
+
+ assertThat(NotificationGroupingUtil.IconComparator().hasSameIcon(n1, n2)).isFalse()
+ }
+
+ @Test
+ @EnableFlags(NotificationBundleUi.FLAG_NAME)
+ fun iconComparator_bundleNotification() {
+ assertThat(NotificationGroupingUtil.IconComparator().hasSameIcon(null,
+ NotificationGroupingUtil.ICON_EXTRACTOR.extractData(testHelper.createRow()))).isFalse()
+ }
+
+ @Test
+ fun iconComparator_twoBundles() {
+ assertThat(NotificationGroupingUtil.IconComparator().hasSameIcon(null, null)).isFalse()
+ }
} \ No newline at end of file
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationShadeDepthControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationShadeDepthControllerTest.kt
index 3ecf302204bc..67af7a54988e 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationShadeDepthControllerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationShadeDepthControllerTest.kt
@@ -33,6 +33,7 @@ import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
import com.android.systemui.kosmos.testScope
import com.android.systemui.plugins.statusbar.StatusBarStateController
import com.android.systemui.shade.ShadeExpansionChangeEvent
+import com.android.systemui.shade.data.repository.fakeShadeDisplaysRepository
import com.android.systemui.shade.domain.interactor.ShadeModeInteractor
import com.android.systemui.statusbar.phone.BiometricUnlockController
import com.android.systemui.statusbar.phone.DozeParameters
@@ -45,6 +46,8 @@ import com.android.systemui.wallpapers.domain.interactor.WallpaperInteractor
import com.android.systemui.window.domain.interactor.WindowRootViewBlurInteractor
import com.android.wm.shell.appzoomout.AppZoomOut
import com.google.common.truth.Truth.assertThat
+import java.util.Optional
+import java.util.function.Consumer
import org.junit.Before
import org.junit.Rule
import org.junit.Test
@@ -65,8 +68,6 @@ import org.mockito.Mockito.reset
import org.mockito.Mockito.verify
import org.mockito.Mockito.`when`
import org.mockito.junit.MockitoJUnit
-import java.util.Optional
-import java.util.function.Consumer
@RunWith(AndroidJUnit4::class)
@RunWithLooper
@@ -75,6 +76,7 @@ class NotificationShadeDepthControllerTest : SysuiTestCase() {
private val kosmos = testKosmos()
private val applicationScope = kosmos.testScope.backgroundScope
+ private val shadeDisplayRepository = kosmos.fakeShadeDisplaysRepository
@Mock private lateinit var statusBarStateController: StatusBarStateController
@Mock private lateinit var blurUtils: BlurUtils
@Mock private lateinit var biometricUnlockController: BiometricUnlockController
@@ -135,7 +137,8 @@ class NotificationShadeDepthControllerTest : SysuiTestCase() {
windowRootViewBlurInteractor,
appZoomOutOptional,
applicationScope,
- dumpManager
+ dumpManager,
+ { shadeDisplayRepository },
)
notificationShadeDepthController.shadeAnimation = shadeAnimation
notificationShadeDepthController.brightnessMirrorSpring = brightnessSpring
@@ -355,6 +358,36 @@ class NotificationShadeDepthControllerTest : SysuiTestCase() {
}
@Test
+ @EnableFlags(Flags.FLAG_SHADE_WINDOW_GOES_AROUND)
+ fun updateBlurCallback_shadeInExternalDisplay_doesSetZeroZoom() {
+ notificationShadeDepthController.onPanelExpansionChanged(
+ ShadeExpansionChangeEvent(fraction = 1f, expanded = true, tracking = false)
+ )
+ notificationShadeDepthController.addListener(listener)
+ shadeDisplayRepository.setDisplayId(1) // not default display.
+
+ notificationShadeDepthController.updateBlurCallback.doFrame(0)
+
+ verify(wallpaperController).setNotificationShadeZoom(eq(0f))
+ verify(listener).onWallpaperZoomOutChanged(eq(0f))
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_SHADE_WINDOW_GOES_AROUND)
+ fun updateBlurCallback_shadeInDefaultDisplay_doesNotSetZeroZoom() {
+ notificationShadeDepthController.onPanelExpansionChanged(
+ ShadeExpansionChangeEvent(fraction = 1f, expanded = true, tracking = false)
+ )
+ notificationShadeDepthController.addListener(listener)
+ shadeDisplayRepository.setDisplayId(0) // shade is in default display
+
+ notificationShadeDepthController.updateBlurCallback.doFrame(0)
+
+ verify(wallpaperController).setNotificationShadeZoom(floatThat { it != 0f })
+ verify(listener).onWallpaperZoomOutChanged(floatThat { it != 0f })
+ }
+
+ @Test
@DisableFlags(Flags.FLAG_NOTIFICATION_SHADE_BLUR)
fun updateBlurCallback_setsOpaque_whenScrim() {
scrimVisibilityCaptor.value.accept(ScrimController.OPAQUE)
@@ -488,10 +521,10 @@ class NotificationShadeDepthControllerTest : SysuiTestCase() {
}
private fun enableSplitShade() {
- `when` (shadeModeInteractor.isSplitShade).thenReturn(true)
+ `when`(shadeModeInteractor.isSplitShade).thenReturn(true)
}
private fun disableSplitShade() {
- `when` (shadeModeInteractor.isSplitShade).thenReturn(false)
+ `when`(shadeModeInteractor.isSplitShade).thenReturn(false)
}
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt
index 8120c2d9f816..5ec9b601bcca 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt
@@ -44,6 +44,7 @@ import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel
import com.android.systemui.statusbar.chips.ui.view.ChipBackgroundContainer
import com.android.systemui.statusbar.core.StatusBarConnectedDisplays
import com.android.systemui.statusbar.core.StatusBarRootModernization
+import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentBuilder
import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel
import com.android.systemui.statusbar.phone.ongoingcall.DisableChipsModernization
import com.android.systemui.statusbar.phone.ongoingcall.EnableChipsModernization
@@ -106,6 +107,7 @@ class CallChipViewModelTest(flags: FlagsParameterization) : SysuiTestCase() {
assertThat(latest).isInstanceOf(OngoingActivityChipModel.Active.IconOnly::class.java)
assertThat((latest as OngoingActivityChipModel.Active).isHidden).isFalse()
+ assertThat((latest as OngoingActivityChipModel.Active).isImportantForPrivacy).isFalse()
}
@Test
@@ -117,6 +119,7 @@ class CallChipViewModelTest(flags: FlagsParameterization) : SysuiTestCase() {
assertThat(latest).isInstanceOf(OngoingActivityChipModel.Active.IconOnly::class.java)
assertThat((latest as OngoingActivityChipModel.Active).isHidden).isFalse()
+ assertThat((latest as OngoingActivityChipModel.Active).isImportantForPrivacy).isFalse()
}
@Test
@@ -128,6 +131,7 @@ class CallChipViewModelTest(flags: FlagsParameterization) : SysuiTestCase() {
assertThat(latest).isInstanceOf(OngoingActivityChipModel.Active.Timer::class.java)
assertThat((latest as OngoingActivityChipModel.Active).isHidden).isFalse()
+ assertThat((latest as OngoingActivityChipModel.Active).isImportantForPrivacy).isFalse()
}
@Test
@@ -152,6 +156,7 @@ class CallChipViewModelTest(flags: FlagsParameterization) : SysuiTestCase() {
assertThat(latest).isInstanceOf(OngoingActivityChipModel.Active.IconOnly::class.java)
assertThat((latest as OngoingActivityChipModel.Active).isHidden).isTrue()
+ assertThat((latest as OngoingActivityChipModel.Active).isImportantForPrivacy).isFalse()
}
@Test
@@ -176,6 +181,7 @@ class CallChipViewModelTest(flags: FlagsParameterization) : SysuiTestCase() {
assertThat(latest).isInstanceOf(OngoingActivityChipModel.Active.IconOnly::class.java)
assertThat((latest as OngoingActivityChipModel.Active).isHidden).isTrue()
+ assertThat((latest as OngoingActivityChipModel.Active).isImportantForPrivacy).isFalse()
}
@Test
@@ -200,6 +206,7 @@ class CallChipViewModelTest(flags: FlagsParameterization) : SysuiTestCase() {
assertThat(latest).isInstanceOf(OngoingActivityChipModel.Active.Timer::class.java)
assertThat((latest as OngoingActivityChipModel.Active).isHidden).isTrue()
+ assertThat((latest as OngoingActivityChipModel.Active).isImportantForPrivacy).isFalse()
}
@Test
@@ -793,8 +800,8 @@ class CallChipViewModelTest(flags: FlagsParameterization) : SysuiTestCase() {
}
private val PROMOTED_CONTENT_WITH_COLOR =
- PromotedNotificationContentModel.Builder("notif")
- .apply {
+ PromotedNotificationContentBuilder("notif")
+ .applyToShared {
this.colors =
PromotedNotificationContentModel.Colors(
backgroundColor = PROMOTED_BACKGROUND_COLOR,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelTest.kt
index d921ab3b284d..a60e7c19d8bd 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelTest.kt
@@ -135,6 +135,7 @@ class CastToOtherDeviceChipViewModelTest : SysuiTestCase() {
)
assertThat(latest).isInstanceOf(OngoingActivityChipModel.Active.Timer::class.java)
+ assertThat((latest as OngoingActivityChipModel.Active).isImportantForPrivacy).isTrue()
val icon =
(((latest as OngoingActivityChipModel.Active).icon)
as OngoingActivityChipModel.ChipIcon.SingleColorIcon)
@@ -157,6 +158,7 @@ class CastToOtherDeviceChipViewModelTest : SysuiTestCase() {
)
assertThat(latest).isInstanceOf(OngoingActivityChipModel.Active.IconOnly::class.java)
+ assertThat((latest as OngoingActivityChipModel.Active).isImportantForPrivacy).isTrue()
val icon =
(((latest as OngoingActivityChipModel.Active).icon)
as OngoingActivityChipModel.ChipIcon.SingleColorIcon)
@@ -177,6 +179,7 @@ class CastToOtherDeviceChipViewModelTest : SysuiTestCase() {
MediaProjectionState.Projecting.EntireScreen(CAST_TO_OTHER_DEVICES_PACKAGE)
assertThat(latest).isInstanceOf(OngoingActivityChipModel.Active.Timer::class.java)
+ assertThat((latest as OngoingActivityChipModel.Active).isImportantForPrivacy).isTrue()
val icon =
(((latest as OngoingActivityChipModel.Active).icon)
as OngoingActivityChipModel.ChipIcon.SingleColorIcon)
@@ -215,6 +218,7 @@ class CastToOtherDeviceChipViewModelTest : SysuiTestCase() {
)
assertThat(latest).isInstanceOf(OngoingActivityChipModel.Active.IconOnly::class.java)
+ assertThat((latest as OngoingActivityChipModel.Active).isImportantForPrivacy).isTrue()
val icon =
(((latest as OngoingActivityChipModel.Active).icon)
as OngoingActivityChipModel.ChipIcon.SingleColorIcon)
@@ -245,6 +249,7 @@ class CastToOtherDeviceChipViewModelTest : SysuiTestCase() {
// Only the projection info will show a timer
assertThat(latest).isInstanceOf(OngoingActivityChipModel.Active.Timer::class.java)
+ assertThat((latest as OngoingActivityChipModel.Active).isImportantForPrivacy).isTrue()
val icon =
(((latest as OngoingActivityChipModel.Active).icon)
as OngoingActivityChipModel.ChipIcon.SingleColorIcon)
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 7f8f5f43e775..9ce43a0792c2 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
@@ -30,7 +30,7 @@ import com.android.systemui.statusbar.StatusBarIconView
import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips
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.statusbar.notification.promoted.shared.model.PromotedNotificationContentBuilder
import com.android.systemui.testKosmos
import com.android.systemui.util.time.fakeSystemClock
import com.google.common.truth.Truth.assertThat
@@ -420,7 +420,7 @@ class SingleNotificationChipInteractorTest : SysuiTestCase() {
// WHEN the notif gets a new UID that starts as visible
activityManagerRepository.fake.startingIsAppVisibleValue = true
val newPromotedContentBuilder =
- PromotedNotificationContentModel.Builder("notif").apply {
+ PromotedNotificationContentBuilder("notif").applyToShared {
this.shortCriticalText = "Arrived"
}
val newPromotedContent = newPromotedContentBuilder.build()
@@ -452,6 +452,6 @@ class SingleNotificationChipInteractorTest : SysuiTestCase() {
companion object {
private const val UID = 885
- private val PROMOTED_CONTENT = PromotedNotificationContentModel.Builder("notif1").build()
+ private val PROMOTED_CONTENT = PromotedNotificationContentBuilder("notif1").build()
}
}
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 0b9b297130a2..202d5cff95d7 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
@@ -36,7 +36,7 @@ import com.android.systemui.statusbar.notification.data.repository.ActiveNotific
import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository
import com.android.systemui.statusbar.notification.data.repository.addNotif
import com.android.systemui.statusbar.notification.data.repository.removeNotif
-import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel
+import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentBuilder
import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel
import com.android.systemui.statusbar.notification.shared.CallType
import com.android.systemui.testKosmos
@@ -65,7 +65,7 @@ class StatusBarNotificationChipsInteractorTest : SysuiTestCase() {
activeNotificationModel(
key = "notif",
statusBarChipIcon = mock<StatusBarIconView>(),
- promotedContent = PromotedNotificationContentModel.Builder("notif").build(),
+ promotedContent = PromotedNotificationContentBuilder("notif").build(),
)
)
)
@@ -96,7 +96,7 @@ class StatusBarNotificationChipsInteractorTest : SysuiTestCase() {
activeNotificationModel(
key = "notif",
statusBarChipIcon = null,
- promotedContent = PromotedNotificationContentModel.Builder("notif").build(),
+ promotedContent = PromotedNotificationContentBuilder("notif").build(),
)
)
)
@@ -115,7 +115,7 @@ class StatusBarNotificationChipsInteractorTest : SysuiTestCase() {
activeNotificationModel(
key = "notif",
statusBarChipIcon = null,
- promotedContent = PromotedNotificationContentModel.Builder("notif").build(),
+ promotedContent = PromotedNotificationContentBuilder("notif").build(),
)
)
)
@@ -135,7 +135,7 @@ class StatusBarNotificationChipsInteractorTest : SysuiTestCase() {
activeNotificationModel(
key = "notif",
statusBarChipIcon = icon,
- promotedContent = PromotedNotificationContentModel.Builder("notif").build(),
+ promotedContent = PromotedNotificationContentBuilder("notif").build(),
)
)
)
@@ -158,12 +158,12 @@ class StatusBarNotificationChipsInteractorTest : SysuiTestCase() {
activeNotificationModel(
key = "notif1",
statusBarChipIcon = firstIcon,
- promotedContent = PromotedNotificationContentModel.Builder("notif1").build(),
+ promotedContent = PromotedNotificationContentBuilder("notif1").build(),
),
activeNotificationModel(
key = "notif2",
statusBarChipIcon = secondIcon,
- promotedContent = PromotedNotificationContentModel.Builder("notif2").build(),
+ promotedContent = PromotedNotificationContentBuilder("notif2").build(),
),
activeNotificationModel(
key = "notif3",
@@ -195,7 +195,7 @@ class StatusBarNotificationChipsInteractorTest : SysuiTestCase() {
key = "notif",
uid = uid,
statusBarChipIcon = mock<StatusBarIconView>(),
- promotedContent = PromotedNotificationContentModel.Builder("notif1").build(),
+ promotedContent = PromotedNotificationContentBuilder("notif1").build(),
)
)
)
@@ -223,14 +223,14 @@ class StatusBarNotificationChipsInteractorTest : SysuiTestCase() {
key = "promotedNormal",
statusBarChipIcon = mock(),
promotedContent =
- PromotedNotificationContentModel.Builder("promotedNormal").build(),
+ PromotedNotificationContentBuilder("promotedNormal").build(),
callType = CallType.None,
),
activeNotificationModel(
key = "promotedCall",
statusBarChipIcon = mock(),
promotedContent =
- PromotedNotificationContentModel.Builder("promotedCall").build(),
+ PromotedNotificationContentBuilder("promotedCall").build(),
callType = CallType.Ongoing,
),
)
@@ -256,7 +256,7 @@ class StatusBarNotificationChipsInteractorTest : SysuiTestCase() {
activeNotificationModel(
key = "notif",
statusBarChipIcon = firstIcon,
- promotedContent = PromotedNotificationContentModel.Builder("notif").build(),
+ promotedContent = PromotedNotificationContentBuilder("notif").build(),
)
)
)
@@ -269,7 +269,7 @@ class StatusBarNotificationChipsInteractorTest : SysuiTestCase() {
activeNotificationModel(
key = "notif",
statusBarChipIcon = secondIcon,
- promotedContent = PromotedNotificationContentModel.Builder("notif").build(),
+ promotedContent = PromotedNotificationContentBuilder("notif").build(),
)
)
)
@@ -282,7 +282,7 @@ class StatusBarNotificationChipsInteractorTest : SysuiTestCase() {
activeNotificationModel(
key = "notif",
statusBarChipIcon = thirdIcon,
- promotedContent = PromotedNotificationContentModel.Builder("notif").build(),
+ promotedContent = PromotedNotificationContentBuilder("notif").build(),
)
)
)
@@ -302,7 +302,7 @@ class StatusBarNotificationChipsInteractorTest : SysuiTestCase() {
activeNotificationModel(
key = "notif",
statusBarChipIcon = mock(),
- promotedContent = PromotedNotificationContentModel.Builder("notif").build(),
+ promotedContent = PromotedNotificationContentBuilder("notif").build(),
)
)
)
@@ -325,7 +325,7 @@ class StatusBarNotificationChipsInteractorTest : SysuiTestCase() {
activeNotificationModel(
key = "notif",
statusBarChipIcon = mock(),
- promotedContent = PromotedNotificationContentModel.Builder("notif").build(),
+ promotedContent = PromotedNotificationContentBuilder("notif").build(),
)
)
)
@@ -348,7 +348,7 @@ class StatusBarNotificationChipsInteractorTest : SysuiTestCase() {
activeNotificationModel(
key = "notif1",
statusBarChipIcon = firstIcon,
- promotedContent = PromotedNotificationContentModel.Builder("notif1").build(),
+ promotedContent = PromotedNotificationContentBuilder("notif1").build(),
)
setNotifs(listOf(notif1))
assertThat(latest!!.map { it.key }).containsExactly("notif1").inOrder()
@@ -359,7 +359,7 @@ class StatusBarNotificationChipsInteractorTest : SysuiTestCase() {
activeNotificationModel(
key = "notif2",
statusBarChipIcon = secondIcon,
- promotedContent = PromotedNotificationContentModel.Builder("notif2").build(),
+ promotedContent = PromotedNotificationContentBuilder("notif2").build(),
)
setNotifs(listOf(notif1, notif2))
@@ -380,7 +380,7 @@ class StatusBarNotificationChipsInteractorTest : SysuiTestCase() {
// WHEN notif1 gets an update
val notif1NewPromotedContent =
- PromotedNotificationContentModel.Builder("notif1").apply {
+ PromotedNotificationContentBuilder("notif1").applyToShared {
this.shortCriticalText = "Arrived"
}
setNotifs(
@@ -426,8 +426,7 @@ class StatusBarNotificationChipsInteractorTest : SysuiTestCase() {
key = notif1Info.key,
uid = notif1Info.uid,
statusBarChipIcon = notif1Info.icon,
- promotedContent =
- PromotedNotificationContentModel.Builder(notif1Info.key).build(),
+ promotedContent = PromotedNotificationContentBuilder(notif1Info.key).build(),
)
)
activityManagerRepository.fake.setIsAppVisible(notif1Info.uid, isAppVisible = false)
@@ -443,8 +442,7 @@ class StatusBarNotificationChipsInteractorTest : SysuiTestCase() {
key = notif2Info.key,
uid = notif2Info.uid,
statusBarChipIcon = notif2Info.icon,
- promotedContent =
- PromotedNotificationContentModel.Builder(notif2Info.key).build(),
+ promotedContent = PromotedNotificationContentBuilder(notif2Info.key).build(),
)
)
activityManagerRepository.fake.setIsAppVisible(notif2Info.uid, isAppVisible = false)
@@ -482,16 +480,14 @@ class StatusBarNotificationChipsInteractorTest : SysuiTestCase() {
key = notif1Info.key,
uid = notif1Info.uid,
statusBarChipIcon = notif1Info.icon,
- promotedContent =
- PromotedNotificationContentModel.Builder(notif1Info.key).build(),
+ promotedContent = PromotedNotificationContentBuilder(notif1Info.key).build(),
)
val notif2 =
activeNotificationModel(
key = notif2Info.key,
uid = notif2Info.uid,
statusBarChipIcon = notif2Info.icon,
- promotedContent =
- PromotedNotificationContentModel.Builder(notif2Info.key).build(),
+ promotedContent = PromotedNotificationContentBuilder(notif2Info.key).build(),
)
setNotifs(listOf(notif1, notif2))
assertThat(latest!!.map { it.key }).containsExactly("notif1", "notif2").inOrder()
@@ -537,16 +533,14 @@ class StatusBarNotificationChipsInteractorTest : SysuiTestCase() {
key = notif1Info.key,
uid = notif1Info.uid,
statusBarChipIcon = notif1Info.icon,
- promotedContent =
- PromotedNotificationContentModel.Builder(notif1Info.key).build(),
+ promotedContent = PromotedNotificationContentBuilder(notif1Info.key).build(),
)
val notif2 =
activeNotificationModel(
key = notif2Info.key,
uid = notif2Info.uid,
statusBarChipIcon = notif2Info.icon,
- promotedContent =
- PromotedNotificationContentModel.Builder(notif2Info.key).build(),
+ promotedContent = PromotedNotificationContentBuilder(notif2Info.key).build(),
)
setNotifs(listOf(notif1, notif2))
assertThat(latest!!.map { it.key }).containsExactly("notif1", "notif2").inOrder()
@@ -567,8 +561,7 @@ class StatusBarNotificationChipsInteractorTest : SysuiTestCase() {
key = notif3Info.key,
uid = notif3Info.uid,
statusBarChipIcon = notif3Info.icon,
- promotedContent =
- PromotedNotificationContentModel.Builder(notif3Info.key).build(),
+ promotedContent = PromotedNotificationContentBuilder(notif3Info.key).build(),
)
setNotifs(listOf(notif1, notif2, notif3))
@@ -597,8 +590,7 @@ class StatusBarNotificationChipsInteractorTest : SysuiTestCase() {
key = notif1Info.key,
uid = notif1Info.uid,
statusBarChipIcon = notif1Info.icon,
- promotedContent =
- PromotedNotificationContentModel.Builder(notif1Info.key).build(),
+ promotedContent = PromotedNotificationContentBuilder(notif1Info.key).build(),
)
setNotifs(listOf(notif1))
@@ -609,8 +601,7 @@ class StatusBarNotificationChipsInteractorTest : SysuiTestCase() {
key = notif2Info.key,
uid = notif2Info.uid,
statusBarChipIcon = notif2Info.icon,
- promotedContent =
- PromotedNotificationContentModel.Builder(notif2Info.key).build(),
+ promotedContent = PromotedNotificationContentBuilder(notif2Info.key).build(),
)
setNotifs(listOf(notif1, notif2))
@@ -637,7 +628,7 @@ class StatusBarNotificationChipsInteractorTest : SysuiTestCase() {
// WHEN notif2 gets an update
val notif2NewPromotedContent =
- PromotedNotificationContentModel.Builder("notif2").apply {
+ PromotedNotificationContentBuilder("notif2").applyToShared {
this.shortCriticalText = "Arrived"
}
setNotifs(
@@ -662,8 +653,7 @@ class StatusBarNotificationChipsInteractorTest : SysuiTestCase() {
key = notif3Info.key,
uid = notif3Info.uid,
statusBarChipIcon = notif3Info.icon,
- promotedContent =
- PromotedNotificationContentModel.Builder(notif3Info.key).build(),
+ promotedContent = PromotedNotificationContentBuilder(notif3Info.key).build(),
)
setNotifs(listOf(notif1, notif2, notif3))
@@ -710,8 +700,7 @@ class StatusBarNotificationChipsInteractorTest : SysuiTestCase() {
activeNotificationModel(
key = "notif|uid1",
statusBarChipIcon = firstIcon,
- promotedContent =
- PromotedNotificationContentModel.Builder("notif|uid1").build(),
+ promotedContent = PromotedNotificationContentBuilder("notif|uid1").build(),
)
)
)
@@ -725,8 +714,7 @@ class StatusBarNotificationChipsInteractorTest : SysuiTestCase() {
activeNotificationModel(
key = "notif|uid2",
statusBarChipIcon = secondIcon,
- promotedContent =
- PromotedNotificationContentModel.Builder("notif|uid2").build(),
+ promotedContent = PromotedNotificationContentBuilder("notif|uid2").build(),
)
)
)
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 b5cfc7e9080d..eecdbbfd408b 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
@@ -43,6 +43,7 @@ import com.android.systemui.statusbar.notification.data.repository.ActiveNotific
import com.android.systemui.statusbar.notification.data.repository.UnconfinedFakeHeadsUpRowRepository
import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository
import com.android.systemui.statusbar.notification.headsup.PinnedStatus
+import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentBuilder
import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel
import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel.When
import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel
@@ -101,7 +102,7 @@ class NotifChipsViewModelTest : SysuiTestCase() {
activeNotificationModel(
key = "notif",
statusBarChipIcon = null,
- promotedContent = PromotedNotificationContentModel.Builder("notif").build(),
+ promotedContent = PromotedNotificationContentBuilder("notif").build(),
)
)
)
@@ -121,7 +122,7 @@ class NotifChipsViewModelTest : SysuiTestCase() {
activeNotificationModel(
key = "notif",
statusBarChipIcon = null,
- promotedContent = PromotedNotificationContentModel.Builder("notif").build(),
+ promotedContent = PromotedNotificationContentBuilder("notif").build(),
)
)
)
@@ -142,7 +143,7 @@ class NotifChipsViewModelTest : SysuiTestCase() {
key = "notif",
appName = "Fake App Name",
statusBarChipIcon = icon,
- promotedContent = PromotedNotificationContentModel.Builder("notif").build(),
+ promotedContent = PromotedNotificationContentBuilder("notif").build(),
)
)
)
@@ -172,7 +173,7 @@ class NotifChipsViewModelTest : SysuiTestCase() {
key = notifKey,
appName = "Fake App Name",
statusBarChipIcon = null,
- promotedContent = PromotedNotificationContentModel.Builder(notifKey).build(),
+ promotedContent = PromotedNotificationContentBuilder(notifKey).build(),
)
)
)
@@ -195,7 +196,7 @@ class NotifChipsViewModelTest : SysuiTestCase() {
val latest by collectLastValue(underTest.chips)
val promotedContentBuilder =
- PromotedNotificationContentModel.Builder("notif").apply {
+ PromotedNotificationContentBuilder("notif").applyToShared {
this.colors =
PromotedNotificationContentModel.Colors(
backgroundColor = 56,
@@ -229,12 +230,12 @@ class NotifChipsViewModelTest : SysuiTestCase() {
activeNotificationModel(
key = "notif1",
statusBarChipIcon = firstIcon,
- promotedContent = PromotedNotificationContentModel.Builder("notif1").build(),
+ promotedContent = PromotedNotificationContentBuilder("notif1").build(),
),
activeNotificationModel(
key = "notif2",
statusBarChipIcon = secondIcon,
- promotedContent = PromotedNotificationContentModel.Builder("notif2").build(),
+ promotedContent = PromotedNotificationContentBuilder("notif2").build(),
),
activeNotificationModel(
key = "notif3",
@@ -264,13 +265,12 @@ class NotifChipsViewModelTest : SysuiTestCase() {
activeNotificationModel(
key = firstKey,
statusBarChipIcon = null,
- promotedContent = PromotedNotificationContentModel.Builder(firstKey).build(),
+ promotedContent = PromotedNotificationContentBuilder(firstKey).build(),
),
activeNotificationModel(
key = secondKey,
statusBarChipIcon = null,
- promotedContent =
- PromotedNotificationContentModel.Builder(secondKey).build(),
+ promotedContent = PromotedNotificationContentBuilder(secondKey).build(),
),
activeNotificationModel(
key = thirdKey,
@@ -294,7 +294,7 @@ class NotifChipsViewModelTest : SysuiTestCase() {
fakeSystemClock.setCurrentTimeMillis(currentTime)
val promotedContentBuilder =
- PromotedNotificationContentModel.Builder("notif").apply {
+ PromotedNotificationContentBuilder("notif").applyToShared {
this.shortCriticalText = "Arrived"
this.time = When.Time(currentTime + 30.minutes.inWholeMilliseconds)
}
@@ -321,7 +321,7 @@ class NotifChipsViewModelTest : SysuiTestCase() {
val latest by collectLastValue(underTest.chips)
val promotedContentBuilder =
- PromotedNotificationContentModel.Builder("notif").apply { this.time = null }
+ PromotedNotificationContentBuilder("notif").applyToShared { this.time = null }
setNotifs(
listOf(
activeNotificationModel(
@@ -346,7 +346,7 @@ class NotifChipsViewModelTest : SysuiTestCase() {
fakeSystemClock.setCurrentTimeMillis(currentTime)
val promotedContentBuilder =
- PromotedNotificationContentModel.Builder("notif").apply {
+ PromotedNotificationContentBuilder("notif").applyToShared {
this.wasPromotedAutomatically = true
this.time = When.Time(currentTime + 30.minutes.inWholeMilliseconds)
}
@@ -374,7 +374,7 @@ class NotifChipsViewModelTest : SysuiTestCase() {
fakeSystemClock.setCurrentTimeMillis(currentTime)
val promotedContentBuilder =
- PromotedNotificationContentModel.Builder("notif").apply {
+ PromotedNotificationContentBuilder("notif").applyToShared {
this.wasPromotedAutomatically = false
this.time = When.Time(currentTime + 30.minutes.inWholeMilliseconds)
}
@@ -402,7 +402,7 @@ class NotifChipsViewModelTest : SysuiTestCase() {
fakeSystemClock.setCurrentTimeMillis(currentTime)
val promotedContentBuilder =
- PromotedNotificationContentModel.Builder("notif").apply {
+ PromotedNotificationContentBuilder("notif").applyToShared {
this.time = When.Time(currentTime + 13.minutes.inWholeMilliseconds)
}
@@ -430,7 +430,7 @@ class NotifChipsViewModelTest : SysuiTestCase() {
fakeSystemClock.setCurrentTimeMillis(currentTime)
val promotedContentBuilder =
- PromotedNotificationContentModel.Builder("notif").apply {
+ PromotedNotificationContentBuilder("notif").applyToShared {
this.time = When.Time(currentTime + 500)
}
@@ -458,7 +458,7 @@ class NotifChipsViewModelTest : SysuiTestCase() {
fakeSystemClock.setCurrentTimeMillis(currentTime)
val promotedContentBuilder =
- PromotedNotificationContentModel.Builder("notif").apply {
+ PromotedNotificationContentBuilder("notif").applyToShared {
this.time = When.Time(currentTime)
}
@@ -486,7 +486,7 @@ class NotifChipsViewModelTest : SysuiTestCase() {
fakeSystemClock.setCurrentTimeMillis(currentTime)
val promotedContentBuilder =
- PromotedNotificationContentModel.Builder("notif").apply {
+ PromotedNotificationContentBuilder("notif").applyToShared {
this.time = When.Time(currentTime - 2.minutes.inWholeMilliseconds)
}
@@ -515,7 +515,7 @@ class NotifChipsViewModelTest : SysuiTestCase() {
fakeSystemClock.setCurrentTimeMillis(currentTime)
val promotedContentBuilder =
- PromotedNotificationContentModel.Builder("notif").apply {
+ PromotedNotificationContentBuilder("notif").applyToShared {
this.time = When.Time(currentTime + 3.minutes.inWholeMilliseconds)
}
@@ -555,7 +555,7 @@ class NotifChipsViewModelTest : SysuiTestCase() {
val whenElapsed = currentElapsed - 1.minutes.inWholeMilliseconds
val promotedContentBuilder =
- PromotedNotificationContentModel.Builder("notif").apply {
+ PromotedNotificationContentBuilder("notif").applyToShared {
this.time =
When.Chronometer(elapsedRealtimeMillis = whenElapsed, isCountDown = false)
}
@@ -592,7 +592,7 @@ class NotifChipsViewModelTest : SysuiTestCase() {
val whenElapsed = currentElapsed + 10.minutes.inWholeMilliseconds
val promotedContentBuilder =
- PromotedNotificationContentModel.Builder("notif").apply {
+ PromotedNotificationContentBuilder("notif").applyToShared {
this.time =
When.Chronometer(elapsedRealtimeMillis = whenElapsed, isCountDown = true)
}
@@ -623,7 +623,7 @@ class NotifChipsViewModelTest : SysuiTestCase() {
fakeSystemClock.setCurrentTimeMillis(currentTime)
val promotedContentBuilder =
- PromotedNotificationContentModel.Builder("notif").apply {
+ PromotedNotificationContentBuilder("notif").applyToShared {
this.time = When.Time(currentTime + 10.minutes.inWholeMilliseconds)
}
setNotifs(
@@ -653,7 +653,7 @@ class NotifChipsViewModelTest : SysuiTestCase() {
fakeSystemClock.setCurrentTimeMillis(currentTime)
val promotedContentBuilder =
- PromotedNotificationContentModel.Builder("notif").apply {
+ PromotedNotificationContentBuilder("notif").applyToShared {
this.time = When.Time(currentTime + 10.minutes.inWholeMilliseconds)
}
setNotifs(
@@ -690,11 +690,11 @@ class NotifChipsViewModelTest : SysuiTestCase() {
fakeSystemClock.setCurrentTimeMillis(currentTime)
val promotedContentBuilder =
- PromotedNotificationContentModel.Builder("notif").apply {
+ PromotedNotificationContentBuilder("notif").applyToShared {
this.time = When.Time(currentTime + 10.minutes.inWholeMilliseconds)
}
val otherPromotedContentBuilder =
- PromotedNotificationContentModel.Builder("other notif").apply {
+ PromotedNotificationContentBuilder("other notif").applyToShared {
this.time = When.Time(currentTime + 10.minutes.inWholeMilliseconds)
}
val icon = createStatusBarIconViewOrNull()
@@ -738,7 +738,7 @@ class NotifChipsViewModelTest : SysuiTestCase() {
fakeSystemClock.setCurrentTimeMillis(currentTime)
val promotedContentBuilder =
- PromotedNotificationContentModel.Builder("notif").apply {
+ PromotedNotificationContentBuilder("notif").applyToShared {
this.time = When.Time(currentTime + 10.minutes.inWholeMilliseconds)
}
setNotifs(
@@ -781,7 +781,7 @@ class NotifChipsViewModelTest : SysuiTestCase() {
activeNotificationModel(
key,
statusBarChipIcon = createStatusBarIconViewOrNull(),
- promotedContent = PromotedNotificationContentModel.Builder(key).build(),
+ promotedContent = PromotedNotificationContentBuilder(key).build(),
)
)
)
@@ -809,7 +809,7 @@ class NotifChipsViewModelTest : SysuiTestCase() {
activeNotificationModel(
key,
statusBarChipIcon = createStatusBarIconViewOrNull(),
- promotedContent = PromotedNotificationContentModel.Builder(key).build(),
+ promotedContent = PromotedNotificationContentBuilder(key).build(),
)
)
)
@@ -842,6 +842,7 @@ class NotifChipsViewModelTest : SysuiTestCase() {
expectedContentDescriptionSubstrings: List<String> = emptyList(),
) {
val active = latest as OngoingActivityChipModel.Active
+ assertThat(active.isImportantForPrivacy).isFalse()
if (StatusBarConnectedDisplays.isEnabled) {
assertThat(active.icon)
.isInstanceOf(
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractorTest.kt
index 538247e26a6b..f8c57655005e 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractorTest.kt
@@ -87,7 +87,7 @@ class ScreenRecordChipInteractorTest : SysuiTestCase() {
screenRecordRepo.screenRecordState.value = ScreenRecordModel.Recording
mediaProjectionRepo.mediaProjectionState.value = MediaProjectionState.NotProjecting
- assertThat(latest).isEqualTo(ScreenRecordChipModel.Recording(recordedTask = null))
+ assertThat((latest as ScreenRecordChipModel.Recording).recordedTask).isNull()
}
@Test
@@ -99,7 +99,7 @@ class ScreenRecordChipInteractorTest : SysuiTestCase() {
mediaProjectionRepo.mediaProjectionState.value =
MediaProjectionState.Projecting.EntireScreen("host.package")
- assertThat(latest).isEqualTo(ScreenRecordChipModel.Recording(recordedTask = null))
+ assertThat((latest as ScreenRecordChipModel.Recording).recordedTask).isNull()
}
@Test
@@ -116,7 +116,48 @@ class ScreenRecordChipInteractorTest : SysuiTestCase() {
task,
)
- assertThat(latest).isEqualTo(ScreenRecordChipModel.Recording(recordedTask = task))
+ assertThat((latest as ScreenRecordChipModel.Recording).recordedTask).isEqualTo(task)
+ }
+
+ @Test
+ fun screenRecordState_projectionIsNotProjecting_hostPackageNull() =
+ testScope.runTest {
+ val latest by collectLastValue(underTest.screenRecordState)
+
+ screenRecordRepo.screenRecordState.value = ScreenRecordModel.Recording
+ mediaProjectionRepo.mediaProjectionState.value = MediaProjectionState.NotProjecting
+
+ assertThat((latest as ScreenRecordChipModel.Recording).hostPackage).isNull()
+ }
+
+ @Test
+ fun screenRecordState_projectionIsEntireScreen_hostPackageMatches() =
+ testScope.runTest {
+ val latest by collectLastValue(underTest.screenRecordState)
+
+ screenRecordRepo.screenRecordState.value = ScreenRecordModel.Recording
+ mediaProjectionRepo.mediaProjectionState.value =
+ MediaProjectionState.Projecting.EntireScreen(hostPackage = "host.package")
+
+ assertThat((latest as ScreenRecordChipModel.Recording).hostPackage)
+ .isEqualTo("host.package")
+ }
+
+ @Test
+ fun screenRecordState_projectionIsSingleTask_hostPackageMatches() =
+ testScope.runTest {
+ val latest by collectLastValue(underTest.screenRecordState)
+
+ screenRecordRepo.screenRecordState.value = ScreenRecordModel.Recording
+ mediaProjectionRepo.mediaProjectionState.value =
+ MediaProjectionState.Projecting.SingleTask(
+ hostPackage = "host.package",
+ hostDeviceName = null,
+ task = createTask(taskId = 1),
+ )
+
+ assertThat((latest as ScreenRecordChipModel.Recording).hostPackage)
+ .isEqualTo("host.package")
}
@Test
@@ -150,7 +191,7 @@ class ScreenRecordChipInteractorTest : SysuiTestCase() {
advanceTimeBy(901)
// THEN we automatically update to the recording state
- assertThat(latest).isEqualTo(ScreenRecordChipModel.Recording(recordedTask = null))
+ assertThat(latest).isInstanceOf(ScreenRecordChipModel.Recording::class.java)
}
@Test
@@ -175,13 +216,14 @@ class ScreenRecordChipInteractorTest : SysuiTestCase() {
)
// THEN we immediately switch to Recording, and we have the task
- assertThat(latest).isEqualTo(ScreenRecordChipModel.Recording(recordedTask = task))
+ assertThat(latest).isInstanceOf(ScreenRecordChipModel.Recording::class.java)
+ assertThat((latest as ScreenRecordChipModel.Recording).recordedTask).isEqualTo(task)
// WHEN more than 900ms has elapsed
advanceTimeBy(200)
// THEN we still stay in the Recording state and we have the task
- assertThat(latest).isEqualTo(ScreenRecordChipModel.Recording(recordedTask = task))
+ assertThat((latest as ScreenRecordChipModel.Recording).recordedTask).isEqualTo(task)
}
@Test
@@ -247,7 +289,7 @@ class ScreenRecordChipInteractorTest : SysuiTestCase() {
// THEN we *do* auto-start 400ms later
advanceTimeBy(401)
- assertThat(latest).isEqualTo(ScreenRecordChipModel.Recording(recordedTask = null))
+ assertThat(latest).isInstanceOf(ScreenRecordChipModel.Recording::class.java)
}
@Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelTest.kt
index 005af366a6c0..91942213ce75 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelTest.kt
@@ -110,6 +110,7 @@ class ScreenRecordChipViewModelTest : SysuiTestCase() {
screenRecordRepo.screenRecordState.value = ScreenRecordModel.Starting(400)
assertThat(latest).isInstanceOf(OngoingActivityChipModel.Active.Countdown::class.java)
+ assertThat((latest as OngoingActivityChipModel.Active).isImportantForPrivacy).isTrue()
assertThat((latest as OngoingActivityChipModel.Active).icon).isNull()
assertThat((latest as OngoingActivityChipModel.Active).onClickListenerLegacy).isNull()
}
@@ -156,6 +157,7 @@ class ScreenRecordChipViewModelTest : SysuiTestCase() {
screenRecordRepo.screenRecordState.value = ScreenRecordModel.Recording
assertThat(latest).isInstanceOf(OngoingActivityChipModel.Active.Timer::class.java)
+ assertThat((latest as OngoingActivityChipModel.Active).isImportantForPrivacy).isTrue()
val icon =
(((latest as OngoingActivityChipModel.Active).icon)
as OngoingActivityChipModel.ChipIcon.SingleColorIcon)
@@ -261,6 +263,7 @@ class ScreenRecordChipViewModelTest : SysuiTestCase() {
mediaProjectionRepo.mediaProjectionState.value = MediaProjectionState.NotProjecting
assertThat(latest).isInstanceOf(OngoingActivityChipModel.Active::class.java)
+ assertThat((latest as OngoingActivityChipModel.Active).isImportantForPrivacy).isTrue()
assertThat((latest as OngoingActivityChipModel.Active.Timer).startTimeMs)
.isEqualTo(1234)
@@ -275,6 +278,7 @@ class ScreenRecordChipViewModelTest : SysuiTestCase() {
// THEN the start time is still the old start time
assertThat(latest).isInstanceOf(OngoingActivityChipModel.Active::class.java)
+ assertThat((latest as OngoingActivityChipModel.Active).isImportantForPrivacy).isTrue()
assertThat((latest as OngoingActivityChipModel.Active.Timer).startTimeMs)
.isEqualTo(1234)
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelTest.kt
index d6b10a89726e..99e378c78ee2 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelTest.kt
@@ -384,6 +384,7 @@ class ShareToAppChipViewModelTest : SysuiTestCase() {
MediaProjectionState.Projecting.NoScreen(NORMAL_PACKAGE, hostDeviceName = null)
assertThat(latest).isInstanceOf(OngoingActivityChipModel.Active.IconOnly::class.java)
+ assertThat((latest as OngoingActivityChipModel.Active).isImportantForPrivacy).isTrue()
val icon =
(((latest as OngoingActivityChipModel.Active).icon)
as OngoingActivityChipModel.ChipIcon.SingleColorIcon)
@@ -407,6 +408,7 @@ class ShareToAppChipViewModelTest : SysuiTestCase() {
)
assertThat(latest).isInstanceOf(OngoingActivityChipModel.Active.Timer::class.java)
+ assertThat((latest as OngoingActivityChipModel.Active).isImportantForPrivacy).isTrue()
val icon =
(((latest as OngoingActivityChipModel.Active).icon)
as OngoingActivityChipModel.ChipIcon.SingleColorIcon)
@@ -424,6 +426,7 @@ class ShareToAppChipViewModelTest : SysuiTestCase() {
MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE)
assertThat(latest).isInstanceOf(OngoingActivityChipModel.Active.Timer::class.java)
+ assertThat((latest as OngoingActivityChipModel.Active).isImportantForPrivacy).isTrue()
val icon =
(((latest as OngoingActivityChipModel.Active).icon)
as OngoingActivityChipModel.ChipIcon.SingleColorIcon)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt
index 7135cf01394a..83b3c9cae3c1 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt
@@ -63,7 +63,7 @@ import com.android.systemui.statusbar.notification.data.repository.activeNotific
import com.android.systemui.statusbar.notification.data.repository.addNotif
import com.android.systemui.statusbar.notification.data.repository.addNotifs
import com.android.systemui.statusbar.notification.data.repository.removeNotif
-import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel
+import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentBuilder
import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel
import com.android.systemui.statusbar.phone.SystemUIDialog
import com.android.systemui.statusbar.phone.ongoingcall.DisableChipsModernization
@@ -358,7 +358,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
addOngoingCallState(key = "call")
val promotedContentBuilder =
- PromotedNotificationContentModel.Builder("notif").apply {
+ PromotedNotificationContentBuilder("notif").applyToShared {
this.shortCriticalText = "Some text here"
}
activeNotificationListRepository.addNotif(
@@ -741,7 +741,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
activeNotificationModel(
key = "notif",
statusBarChipIcon = icon,
- promotedContent = PromotedNotificationContentModel.Builder("notif").build(),
+ promotedContent = PromotedNotificationContentBuilder("notif").build(),
)
)
)
@@ -765,7 +765,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
activeNotificationModel(
key = "notif",
statusBarChipIcon = icon,
- promotedContent = PromotedNotificationContentModel.Builder("notif").build(),
+ promotedContent = PromotedNotificationContentBuilder("notif").build(),
)
)
)
@@ -791,14 +791,12 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
activeNotificationModel(
key = "firstNotif",
statusBarChipIcon = firstIcon,
- promotedContent =
- PromotedNotificationContentModel.Builder("firstNotif").build(),
+ promotedContent = PromotedNotificationContentBuilder("firstNotif").build(),
),
activeNotificationModel(
key = "secondNotif",
statusBarChipIcon = secondIcon,
- promotedContent =
- PromotedNotificationContentModel.Builder("secondNotif").build(),
+ promotedContent = PromotedNotificationContentBuilder("secondNotif").build(),
),
)
)
@@ -822,14 +820,12 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
activeNotificationModel(
key = "firstNotif",
statusBarChipIcon = firstIcon,
- promotedContent =
- PromotedNotificationContentModel.Builder("firstNotif").build(),
+ promotedContent = PromotedNotificationContentBuilder("firstNotif").build(),
),
activeNotificationModel(
key = "secondNotif",
statusBarChipIcon = secondIcon,
- promotedContent =
- PromotedNotificationContentModel.Builder("secondNotif").build(),
+ promotedContent = PromotedNotificationContentBuilder("secondNotif").build(),
),
)
)
@@ -857,20 +853,17 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
activeNotificationModel(
key = "firstNotif",
statusBarChipIcon = firstIcon,
- promotedContent =
- PromotedNotificationContentModel.Builder("firstNotif").build(),
+ promotedContent = PromotedNotificationContentBuilder("firstNotif").build(),
),
activeNotificationModel(
key = "secondNotif",
statusBarChipIcon = secondIcon,
- promotedContent =
- PromotedNotificationContentModel.Builder("secondNotif").build(),
+ promotedContent = PromotedNotificationContentBuilder("secondNotif").build(),
),
activeNotificationModel(
key = "thirdNotif",
statusBarChipIcon = thirdIcon,
- promotedContent =
- PromotedNotificationContentModel.Builder("thirdNotif").build(),
+ promotedContent = PromotedNotificationContentBuilder("thirdNotif").build(),
),
)
)
@@ -896,26 +889,22 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
activeNotificationModel(
key = "firstNotif",
statusBarChipIcon = firstIcon,
- promotedContent =
- PromotedNotificationContentModel.Builder("firstNotif").build(),
+ promotedContent = PromotedNotificationContentBuilder("firstNotif").build(),
),
activeNotificationModel(
key = "secondNotif",
statusBarChipIcon = secondIcon,
- promotedContent =
- PromotedNotificationContentModel.Builder("secondNotif").build(),
+ promotedContent = PromotedNotificationContentBuilder("secondNotif").build(),
),
activeNotificationModel(
key = "thirdNotif",
statusBarChipIcon = thirdIcon,
- promotedContent =
- PromotedNotificationContentModel.Builder("thirdNotif").build(),
+ promotedContent = PromotedNotificationContentBuilder("thirdNotif").build(),
),
activeNotificationModel(
key = "fourthNotif",
statusBarChipIcon = fourthIcon,
- promotedContent =
- PromotedNotificationContentModel.Builder("fourthNotif").build(),
+ promotedContent = PromotedNotificationContentBuilder("fourthNotif").build(),
),
)
)
@@ -941,20 +930,17 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
activeNotificationModel(
key = "firstNotif",
statusBarChipIcon = createStatusBarIconViewOrNull(),
- promotedContent =
- PromotedNotificationContentModel.Builder("firstNotif").build(),
+ promotedContent = PromotedNotificationContentBuilder("firstNotif").build(),
),
activeNotificationModel(
key = "secondNotif",
statusBarChipIcon = createStatusBarIconViewOrNull(),
- promotedContent =
- PromotedNotificationContentModel.Builder("secondNotif").build(),
+ promotedContent = PromotedNotificationContentBuilder("secondNotif").build(),
),
activeNotificationModel(
key = "thirdNotif",
statusBarChipIcon = createStatusBarIconViewOrNull(),
- promotedContent =
- PromotedNotificationContentModel.Builder("thirdNotif").build(),
+ promotedContent = PromotedNotificationContentBuilder("thirdNotif").build(),
),
)
)
@@ -973,26 +959,22 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
activeNotificationModel(
key = "firstNotif",
statusBarChipIcon = createStatusBarIconViewOrNull(),
- promotedContent =
- PromotedNotificationContentModel.Builder("firstNotif").build(),
+ promotedContent = PromotedNotificationContentBuilder("firstNotif").build(),
),
activeNotificationModel(
key = "secondNotif",
statusBarChipIcon = createStatusBarIconViewOrNull(),
- promotedContent =
- PromotedNotificationContentModel.Builder("secondNotif").build(),
+ promotedContent = PromotedNotificationContentBuilder("secondNotif").build(),
),
activeNotificationModel(
key = "thirdNotif",
statusBarChipIcon = createStatusBarIconViewOrNull(),
- promotedContent =
- PromotedNotificationContentModel.Builder("thirdNotif").build(),
+ promotedContent = PromotedNotificationContentBuilder("thirdNotif").build(),
),
activeNotificationModel(
key = "fourthNotif",
statusBarChipIcon = createStatusBarIconViewOrNull(),
- promotedContent =
- PromotedNotificationContentModel.Builder("fourthNotif").build(),
+ promotedContent = PromotedNotificationContentBuilder("fourthNotif").build(),
),
)
)
@@ -1016,14 +998,12 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
activeNotificationModel(
key = "firstNotif",
statusBarChipIcon = firstIcon,
- promotedContent =
- PromotedNotificationContentModel.Builder("firstNotif").build(),
+ promotedContent = PromotedNotificationContentBuilder("firstNotif").build(),
),
activeNotificationModel(
key = "secondNotif",
statusBarChipIcon = createStatusBarIconViewOrNull(),
- promotedContent =
- PromotedNotificationContentModel.Builder("secondNotif").build(),
+ promotedContent = PromotedNotificationContentBuilder("secondNotif").build(),
),
)
)
@@ -1050,20 +1030,17 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
activeNotificationModel(
key = "firstNotif",
statusBarChipIcon = firstIcon,
- promotedContent =
- PromotedNotificationContentModel.Builder("firstNotif").build(),
+ promotedContent = PromotedNotificationContentBuilder("firstNotif").build(),
),
activeNotificationModel(
key = "secondNotif",
statusBarChipIcon = secondIcon,
- promotedContent =
- PromotedNotificationContentModel.Builder("secondNotif").build(),
+ promotedContent = PromotedNotificationContentBuilder("secondNotif").build(),
),
activeNotificationModel(
key = "thirdNotif",
statusBarChipIcon = thirdIcon,
- promotedContent =
- PromotedNotificationContentModel.Builder("thirdNotif").build(),
+ promotedContent = PromotedNotificationContentBuilder("thirdNotif").build(),
),
)
)
@@ -1092,7 +1069,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
activeNotificationModel(
key = "notif",
statusBarChipIcon = createStatusBarIconViewOrNull(),
- promotedContent = PromotedNotificationContentModel.Builder("notif").build(),
+ promotedContent = PromotedNotificationContentBuilder("notif").build(),
)
)
@@ -1114,14 +1091,14 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
activeNotificationModel(
key = "notif1",
statusBarChipIcon = createStatusBarIconViewOrNull(),
- promotedContent = PromotedNotificationContentModel.Builder("notif1").build(),
+ promotedContent = PromotedNotificationContentBuilder("notif1").build(),
)
)
activeNotificationListRepository.addNotif(
activeNotificationModel(
key = "notif2",
statusBarChipIcon = createStatusBarIconViewOrNull(),
- promotedContent = PromotedNotificationContentModel.Builder("notif2").build(),
+ promotedContent = PromotedNotificationContentBuilder("notif2").build(),
)
)
@@ -1143,14 +1120,14 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
activeNotificationModel(
key = "notif1",
statusBarChipIcon = createStatusBarIconViewOrNull(),
- promotedContent = PromotedNotificationContentModel.Builder("notif1").build(),
+ promotedContent = PromotedNotificationContentBuilder("notif1").build(),
)
)
activeNotificationListRepository.addNotif(
activeNotificationModel(
key = "notif2",
statusBarChipIcon = createStatusBarIconViewOrNull(),
- promotedContent = PromotedNotificationContentModel.Builder("notif2").build(),
+ promotedContent = PromotedNotificationContentBuilder("notif2").build(),
)
)
@@ -1159,6 +1136,11 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
.inOrder()
}
+ // The ranking between different chips should stay consistent between
+ // OngoingActivityChipsViewModel and PromotedNotificationsInteractor.
+ // Make sure to also change
+ // PromotedNotificationsInteractorTest#orderedChipNotificationKeys_rankingIsCorrect
+ // if you change this test.
@EnableChipsModernization
@Test
fun chips_screenRecordAndCallAndPromotedNotifs_secondNotifInOverflow() =
@@ -1173,7 +1155,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
activeNotificationModel(
key = "notif",
statusBarChipIcon = notifIcon,
- promotedContent = PromotedNotificationContentModel.Builder("notif").build(),
+ promotedContent = PromotedNotificationContentBuilder("notif").build(),
)
)
addOngoingCallState(key = callNotificationKey)
@@ -1184,7 +1166,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
activeNotificationModel(
key = "notif2",
statusBarChipIcon = notifIcon2,
- promotedContent = PromotedNotificationContentModel.Builder("notif2").build(),
+ promotedContent = PromotedNotificationContentBuilder("notif2").build(),
)
)
@@ -1209,7 +1191,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
activeNotificationModel(
key = "notif",
statusBarChipIcon = notifIcon,
- promotedContent = PromotedNotificationContentModel.Builder("notif").build(),
+ promotedContent = PromotedNotificationContentBuilder("notif").build(),
)
)
)
@@ -1260,7 +1242,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
activeNotificationModel(
key = "notif",
statusBarChipIcon = notifIcon,
- promotedContent = PromotedNotificationContentModel.Builder("notif").build(),
+ promotedContent = PromotedNotificationContentBuilder("notif").build(),
)
)
@@ -1299,7 +1281,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
activeNotificationModel(
key = "notif",
statusBarChipIcon = notifIcon,
- promotedContent = PromotedNotificationContentModel.Builder("notif").build(),
+ promotedContent = PromotedNotificationContentBuilder("notif").build(),
)
)
// And everything else hidden
@@ -1377,7 +1359,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
activeNotificationModel(
key = "notif1",
statusBarChipIcon = notif1Icon,
- promotedContent = PromotedNotificationContentModel.Builder("notif1").build(),
+ promotedContent = PromotedNotificationContentBuilder("notif1").build(),
)
)
)
@@ -1431,7 +1413,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
activeNotificationModel(
key = "notif2",
statusBarChipIcon = notif2Icon,
- promotedContent = PromotedNotificationContentModel.Builder("notif2").build(),
+ promotedContent = PromotedNotificationContentBuilder("notif2").build(),
)
)
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 ba2d40ba6a0d..3d7ced747450 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
@@ -16,6 +16,7 @@
package com.android.systemui.statusbar.notification.collection.coordinator
import android.app.Notification
+import android.app.Notification.FLAG_FOREGROUND_SERVICE
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Person
@@ -31,6 +32,10 @@ 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.mediaprojection.data.model.MediaProjectionState
+import com.android.systemui.mediaprojection.data.repository.fakeMediaProjectionRepository
+import com.android.systemui.screenrecord.data.model.ScreenRecordModel
+import com.android.systemui.screenrecord.data.repository.screenRecordRepository
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
@@ -169,6 +174,87 @@ class ColorizedFgsCoordinatorTest : SysuiTestCase() {
@Test
@EnableFlags(PromotedNotificationUi.FLAG_NAME)
+ fun testIncludeScreenRecordNotifInSection_importanceDefault() =
+ kosmos.runTest {
+ // GIVEN a screen record event + screen record notif that has a status bar chip
+ screenRecordRepository.screenRecordState.value = ScreenRecordModel.Recording
+ fakeMediaProjectionRepository.mediaProjectionState.value =
+ MediaProjectionState.Projecting.EntireScreen(hostPackage = "test_pkg")
+ val screenRecordEntry =
+ buildNotificationEntry(tag = "screenRecord", promoted = false) {
+ setImportance(NotificationManager.IMPORTANCE_DEFAULT)
+ setFlag(context, FLAG_FOREGROUND_SERVICE, true)
+ }
+
+ renderNotificationListInteractor.setRenderedList(listOf(screenRecordEntry))
+
+ val orderedChipNotificationKeys by
+ collectLastValue(promotedNotificationsInteractor.orderedChipNotificationKeys)
+
+ assertThat(orderedChipNotificationKeys)
+ .containsExactly("0|test_pkg|0|screenRecord|0")
+ .inOrder()
+
+ // THEN the entry is in the fgs section
+ assertTrue(sectioner.isInSection(screenRecordEntry))
+ }
+
+ @Test
+ @EnableFlags(PromotedNotificationUi.FLAG_NAME)
+ fun testDiscludeScreenRecordNotifInSection_importanceMin() =
+ kosmos.runTest {
+ // GIVEN a screen record event + screen record notif that has a status bar chip
+ screenRecordRepository.screenRecordState.value = ScreenRecordModel.Recording
+ fakeMediaProjectionRepository.mediaProjectionState.value =
+ MediaProjectionState.Projecting.EntireScreen(hostPackage = "test_pkg")
+ val screenRecordEntry =
+ buildNotificationEntry(tag = "screenRecord", promoted = false) {
+ setImportance(NotificationManager.IMPORTANCE_MIN)
+ setFlag(context, FLAG_FOREGROUND_SERVICE, true)
+ }
+
+ renderNotificationListInteractor.setRenderedList(listOf(screenRecordEntry))
+
+ val orderedChipNotificationKeys by
+ collectLastValue(promotedNotificationsInteractor.orderedChipNotificationKeys)
+
+ assertThat(orderedChipNotificationKeys)
+ .containsExactly("0|test_pkg|0|screenRecord|0")
+ .inOrder()
+
+ // THEN the entry is NOT in the fgs section
+ assertFalse(sectioner.isInSection(screenRecordEntry))
+ }
+
+ @Test
+ @DisableFlags(PromotedNotificationUi.FLAG_NAME)
+ fun testDiscludeScreenRecordNotifInSection_flagDisabled() =
+ kosmos.runTest {
+ // GIVEN a screen record event + screen record notif that has a status bar chip
+ screenRecordRepository.screenRecordState.value = ScreenRecordModel.Recording
+ fakeMediaProjectionRepository.mediaProjectionState.value =
+ MediaProjectionState.Projecting.EntireScreen(hostPackage = "test_pkg")
+ val screenRecordEntry =
+ buildNotificationEntry(tag = "screenRecord", promoted = false) {
+ setImportance(NotificationManager.IMPORTANCE_DEFAULT)
+ setFlag(context, FLAG_FOREGROUND_SERVICE, true)
+ }
+
+ renderNotificationListInteractor.setRenderedList(listOf(screenRecordEntry))
+
+ val orderedChipNotificationKeys by
+ collectLastValue(promotedNotificationsInteractor.orderedChipNotificationKeys)
+
+ assertThat(orderedChipNotificationKeys)
+ .containsExactly("0|test_pkg|0|screenRecord|0")
+ .inOrder()
+
+ // THEN the entry is NOT in the fgs section
+ assertFalse(sectioner.isInSection(screenRecordEntry))
+ }
+
+ @Test
+ @EnableFlags(PromotedNotificationUi.FLAG_NAME)
fun promoterSelectsPromotedOngoing_flagEnabled() {
val promoter: NotifPromoter = withArgCaptor { verify(notifPipeline).addPromoter(capture()) }
@@ -234,8 +320,4 @@ class ColorizedFgsCoordinatorTest : SysuiTestCase() {
val person = Person.Builder().setName("person").build()
return Notification.CallStyle.forOngoingCall(person, pendingIntent)
}
-
- companion object {
- private const val NOTIF_USER_ID = 0
- }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractorTest.kt
index d3befa921e9e..29bb29f25797 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractorTest.kt
@@ -29,7 +29,7 @@ import com.android.systemui.statusbar.notification.data.model.activeNotification
import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationsStore
import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository
import com.android.systemui.statusbar.notification.data.repository.setActiveNotifs
-import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel
+import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentBuilder
import com.android.systemui.statusbar.notification.shared.CallType
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
@@ -170,7 +170,7 @@ class ActiveNotificationsInteractorTest : SysuiTestCase() {
val promoted1 =
activeNotificationModel(
key = "notif1",
- promotedContent = PromotedNotificationContentModel.Builder("notif1").build(),
+ promotedContent = PromotedNotificationContentBuilder("notif1").build(),
)
val notPromoted2 = activeNotificationModel(key = "notif2", promotedContent = null)
@@ -208,14 +208,14 @@ class ActiveNotificationsInteractorTest : SysuiTestCase() {
val promoted1 =
activeNotificationModel(
key = "notif1",
- promotedContent = PromotedNotificationContentModel.Builder("notif1").build(),
+ promotedContent = PromotedNotificationContentBuilder("notif1").build(),
)
val notPromoted2 = activeNotificationModel(key = "notif2", promotedContent = null)
val notPromoted3 = activeNotificationModel(key = "notif3", promotedContent = null)
val promoted4 =
activeNotificationModel(
key = "notif4",
- promotedContent = PromotedNotificationContentModel.Builder("notif4").build(),
+ promotedContent = PromotedNotificationContentBuilder("notif4").build(),
)
activeNotificationListRepository.activeNotifications.value =
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationsListInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationsListInteractorTest.kt
index 35b19c19d5ce..5c749e6e35d6 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationsListInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationsListInteractorTest.kt
@@ -28,7 +28,8 @@ import com.android.systemui.statusbar.notification.collection.GroupEntry
import com.android.systemui.statusbar.notification.collection.NotificationEntry
import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository
import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUi
-import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel
+import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentBuilder
+import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModels
import com.android.systemui.statusbar.notification.shared.byKey
import com.android.systemui.testKosmos
import com.android.systemui.util.mockito.mock
@@ -130,7 +131,7 @@ class RenderNotificationsListInteractorTest : SysuiTestCase() {
val promoted2 =
mockNotificationEntry(
"key2",
- promotedContent = PromotedNotificationContentModel.Builder("key2").build(),
+ promotedContent = PromotedNotificationContentBuilder("key2").build(),
)
underTest.setRenderedList(listOf(notPromoted1, promoted2))
@@ -149,7 +150,7 @@ class RenderNotificationsListInteractorTest : SysuiTestCase() {
private fun mockNotificationEntry(
key: String,
rank: Int = 0,
- promotedContent: PromotedNotificationContentModel? = null,
+ promotedContent: PromotedNotificationContentModels? = null,
): NotificationEntry {
val nBuilder = Notification.Builder(context, "a")
val notification = nBuilder.build()
@@ -165,7 +166,7 @@ class RenderNotificationsListInteractorTest : SysuiTestCase() {
whenever(this.representativeEntry).thenReturn(this)
whenever(this.ranking).thenReturn(RankingBuilder().setRank(rank).build())
whenever(this.sbn).thenReturn(mockSbn)
- whenever(this.promotedNotificationContentModel).thenReturn(promotedContent)
+ whenever(this.promotedNotificationContentModels).thenReturn(promotedContent)
}
}
}
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 ee698ae20adb..41120a16e4ea 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
@@ -33,8 +33,10 @@ import com.android.systemui.statusbar.notification.data.repository.notifications
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.PromotedNotificationContentBuilder
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.promoted.shared.model.PromotedNotificationContentModels
import com.android.systemui.statusbar.notification.shared.byIsAmbient
import com.android.systemui.statusbar.notification.shared.byIsLastMessageFromReply
import com.android.systemui.statusbar.notification.shared.byIsPromoted
@@ -354,6 +356,6 @@ private val testIcons =
private fun promotedContent(
key: String,
style: PromotedNotificationContentModel.Style,
-): PromotedNotificationContentModel {
- return PromotedNotificationContentModel.Builder(key).apply { this.style = style }.build()
+): PromotedNotificationContentModels {
+ return PromotedNotificationContentBuilder(key).applyToShared { this.style = style }.build()
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractorImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractorImplTest.kt
index 0ac944a43de6..cc016b9768b7 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractorImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractorImplTest.kt
@@ -33,18 +33,21 @@ 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.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_NONE
+import com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_PUBLIC
import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips
import com.android.systemui.statusbar.notification.collection.NotificationEntry
import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
import com.android.systemui.statusbar.notification.promoted.AutomaticPromotionCoordinator.Companion.EXTRA_WAS_AUTOMATICALLY_PROMOTED
-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.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel.When
+import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModels
import com.android.systemui.statusbar.notification.row.RowImageInflater
import com.android.systemui.testKosmos
import com.android.systemui.util.time.fakeSystemClock
import com.android.systemui.util.time.systemClock
import com.google.common.truth.Truth.assertThat
+import kotlin.test.assertNotNull
import kotlin.time.Duration
import kotlin.time.Duration.Companion.minutes
import org.junit.Test
@@ -112,12 +115,43 @@ class PromotedNotificationContentExtractorImplTest : SysuiTestCase() {
setContentText(TEST_CONTENT_TEXT)
}
- val content = extractContent(entry)
+ val content = requireContent(entry)
- assertThat(content).isNotNull()
- assertThat(content?.subText).isEqualTo(TEST_SUB_TEXT)
- assertThat(content?.title).isEqualTo(TEST_CONTENT_TITLE)
- assertThat(content?.text).isEqualTo(TEST_CONTENT_TEXT)
+ content.privateVersion.apply {
+ assertThat(subText).isEqualTo(TEST_SUB_TEXT)
+ assertThat(title).isEqualTo(TEST_CONTENT_TITLE)
+ assertThat(text).isEqualTo(TEST_CONTENT_TEXT)
+ }
+
+ content.publicVersion.apply {
+ assertThat(subText).isNull()
+ assertThat(title).isNull()
+ assertThat(text).isNull()
+ }
+ }
+
+ @Test
+ @EnableFlags(PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME)
+ fun extractsContent_commonFields_noRedaction() {
+ val entry = createEntry {
+ setSubText(TEST_SUB_TEXT)
+ setContentTitle(TEST_CONTENT_TITLE)
+ setContentText(TEST_CONTENT_TEXT)
+ }
+
+ val content = requireContent(entry, redactionType = REDACTION_TYPE_NONE)
+
+ content.privateVersion.apply {
+ assertThat(subText).isEqualTo(TEST_SUB_TEXT)
+ assertThat(title).isEqualTo(TEST_CONTENT_TITLE)
+ assertThat(text).isEqualTo(TEST_CONTENT_TEXT)
+ }
+
+ content.publicVersion.apply {
+ assertThat(subText).isEqualTo(TEST_SUB_TEXT)
+ assertThat(title).isEqualTo(TEST_CONTENT_TITLE)
+ assertThat(text).isEqualTo(TEST_CONTENT_TEXT)
+ }
}
@Test
@@ -125,9 +159,9 @@ class PromotedNotificationContentExtractorImplTest : SysuiTestCase() {
fun extractContent_wasPromotedAutomatically_false() {
val entry = createEntry { extras.putBoolean(EXTRA_WAS_AUTOMATICALLY_PROMOTED, false) }
- val content = extractContent(entry)
+ val content = requireContent(entry).privateVersion
- assertThat(content!!.wasPromotedAutomatically).isFalse()
+ assertThat(content.wasPromotedAutomatically).isFalse()
}
@Test
@@ -135,9 +169,9 @@ class PromotedNotificationContentExtractorImplTest : SysuiTestCase() {
fun extractContent_wasPromotedAutomatically_true() {
val entry = createEntry { extras.putBoolean(EXTRA_WAS_AUTOMATICALLY_PROMOTED, true) }
- val content = extractContent(entry)
+ val content = requireContent(entry).privateVersion
- assertThat(content!!.wasPromotedAutomatically).isTrue()
+ assertThat(content.wasPromotedAutomatically).isTrue()
}
@Test
@@ -146,10 +180,9 @@ class PromotedNotificationContentExtractorImplTest : SysuiTestCase() {
fun extractContent_apiFlagOff_shortCriticalTextNotExtracted() {
val entry = createEntry { setShortCriticalText(TEST_SHORT_CRITICAL_TEXT) }
- val content = extractContent(entry)
+ val content = requireContent(entry).privateVersion
- assertThat(content).isNotNull()
- assertThat(content?.text).isNull()
+ assertThat(content.text).isNull()
}
@Test
@@ -161,10 +194,9 @@ class PromotedNotificationContentExtractorImplTest : SysuiTestCase() {
fun extractContent_apiFlagOn_shortCriticalTextExtracted() {
val entry = createEntry { setShortCriticalText(TEST_SHORT_CRITICAL_TEXT) }
- val content = extractContent(entry)
+ val content = requireContent(entry).privateVersion
- assertThat(content).isNotNull()
- assertThat(content?.shortCriticalText).isEqualTo(TEST_SHORT_CRITICAL_TEXT)
+ assertThat(content.shortCriticalText).isEqualTo(TEST_SHORT_CRITICAL_TEXT)
}
@Test
@@ -176,10 +208,9 @@ class PromotedNotificationContentExtractorImplTest : SysuiTestCase() {
fun extractContent_noShortCriticalTextSet_textIsNull() {
val entry = createEntry { setShortCriticalText(null) }
- val content = extractContent(entry)
+ val content = requireContent(entry).privateVersion
- assertThat(content).isNotNull()
- assertThat(content?.shortCriticalText).isNull()
+ assertThat(content.shortCriticalText).isNull()
}
@Test
@@ -379,17 +410,14 @@ class PromotedNotificationContentExtractorImplTest : SysuiTestCase() {
setWhen(providedCurrentTime)
}
- val content = extractContent(entry)
-
- assertThat(content).isNotNull()
+ val content = requireContent(entry).privateVersion
when (expected) {
- ExpectedTime.Null -> assertThat(content?.time).isNull()
+ ExpectedTime.Null -> assertThat(content.time).isNull()
ExpectedTime.Time -> {
- val actual = content?.time as? When.Time
- assertThat(actual).isNotNull()
- assertThat(actual?.currentTimeMillis).isEqualTo(expectedCurrentTime)
+ val actual = assertNotNull(content.time as? When.Time)
+ assertThat(actual.currentTimeMillis).isEqualTo(expectedCurrentTime)
}
ExpectedTime.CountDown,
@@ -398,23 +426,24 @@ class PromotedNotificationContentExtractorImplTest : SysuiTestCase() {
expectedCurrentTime + systemClock.elapsedRealtime() -
systemClock.currentTimeMillis()
- val actual = content?.time as? When.Chronometer
- assertThat(actual).isNotNull()
- assertThat(actual?.elapsedRealtimeMillis).isEqualTo(expectedElapsedRealtime)
- assertThat(actual?.isCountDown).isEqualTo(expected == ExpectedTime.CountDown)
+ val actual = assertNotNull(content.time as? When.Chronometer)
+ assertThat(actual.elapsedRealtimeMillis).isEqualTo(expectedElapsedRealtime)
+ assertThat(actual.isCountDown).isEqualTo(expected == ExpectedTime.CountDown)
}
}
}
+ // TODO: Add tests for the style of the publicVersion once we implement that
+
@Test
@EnableFlags(PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME)
fun extractContent_fromBaseStyle() {
val entry = createEntry { setStyle(null) }
- val content = extractContent(entry)
+ val content = requireContent(entry)
- assertThat(content).isNotNull()
- assertThat(content?.style).isEqualTo(Style.Base)
+ assertThat(content.privateVersion.style).isEqualTo(Style.Base)
+ assertThat(content.publicVersion.style).isEqualTo(Style.Base)
}
@Test
@@ -422,10 +451,10 @@ class PromotedNotificationContentExtractorImplTest : SysuiTestCase() {
fun extractContent_fromBigPictureStyle() {
val entry = createEntry { setStyle(BigPictureStyle()) }
- val content = extractContent(entry)
+ val content = requireContent(entry)
- assertThat(content).isNotNull()
- assertThat(content?.style).isEqualTo(Style.BigPicture)
+ assertThat(content.privateVersion.style).isEqualTo(Style.BigPicture)
+ assertThat(content.publicVersion.style).isEqualTo(Style.Base)
}
@Test
@@ -442,12 +471,15 @@ class PromotedNotificationContentExtractorImplTest : SysuiTestCase() {
)
}
- val content = extractContent(entry)
+ val content = requireContent(entry)
- assertThat(content).isNotNull()
- assertThat(content?.style).isEqualTo(Style.BigText)
- assertThat(content?.title).isEqualTo(TEST_BIG_CONTENT_TITLE)
- assertThat(content?.text).isEqualTo(TEST_BIG_TEXT)
+ assertThat(content.privateVersion.style).isEqualTo(Style.BigText)
+ assertThat(content.privateVersion.title).isEqualTo(TEST_BIG_CONTENT_TITLE)
+ assertThat(content.privateVersion.text).isEqualTo(TEST_BIG_TEXT)
+
+ assertThat(content.publicVersion.style).isEqualTo(Style.Base)
+ assertThat(content.publicVersion.title).isNull()
+ assertThat(content.publicVersion.text).isNull()
}
@Test
@@ -464,12 +496,15 @@ class PromotedNotificationContentExtractorImplTest : SysuiTestCase() {
)
}
- val content = extractContent(entry)
+ val content = requireContent(entry)
- assertThat(content).isNotNull()
- assertThat(content?.style).isEqualTo(Style.BigText)
- assertThat(content?.title).isEqualTo(TEST_CONTENT_TITLE)
- assertThat(content?.text).isEqualTo(TEST_BIG_TEXT)
+ assertThat(content.privateVersion.style).isEqualTo(Style.BigText)
+ assertThat(content.privateVersion.title).isEqualTo(TEST_CONTENT_TITLE)
+ assertThat(content.privateVersion.text).isEqualTo(TEST_BIG_TEXT)
+
+ assertThat(content.publicVersion.style).isEqualTo(Style.Base)
+ assertThat(content.publicVersion.title).isNull()
+ assertThat(content.publicVersion.text).isNull()
}
@Test
@@ -486,12 +521,15 @@ class PromotedNotificationContentExtractorImplTest : SysuiTestCase() {
)
}
- val content = extractContent(entry)
+ val content = requireContent(entry)
- assertThat(content).isNotNull()
- assertThat(content?.style).isEqualTo(Style.BigText)
- assertThat(content?.title).isEqualTo(TEST_BIG_CONTENT_TITLE)
- assertThat(content?.text).isEqualTo(TEST_CONTENT_TEXT)
+ assertThat(content.privateVersion.style).isEqualTo(Style.BigText)
+ assertThat(content.privateVersion.title).isEqualTo(TEST_BIG_CONTENT_TITLE)
+ assertThat(content.privateVersion.text).isEqualTo(TEST_CONTENT_TEXT)
+
+ assertThat(content.publicVersion.style).isEqualTo(Style.Base)
+ assertThat(content.publicVersion.title).isNull()
+ assertThat(content.publicVersion.text).isNull()
}
@Test
@@ -506,11 +544,14 @@ class PromotedNotificationContentExtractorImplTest : SysuiTestCase() {
)
val entry = createEntry { setStyle(CallStyle.forOngoingCall(TEST_PERSON, hangUpIntent)) }
- val content = extractContent(entry)
+ val content = requireContent(entry)
- assertThat(content).isNotNull()
- assertThat(content?.style).isEqualTo(Style.Call)
- assertThat(content?.title).isEqualTo(TEST_PERSON_NAME)
+ assertThat(content.privateVersion.style).isEqualTo(Style.Call)
+ assertThat(content.privateVersion.title).isEqualTo(TEST_PERSON_NAME)
+
+ assertThat(content.publicVersion.style).isEqualTo(Style.Base)
+ assertThat(content.publicVersion.title).isNull()
+ assertThat(content.publicVersion.text).isNull()
}
@Test
@@ -524,13 +565,17 @@ class PromotedNotificationContentExtractorImplTest : SysuiTestCase() {
setStyle(ProgressStyle().addProgressSegment(Segment(100)).setProgress(75))
}
- val content = extractContent(entry)
+ val content = requireContent(entry)
- assertThat(content).isNotNull()
- assertThat(content?.style).isEqualTo(Style.Progress)
- assertThat(content?.newProgress).isNotNull()
- assertThat(content?.newProgress?.progress).isEqualTo(75)
- assertThat(content?.newProgress?.progressMax).isEqualTo(100)
+ assertThat(content.privateVersion.style).isEqualTo(Style.Progress)
+ val newProgress = assertNotNull(content.privateVersion.newProgress)
+ assertThat(newProgress.progress).isEqualTo(75)
+ assertThat(newProgress.progressMax).isEqualTo(100)
+
+ assertThat(content.publicVersion.style).isEqualTo(Style.Base)
+ assertThat(content.publicVersion.title).isNull()
+ assertThat(content.publicVersion.text).isNull()
+ assertThat(content.publicVersion.newProgress).isNull()
}
@Test
@@ -540,10 +585,11 @@ class PromotedNotificationContentExtractorImplTest : SysuiTestCase() {
setStyle(MessagingStyle(TEST_PERSON).addMessage("message text", 0L, TEST_PERSON))
}
- val content = extractContent(entry)
+ val content = requireContent(entry)
- assertThat(content).isNotNull()
- assertThat(content?.style).isEqualTo(Style.Ineligible)
+ assertThat(content.privateVersion.style).isEqualTo(Style.Ineligible)
+
+ assertThat(content.publicVersion.style).isEqualTo(Style.Ineligible)
}
@Test
@@ -553,18 +599,13 @@ class PromotedNotificationContentExtractorImplTest : SysuiTestCase() {
setProgress(TEST_PROGRESS_MAX, TEST_PROGRESS, /* indeterminate= */ false)
}
- val content = extractContent(entry)
-
- assertThat(content).isNotNull()
+ val content = requireContent(entry)
- val oldProgress = content?.oldProgress
- assertThat(oldProgress).isNotNull()
+ val oldProgress = assertNotNull(content.privateVersion.oldProgress)
- assertThat(content).isNotNull()
- assertThat(content?.oldProgress).isNotNull()
- assertThat(content?.oldProgress?.progress).isEqualTo(TEST_PROGRESS)
- assertThat(content?.oldProgress?.max).isEqualTo(TEST_PROGRESS_MAX)
- assertThat(content?.oldProgress?.isIndeterminate).isFalse()
+ assertThat(oldProgress.progress).isEqualTo(TEST_PROGRESS)
+ assertThat(oldProgress.max).isEqualTo(TEST_PROGRESS_MAX)
+ assertThat(oldProgress.isIndeterminate).isFalse()
}
@Test
@@ -574,18 +615,25 @@ class PromotedNotificationContentExtractorImplTest : SysuiTestCase() {
setProgress(TEST_PROGRESS_MAX, TEST_PROGRESS, /* indeterminate= */ true)
}
- val content = extractContent(entry)
+ val content = requireContent(entry)
+ val oldProgress = assertNotNull(content.privateVersion.oldProgress)
- assertThat(content).isNotNull()
- assertThat(content?.oldProgress).isNotNull()
- assertThat(content?.oldProgress?.progress).isEqualTo(TEST_PROGRESS)
- assertThat(content?.oldProgress?.max).isEqualTo(TEST_PROGRESS_MAX)
- assertThat(content?.oldProgress?.isIndeterminate).isTrue()
+ assertThat(oldProgress.progress).isEqualTo(TEST_PROGRESS)
+ assertThat(oldProgress.max).isEqualTo(TEST_PROGRESS_MAX)
+ assertThat(oldProgress.isIndeterminate).isTrue()
}
- private fun extractContent(entry: NotificationEntry): PromotedNotificationContentModel? {
+ private fun requireContent(
+ entry: NotificationEntry,
+ redactionType: Int = REDACTION_TYPE_PUBLIC,
+ ): PromotedNotificationContentModels = assertNotNull(extractContent(entry, redactionType))
+
+ private fun extractContent(
+ entry: NotificationEntry,
+ redactionType: Int = REDACTION_TYPE_PUBLIC,
+ ): PromotedNotificationContentModels? {
val recoveredBuilder = Notification.Builder(context, entry.sbn.notification)
- return underTest.extractContent(entry, recoveredBuilder, imageModelProvider)
+ return underTest.extractContent(entry, recoveredBuilder, redactionType, imageModelProvider)
}
private fun createEntry(
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
index 6192399c522b..42c3f6603ad8 100644
--- 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
@@ -16,6 +16,8 @@
package com.android.systemui.statusbar.notification.promoted.domain.interactor
+import android.app.Notification.FLAG_FOREGROUND_SERVICE
+import android.app.Notification.FLAG_ONGOING_EVENT
import android.platform.test.annotations.EnableFlags
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
@@ -25,15 +27,26 @@ 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.mediaprojection.data.model.MediaProjectionState
+import com.android.systemui.mediaprojection.data.repository.fakeMediaProjectionRepository
+import com.android.systemui.mediaprojection.taskswitcher.FakeActivityTaskManager.Companion.createTask
+import com.android.systemui.screenrecord.data.model.ScreenRecordModel
+import com.android.systemui.screenrecord.data.repository.screenRecordRepository
+import com.android.systemui.statusbar.chips.call.ui.viewmodel.CallChipViewModelTest.Companion.createStatusBarIconViewOrNull
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.collection.buildNotificationEntry
import com.android.systemui.statusbar.notification.collection.buildOngoingCallEntry
import com.android.systemui.statusbar.notification.collection.buildPromotedOngoingEntry
+import com.android.systemui.statusbar.notification.data.model.activeNotificationModel
+import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository
+import com.android.systemui.statusbar.notification.data.repository.addNotif
import com.android.systemui.statusbar.notification.domain.interactor.renderNotificationListInteractor
import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUi
+import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentBuilder
import com.android.systemui.statusbar.phone.ongoingcall.StatusBarChipsModernization
+import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallTestHelper.addOngoingCallState
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
import org.junit.Before
@@ -76,6 +89,7 @@ class PromotedNotificationsInteractorTest : SysuiTestCase() {
// 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")
+ .inOrder()
}
@Test
@@ -96,6 +110,521 @@ class PromotedNotificationsInteractorTest : SysuiTestCase() {
// 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")
+ .inOrder()
+ }
+
+ @Test
+ fun orderedChipNotificationKeys_noScreenRecordNotif_isEmpty() =
+ kosmos.runTest {
+ screenRecordRepository.screenRecordState.value = ScreenRecordModel.Recording
+ fakeMediaProjectionRepository.mediaProjectionState.value =
+ MediaProjectionState.Projecting.EntireScreen(hostPackage = "test_pkg")
+
+ renderNotificationListInteractor.setRenderedList(emptyList())
+
+ val orderedChipNotificationKeys by
+ collectLastValue(underTest.orderedChipNotificationKeys)
+
+ assertThat(orderedChipNotificationKeys).isEmpty()
+ }
+
+ @Test
+ fun orderedChipNotificationKeys_nullHostPackageForScreenRecord_isEmpty() =
+ kosmos.runTest {
+ screenRecordRepository.screenRecordState.value = ScreenRecordModel.Recording
+ // hostPackage would be provided through mediaProjectionState
+ fakeMediaProjectionRepository.mediaProjectionState.value =
+ MediaProjectionState.NotProjecting
+
+ val entry = buildNotificationEntry(tag = "record", promoted = false)
+ renderNotificationListInteractor.setRenderedList(listOf(entry))
+
+ val orderedChipNotificationKeys by
+ collectLastValue(underTest.orderedChipNotificationKeys)
+
+ assertThat(orderedChipNotificationKeys).isEmpty()
+ }
+
+ @Test
+ fun orderedChipNotificationKeys_containsPromotedScreenRecordNotif() =
+ kosmos.runTest {
+ screenRecordRepository.screenRecordState.value = ScreenRecordModel.Recording
+ fakeMediaProjectionRepository.mediaProjectionState.value =
+ MediaProjectionState.Projecting.EntireScreen(hostPackage = "test_pkg")
+
+ val screenRecordEntry = buildNotificationEntry(tag = "record", promoted = true)
+ renderNotificationListInteractor.setRenderedList(listOf(screenRecordEntry))
+
+ val orderedChipNotificationKeys by
+ collectLastValue(underTest.orderedChipNotificationKeys)
+
+ assertThat(orderedChipNotificationKeys)
+ .containsExactly("0|test_pkg|0|record|0")
+ .inOrder()
+ }
+
+ @Test
+ fun orderedChipNotificationKeys_containsNotPromotedScreenRecordNotif_ifOngoing() =
+ kosmos.runTest {
+ screenRecordRepository.screenRecordState.value = ScreenRecordModel.Recording
+ fakeMediaProjectionRepository.mediaProjectionState.value =
+ MediaProjectionState.Projecting.EntireScreen(hostPackage = "test_pkg")
+
+ val screenRecordEntry =
+ buildNotificationEntry(tag = "record", promoted = false) {
+ setFlag(context, FLAG_ONGOING_EVENT, true)
+ }
+ renderNotificationListInteractor.setRenderedList(listOf(screenRecordEntry))
+
+ val orderedChipNotificationKeys by
+ collectLastValue(underTest.orderedChipNotificationKeys)
+
+ assertThat(orderedChipNotificationKeys)
+ .containsExactly("0|test_pkg|0|record|0")
+ .inOrder()
+ }
+
+ @Test
+ fun orderedChipNotificationKeys_containsNotPromotedScreenRecordNotif_ifFgs() =
+ kosmos.runTest {
+ screenRecordRepository.screenRecordState.value = ScreenRecordModel.Recording
+ fakeMediaProjectionRepository.mediaProjectionState.value =
+ MediaProjectionState.Projecting.EntireScreen(hostPackage = "test_pkg")
+
+ val screenRecordEntry =
+ buildNotificationEntry(tag = "record", promoted = false) {
+ setFlag(context, FLAG_FOREGROUND_SERVICE, true)
+ }
+ renderNotificationListInteractor.setRenderedList(listOf(screenRecordEntry))
+
+ val orderedChipNotificationKeys by
+ collectLastValue(underTest.orderedChipNotificationKeys)
+
+ assertThat(orderedChipNotificationKeys)
+ .containsExactly("0|test_pkg|0|record|0")
+ .inOrder()
+ }
+
+ @Test
+ fun orderedChipNotificationKeys_doesNotContainScreenRecordNotif_ifNotOngoingOrFgs() =
+ kosmos.runTest {
+ screenRecordRepository.screenRecordState.value = ScreenRecordModel.Recording
+ fakeMediaProjectionRepository.mediaProjectionState.value =
+ MediaProjectionState.Projecting.EntireScreen(hostPackage = "test_pkg")
+
+ val screenRecordEntry =
+ buildNotificationEntry(tag = "record", promoted = false) {
+ setFlag(context, FLAG_ONGOING_EVENT, false)
+ setFlag(context, FLAG_FOREGROUND_SERVICE, false)
+ }
+ renderNotificationListInteractor.setRenderedList(listOf(screenRecordEntry))
+
+ val orderedChipNotificationKeys by
+ collectLastValue(underTest.orderedChipNotificationKeys)
+
+ assertThat(orderedChipNotificationKeys).isEmpty()
+ }
+
+ @Test
+ fun orderedChipNotificationKeys_containsFgsScreenRecordNotif_whenNonFgsNotifExists() =
+ kosmos.runTest {
+ screenRecordRepository.screenRecordState.value = ScreenRecordModel.Recording
+ fakeMediaProjectionRepository.mediaProjectionState.value =
+ MediaProjectionState.Projecting.EntireScreen(hostPackage = "test_pkg")
+
+ val fgsEntry =
+ buildNotificationEntry(tag = "recordFgs", promoted = false) {
+ setFlag(context, FLAG_FOREGROUND_SERVICE, true)
+ }
+ val notFgsEntry =
+ buildNotificationEntry(tag = "recordNotFgs", promoted = false) {
+ setFlag(context, FLAG_FOREGROUND_SERVICE, false)
+ }
+ renderNotificationListInteractor.setRenderedList(listOf(fgsEntry, notFgsEntry))
+
+ val orderedChipNotificationKeys by
+ collectLastValue(underTest.orderedChipNotificationKeys)
+
+ assertThat(orderedChipNotificationKeys)
+ .containsExactly("0|test_pkg|0|recordFgs|0")
+ .inOrder()
+ }
+
+ @Test
+ fun orderedChipNotificationKeys_containsOngoingScreenRecordNotif_whenNonOngoingNotifExists() =
+ kosmos.runTest {
+ screenRecordRepository.screenRecordState.value = ScreenRecordModel.Recording
+ fakeMediaProjectionRepository.mediaProjectionState.value =
+ MediaProjectionState.Projecting.EntireScreen(hostPackage = "test_pkg")
+
+ val ongoingEntry =
+ buildNotificationEntry(tag = "recordOngoing", promoted = false) {
+ setFlag(context, FLAG_ONGOING_EVENT, true)
+ }
+ val notOngoingEntry =
+ buildNotificationEntry(tag = "recordNotOngoing", promoted = false) {
+ setFlag(context, FLAG_ONGOING_EVENT, false)
+ }
+ renderNotificationListInteractor.setRenderedList(listOf(notOngoingEntry, ongoingEntry))
+
+ val orderedChipNotificationKeys by
+ collectLastValue(underTest.orderedChipNotificationKeys)
+
+ assertThat(orderedChipNotificationKeys)
+ .containsExactly("0|test_pkg|0|recordOngoing|0")
+ .inOrder()
+ }
+
+ @Test
+ fun orderedChipNotificationKeys_containsFgsOngoingScreenRecordNotif_whenNonFgsOngoingNotifExists() =
+ kosmos.runTest {
+ screenRecordRepository.screenRecordState.value = ScreenRecordModel.Recording
+ fakeMediaProjectionRepository.mediaProjectionState.value =
+ MediaProjectionState.Projecting.EntireScreen(hostPackage = "test_pkg")
+
+ val ongoingAndFgsEntry =
+ buildNotificationEntry(tag = "recordBoth", promoted = false) {
+ setFlag(context, FLAG_FOREGROUND_SERVICE, true)
+ setFlag(context, FLAG_ONGOING_EVENT, true)
+ }
+ val ongoingButNotFgsEntry =
+ buildNotificationEntry(tag = "recordOngoing", promoted = false) {
+ setFlag(context, FLAG_ONGOING_EVENT, true)
+ setFlag(context, FLAG_FOREGROUND_SERVICE, false)
+ }
+ val fgsButNotOngoingEntry =
+ buildNotificationEntry(tag = "recordFgs", promoted = false) {
+ setFlag(context, FLAG_FOREGROUND_SERVICE, true)
+ setFlag(context, FLAG_ONGOING_EVENT, false)
+ }
+ renderNotificationListInteractor.setRenderedList(
+ listOf(fgsButNotOngoingEntry, ongoingButNotFgsEntry, ongoingAndFgsEntry)
+ )
+
+ val orderedChipNotificationKeys by
+ collectLastValue(underTest.orderedChipNotificationKeys)
+
+ assertThat(orderedChipNotificationKeys)
+ .containsExactly("0|test_pkg|0|recordBoth|0")
+ .inOrder()
+ }
+
+ @Test
+ fun orderedChipNotificationKeys_twoEquivalentNotifsForScreenRecord_isEmpty() =
+ kosmos.runTest {
+ screenRecordRepository.screenRecordState.value = ScreenRecordModel.Recording
+ fakeMediaProjectionRepository.mediaProjectionState.value =
+ MediaProjectionState.Projecting.EntireScreen(hostPackage = "test_pkg")
+
+ val entry1 =
+ buildNotificationEntry(tag = "entry1", promoted = false) {
+ setFlag(context, FLAG_FOREGROUND_SERVICE, true)
+ }
+ val entry2 =
+ buildNotificationEntry(tag = "entry2", promoted = false) {
+ setFlag(context, FLAG_FOREGROUND_SERVICE, true)
+ }
+ renderNotificationListInteractor.setRenderedList(listOf(entry1, entry2))
+
+ val orderedChipNotificationKeys by
+ collectLastValue(underTest.orderedChipNotificationKeys)
+
+ assertThat(orderedChipNotificationKeys).isEmpty()
+ }
+
+ @Test
+ fun orderedChipNotificationKeys_noMediProjNotif_isEmpty() =
+ kosmos.runTest {
+ fakeMediaProjectionRepository.mediaProjectionState.value =
+ MediaProjectionState.Projecting.SingleTask(
+ hostPackage = "test_pkg",
+ hostDeviceName = null,
+ createTask(taskId = 1),
+ )
+
+ renderNotificationListInteractor.setRenderedList(emptyList())
+
+ val orderedChipNotificationKeys by
+ collectLastValue(underTest.orderedChipNotificationKeys)
+
+ assertThat(orderedChipNotificationKeys).isEmpty()
+ }
+
+ @Test
+ fun orderedChipNotificationKeys_containsPromotedMediaProjNotif() =
+ kosmos.runTest {
+ fakeMediaProjectionRepository.mediaProjectionState.value =
+ MediaProjectionState.Projecting.SingleTask(
+ hostPackage = "test_pkg",
+ hostDeviceName = null,
+ createTask(taskId = 1),
+ )
+
+ val mediaProjEntry = buildNotificationEntry(tag = "proj", promoted = true)
+ renderNotificationListInteractor.setRenderedList(listOf(mediaProjEntry))
+
+ val orderedChipNotificationKeys by
+ collectLastValue(underTest.orderedChipNotificationKeys)
+
+ assertThat(orderedChipNotificationKeys).containsExactly("0|test_pkg|0|proj|0").inOrder()
+ }
+
+ @Test
+ fun orderedChipNotificationKeys_containsNotPromotedMediaProjNotif_ifOngoing() =
+ kosmos.runTest {
+ fakeMediaProjectionRepository.mediaProjectionState.value =
+ MediaProjectionState.Projecting.SingleTask(
+ hostPackage = "test_pkg",
+ hostDeviceName = null,
+ createTask(taskId = 1),
+ )
+
+ val mediaProjEntry =
+ buildNotificationEntry(tag = "proj", promoted = false) {
+ setFlag(context, FLAG_ONGOING_EVENT, true)
+ }
+ renderNotificationListInteractor.setRenderedList(listOf(mediaProjEntry))
+
+ val orderedChipNotificationKeys by
+ collectLastValue(underTest.orderedChipNotificationKeys)
+
+ assertThat(orderedChipNotificationKeys).containsExactly("0|test_pkg|0|proj|0").inOrder()
+ }
+
+ @Test
+ fun orderedChipNotificationKeys_containsNotPromotedMediaProjNotif_ifFgs() =
+ kosmos.runTest {
+ fakeMediaProjectionRepository.mediaProjectionState.value =
+ MediaProjectionState.Projecting.SingleTask(
+ hostPackage = "test_pkg",
+ hostDeviceName = null,
+ createTask(taskId = 1),
+ )
+
+ val mediaProjEntry =
+ buildNotificationEntry(tag = "proj", promoted = false) {
+ setFlag(context, FLAG_FOREGROUND_SERVICE, true)
+ }
+ renderNotificationListInteractor.setRenderedList(listOf(mediaProjEntry))
+
+ val orderedChipNotificationKeys by
+ collectLastValue(underTest.orderedChipNotificationKeys)
+
+ assertThat(orderedChipNotificationKeys).containsExactly("0|test_pkg|0|proj|0").inOrder()
+ }
+
+ @Test
+ fun orderedChipNotificationKeys_doesNotContainMediaProjNotif_ifNotOngoingOrFgs() =
+ kosmos.runTest {
+ fakeMediaProjectionRepository.mediaProjectionState.value =
+ MediaProjectionState.Projecting.SingleTask(
+ hostPackage = "test_pkg",
+ hostDeviceName = null,
+ createTask(taskId = 1),
+ )
+
+ val mediaProjEntry =
+ buildNotificationEntry(tag = "proj", promoted = false) {
+ setFlag(context, FLAG_ONGOING_EVENT, false)
+ setFlag(context, FLAG_FOREGROUND_SERVICE, false)
+ }
+ renderNotificationListInteractor.setRenderedList(listOf(mediaProjEntry))
+
+ val orderedChipNotificationKeys by
+ collectLastValue(underTest.orderedChipNotificationKeys)
+
+ assertThat(orderedChipNotificationKeys).isEmpty()
+ }
+
+ @Test
+ fun orderedChipNotificationKeys_containsFgsMediaProjNotif_whenNonFgsNotifExists() =
+ kosmos.runTest {
+ fakeMediaProjectionRepository.mediaProjectionState.value =
+ MediaProjectionState.Projecting.SingleTask(
+ hostPackage = "test_pkg",
+ hostDeviceName = null,
+ createTask(taskId = 1),
+ )
+
+ val fgsEntry =
+ buildNotificationEntry(tag = "projFgs", promoted = false) {
+ setFlag(context, FLAG_FOREGROUND_SERVICE, true)
+ }
+ val notFgsEntry =
+ buildNotificationEntry(tag = "projNotFgs", promoted = false) {
+ setFlag(context, FLAG_FOREGROUND_SERVICE, false)
+ }
+ renderNotificationListInteractor.setRenderedList(listOf(fgsEntry, notFgsEntry))
+
+ val orderedChipNotificationKeys by
+ collectLastValue(underTest.orderedChipNotificationKeys)
+
+ assertThat(orderedChipNotificationKeys)
+ .containsExactly("0|test_pkg|0|projFgs|0")
+ .inOrder()
+ }
+
+ @Test
+ fun orderedChipNotificationKeys_containsOngoingMediaProjNotif_whenNonOngoingNotifExists() =
+ kosmos.runTest {
+ fakeMediaProjectionRepository.mediaProjectionState.value =
+ MediaProjectionState.Projecting.SingleTask(
+ hostPackage = "test_pkg",
+ hostDeviceName = null,
+ createTask(taskId = 1),
+ )
+
+ val ongoingEntry =
+ buildNotificationEntry(tag = "projOngoing", promoted = false) {
+ setFlag(context, FLAG_ONGOING_EVENT, true)
+ }
+ val notOngoingEntry =
+ buildNotificationEntry(tag = "projNotOngoing", promoted = false) {
+ setFlag(context, FLAG_ONGOING_EVENT, false)
+ }
+ renderNotificationListInteractor.setRenderedList(listOf(notOngoingEntry, ongoingEntry))
+
+ val orderedChipNotificationKeys by
+ collectLastValue(underTest.orderedChipNotificationKeys)
+
+ assertThat(orderedChipNotificationKeys)
+ .containsExactly("0|test_pkg|0|projOngoing|0")
+ .inOrder()
+ }
+
+ @Test
+ fun orderedChipNotificationKeys_containsFgsOngoingMediaProjNotif_whenNonFgsOngoingNotifExists() =
+ kosmos.runTest {
+ fakeMediaProjectionRepository.mediaProjectionState.value =
+ MediaProjectionState.Projecting.SingleTask(
+ hostPackage = "test_pkg",
+ hostDeviceName = null,
+ createTask(taskId = 1),
+ )
+
+ val ongoingAndFgsEntry =
+ buildNotificationEntry(tag = "projBoth", promoted = false) {
+ setFlag(context, FLAG_FOREGROUND_SERVICE, true)
+ setFlag(context, FLAG_ONGOING_EVENT, true)
+ }
+ val ongoingButNotFgsEntry =
+ buildNotificationEntry(tag = "projOngoing", promoted = false) {
+ setFlag(context, FLAG_ONGOING_EVENT, true)
+ setFlag(context, FLAG_FOREGROUND_SERVICE, false)
+ }
+ val fgsButNotOngoingEntry =
+ buildNotificationEntry(tag = "projFgs", promoted = false) {
+ setFlag(context, FLAG_FOREGROUND_SERVICE, true)
+ setFlag(context, FLAG_ONGOING_EVENT, false)
+ }
+ renderNotificationListInteractor.setRenderedList(
+ listOf(fgsButNotOngoingEntry, ongoingButNotFgsEntry, ongoingAndFgsEntry)
+ )
+
+ val orderedChipNotificationKeys by
+ collectLastValue(underTest.orderedChipNotificationKeys)
+
+ assertThat(orderedChipNotificationKeys)
+ .containsExactly("0|test_pkg|0|projBoth|0")
+ .inOrder()
+ }
+
+ @Test
+ fun orderedChipNotificationKeys_twoEquivalentNotifsForMediaProj_isEmpty() =
+ kosmos.runTest {
+ fakeMediaProjectionRepository.mediaProjectionState.value =
+ MediaProjectionState.Projecting.SingleTask(
+ hostPackage = "test_pkg",
+ hostDeviceName = null,
+ createTask(taskId = 1),
+ )
+
+ val entry1 =
+ buildNotificationEntry(tag = "entry1", promoted = false) {
+ setFlag(context, FLAG_FOREGROUND_SERVICE, true)
+ }
+ val entry2 =
+ buildNotificationEntry(tag = "entry2", promoted = false) {
+ setFlag(context, FLAG_FOREGROUND_SERVICE, true)
+ }
+ renderNotificationListInteractor.setRenderedList(listOf(entry1, entry2))
+
+ val orderedChipNotificationKeys by
+ collectLastValue(underTest.orderedChipNotificationKeys)
+
+ assertThat(orderedChipNotificationKeys).isEmpty()
+ }
+
+ @Test
+ fun orderedChipNotificationKeys_maintainsPromotedNotifOrder() =
+ kosmos.runTest {
+ activeNotificationListRepository.addNotif(
+ activeNotificationModel(
+ key = "notif1",
+ statusBarChipIcon = createStatusBarIconViewOrNull(),
+ promotedContent = PromotedNotificationContentBuilder("notif1").build(),
+ )
+ )
+ activeNotificationListRepository.addNotif(
+ activeNotificationModel(
+ key = "notif2",
+ statusBarChipIcon = createStatusBarIconViewOrNull(),
+ promotedContent = PromotedNotificationContentBuilder("notif2").build(),
+ )
+ )
+
+ val orderedChipNotificationKeys by
+ collectLastValue(underTest.orderedChipNotificationKeys)
+
+ assertThat(orderedChipNotificationKeys).containsExactly("notif1", "notif2").inOrder()
+ }
+
+ // The ranking between different chips should stay consistent between
+ // PromotedNotificationsInteractor and OngoingActivityChipsViewModel.
+ // See OngoingActivityChipsWithNotifsViewModelTest#chips_screenRecordAndCallAndPromotedNotifs
+ // test for the right ranking.
+ @Test
+ fun orderedChipNotificationKeys_rankingIsCorrect() =
+ kosmos.runTest {
+ // Screen record
+ screenRecordRepository.screenRecordState.value = ScreenRecordModel.Recording
+ fakeMediaProjectionRepository.mediaProjectionState.value =
+ MediaProjectionState.Projecting.SingleTask(
+ hostPackage = "screen.record.package",
+ hostDeviceName = null,
+ createTask(taskId = 1),
+ )
+ activeNotificationListRepository.addNotif(
+ activeNotificationModel(
+ key = "screenRecordKey",
+ packageName = "screen.record.package",
+ isOngoingEvent = true,
+ )
+ )
+ // Call
+ addOngoingCallState(key = "callKey")
+ // Other promoted notifs
+ activeNotificationListRepository.addNotif(
+ activeNotificationModel(
+ key = "notif1",
+ statusBarChipIcon = createStatusBarIconViewOrNull(),
+ promotedContent = PromotedNotificationContentBuilder("notif1").build(),
+ )
+ )
+ activeNotificationListRepository.addNotif(
+ activeNotificationModel(
+ key = "notif2",
+ statusBarChipIcon = createStatusBarIconViewOrNull(),
+ promotedContent = PromotedNotificationContentBuilder("notif2").build(),
+ )
+ )
+
+ val orderedChipNotificationKeys by
+ collectLastValue(underTest.orderedChipNotificationKeys)
+
+ assertThat(orderedChipNotificationKeys)
+ .containsExactly("screenRecordKey", "callKey", "notif1", "notif2")
+ .inOrder()
}
@Test
@@ -114,8 +643,7 @@ class PromotedNotificationsInteractorTest : SysuiTestCase() {
collectLastValue(underTest.aodPromotedNotification)
// THEN the ron is first because the call has no content
- assertThat(topPromotedNotificationContent?.identity?.key)
- .isEqualTo("0|test_pkg|0|ron|0")
+ assertThat(topPromotedNotificationContent?.key).isEqualTo("0|test_pkg|0|ron|0")
}
@Test
@@ -134,8 +662,7 @@ class PromotedNotificationsInteractorTest : SysuiTestCase() {
collectLastValue(underTest.aodPromotedNotification)
// THEN the call is the top notification
- assertThat(topPromotedNotificationContent?.identity?.key)
- .isEqualTo("0|test_pkg|0|call|0")
+ assertThat(topPromotedNotificationContent?.key).isEqualTo("0|test_pkg|0|call|0")
}
@Test
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 99f2596dbf1d..19b1046f1931 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,11 +67,11 @@ 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;
-import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel;
+import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentBuilder;
+import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModels;
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;
@@ -389,8 +389,8 @@ public class NotificationContentInflaterTest extends SysuiTestCase {
@Test
@DisableFlags({PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME})
public void testExtractsPromotedContent_notWhenBothFlagsDisabled() throws Exception {
- final PromotedNotificationContentModel content =
- new PromotedNotificationContentModel.Builder("key").build();
+ final PromotedNotificationContentModels content =
+ new PromotedNotificationContentBuilder("key").build();
mPromotedNotificationContentExtractor.resetForEntry(mRow.getEntry(), content);
inflateAndWait(mNotificationInflater, FLAG_CONTENT_VIEW_ALL, mRow);
@@ -401,43 +401,43 @@ public class NotificationContentInflaterTest extends SysuiTestCase {
@Test
@EnableFlags(PromotedNotificationUi.FLAG_NAME)
@DisableFlags(StatusBarNotifChips.FLAG_NAME)
- public void testExtractsPromotedContent_whenPromotedNotificationUiFlagEnabled()
+ public void testExtractsPromotedContent_whePromotedNotificationUiFlagEnabled()
throws Exception {
- final PromotedNotificationContentModel content =
- new PromotedNotificationContentModel.Builder("key").build();
+ final PromotedNotificationContentModels content =
+ new PromotedNotificationContentBuilder("key").build();
mPromotedNotificationContentExtractor.resetForEntry(mRow.getEntry(), content);
inflateAndWait(mNotificationInflater, FLAG_CONTENT_VIEW_ALL, mRow);
mPromotedNotificationContentExtractor.verifyOneExtractCall();
- assertEquals(content, mRow.getEntry().getPromotedNotificationContentModel());
+ assertEquals(content, mRow.getEntry().getPromotedNotificationContentModels());
}
@Test
@EnableFlags(StatusBarNotifChips.FLAG_NAME)
@DisableFlags(PromotedNotificationUi.FLAG_NAME)
public void testExtractsPromotedContent_whenStatusBarNotifChipsFlagEnabled() throws Exception {
- final PromotedNotificationContentModel content =
- new PromotedNotificationContentModel.Builder("key").build();
+ final PromotedNotificationContentModels content =
+ new PromotedNotificationContentBuilder("key").build();
mPromotedNotificationContentExtractor.resetForEntry(mRow.getEntry(), content);
inflateAndWait(mNotificationInflater, FLAG_CONTENT_VIEW_ALL, mRow);
mPromotedNotificationContentExtractor.verifyOneExtractCall();
- assertEquals(content, mRow.getEntry().getPromotedNotificationContentModel());
+ assertEquals(content, mRow.getEntry().getPromotedNotificationContentModels());
}
@Test
@EnableFlags({PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME})
public void testExtractsPromotedContent_whenBothFlagsEnabled() throws Exception {
- final PromotedNotificationContentModel content =
- new PromotedNotificationContentModel.Builder("key").build();
+ final PromotedNotificationContentModels content =
+ new PromotedNotificationContentBuilder("key").build();
mPromotedNotificationContentExtractor.resetForEntry(mRow.getEntry(), content);
inflateAndWait(mNotificationInflater, FLAG_CONTENT_VIEW_ALL, mRow);
mPromotedNotificationContentExtractor.verifyOneExtractCall();
- assertEquals(content, mRow.getEntry().getPromotedNotificationContentModel());
+ assertEquals(content, mRow.getEntry().getPromotedNotificationContentModels());
}
@Test
@@ -448,7 +448,7 @@ public class NotificationContentInflaterTest extends SysuiTestCase {
inflateAndWait(mNotificationInflater, FLAG_CONTENT_VIEW_ALL, mRow);
mPromotedNotificationContentExtractor.verifyOneExtractCall();
- assertNull(mRow.getEntry().getPromotedNotificationContentModel());
+ assertNull(mRow.getEntry().getPromotedNotificationContentModels());
}
@Test
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 0ac5fe95957c..16663def16a4 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
@@ -67,8 +67,8 @@ import com.android.systemui.statusbar.notification.collection.NotificationEntryB
import com.android.systemui.statusbar.notification.promoted.domain.interactor.PackageDemotionInteractor
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.row.icon.mockAppIconProvider
+import com.android.systemui.statusbar.notification.row.icon.mockNotificationIconStyleProvider
import com.android.systemui.testKosmos
import com.android.telecom.telecomManager
import com.google.common.truth.Truth.assertThat
@@ -80,6 +80,7 @@ import org.mockito.ArgumentMatchers.anyBoolean
import org.mockito.ArgumentMatchers.anyInt
import org.mockito.ArgumentMatchers.anyString
import org.mockito.kotlin.any
+import org.mockito.kotlin.anyOrNull
import org.mockito.kotlin.argumentCaptor
import org.mockito.kotlin.eq
import org.mockito.kotlin.mock
@@ -101,6 +102,8 @@ class NotificationInfoTest : SysuiTestCase() {
private lateinit var entry: NotificationEntry
private val mockPackageManager = kosmos.mockPackageManager
+ private val mockAppIconProvider = kosmos.mockAppIconProvider
+ private val mockIconStyleProvider = kosmos.mockNotificationIconStyleProvider
private val uiEventLogger = kosmos.uiEventLoggerFake
private val testableLooper by lazy { kosmos.testableLooper }
@@ -202,7 +205,8 @@ class NotificationInfoTest : SysuiTestCase() {
}
@Test
- fun testBindNotification_SetsPackageIcon() {
+ @DisableFlags(com.android.systemui.Flags.FLAG_NOTIFICATIONS_REDESIGN_GUTS)
+ fun testBindNotification_SetsPackageIcon_flagOff() {
val iconDrawable = mock<Drawable>()
whenever(mockPackageManager.getApplicationIcon(any<ApplicationInfo>()))
.thenReturn(iconDrawable)
@@ -212,6 +216,26 @@ class NotificationInfoTest : SysuiTestCase() {
}
@Test
+ @EnableFlags(com.android.systemui.Flags.FLAG_NOTIFICATIONS_REDESIGN_GUTS)
+ fun testBindNotification_SetsPackageIcon_flagOn() {
+ val iconDrawable = mock<Drawable>()
+ whenever(mockIconStyleProvider.shouldShowWorkProfileBadge(anyOrNull(), anyOrNull()))
+ .thenReturn(false)
+ whenever(
+ mockAppIconProvider.getOrFetchAppIcon(
+ anyOrNull(),
+ anyOrNull(),
+ anyBoolean(),
+ anyBoolean(),
+ )
+ )
+ .thenReturn(iconDrawable)
+ bindNotification()
+ val iconView = underTest.findViewById<ImageView>(R.id.pkg_icon)
+ assertThat(iconView.drawable).isEqualTo(iconDrawable)
+ }
+
+ @Test
fun testBindNotification_noDelegate() {
bindNotification()
val nameView = underTest.findViewById<TextView>(R.id.delegate_name)
@@ -894,8 +918,8 @@ class NotificationInfoTest : SysuiTestCase() {
private fun bindNotification(
pm: PackageManager = this.mockPackageManager,
iNotificationManager: INotificationManager = this.mockINotificationManager,
- appIconProvider: AppIconProvider = kosmos.appIconProvider,
- iconStyleProvider: NotificationIconStyleProvider = kosmos.notificationIconStyleProvider,
+ appIconProvider: AppIconProvider = this.mockAppIconProvider,
+ iconStyleProvider: NotificationIconStyleProvider = this.mockIconStyleProvider,
onUserInteractionCallback: OnUserInteractionCallback = this.onUserInteractionCallback,
channelEditorDialogController: ChannelEditorDialogController =
this.channelEditorDialogController,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationMenuRowTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationMenuRowTest.java
index 95366568a37a..5ad4a4fab056 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationMenuRowTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationMenuRowTest.java
@@ -103,46 +103,6 @@ public class NotificationMenuRowTest extends LeakCheckedTest {
row.resetMenu();
}
-
- @Test
- public void testNoAppOpsInSlowSwipe() {
- when(mRow.getShowSnooze()).thenReturn(false);
- Settings.Global.putInt(mContext.getContentResolver(), SHOW_NEW_NOTIF_DISMISS, 0);
-
- NotificationMenuRow row = new NotificationMenuRow(mContext, mPeopleNotificationIdentifier);
- row.createMenu(mRow);
-
- ViewGroup container = (ViewGroup) row.getMenuView();
- // noti blocking
- assertEquals(1, container.getChildCount());
- }
-
- @Test
- public void testNoSnoozeInSlowSwipe() {
- when(mRow.getShowSnooze()).thenReturn(false);
- Settings.Global.putInt(mContext.getContentResolver(), SHOW_NEW_NOTIF_DISMISS, 0);
-
- NotificationMenuRow row = new NotificationMenuRow(mContext, mPeopleNotificationIdentifier);
- row.createMenu(mRow);
-
- ViewGroup container = (ViewGroup) row.getMenuView();
- // just for noti blocking
- assertEquals(1, container.getChildCount());
- }
-
- @Test
- public void testSnoozeInSlowSwipe() {
- when(mRow.getShowSnooze()).thenReturn(true);
- Settings.Global.putInt(mContext.getContentResolver(), SHOW_NEW_NOTIF_DISMISS, 0);
-
- NotificationMenuRow row = new NotificationMenuRow(mContext, mPeopleNotificationIdentifier);
- row.createMenu(mRow);
-
- ViewGroup container = (ViewGroup) row.getMenuView();
- // one for snooze and one for noti blocking
- assertEquals(2, container.getChildCount());
- }
-
@Test
public void testSlowSwipe_newDismiss() {
when(mRow.getShowSnooze()).thenReturn(true);
@@ -237,6 +197,7 @@ public class NotificationMenuRowTest extends LeakCheckedTest {
new NotificationMenuRow(mContext, mPeopleNotificationIdentifier));
doReturn(30f).when(row).getSnapBackThreshold();
doReturn(50f).when(row).getDismissThreshold();
+ doReturn(70).when(row).getSpaceForMenu();
when(row.isMenuOnLeft()).thenReturn(true);
when(row.getTranslation()).thenReturn(40f);
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 063a04ab9f37..dcba3e447dda 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
@@ -44,7 +44,7 @@ import com.android.systemui.statusbar.notification.ConversationNotificationProce
import com.android.systemui.statusbar.notification.collection.NotificationEntry
import com.android.systemui.statusbar.notification.promoted.FakePromotedNotificationContentExtractor
import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUi
-import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel
+import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentBuilder
import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.BindParams
import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_ALL
import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_CONTRACTED
@@ -456,7 +456,7 @@ class NotificationRowContentBinderImplTest : SysuiTestCase() {
@Test
@DisableFlags(PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME)
fun testExtractsPromotedContent_notWhenBothFlagsDisabled() {
- val content = PromotedNotificationContentModel.Builder("key").build()
+ val content = PromotedNotificationContentBuilder("key").build()
promotedNotificationContentExtractor.resetForEntry(row.entry, content)
inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_ALL, row)
@@ -468,38 +468,38 @@ class NotificationRowContentBinderImplTest : SysuiTestCase() {
@EnableFlags(PromotedNotificationUi.FLAG_NAME)
@DisableFlags(StatusBarNotifChips.FLAG_NAME)
fun testExtractsPromotedContent_whenPromotedNotificationUiFlagEnabled() {
- val content = PromotedNotificationContentModel.Builder("key").build()
+ val content = PromotedNotificationContentBuilder("key").build()
promotedNotificationContentExtractor.resetForEntry(row.entry, content)
inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_ALL, row)
promotedNotificationContentExtractor.verifyOneExtractCall()
- Assert.assertEquals(content, row.entry.promotedNotificationContentModel)
+ Assert.assertEquals(content, row.entry.promotedNotificationContentModels)
}
@Test
@EnableFlags(StatusBarNotifChips.FLAG_NAME)
@DisableFlags(PromotedNotificationUi.FLAG_NAME)
fun testExtractsPromotedContent_whenStatusBarNotifChipsFlagEnabled() {
- val content = PromotedNotificationContentModel.Builder("key").build()
+ val content = PromotedNotificationContentBuilder("key").build()
promotedNotificationContentExtractor.resetForEntry(row.entry, content)
inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_ALL, row)
promotedNotificationContentExtractor.verifyOneExtractCall()
- Assert.assertEquals(content, row.entry.promotedNotificationContentModel)
+ Assert.assertEquals(content, row.entry.promotedNotificationContentModels)
}
@Test
@EnableFlags(PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME)
fun testExtractsPromotedContent_whenBothFlagsEnabled() {
- val content = PromotedNotificationContentModel.Builder("key").build()
+ val content = PromotedNotificationContentBuilder("key").build()
promotedNotificationContentExtractor.resetForEntry(row.entry, content)
inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_ALL, row)
promotedNotificationContentExtractor.verifyOneExtractCall()
- Assert.assertEquals(content, row.entry.promotedNotificationContentModel)
+ Assert.assertEquals(content, row.entry.promotedNotificationContentModels)
}
@Test
@@ -510,7 +510,7 @@ class NotificationRowContentBinderImplTest : SysuiTestCase() {
inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_ALL, row)
promotedNotificationContentExtractor.verifyOneExtractCall()
- Assert.assertNull(row.entry.promotedNotificationContentModel)
+ Assert.assertNull(row.entry.promotedNotificationContentModels)
}
@Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManagerImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManagerImplTest.kt
index 936b971c889b..f52f96efb9d1 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManagerImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManagerImplTest.kt
@@ -139,7 +139,7 @@ class MagneticNotificationRowManagerImplTest : SysuiTestCase() {
underTest.setMagneticRowTranslation(swipedRow, translation = 100f)
// WHEN setting a translation that will fall below the threshold
- val translation = threshold / underTest.swipedRowMultiplier - 50f
+ val translation = 50f
underTest.setMagneticRowTranslation(swipedRow, translation)
// THEN the targets continue to be pulled and translations are set
@@ -162,7 +162,7 @@ class MagneticNotificationRowManagerImplTest : SysuiTestCase() {
underTest.setMagneticRowTranslation(swipedRow, translation = 100f)
// WHEN setting a translation that will fall below the threshold
- val translation = threshold / underTest.swipedRowMultiplier - 50f
+ val translation = 50f
underTest.setMagneticRowTranslation(swipedRow, translation)
// THEN the targets continue to be pulled and reduced translations are set
@@ -185,7 +185,7 @@ class MagneticNotificationRowManagerImplTest : SysuiTestCase() {
underTest.setMagneticRowTranslation(swipedRow, translation = 100f)
// WHEN setting a translation that will fall above the threshold
- val translation = threshold / underTest.swipedRowMultiplier + 50f
+ val translation = 150f
underTest.setMagneticRowTranslation(swipedRow, translation)
// THEN the swiped view detaches and the correct detach haptics play
@@ -208,7 +208,7 @@ class MagneticNotificationRowManagerImplTest : SysuiTestCase() {
underTest.setMagneticRowTranslation(swipedRow, translation = 100f)
// WHEN setting a translation that will fall above the threshold
- val translation = threshold / underTest.swipedRowMultiplier + 50f
+ val translation = 150f
underTest.setMagneticRowTranslation(swipedRow, translation)
// THEN the swiped view does not detach and the reduced translation is set
@@ -355,7 +355,7 @@ class MagneticNotificationRowManagerImplTest : SysuiTestCase() {
underTest.setMagneticRowTranslation(swipedRow, translation = 100f)
// Set a translation that will fall above the threshold
- val translation = threshold / underTest.swipedRowMultiplier + 50f
+ val translation = 150f
underTest.setMagneticRowTranslation(swipedRow, translation)
assertThat(underTest.currentState).isEqualTo(State.DETACHED)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java
index 1ea41de63e64..716353945be2 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java
@@ -186,7 +186,8 @@ public class BiometricsUnlockControllerTest extends SysuiTestCase {
.thenReturn(false);
mBiometricUnlockController.onBiometricAuthenticated(UserHandle.USER_CURRENT,
BiometricSourceType.FINGERPRINT, true /* isStrongBiometric */);
- verify(mStatusBarKeyguardViewManager).showPrimaryBouncer(anyBoolean());
+ verify(mStatusBarKeyguardViewManager).showPrimaryBouncer(anyBoolean(),
+ eq("BiometricUnlockController#MODE_SHOW_BOUNCER"));
verify(mStatusBarKeyguardViewManager, never()).notifyKeyguardAuthenticated(anyBoolean());
assertThat(mBiometricUnlockController.getMode())
.isEqualTo(BiometricUnlockController.MODE_SHOW_BOUNCER);
@@ -198,7 +199,8 @@ public class BiometricsUnlockControllerTest extends SysuiTestCase {
.thenReturn(false);
mBiometricUnlockController.onBiometricAuthenticated(UserHandle.USER_CURRENT,
BiometricSourceType.FINGERPRINT, false /* isStrongBiometric */);
- verify(mStatusBarKeyguardViewManager).showPrimaryBouncer(anyBoolean());
+ verify(mStatusBarKeyguardViewManager).showPrimaryBouncer(anyBoolean(),
+ eq("BiometricUnlockController#MODE_SHOW_BOUNCER"));
assertThat(mBiometricUnlockController.getMode())
.isEqualTo(BiometricUnlockController.MODE_SHOW_BOUNCER);
assertThat(mBiometricUnlockController.getBiometricType())
@@ -248,7 +250,8 @@ public class BiometricsUnlockControllerTest extends SysuiTestCase {
mBiometricUnlockController.onBiometricAuthenticated(UserHandle.USER_CURRENT,
BiometricSourceType.FINGERPRINT, true /* isStrongBiometric */);
- verify(mStatusBarKeyguardViewManager, never()).showPrimaryBouncer(anyBoolean());
+ verify(mStatusBarKeyguardViewManager, never()).showPrimaryBouncer(anyBoolean(),
+ eq("BiometricUnlockController#MODE_SHOW_BOUNCER"));
verify(mStatusBarKeyguardViewManager).notifyKeyguardAuthenticated(eq(false));
assertThat(mBiometricUnlockController.getMode())
.isEqualTo(BiometricUnlockController.MODE_UNLOCK_COLLAPSING);
@@ -327,7 +330,8 @@ public class BiometricsUnlockControllerTest extends SysuiTestCase {
mBiometricUnlockController.onBiometricAuthenticated(UserHandle.USER_CURRENT,
BiometricSourceType.FACE, true /* isStrongBiometric */);
- verify(mStatusBarKeyguardViewManager).showPrimaryBouncer(anyBoolean());
+ verify(mStatusBarKeyguardViewManager).showPrimaryBouncer(anyBoolean(),
+ eq("BiometricUnlockController#MODE_SHOW_BOUNCER"));
assertThat(mBiometricUnlockController.getMode())
.isEqualTo(BiometricUnlockController.MODE_SHOW_BOUNCER);
}
@@ -359,7 +363,8 @@ public class BiometricsUnlockControllerTest extends SysuiTestCase {
mBiometricUnlockController.onBiometricAuthenticated(UserHandle.USER_CURRENT,
BiometricSourceType.FACE, true /* isStrongBiometric */);
- verify(mStatusBarKeyguardViewManager, never()).showPrimaryBouncer(anyBoolean());
+ verify(mStatusBarKeyguardViewManager, never()).showPrimaryBouncer(anyBoolean(),
+ eq("BiometricUnlockController#MODE_SHOW_BOUNCER"));
assertThat(mBiometricUnlockController.getMode())
.isEqualTo(BiometricUnlockController.MODE_NONE);
}
@@ -438,17 +443,20 @@ public class BiometricsUnlockControllerTest extends SysuiTestCase {
// WHEN udfps fails once - then don't show the bouncer yet
mBiometricUnlockController.onBiometricAuthFailed(BiometricSourceType.FINGERPRINT);
- verify(mStatusBarKeyguardViewManager, never()).showPrimaryBouncer(anyBoolean());
+ verify(mStatusBarKeyguardViewManager, never()).showPrimaryBouncer(anyBoolean(),
+ eq("BiometricUnlockController#MODE_SHOW_BOUNCER"));
// WHEN udfps fails the second time - then don't show the bouncer yet
mBiometricUnlockController.onBiometricAuthFailed(BiometricSourceType.FINGERPRINT);
- verify(mStatusBarKeyguardViewManager, never()).showPrimaryBouncer(anyBoolean());
+ verify(mStatusBarKeyguardViewManager, never()).showPrimaryBouncer(anyBoolean(),
+ eq("BiometricUnlockController#MODE_SHOW_BOUNCER"));
// WHEN udpfs fails the third time
mBiometricUnlockController.onBiometricAuthFailed(BiometricSourceType.FINGERPRINT);
// THEN show the bouncer
- verify(mStatusBarKeyguardViewManager).showPrimaryBouncer(true);
+ verify(mStatusBarKeyguardViewManager).showPrimaryBouncer(true,
+ "BiometricUnlockController#MODE_SHOW_BOUNCER");
}
@Test
@@ -460,14 +468,16 @@ public class BiometricsUnlockControllerTest extends SysuiTestCase {
mBiometricUnlockController.onBiometricAuthFailed(BiometricSourceType.FINGERPRINT);
mBiometricUnlockController.onBiometricAuthFailed(BiometricSourceType.FINGERPRINT);
mBiometricUnlockController.onBiometricAuthFailed(BiometricSourceType.FINGERPRINT);
- verify(mStatusBarKeyguardViewManager, never()).showPrimaryBouncer(anyBoolean());
+ verify(mStatusBarKeyguardViewManager, never()).showPrimaryBouncer(anyBoolean(),
+ eq("BiometricUnlockController#MODE_SHOW_BOUNCER"));
// WHEN lockout is received
mBiometricUnlockController.onBiometricError(FingerprintManager.FINGERPRINT_ERROR_LOCKOUT,
"Lockout", BiometricSourceType.FINGERPRINT);
// THEN show bouncer
- verify(mStatusBarKeyguardViewManager).showPrimaryBouncer(true);
+ verify(mStatusBarKeyguardViewManager).showPrimaryBouncer(true,
+ "BiometricUnlockController#MODE_SHOW_BOUNCER");
}
@Test
@@ -544,7 +554,8 @@ public class BiometricsUnlockControllerTest extends SysuiTestCase {
BiometricSourceType.FINGERPRINT, true /* isStrongBiometric */);
// THEN shows primary bouncer
- verify(mStatusBarKeyguardViewManager).showPrimaryBouncer(anyBoolean());
+ verify(mStatusBarKeyguardViewManager).showPrimaryBouncer(anyBoolean(),
+ eq("BiometricUnlockController#MODE_SHOW_BOUNCER"));
}
@Test
@@ -554,7 +565,8 @@ public class BiometricsUnlockControllerTest extends SysuiTestCase {
BiometricSourceType.FACE, false /* isStrongBiometric */);
// THEN shows primary bouncer
- verify(mStatusBarKeyguardViewManager).showPrimaryBouncer(anyBoolean());
+ verify(mStatusBarKeyguardViewManager).showPrimaryBouncer(anyBoolean(),
+ eq("BiometricUnlockController#MODE_SHOW_BOUNCER"));
}
private void givenFingerprintModeUnlockCollapsing() {
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 1cc291199531..d9e256228428 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
@@ -124,7 +124,8 @@ public class StatusBarRemoteInputCallbackTest extends SysuiTestCase {
mRemoteInputCallback.onLockedRemoteInput(
mock(ExpandableNotificationRow.class), mock(View.class));
- verify(mStatusBarKeyguardViewManager).showBouncer(true);
+ verify(mStatusBarKeyguardViewManager).showBouncer(true,
+ "StatusBarRemoteInputCallback#onLockedRemoteInput");
}
@Test
@DisableFlags(ExpandHeadsUpOnInlineReply.FLAG_NAME)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerTest.kt
index c58b4bc9953c..18074d53e87b 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerTest.kt
@@ -40,7 +40,7 @@ import com.android.systemui.statusbar.notification.data.model.activeNotification
import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationsStore
import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository
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.PromotedNotificationContentBuilder
import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel
import com.android.systemui.statusbar.notification.shared.CallType
import com.android.systemui.statusbar.phone.ongoingcall.data.repository.ongoingCallRepository
@@ -170,7 +170,7 @@ class OngoingCallControllerTest : SysuiTestCase() {
@Test
fun interactorHasOngoingCallNotif_repoHasPromotedContent() =
testScope.runTest {
- val promotedContent = PromotedNotificationContentModel.Builder("ongoingNotif").build()
+ val promotedContent = PromotedNotificationContentBuilder("ongoingNotif").build()
setNotifOnRepo(
activeNotificationModel(
key = "ongoingNotif",
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractorTest.kt
index 84f1d5cd4895..c071327ae398 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractorTest.kt
@@ -29,7 +29,7 @@ import com.android.systemui.kosmos.useUnconfinedTestDispatcher
import com.android.systemui.statusbar.StatusBarIconView
import com.android.systemui.statusbar.data.repository.fakeStatusBarModeRepository
import com.android.systemui.statusbar.gesture.swipeStatusBarAwayGestureHandler
-import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel
+import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentBuilder
import com.android.systemui.statusbar.phone.ongoingcall.EnableChipsModernization
import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallModel
import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallTestHelper.addOngoingCallState
@@ -75,7 +75,7 @@ class OngoingCallInteractorTest : SysuiTestCase() {
val startTimeMs = 1000L
val testIconView: StatusBarIconView = mock()
val testIntent: PendingIntent = mock()
- val testPromotedContent = PromotedNotificationContentModel.Builder(key).build()
+ val testPromotedContent = PromotedNotificationContentBuilder(key).build()
addOngoingCallState(
key = key,
startTimeMs = startTimeMs,
@@ -106,7 +106,7 @@ class OngoingCallInteractorTest : SysuiTestCase() {
val startTimeMs = 1000L
val testIconView: StatusBarIconView = mock()
val testIntent: PendingIntent = mock()
- val testPromotedContent = PromotedNotificationContentModel.Builder(key).build()
+ val testPromotedContent = PromotedNotificationContentBuilder(key).build()
addOngoingCallState(
key = key,
startTimeMs = startTimeMs,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ui/IconManagerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ui/IconManagerTest.kt
index 90732d0183d2..318eb87f500b 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ui/IconManagerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ui/IconManagerTest.kt
@@ -14,6 +14,8 @@
* limitations under the License.
*/
+@file:OptIn(ExperimentalKairosApi::class)
+
package com.android.systemui.statusbar.phone.ui
import android.app.Flags
@@ -28,13 +30,17 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.internal.statusbar.StatusBarIcon
import com.android.systemui.SysuiTestCase
+import com.android.systemui.kairos.ExperimentalKairosApi
+import com.android.systemui.kairos.KairosNetwork
import com.android.systemui.statusbar.StatusBarIconView
import com.android.systemui.statusbar.connectivity.ui.MobileContextProvider
import com.android.systemui.statusbar.phone.StatusBarLocation
import com.android.systemui.statusbar.pipeline.mobile.ui.MobileUiAdapter
+import com.android.systemui.statusbar.pipeline.mobile.ui.MobileUiAdapterKairos
import com.android.systemui.statusbar.pipeline.wifi.ui.WifiUiAdapter
import com.android.systemui.util.Assert
import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.CoroutineScope
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -58,7 +64,10 @@ class IconManagerTest : SysuiTestCase() {
StatusBarLocation.HOME,
mock<WifiUiAdapter>(defaultAnswer = RETURNS_DEEP_STUBS),
mock<MobileUiAdapter>(defaultAnswer = RETURNS_DEEP_STUBS),
+ { mock<MobileUiAdapterKairos>(defaultAnswer = RETURNS_DEEP_STUBS) },
mock<MobileContextProvider>(defaultAnswer = RETURNS_DEEP_STUBS),
+ mock<KairosNetwork>(defaultAnswer = RETURNS_DEEP_STUBS),
+ mock<CoroutineScope>(defaultAnswer = RETURNS_DEEP_STUBS),
)
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ui/StatusBarIconControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ui/StatusBarIconControllerTest.java
index 891ff38764fe..8e3117f47f86 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ui/StatusBarIconControllerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ui/StatusBarIconControllerTest.java
@@ -35,6 +35,7 @@ import androidx.test.filters.SmallTest;
import com.android.internal.statusbar.StatusBarIcon;
import com.android.systemui.demomode.DemoModeController;
import com.android.systemui.dump.DumpManager;
+import com.android.systemui.kairos.KairosNetwork;
import com.android.systemui.plugins.DarkIconDispatcher;
import com.android.systemui.statusbar.CommandQueue;
import com.android.systemui.statusbar.StatusBarIconView;
@@ -45,12 +46,15 @@ import com.android.systemui.statusbar.phone.StatusBarLocation;
import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags;
import com.android.systemui.statusbar.pipeline.icons.shared.BindableIconsRegistry;
import com.android.systemui.statusbar.pipeline.mobile.ui.MobileUiAdapter;
+import com.android.systemui.statusbar.pipeline.mobile.ui.MobileUiAdapterKairos;
import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel;
import com.android.systemui.statusbar.pipeline.wifi.ui.WifiUiAdapter;
import com.android.systemui.statusbar.policy.ConfigurationController;
import com.android.systemui.tuner.TunerService;
import com.android.systemui.utils.leaks.LeakCheckedTest;
+import kotlinx.coroutines.CoroutineScope;
+
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -76,7 +80,9 @@ public class StatusBarIconControllerTest extends LeakCheckedTest {
public void testSetCalledOnAdd_IconManager() {
LinearLayout layout = new LinearLayout(mContext);
TestIconManager manager =
- new TestIconManager(layout, mMobileUiAdapter, mMobileContextProvider);
+ new TestIconManager(layout, mMobileUiAdapter, mMobileContextProvider,
+ mock(MobileUiAdapterKairos.class), mock(
+ KairosNetwork.class), mock(CoroutineScope.class));
testCallOnAdd_forManager(manager);
}
@@ -89,7 +95,9 @@ public class StatusBarIconControllerTest extends LeakCheckedTest {
mock(WifiUiAdapter.class),
mMobileUiAdapter,
mMobileContextProvider,
- mock(DarkIconDispatcher.class));
+ mock(DarkIconDispatcher.class),
+ mock(MobileUiAdapterKairos.class), mock(KairosNetwork.class),
+ mock(CoroutineScope.class));
testCallOnAdd_forManager(manager);
}
@@ -139,12 +147,18 @@ public class StatusBarIconControllerTest extends LeakCheckedTest {
WifiUiAdapter wifiUiAdapter,
MobileUiAdapter mobileUiAdapter,
MobileContextProvider contextProvider,
- DarkIconDispatcher darkIconDispatcher) {
+ DarkIconDispatcher darkIconDispatcher,
+ MobileUiAdapterKairos mobileUiAdapterKairos,
+ KairosNetwork kairosNetwork,
+ CoroutineScope appScope) {
super(group,
location,
wifiUiAdapter,
mobileUiAdapter,
+ () -> mobileUiAdapterKairos,
contextProvider,
+ kairosNetwork,
+ appScope,
darkIconDispatcher);
}
@@ -167,13 +181,19 @@ public class StatusBarIconControllerTest extends LeakCheckedTest {
TestIconManager(
ViewGroup group,
MobileUiAdapter adapter,
- MobileContextProvider contextProvider
+ MobileContextProvider contextProvider,
+ MobileUiAdapterKairos adapterKairos,
+ KairosNetwork kairosNetwork,
+ CoroutineScope appScope
) {
super(group,
StatusBarLocation.HOME,
mock(WifiUiAdapter.class),
adapter,
- contextProvider);
+ () -> adapterKairos,
+ contextProvider,
+ kairosNetwork,
+ appScope);
}
@Override
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt
index 9e914ad0a660..9e914ad0a660 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/unfold/DisplaySwitchLatencyTrackerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/unfold/DisplaySwitchLatencyTrackerTest.kt
index 18a124cf362e..033503f9ad8e 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/unfold/DisplaySwitchLatencyTrackerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/unfold/DisplaySwitchLatencyTrackerTest.kt
@@ -52,6 +52,7 @@ import com.android.systemui.unfold.DisplaySwitchLatencyTracker.Companion.FOLDABL
import com.android.systemui.unfold.DisplaySwitchLatencyTracker.Companion.FOLDABLE_DEVICE_STATE_HALF_OPEN
import com.android.systemui.unfold.DisplaySwitchLatencyTracker.Companion.SCREEN_EVENT_TIMEOUT
import com.android.systemui.unfold.DisplaySwitchLatencyTracker.DisplaySwitchLatencyEvent
+import com.android.systemui.unfold.data.repository.ScreenTimeoutPolicyRepository
import com.android.systemui.unfold.data.repository.UnfoldTransitionRepositoryImpl
import com.android.systemui.unfold.domain.interactor.UnfoldTransitionInteractor
import com.android.systemui.unfoldedDeviceState
@@ -97,6 +98,8 @@ class DisplaySwitchLatencyTrackerTest : SysuiTestCase() {
private val animationStatusRepository = kosmos.fakeAnimationStatusRepository
private val keyguardInteractor = mock<KeyguardInteractor>()
private val displaySwitchLatencyLogger = mock<DisplaySwitchLatencyLogger>()
+ private val screenTimeoutPolicyRepository = mock<ScreenTimeoutPolicyRepository>()
+ private val screenTimeoutActive = MutableStateFlow(true)
private val latencyTracker = mock<LatencyTracker>()
private val deviceStateManager = kosmos.deviceStateManager
@@ -136,6 +139,7 @@ class DisplaySwitchLatencyTrackerTest : SysuiTestCase() {
whenever(resources.getIntArray(R.array.config_foldedDeviceStates))
.thenReturn(nonEmptyClosedDeviceStatesArray)
whenever(keyguardInteractor.isAodAvailable).thenReturn(isAodAvailable)
+ whenever(screenTimeoutPolicyRepository.screenTimeoutActive).thenReturn(screenTimeoutActive)
animationStatusRepository.onAnimationStatusChanged(true)
powerInteractor.setAwakeForTest()
powerInteractor.setScreenPowerState(SCREEN_ON)
@@ -144,6 +148,7 @@ class DisplaySwitchLatencyTrackerTest : SysuiTestCase() {
mockContext,
foldStateRepository,
powerInteractor,
+ screenTimeoutPolicyRepository,
unfoldTransitionInteractor,
animationStatusRepository,
keyguardInteractor,
@@ -196,6 +201,7 @@ class DisplaySwitchLatencyTrackerTest : SysuiTestCase() {
mockContext,
foldStateRepository,
powerInteractor,
+ screenTimeoutPolicyRepository,
unfoldTransitionInteractorWithEmptyProgressProvider,
animationStatusRepository,
keyguardInteractor,
@@ -625,6 +631,44 @@ class DisplaySwitchLatencyTrackerTest : SysuiTestCase() {
}
}
+ @Test
+ fun displaySwitch_screenTimeoutActive_logsNoScreenWakelocks() {
+ testScope.runTest {
+ startInFoldedState(displaySwitchLatencyTracker)
+ screenTimeoutActive.value = true
+
+ startUnfolding()
+ advanceTimeBy(100.milliseconds)
+ finishUnfolding()
+
+ val event = capturedLogEvent()
+ assertThat(event.screenWakelockStatus)
+ .isEqualTo(
+ SysUiStatsLog
+ .DISPLAY_SWITCH_LATENCY_TRACKED__SCREEN_WAKELOCK_STATUS__SCREEN_WAKELOCK_STATUS_NO_WAKELOCKS
+ )
+ }
+ }
+
+ @Test
+ fun displaySwitch_screenTimeoutNotActive_logsHasScreenWakelocks() {
+ testScope.runTest {
+ startInFoldedState(displaySwitchLatencyTracker)
+ screenTimeoutActive.value = false
+
+ startUnfolding()
+ advanceTimeBy(100.milliseconds)
+ finishUnfolding()
+
+ val event = capturedLogEvent()
+ assertThat(event.screenWakelockStatus)
+ .isEqualTo(
+ SysUiStatsLog
+ .DISPLAY_SWITCH_LATENCY_TRACKED__SCREEN_WAKELOCK_STATUS__SCREEN_WAKELOCK_STATUS_HAS_SCREEN_WAKELOCKS
+ )
+ }
+ }
+
private fun capturedLogEvent(): DisplaySwitchLatencyEvent {
verify(displaySwitchLatencyLogger).log(capture(loggerArgumentCaptor))
return loggerArgumentCaptor.value
@@ -662,6 +706,9 @@ class DisplaySwitchLatencyTrackerTest : SysuiTestCase() {
fromFoldableDeviceState = fromFoldableDeviceState,
toFoldableDeviceState = toFoldableDeviceState,
toState = toState,
+ screenWakelockStatus =
+ SysUiStatsLog
+ .DISPLAY_SWITCH_LATENCY_TRACKED__SCREEN_WAKELOCK_STATUS__SCREEN_WAKELOCK_STATUS_NO_WAKELOCKS,
trackingResult = SysUiStatsLog.DISPLAY_SWITCH_LATENCY_TRACKED__TRACKING_RESULT__SUCCESS,
)
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractorTest.kt
index 3eada258f616..07706414393b 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractorTest.kt
@@ -27,6 +27,8 @@ import android.graphics.drawable.Drawable
import android.os.Process
import android.os.UserHandle
import android.os.UserManager
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
import android.provider.Settings
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
@@ -34,6 +36,7 @@ import com.android.internal.logging.UiEventLogger
import com.android.keyguard.KeyguardUpdateMonitor
import com.android.keyguard.KeyguardUpdateMonitorCallback
import com.android.systemui.Flags as AConfigFlags
+import com.android.systemui.Flags.FLAG_USER_SWITCHER_ADD_SIGN_OUT_OPTION
import com.android.systemui.GuestResetOrExitSessionReceiver
import com.android.systemui.GuestResumeSessionReceiver
import com.android.systemui.SysuiTestCase
@@ -68,6 +71,7 @@ import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import junit.framework.Assert.assertNotNull
+import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.runCurrent
@@ -101,10 +105,13 @@ class UserSwitcherInteractorTest : SysuiTestCase() {
@Mock private lateinit var resumeSessionReceiver: GuestResumeSessionReceiver
@Mock private lateinit var resetOrExitSessionReceiver: GuestResetOrExitSessionReceiver
@Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
+ @Mock private lateinit var userLogoutInteractor: UserLogoutInteractor
private val kosmos = testKosmos()
+ private val logoutEnabledStateFlow = MutableStateFlow<Boolean>(false)
private val testScope = kosmos.testScope
private lateinit var spyContext: Context
+
private lateinit var userRepository: FakeUserRepository
private lateinit var keyguardReply: KeyguardInteractorFactory.WithDependencies
private lateinit var keyguardRepository: FakeKeyguardRepository
@@ -118,6 +125,8 @@ class UserSwitcherInteractorTest : SysuiTestCase() {
whenever(manager.getUserIcon(anyInt())).thenReturn(ICON)
whenever(manager.canAddMoreUsers(any())).thenReturn(true)
+ whenever(userLogoutInteractor.isLogoutEnabled).thenReturn(logoutEnabledStateFlow)
+
overrideResource(com.android.settingslib.R.drawable.ic_account_circle, GUEST_ICON)
overrideResource(R.dimen.max_avatar_size, 10)
overrideResource(
@@ -493,6 +502,42 @@ class UserSwitcherInteractorTest : SysuiTestCase() {
}
@Test
+ @DisableFlags(FLAG_USER_SWITCHER_ADD_SIGN_OUT_OPTION)
+ fun actions_logoutEnabled_flagDisabled_signOutIsNotShown() {
+ createUserInteractor()
+ testScope.runTest {
+ val userInfos = createUserInfos(count = 1, includeGuest = false)
+ userRepository.setUserInfos(userInfos)
+ userRepository.setSelectedUserInfo(userInfos[0])
+ userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = false))
+ keyguardRepository.setKeyguardShowing(true)
+ logoutEnabledStateFlow.value = true
+
+ val value = collectLastValue(underTest.actions)
+
+ assertThat(value()).isEqualTo(emptyList<UserActionModel>())
+ }
+ }
+
+ @Test
+ @EnableFlags(FLAG_USER_SWITCHER_ADD_SIGN_OUT_OPTION)
+ fun actions_logoutEnabled_flagEnabled_signOutIsShown() {
+ createUserInteractor()
+ testScope.runTest {
+ val userInfos = createUserInfos(count = 1, includeGuest = false)
+ userRepository.setUserInfos(userInfos)
+ userRepository.setSelectedUserInfo(userInfos[0])
+ userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = false))
+ keyguardRepository.setKeyguardShowing(true)
+ logoutEnabledStateFlow.value = true
+
+ val value = collectLastValue(underTest.actions)
+
+ assertThat(value()).isEqualTo(listOf(UserActionModel.SIGN_OUT))
+ }
+ }
+
+ @Test
fun executeAction_addUser_dismissesDialogAndStartsActivity() {
createUserInteractor()
testScope.runTest {
@@ -569,14 +614,23 @@ class UserSwitcherInteractorTest : SysuiTestCase() {
verify(uiEventLogger, times(1))
.log(MultiUserActionsEvent.CREATE_GUEST_FROM_USER_SWITCHER)
assertThat(dialogRequests)
- .contains(
- ShowDialogRequestModel.ShowUserCreationDialog(isGuest = true),
- )
+ .contains(ShowDialogRequestModel.ShowUserCreationDialog(isGuest = true))
verify(activityManager).switchUser(guestUserInfo.id)
}
}
@Test
+ fun executeAction_signOut() {
+ createUserInteractor()
+ testScope.runTest {
+ underTest.executeAction(UserActionModel.SIGN_OUT)
+ runCurrent()
+
+ verify(userLogoutInteractor).logOut()
+ }
+ }
+
+ @Test
fun selectUser_alreadySelectedGuestReSelected_exitGuestDialog() {
createUserInteractor()
testScope.runTest {
@@ -739,7 +793,7 @@ class UserSwitcherInteractorTest : SysuiTestCase() {
fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly(
spyContext,
- Intent(Intent.ACTION_LOCALE_CHANGED)
+ Intent(Intent.ACTION_LOCALE_CHANGED),
)
runCurrent()
@@ -972,7 +1026,7 @@ class UserSwitcherInteractorTest : SysuiTestCase() {
50,
"Work Profile",
/* iconPath= */ "",
- /* flags= */ UserInfo.FLAG_MANAGED_PROFILE
+ /* flags= */ UserInfo.FLAG_MANAGED_PROFILE,
)
)
userRepository.setUserInfos(userInfos)
@@ -1010,7 +1064,7 @@ class UserSwitcherInteractorTest : SysuiTestCase() {
userRepository.setSettings(
UserSwitcherSettingsModel(
isUserSwitcherEnabled = true,
- isAddUsersFromLockscreen = true
+ isAddUsersFromLockscreen = true,
)
)
@@ -1034,7 +1088,7 @@ class UserSwitcherInteractorTest : SysuiTestCase() {
userRepository.setSettings(
UserSwitcherSettingsModel(
isUserSwitcherEnabled = true,
- isAddUsersFromLockscreen = true
+ isAddUsersFromLockscreen = true,
)
)
@@ -1068,7 +1122,7 @@ class UserSwitcherInteractorTest : SysuiTestCase() {
whenever(
manager.hasUserRestrictionForUser(
UserManager.DISALLOW_ADD_USER,
- UserHandle.of(id)
+ UserHandle.of(id),
)
)
.thenReturn(true)
@@ -1170,7 +1224,7 @@ class UserSwitcherInteractorTest : SysuiTestCase() {
whenever(
manager.hasUserRestrictionForUser(
UserManager.DISALLOW_ADD_USER,
- UserHandle.of(0)
+ UserHandle.of(0),
)
)
.thenReturn(true)
@@ -1195,7 +1249,7 @@ class UserSwitcherInteractorTest : SysuiTestCase() {
model = model,
id = index,
isSelected = index == selectedIndex,
- isGuest = includeGuest && index == count - 1
+ isGuest = includeGuest && index == count - 1,
)
}
}
@@ -1263,14 +1317,12 @@ class UserSwitcherInteractorTest : SysuiTestCase() {
assertThat(record.isSwitchToEnabled).isEqualTo(isSwitchToEnabled)
}
- private fun assertRecordForAction(
- record: UserRecord,
- type: UserActionModel,
- ) {
+ private fun assertRecordForAction(record: UserRecord, type: UserActionModel) {
assertThat(record.isGuest).isEqualTo(type == UserActionModel.ENTER_GUEST_MODE)
assertThat(record.isAddUser).isEqualTo(type == UserActionModel.ADD_USER)
assertThat(record.isAddSupervisedUser)
.isEqualTo(type == UserActionModel.ADD_SUPERVISED_USER)
+ assertThat(record.isSignOut).isEqualTo(type === UserActionModel.SIGN_OUT)
}
private fun createUserInteractor(startAsProcessUser: Boolean = true) {
@@ -1317,13 +1369,11 @@ class UserSwitcherInteractorTest : SysuiTestCase() {
featureFlags = kosmos.fakeFeatureFlagsClassic,
userRestrictionChecker = mock(),
processWrapper = kosmos.processWrapper,
+ userLogoutInteractor = userLogoutInteractor,
)
}
- private fun createUserInfos(
- count: Int,
- includeGuest: Boolean,
- ): List<UserInfo> {
+ private fun createUserInfos(count: Int, includeGuest: Boolean): List<UserInfo> {
return (0 until count).map { index ->
val isGuest = includeGuest && index == count - 1
createUserInfo(
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/user/ui/viewmodel/StatusBarUserChipViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/user/ui/viewmodel/StatusBarUserChipViewModelTest.kt
index 5d51c6d16c5a..d51e66d6f3b0 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/user/ui/viewmodel/StatusBarUserChipViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/user/ui/viewmodel/StatusBarUserChipViewModelTest.kt
@@ -44,9 +44,12 @@ import com.android.systemui.user.data.repository.FakeUserRepository
import com.android.systemui.user.domain.interactor.GuestUserInteractor
import com.android.systemui.user.domain.interactor.HeadlessSystemUserMode
import com.android.systemui.user.domain.interactor.RefreshUsersScheduler
+import com.android.systemui.user.domain.interactor.UserLogoutInteractor
import com.android.systemui.user.domain.interactor.UserSwitcherInteractor
import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.toList
@@ -78,13 +81,13 @@ class StatusBarUserChipViewModelTest : SysuiTestCase() {
@Mock private lateinit var resumeSessionReceiver: GuestResumeSessionReceiver
@Mock private lateinit var resetOrExitSessionReceiver: GuestResetOrExitSessionReceiver
@Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
+ @Mock private lateinit var userLogoutInteractor: UserLogoutInteractor
private lateinit var underTest: StatusBarUserChipViewModel
private val userRepository = FakeUserRepository()
private lateinit var guestUserInteractor: GuestUserInteractor
private lateinit var refreshUsersScheduler: RefreshUsersScheduler
-
private val testDispatcher = UnconfinedTestDispatcher()
private val testScope = TestScope(testDispatcher)
@@ -92,6 +95,9 @@ class StatusBarUserChipViewModelTest : SysuiTestCase() {
fun setUp() {
MockitoAnnotations.initMocks(this)
+ val logoutEnabledStateFlow = MutableStateFlow<Boolean>(false)
+ whenever(userLogoutInteractor.isLogoutEnabled).thenReturn(logoutEnabledStateFlow)
+
doAnswer { invocation ->
val userId = invocation.arguments[0] as Int
when (userId) {
@@ -251,9 +257,7 @@ class StatusBarUserChipViewModelTest : SysuiTestCase() {
headlessSystemUserMode = headlessSystemUserMode,
applicationScope = testScope.backgroundScope,
telephonyInteractor =
- TelephonyInteractor(
- repository = FakeTelephonyRepository(),
- ),
+ TelephonyInteractor(repository = FakeTelephonyRepository()),
broadcastDispatcher = fakeBroadcastDispatcher,
keyguardUpdateMonitor = keyguardUpdateMonitor,
backgroundDispatcher = testDispatcher,
@@ -263,7 +267,8 @@ class StatusBarUserChipViewModelTest : SysuiTestCase() {
guestUserInteractor = guestUserInteractor,
uiEventLogger = uiEventLogger,
userRestrictionChecker = mock(),
- processWrapper = ProcessWrapperFake(activityManager)
+ processWrapper = ProcessWrapperFake(activityManager),
+ userLogoutInteractor = userLogoutInteractor,
)
)
}
@@ -293,7 +298,7 @@ class StatusBarUserChipViewModelTest : SysuiTestCase() {
USER_NAME_0.text!!,
/* iconPath */ "",
/* flags */ UserInfo.FLAG_FULL,
- /* userType */ UserManager.USER_TYPE_FULL_SYSTEM
+ /* userType */ UserManager.USER_TYPE_FULL_SYSTEM,
)
private val USER_1 =
@@ -302,7 +307,7 @@ class StatusBarUserChipViewModelTest : SysuiTestCase() {
USER_NAME_1.text!!,
/* iconPath */ "",
/* flags */ UserInfo.FLAG_FULL,
- /* userType */ UserManager.USER_TYPE_FULL_SYSTEM
+ /* userType */ UserManager.USER_TYPE_FULL_SYSTEM,
)
private val USER_2 =
@@ -311,7 +316,7 @@ class StatusBarUserChipViewModelTest : SysuiTestCase() {
USER_NAME_2.text!!,
/* iconPath */ "",
/* flags */ UserInfo.FLAG_FULL,
- /* userType */ UserManager.USER_TYPE_FULL_SYSTEM
+ /* userType */ UserManager.USER_TYPE_FULL_SYSTEM,
)
}
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt
index 8ff088f5d29b..087ccb83afe5 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt
@@ -44,6 +44,7 @@ import com.android.systemui.user.data.repository.FakeUserRepository
import com.android.systemui.user.domain.interactor.GuestUserInteractor
import com.android.systemui.user.domain.interactor.HeadlessSystemUserMode
import com.android.systemui.user.domain.interactor.RefreshUsersScheduler
+import com.android.systemui.user.domain.interactor.UserLogoutInteractor
import com.android.systemui.user.domain.interactor.UserSwitcherInteractor
import com.android.systemui.user.legacyhelper.ui.LegacyUserUiHelper
import com.android.systemui.user.shared.model.UserActionModel
@@ -51,6 +52,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.flow.MutableStateFlow
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
@@ -79,6 +81,7 @@ class UserSwitcherViewModelTest : SysuiTestCase() {
@Mock private lateinit var resumeSessionReceiver: GuestResumeSessionReceiver
@Mock private lateinit var resetOrExitSessionReceiver: GuestResetOrExitSessionReceiver
@Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
+ @Mock private lateinit var userLogoutInteractor: UserLogoutInteractor
private lateinit var underTest: UserSwitcherViewModel
@@ -94,6 +97,10 @@ class UserSwitcherViewModelTest : SysuiTestCase() {
whenever(manager.canAddMoreUsers(any())).thenReturn(true)
whenever(manager.getUserSwitchability(any()))
.thenReturn(UserManager.SWITCHABILITY_STATUS_OK)
+
+ val logoutEnabledStateFlow = MutableStateFlow<Boolean>(false)
+ whenever(userLogoutInteractor.isLogoutEnabled).thenReturn(logoutEnabledStateFlow)
+
overrideResource(
com.android.internal.R.string.config_supervisedUserCreationPackage,
SUPERVISED_USER_CREATION_PACKAGE,
@@ -113,15 +120,11 @@ class UserSwitcherViewModelTest : SysuiTestCase() {
UserInfo.FLAG_ADMIN or
UserInfo.FLAG_FULL,
UserManager.USER_TYPE_FULL_SYSTEM,
- ),
+ )
)
userRepository.setUserInfos(userInfos)
userRepository.setSelectedUserInfo(userInfos[0])
- userRepository.setSettings(
- UserSwitcherSettingsModel(
- isUserSwitcherEnabled = true,
- )
- )
+ userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
}
val refreshUsersScheduler =
@@ -163,9 +166,7 @@ class UserSwitcherViewModelTest : SysuiTestCase() {
headlessSystemUserMode = headlessSystemUserMode,
applicationScope = testScope.backgroundScope,
telephonyInteractor =
- TelephonyInteractor(
- repository = FakeTelephonyRepository(),
- ),
+ TelephonyInteractor(repository = FakeTelephonyRepository()),
broadcastDispatcher = fakeBroadcastDispatcher,
keyguardUpdateMonitor = keyguardUpdateMonitor,
backgroundDispatcher = testDispatcher,
@@ -175,7 +176,8 @@ class UserSwitcherViewModelTest : SysuiTestCase() {
guestUserInteractor = guestUserInteractor,
uiEventLogger = uiEventLogger,
userRestrictionChecker = mock(),
- processWrapper = ProcessWrapperFake(activityManager)
+ processWrapper = ProcessWrapperFake(activityManager),
+ userLogoutInteractor = userLogoutInteractor,
),
guestUserInteractor = guestUserInteractor,
)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/window/ui/viewmodel/WindowRootViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/window/ui/viewmodel/WindowRootViewModelTest.kt
index 3da4f29a6fcb..dc344aa1a66f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/window/ui/viewmodel/WindowRootViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/window/ui/viewmodel/WindowRootViewModelTest.kt
@@ -73,7 +73,7 @@ class WindowRootViewModelTest : SysuiTestCase() {
assertThat(blurRadius).isEqualTo(0f)
- kosmos.windowRootViewBlurRepository.blurRadius.value = 60
+ kosmos.windowRootViewBlurRepository.blurRequestedByShade.value = 60
runCurrent()
assertThat(blurRadius).isEqualTo(0f)
diff --git a/packages/SystemUI/res/drawable/unpin_icon.xml b/packages/SystemUI/res/drawable/unpin_icon.xml
new file mode 100644
index 000000000000..4e2e15893884
--- /dev/null
+++ b/packages/SystemUI/res/drawable/unpin_icon.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="960"
+ android:viewportHeight="960"
+ android:tint="?attr/colorControlNormal">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M680,120L680,200L640,200L640,527L560,447L560,200L400,200L400,287L313,200L280,167L280,167L280,120L680,120ZM480,920L440,880L440,640L240,640L240,560L320,480L320,434L56,168L112,112L848,848L790,904L526,640L520,640L520,880L480,920ZM354,560L446,560L402,516L400,514L354,560ZM480,367L480,367L480,367L480,367ZM402,516L402,516L402,516L402,516Z"/>
+</vector>
diff --git a/packages/SystemUI/res/drawable/vector_drawable_progress_indeterminate_horizontal_trimmed.xml b/packages/SystemUI/res/drawable/vector_drawable_progress_indeterminate_horizontal_trimmed.xml
index aec204f45aa7..7f6dc49505bb 100644
--- a/packages/SystemUI/res/drawable/vector_drawable_progress_indeterminate_horizontal_trimmed.xml
+++ b/packages/SystemUI/res/drawable/vector_drawable_progress_indeterminate_horizontal_trimmed.xml
@@ -38,7 +38,7 @@
<path
android:name="rect"
android:pathData="M -144.0,-5.0 l 288.0,0 l 0,10.0 l -288.0,0 Z"
- android:fillColor="?androidprv:attr/colorAccentPrimaryVariant" />
+ android:fillColor="@androidprv:color/materialColorPrimary" />
</group>
</group>
</vector> \ No newline at end of file
diff --git a/packages/SystemUI/res/layout/activity_rear_display_enabled.xml b/packages/SystemUI/res/layout/activity_rear_display_enabled.xml
index f900626b4da6..6b633e03f1f2 100644
--- a/packages/SystemUI/res/layout/activity_rear_display_enabled.xml
+++ b/packages/SystemUI/res/layout/activity_rear_display_enabled.xml
@@ -56,6 +56,7 @@
android:gravity="center_horizontal" />
<TextView
+ android:id="@+id/seekbar_instructions"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/rear_display_unfolded_front_screen_on_slide_to_cancel"
@@ -73,4 +74,13 @@
android:background="@null"
android:gravity="center_horizontal" />
+ <Button
+ android:id="@+id/cancel_button"
+ android:text="@string/cancel"
+ android:layout_width="@dimen/rear_display_animation_width_opened"
+ android:layout_height="wrap_content"
+ android:gravity="center_horizontal"
+ android:visibility="gone"
+ style="@style/Widget.Dialog.Button.BorderButton"/>
+
</LinearLayout>
diff --git a/packages/SystemUI/res/layout/promoted_permission_guts.xml b/packages/SystemUI/res/layout/promoted_permission_guts.xml
new file mode 100644
index 000000000000..50e5ae3c05ed
--- /dev/null
+++ b/packages/SystemUI/res/layout/promoted_permission_guts.xml
@@ -0,0 +1,73 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 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.
+-->
+
+<com.android.systemui.statusbar.notification.row.PromotedPermissionGutsContent
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:paddingTop="2dp"
+ android:paddingBottom="2dp"
+ android:background="@androidprv:color/materialColorSurfaceContainerHigh"
+ android:theme="@style/Theme.SystemUI"
+ >
+
+ <RelativeLayout
+ android:id="@+id/promoted_guts"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:minHeight="@dimen/notification_2025_min_height">
+
+ <ImageView
+ android:id="@+id/unpin_icon"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:src="@drawable/unpin_icon"
+ android:layout_alignParentTop="true"
+ android:layout_centerHorizontal="true"
+ android:padding="@dimen/notification_importance_button_padding"
+ />
+
+ <TextView
+ android:id="@+id/demote_explain"
+ android:layout_width="400dp"
+ android:layout_height="wrap_content"
+ android:layout_alignParentLeft="true"
+ android:layout_below="@id/unpin_icon"
+ android:layout_toLeftOf="@id/undo"
+ android:padding="@*android:dimen/notification_content_margin_end"
+ android:textColor="@androidprv:color/materialColorOnSurface"
+ android:minWidth="@dimen/min_clickable_item_size"
+ android:minHeight="@dimen/min_clickable_item_size"
+ style="@style/TextAppearance.NotificationInfo.Button" />
+
+ <TextView
+ android:id="@+id/undo"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/unpin_icon"
+ android:layout_alignParentRight="true"
+ android:padding="@*android:dimen/notification_content_margin_end"
+ android:textColor="@androidprv:color/materialColorOnSurface"
+ android:minWidth="@dimen/min_clickable_item_size"
+ android:minHeight="@dimen/min_clickable_item_size"
+ android:text="@string/snooze_undo"
+ style="@style/TextAppearance.NotificationInfo.Button" />
+ </RelativeLayout>
+
+</com.android.systemui.statusbar.notification.row.PromotedPermissionGutsContent>
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index d0ae307b6919..7d983068f34e 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -989,7 +989,7 @@
<dimen name="keyguard_security_container_padding_top">20dp</dimen>
- <dimen name="keyguard_translate_distance_on_swipe_up">-200dp</dimen>
+ <dimen name="keyguard_translate_distance_on_swipe_up">-180dp</dimen>
<dimen name="keyguard_indication_margin_bottom">32dp</dimen>
<dimen name="ambient_indication_margin_bottom">71dp</dimen>
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index b627bdf22a6c..681bd53f1a40 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -2551,6 +2551,9 @@
<!-- Label for header of customize QS [CHAR LIMIT=60] -->
<string name="drag_to_rearrange_tiles">Hold and drag to rearrange tiles</string>
+ <!-- Label for placing tiles in edit mode for QS [CHAR LIMIT=60] -->
+ <string name="tap_to_position_tile">Tap to position tile</string>
+
<!-- Label for area where tiles can be dragged in to [CHAR LIMIT=60] -->
<string name="drag_to_remove_tiles">Drag here to remove</string>
@@ -2592,6 +2595,12 @@
<!-- Accessibility description of action to remove QS tile on click. It will read as "Double-tap to remove tile" in screen readers [CHAR LIMIT=NONE] -->
<string name="accessibility_qs_edit_remove_tile_action">remove tile</string>
+ <!-- Accessibility description of action to select the QS tile to place on click. It will read as "Double-tap to toggle placement mode" in screen readers [CHAR LIMIT=NONE] -->
+ <string name="accessibility_qs_edit_toggle_placement_mode">toggle placement mode</string>
+
+ <!-- Accessibility description of action to toggle the QS tile selection. It will read as "Double-tap to toggle selection" in screen readers [CHAR LIMIT=NONE] -->
+ <string name="accessibility_qs_edit_toggle_selection">toggle selection</string>
+
<!-- Accessibility action of action to add QS tile to end. It will read as "Double-tap to add tile to the last position" in screen readers [CHAR LIMIT=NONE] -->
<string name="accessibility_qs_edit_tile_add_action">add tile to the last position</string>
@@ -4200,6 +4209,12 @@
All Quick Settings tiles will reset to the device’s original settings
</string>
+
+ <!-- Content of the Reset Tiles dialog in QS Edit mode. [CHAR LIMIT=NONE] -->
+ <string name="demote_explain_text">
+ <xliff:g id="application" example= "Superfast Food Delivery">%1$s</xliff:g> will no longer show Live Updates here. You can change this any time in Settings.
+ </string>
+
<!-- Template that joins disabled message with the label for the voice over. [CHAR LIMIT=NONE] -->
<string name="volume_slider_disabled_message_template"><xliff:g example="Notification" id="stream_name">%1$s</xliff:g>, <xliff:g example="Disabled because ring is muted" id="disabled_message">%2$s</xliff:g></string>
</resources>
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/PreviewPositionHelper.java b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/PreviewPositionHelper.java
index ea7321627322..b8cd5bec2cbe 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/PreviewPositionHelper.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/PreviewPositionHelper.java
@@ -208,4 +208,19 @@ public class PreviewPositionHelper {
}
mMatrix.postTranslate(translateX, translateY);
}
+
+ /**
+ * A factory that returns a new instance of the {@link PreviewPositionHelper}.
+ * <p>{@link PreviewPositionHelper} is a stateful helper, and hence when using it in distinct
+ * scenarios, prefer fetching an object using this factory</p>
+ * <p>Additionally, helpful for injecting mocks in tests</p>
+ */
+ public static class PreviewPositionHelperFactory {
+ /**
+ * Returns a new {@link PreviewPositionHelper} for use in a distinct scenario.
+ */
+ public PreviewPositionHelper create() {
+ return new PreviewPositionHelper();
+ }
+ }
}
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardViewController.java
index 892851cd7056..8a307145023d 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardViewController.java
@@ -160,7 +160,7 @@ public interface KeyguardViewController {
/**
* Shows the primary bouncer.
*/
- void showPrimaryBouncer(boolean scrimmed);
+ void showPrimaryBouncer(boolean scrimmed, String reason);
/**
* When the primary bouncer is fully visible or is showing but animation didn't finish yet.
diff --git a/packages/SystemUI/src/com/android/systemui/ambient/touch/scrim/BouncerScrimController.java b/packages/SystemUI/src/com/android/systemui/ambient/touch/scrim/BouncerScrimController.java
index 6f2dd799c409..633c13e9e94d 100644
--- a/packages/SystemUI/src/com/android/systemui/ambient/touch/scrim/BouncerScrimController.java
+++ b/packages/SystemUI/src/com/android/systemui/ambient/touch/scrim/BouncerScrimController.java
@@ -34,7 +34,7 @@ public class BouncerScrimController implements ScrimController {
@Override
public void show(boolean scrimmed) {
- mStatusBarKeyguardViewManager.showPrimaryBouncer(scrimmed);
+ mStatusBarKeyguardViewManager.showPrimaryBouncer(scrimmed, "BouncerScrimController#show");
}
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/assist/AssistManager.java b/packages/SystemUI/src/com/android/systemui/assist/AssistManager.java
index da1c1bc49d23..2d44d401b0b0 100644
--- a/packages/SystemUI/src/com/android/systemui/assist/AssistManager.java
+++ b/packages/SystemUI/src/com/android/systemui/assist/AssistManager.java
@@ -130,6 +130,8 @@ public class AssistManager {
AssistUtils.INVOCATION_TYPE_POWER_BUTTON_LONG_PRESS;
public static final int INVOCATION_TYPE_NAV_HANDLE_LONG_PRESS =
AssistUtils.INVOCATION_TYPE_NAV_HANDLE_LONG_PRESS;
+ public static final int INVOCATION_TYPE_LAUNCHER_SYSTEM_SHORTCUT =
+ AssistUtils.INVOCATION_TYPE_LAUNCHER_SYSTEM_SHORTCUT;
public static final int DISMISS_REASON_INVOCATION_CANCELLED = 1;
public static final int DISMISS_REASON_TAP = 2;
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
index dfe8eb28b2a6..659d3b46fea9 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
@@ -880,7 +880,7 @@ public class UdfpsController implements DozeReceiver, Dumpable {
Log.v(TAG, "aod lock icon long-press rejected by the falsing manager.");
return;
}
- mKeyguardViewManager.showPrimaryBouncer(true);
+ mKeyguardViewManager.showPrimaryBouncer(true, "UdfpsController#onAodInterrupt");
// play the same haptic as the DeviceEntryIcon longpress
if (mOverlay != null && mOverlay.getTouchOverlay() != null) {
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractor.kt b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractor.kt
index 0c6d7920d7f3..48e08fcd90c5 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractor.kt
@@ -135,7 +135,7 @@ constructor(
// TODO(b/243695312): Encapsulate all of the show logic for the bouncer.
/** Show the bouncer if necessary and set the relevant states. */
@JvmOverloads
- fun show(isScrimmed: Boolean): Boolean {
+ fun show(isScrimmed: Boolean, reason: String): Boolean {
// When the scene container framework is enabled, instead of calling this, call
// SceneInteractor#changeScene(Scenes.Bouncer, ...).
SceneContainerFlag.assertInLegacyMode()
@@ -176,6 +176,7 @@ constructor(
return false
}
+ Log.i(TAG, "Show primary bouncer requested, reason: $reason")
repository.setPrimaryShowingSoon(true)
if (usePrimaryBouncerPassiveAuthDelay()) {
Log.d(TAG, "delay bouncer, passive auth may succeed")
diff --git a/packages/SystemUI/src/com/android/systemui/common/ui/compose/gestures/EagerTap.kt b/packages/SystemUI/src/com/android/systemui/common/ui/compose/gestures/EagerTap.kt
new file mode 100644
index 000000000000..078ea569a63c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/common/ui/compose/gestures/EagerTap.kt
@@ -0,0 +1,91 @@
+/*
+ * 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.common.ui.compose.gestures
+
+import androidx.compose.foundation.gestures.awaitEachGesture
+import androidx.compose.foundation.gestures.awaitFirstDown
+import androidx.compose.foundation.gestures.waitForUpOrCancellation
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.input.pointer.PointerInputChange
+import androidx.compose.ui.input.pointer.PointerInputScope
+import kotlinx.coroutines.coroutineScope
+
+/**
+ * Detects taps and double taps without waiting for the double tap minimum delay in between
+ *
+ * Using [detectTapGestures] with both a single tap and a double tap defined will send only one of
+ * these event per user interaction. This variant will send the single tap at all times, with the
+ * optional double tap if the user pressed a second time in a short period of time.
+ *
+ * Warning: Use this only if you know that reporting a single tap followed by a double tap won't be
+ * a problem in your use case.
+ *
+ * @param doubleTapEnabled whether this should listen for double tap events. This value is captured
+ * at the first down movement.
+ * @param onDoubleTap the double tap callback
+ * @param onTap the single tap callback
+ */
+suspend fun PointerInputScope.detectEagerTapGestures(
+ doubleTapEnabled: () -> Boolean,
+ onDoubleTap: (Offset) -> Unit,
+ onTap: () -> Unit,
+) = coroutineScope {
+ awaitEachGesture {
+ val down = awaitFirstDown()
+ down.consume()
+
+ // Capture whether double tap is enabled on first down as this state can change following
+ // the first tap
+ val isDoubleTapEnabled = doubleTapEnabled()
+
+ // wait for first tap up or long press
+ val upOrCancel = waitForUpOrCancellation()
+
+ if (upOrCancel != null) {
+ // tap was successful.
+ upOrCancel.consume()
+ onTap.invoke()
+
+ if (isDoubleTapEnabled) {
+ // check for second tap
+ val secondDown =
+ withTimeoutOrNull(viewConfiguration.doubleTapTimeoutMillis) {
+ val minUptime =
+ upOrCancel.uptimeMillis + viewConfiguration.doubleTapMinTimeMillis
+ var change: PointerInputChange
+ // The second tap doesn't count if it happens before DoubleTapMinTime of the
+ // first tap
+ do {
+ change = awaitFirstDown()
+ } while (change.uptimeMillis < minUptime)
+ change
+ }
+
+ if (secondDown != null) {
+ // Second tap down detected
+
+ // Might have a long second press as the second tap
+ val secondUp = waitForUpOrCancellation()
+ if (secondUp != null) {
+ secondUp.consume()
+ onDoubleTap(secondUp.position)
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalLockIconViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalLockIconViewModel.kt
index 19eeabd98c88..931639c8b247 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalLockIconViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalLockIconViewModel.kt
@@ -130,7 +130,9 @@ constructor(
if (SceneContainerFlag.isEnabled) {
deviceEntryInteractor.attemptDeviceEntry()
} else {
- keyguardViewController.get().showPrimaryBouncer(/* scrim */ true)
+ keyguardViewController
+ .get()
+ .showPrimaryBouncer(/* scrim */ true, "CommunalLockIconViewModel#onUserInteraction")
}
deviceEntrySourceInteractor.attemptEnterDeviceFromDeviceEntryIcon()
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
index 6db2ebc0df2c..099a7f067482 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
@@ -140,6 +140,7 @@ import com.android.systemui.animation.TransitionAnimator;
import com.android.systemui.broadcast.BroadcastDispatcher;
import com.android.systemui.classifier.FalsingCollector;
import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor;
+import com.android.systemui.communal.domain.interactor.CommunalSettingsInteractor;
import com.android.systemui.communal.ui.viewmodel.CommunalTransitionViewModel;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.dagger.qualifiers.UiBackground;
@@ -364,6 +365,7 @@ public class KeyguardViewMediator implements CoreStartable,
private final Lazy<NotificationShadeDepthController> mNotificationShadeDepthController;
private final Lazy<ShadeController> mShadeController;
private final Lazy<CommunalSceneInteractor> mCommunalSceneInteractor;
+ private final Lazy<CommunalSettingsInteractor> mCommunalSettingsInteractor;
/*
* Records the user id on request to go away, for validation when WM calls back to start the
* exit animation.
@@ -1567,6 +1569,7 @@ public class KeyguardViewMediator implements CoreStartable,
KeyguardInteractor keyguardInteractor,
KeyguardTransitionBootInteractor transitionBootInteractor,
Lazy<CommunalSceneInteractor> communalSceneInteractor,
+ Lazy<CommunalSettingsInteractor> communalSettingsInteractor,
WindowManagerOcclusionManager wmOcclusionManager) {
mContext = context;
mUserTracker = userTracker;
@@ -1609,6 +1612,7 @@ public class KeyguardViewMediator implements CoreStartable,
mKeyguardInteractor = keyguardInteractor;
mTransitionBootInteractor = transitionBootInteractor;
mCommunalSceneInteractor = communalSceneInteractor;
+ mCommunalSettingsInteractor = communalSettingsInteractor;
mStatusBarStateController = statusBarStateController;
statusBarStateController.addCallback(this);
@@ -2429,9 +2433,18 @@ public class KeyguardViewMediator implements CoreStartable,
private void doKeyguardLocked(Bundle options) {
// If the power button behavior requests to open the glanceable hub.
if (options != null && options.getBoolean(EXTRA_TRIGGER_HUB)) {
- // Set the hub to show immediately when the SysUI window shows, then continue to lock
- // the device.
- mCommunalSceneInteractor.get().showHubFromPowerButton();
+ if (mCommunalSettingsInteractor.get().getAutoOpenEnabled().getValue()) {
+ // Set the hub to show immediately when the SysUI window shows, then continue to
+ // lock the device.
+ mCommunalSceneInteractor.get().showHubFromPowerButton();
+ } else {
+ // If the hub is not available, go to sleep instead of locking. This can happen
+ // because the power button behavior does not check all possible reasons the hub
+ // might be disabled.
+ mPM.goToSleep(android.os.SystemClock.uptimeMillis(),
+ PowerManager.GO_TO_SLEEP_REASON_POWER_BUTTON, 0);
+ return;
+ }
}
int currentUserId = mSelectedUserInteractor.getSelectedUserId();
@@ -3765,13 +3778,7 @@ public class KeyguardViewMediator implements CoreStartable,
Log.d(TAG, "Status bar manager is disabled for visible background users");
}
} else {
- try {
- mStatusBarService.disableForUser(flags, mStatusBarDisableToken,
- mContext.getPackageName(),
- mSelectedUserInteractor.getSelectedUserId());
- } catch (RemoteException e) {
- Log.d(TAG, "Failed to force clear flags", e);
- }
+ statusBarServiceDisableForUser(flags, "Failed to force clear flags");
}
}
@@ -3807,18 +3814,29 @@ public class KeyguardViewMediator implements CoreStartable,
// Handled in StatusBarDisableFlagsInteractor.
if (!KeyguardWmStateRefactor.isEnabled()) {
- try {
- mStatusBarService.disableForUser(flags, mStatusBarDisableToken,
- mContext.getPackageName(),
- mSelectedUserInteractor.getSelectedUserId());
- } catch (RemoteException e) {
- Log.d(TAG, "Failed to set disable flags: " + flags, e);
- }
+ statusBarServiceDisableForUser(flags, "Failed to set disable flags: ");
}
}
}
}
+ private void statusBarServiceDisableForUser(int flags, String loggingContext) {
+ Runnable runnable = () -> {
+ try {
+ mStatusBarService.disableForUser(flags, mStatusBarDisableToken,
+ mContext.getPackageName(),
+ mSelectedUserInteractor.getSelectedUserId());
+ } catch (RemoteException e) {
+ Log.d(TAG, loggingContext + " " + flags, e);
+ }
+ };
+ if (com.android.systemui.Flags.bouncerUiRevamp()) {
+ mUiBgExecutor.execute(runnable);
+ } else {
+ runnable.run();
+ }
+ }
+
/**
* Handle message sent by {@link #resetStateLocked}
* @see #RESET
@@ -4099,12 +4117,23 @@ public class KeyguardViewMediator implements CoreStartable,
|| aodShowing != mAodShowing || forceCallbacks;
mShowing = showing;
mAodShowing = aodShowing;
- if (notifyDefaultDisplayCallbacks) {
- notifyDefaultDisplayCallbacks(showing);
- }
- if (updateActivityLockScreenState) {
- updateActivityLockScreenState(showing, aodShowing, reason);
+
+ if (KeyguardWmReorderAtmsCalls.isEnabled()) {
+ if (updateActivityLockScreenState) {
+ updateActivityLockScreenState(showing, aodShowing, reason);
+ }
+ if (notifyDefaultDisplayCallbacks) {
+ notifyDefaultDisplayCallbacks(showing);
+ }
+ } else {
+ if (notifyDefaultDisplayCallbacks) {
+ notifyDefaultDisplayCallbacks(showing);
+ }
+ if (updateActivityLockScreenState) {
+ updateActivityLockScreenState(showing, aodShowing, reason);
+ }
}
+
}
private void notifyDefaultDisplayCallbacks(boolean showing) {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardWmReorderAtmsCalls.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardWmReorderAtmsCalls.kt
new file mode 100644
index 000000000000..7ac52813ff71
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardWmReorderAtmsCalls.kt
@@ -0,0 +1,53 @@
+/*
+ * 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.keyguard
+
+import com.android.systemui.Flags
+import com.android.systemui.flags.FlagToken
+import com.android.systemui.flags.RefactorFlagUtils
+
+/** Helper for reading or using the keyguard wm state refactor flag state. */
+@Suppress("NOTHING_TO_INLINE")
+object KeyguardWmReorderAtmsCalls {
+ /** The aconfig flag name */
+ const val FLAG_NAME = Flags.FLAG_KEYGUARD_WM_REORDER_ATMS_CALLS
+
+ /** A token used for dependency declaration */
+ val token: FlagToken
+ get() = FlagToken(FLAG_NAME, isEnabled)
+
+ /** Is the refactor enabled */
+ @JvmStatic
+ inline val isEnabled
+ get() = Flags.keyguardWmReorderAtmsCalls()
+
+ /**
+ * Called to ensure code is only run when the flag is enabled. This protects users from the
+ * unintended behaviors caused by accidentally running new logic, while also crashing on an eng
+ * build to ensure that the refactor author catches issues in testing.
+ */
+ @JvmStatic
+ inline fun isUnexpectedlyInLegacyMode() =
+ RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_NAME)
+
+ /**
+ * Called to ensure code is only run when the flag is disabled. This will throw an exception if
+ * the flag is enabled to ensure that the refactor author catches issues in testing.
+ */
+ @JvmStatic
+ inline fun assertInLegacyMode() = RefactorFlagUtils.assertInLegacyMode(isEnabled, FLAG_NAME)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardWmStateRefactor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardWmStateRefactor.kt
index ddccc5d9e96d..41d14b9e727f 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardWmStateRefactor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardWmStateRefactor.kt
@@ -20,7 +20,16 @@ import com.android.systemui.Flags
import com.android.systemui.flags.FlagToken
import com.android.systemui.flags.RefactorFlagUtils
-/** Helper for reading or using the keyguard wm state refactor flag state. */
+/**
+ * Helper for reading or using the keyguard_wm_state_refactor flag state.
+ *
+ * keyguard_wm_state_refactor works both with and without flexiglass (scene_container), but
+ * flexiglass requires keyguard_wm_state_refactor. For this reason, this class will return isEnabled
+ * if either keyguard_wm_state_refactor OR scene_container are enabled. This enables us to roll out
+ * keyguard_wm_state_refactor independently of scene_container, while also ensuring that
+ * scene_container rolling out ahead of keyguard_wm_state_refactor causes code gated by
+ * KeyguardWmStateRefactor to be enabled as well.
+ */
@Suppress("NOTHING_TO_INLINE")
object KeyguardWmStateRefactor {
/** The aconfig flag name */
@@ -30,10 +39,9 @@ object KeyguardWmStateRefactor {
val token: FlagToken
get() = FlagToken(FLAG_NAME, isEnabled)
- /** Is the refactor enabled */
@JvmStatic
inline val isEnabled
- get() = Flags.keyguardWmStateRefactor()
+ get() = Flags.keyguardWmStateRefactor() || Flags.sceneContainer()
/**
* 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/keyguard/dagger/KeyguardModule.java b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java
index 6b1248b6983e..1fe6eb9ce7c8 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java
@@ -42,6 +42,7 @@ import com.android.systemui.broadcast.BroadcastDispatcher;
import com.android.systemui.classifier.FalsingCollector;
import com.android.systemui.classifier.FalsingModule;
import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor;
+import com.android.systemui.communal.domain.interactor.CommunalSettingsInteractor;
import com.android.systemui.communal.ui.viewmodel.CommunalTransitionViewModel;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dagger.qualifiers.Main;
@@ -182,6 +183,7 @@ public interface KeyguardModule {
KeyguardInteractor keyguardInteractor,
KeyguardTransitionBootInteractor transitionBootInteractor,
Lazy<CommunalSceneInteractor> communalSceneInteractor,
+ Lazy<CommunalSettingsInteractor> communalSettingsInteractor,
WindowManagerOcclusionManager windowManagerOcclusionManager) {
return new KeyguardViewMediator(
context,
@@ -234,6 +236,7 @@ public interface KeyguardModule {
keyguardInteractor,
transitionBootInteractor,
communalSceneInteractor,
+ communalSettingsInteractor,
windowManagerOcclusionManager);
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt
index f8c7a86687dd..f4e804ac5abf 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt
@@ -24,6 +24,7 @@ import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor
import com.android.systemui.communal.domain.interactor.CommunalSettingsInteractor
import com.android.systemui.communal.shared.model.CommunalScenes
import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.keyguard.KeyguardWmStateRefactor
@@ -62,6 +63,7 @@ constructor(
override val internalTransitionInteractor: InternalKeyguardTransitionInteractor,
transitionInteractor: KeyguardTransitionInteractor,
@Background private val scope: CoroutineScope,
+ @Application private val applicationScope: CoroutineScope,
@Background bgDispatcher: CoroutineDispatcher,
@Main mainDispatcher: CoroutineDispatcher,
keyguardInteractor: KeyguardInteractor,
@@ -175,7 +177,7 @@ constructor(
private fun listenForLockscreenToPrimaryBouncerDragging() {
if (SceneContainerFlag.isEnabled) return
var transitionId: UUID? = null
- scope.launch("$TAG#listenForLockscreenToPrimaryBouncerDragging") {
+ applicationScope.launch("$TAG#listenForLockscreenToPrimaryBouncerDragging") {
shadeRepository.legacyShadeExpansion.collect { shadeExpansion ->
val statusBarState = keyguardInteractor.statusBarState.value
val isKeyguardUnlocked = keyguardInteractor.isKeyguardDismissible.value
@@ -204,7 +206,7 @@ constructor(
id,
// This maps the shadeExpansion to a much faster curve, to match
// the existing logic
- 1f - MathUtils.constrainedMap(0f, 1f, 0.95f, 1f, shadeExpansion),
+ 1f - MathUtils.constrainedMap(0f, 1f, 0.88f, 1f, shadeExpansion),
nextState,
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissInteractor.kt
index 0a4022ad4de8..e6f8406726f0 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissInteractor.kt
@@ -159,7 +159,10 @@ constructor(
if (alternateBouncerInteractor.canShowAlternateBouncer.value) {
alternateBouncerInteractor.forceShow()
} else {
- primaryBouncerInteractor.show(true)
+ primaryBouncerInteractor.show(
+ true,
+ "KeyguardDismissInteractor#dismissKeyguardWithCallback",
+ )
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
index 7977000ed5c8..2d5ff61a5015 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
@@ -19,7 +19,6 @@ import android.app.StatusBarManager
import android.graphics.Point
import android.util.Log
import android.util.MathUtils
-import com.android.app.animation.Interpolators
import com.android.systemui.bouncer.data.repository.KeyguardBouncerRepository
import com.android.systemui.common.shared.model.NotificationContainerBounds
import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor
@@ -371,9 +370,11 @@ constructor(
currentKeyguardState == LOCKSCREEN &&
legacyShadeExpansion != 1f
) {
- emit(MathUtils.constrainedMap(0f, 1f, 0.95f, 1f, legacyShadeExpansion))
+ emit(MathUtils.constrainedMap(0f, 1f, 0.82f, 1f, legacyShadeExpansion))
} else if (
- (legacyShadeExpansion == 0f || legacyShadeExpansion == 1f) && !onGlanceableHub
+ !onGlanceableHub &&
+ isKeyguardDismissible &&
+ (legacyShadeExpansion == 0f || legacyShadeExpansion == 1f)
) {
// Resets alpha state
emit(1f)
@@ -401,15 +402,7 @@ constructor(
// 0f and 1f need to be ignored in the legacy shade expansion. These can
// flip arbitrarily as the legacy shade is reset, and would cause the
// translation value to jump around unexpectedly.
- emit(
- MathUtils.lerp(
- translationDistance,
- 0,
- Interpolators.FAST_OUT_LINEAR_IN.getInterpolation(
- legacyShadeExpansion
- ),
- )
- )
+ emit(MathUtils.lerp(translationDistance, 0, legacyShadeExpansion))
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardKeyEventInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardKeyEventInteractor.kt
index 6d9b276031e9..ced96e93d87d 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardKeyEventInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardKeyEventInteractor.kt
@@ -136,7 +136,10 @@ constructor(
return true
}
StatusBarState.KEYGUARD -> {
- statusBarKeyguardViewManager.showPrimaryBouncer(true)
+ statusBarKeyguardViewManager.showPrimaryBouncer(
+ true,
+ "KeyguardKeyEventInteractor#collapseShadeLockedOrShowPrimaryBouncer",
+ )
return true
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
index af58d10f3066..df58b215167a 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
@@ -20,6 +20,8 @@ package com.android.systemui.keyguard.domain.interactor
import android.annotation.SuppressLint
import android.util.Log
import com.android.app.tracing.coroutines.flow.filterTraced
+import com.android.app.tracing.coroutines.flow.shareInTraced
+import com.android.app.tracing.coroutines.flow.stateInTraced
import com.android.app.tracing.coroutines.flow.traceAs
import com.android.app.tracing.coroutines.launchTraced as launch
import com.android.app.tracing.coroutines.traceCoroutine
@@ -64,8 +66,6 @@ import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.onStart
-import kotlinx.coroutines.flow.shareIn
-import kotlinx.coroutines.flow.stateIn
/** Encapsulates business-logic related to the keyguard transitions. */
@SysUISingleton
@@ -102,12 +102,18 @@ constructor(
val transitions = repository.transitions
val transitionState: StateFlow<TransitionStep> =
- transitions.stateIn(scope, SharingStarted.Eagerly, TransitionStep())
+ transitions.stateInTraced(
+ "KTF-transitionState",
+ scope,
+ SharingStarted.Eagerly,
+ TransitionStep(),
+ )
private val sceneTransitionPair =
sceneInteractor.transitionState
.pairwise()
- .stateIn(
+ .stateInTraced(
+ "KTF-sceneTransitionPair",
scope,
SharingStarted.Eagerly,
WithPrev(
@@ -130,11 +136,16 @@ constructor(
repository.transitions
.pairwise()
.filter { it.newValue.transitionState == TransitionState.STARTED }
- .shareIn(scope, SharingStarted.Eagerly, replay = 1)
+ .shareInTraced(
+ "KTF-startedStepWithPrecedingStep",
+ scope,
+ SharingStarted.Eagerly,
+ replay = 1,
+ )
init {
// Collect non-canceled steps and emit transition values.
- scope.launch {
+ scope.launch("KTF-update-non-canceled") {
repository.transitions
.filter { it.transitionState != TransitionState.CANCELED }
.collect { step ->
@@ -145,7 +156,7 @@ constructor(
}
}
- scope.launch {
+ scope.launch("KTF-update-transitionMap") {
repository.transitions.collect {
// FROM->TO
transitionMap[Edge.create(it.from, it.to)]?.emit(it)
@@ -160,7 +171,7 @@ constructor(
// need to ensure we emit transitionValue(A) = 0f, since no further steps will be emitted
// where the from or to states are A. This would leave transitionValue(A) stuck at an
// arbitrary non-zero value.
- scope.launch {
+ scope.launch("KTF-update-canceled") {
startedStepWithPrecedingStep.collect { (prevStep, startedStep) ->
if (
prevStep.transitionState == TransitionState.CANCELED &&
@@ -180,7 +191,7 @@ constructor(
// Safety: When any transition is FINISHED, ensure all other transitionValue flows other
// than the FINISHED state are reset to a value of 0f. There have been rare but severe
// bugs that get the device stuck in a bad state when these are not properly reset.
- scope.launch {
+ scope.launch("KTF-update-finished") {
repository.transitions
.filter { it.transitionState == TransitionState.FINISHED }
.collect {
@@ -201,7 +212,7 @@ constructor(
* If the screen is turning off, finish the current transition immediately. Further
* frames won't be visible anyway.
*/
- scope.launch {
+ scope.launch("KTF-force-finish") {
powerInteractor.screenPowerState
.filter { it == ScreenPowerState.SCREEN_TURNING_OFF }
.collect { repository.forceFinishCurrentTransition() }
@@ -347,6 +358,7 @@ constructor(
}
}
}
+ .traceAs("KTF-transition-simulator")
/**
* This function is similar to flatMapLatest but it will additionally emit a FINISHED
@@ -372,7 +384,7 @@ constructor(
traceCoroutine("cancelAndJoin") { job?.cancelAndJoin() }
job =
- launch("inner") {
+ launch("KTF-flatMapLatestWithFinished") {
val innerFlow = transform(value)
try {
innerFlow.collect { step ->
@@ -398,7 +410,6 @@ constructor(
}
}
}
- .traceAs("flatMapLatestWithFinished")
/**
* Converts old KTF states to UNDEFINED when [SceneContainerFlag] is enabled.
@@ -451,7 +462,12 @@ constructor(
val startedKeyguardTransitionStep: StateFlow<TransitionStep> =
repository.transitions
.filter { step -> step.transitionState == TransitionState.STARTED }
- .stateIn(scope, SharingStarted.Eagerly, TransitionStep())
+ .stateInTraced(
+ "KTF-startedKeyguardTransitionStep",
+ scope,
+ SharingStarted.Eagerly,
+ TransitionStep(),
+ )
/**
* The [KeyguardState] we're currently in.
@@ -517,7 +533,7 @@ constructor(
it.from
}
}
- .stateIn(scope, SharingStarted.Eagerly, OFF)
+ .stateInTraced("KTF-currentKeyguardState", scope, SharingStarted.Eagerly, OFF)
val isInTransition =
combine(isInTransitionWhere({ true }, { true }), sceneInteractor.transitionState) {
@@ -629,7 +645,7 @@ constructor(
repository.transitions
.filter { it.transitionState == TransitionState.FINISHED }
.map { it.to }
- .stateIn(scope, SharingStarted.Eagerly, OFF)
+ .stateInTraced("KTF-finishedKeyguardState", scope, SharingStarted.Eagerly, OFF)
companion object {
private val TAG = KeyguardTransitionInteractor::class.simpleName
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractor.kt
index 68d595ebf0b6..b4e9d8296a74 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractor.kt
@@ -196,39 +196,50 @@ constructor(
.distinctUntilChanged()
}
- private val lockscreenVisibilityWithScenes =
- combine(
- sceneInteractor.get().transitionState.flatMapLatestConflated {
- when (it) {
- is Idle -> {
- when (it.currentScene) {
- in keyguardContent -> flowOf(true)
- in nonKeyguardContent -> flowOf(false)
- in keyguardAgnosticContent -> isDeviceNotEnteredDirectly
- else ->
- throw IllegalStateException("Unknown scene: ${it.currentScene}")
- }
- }
- is Transition -> {
- when {
- it.isTransitioningSets(from = keyguardContent) -> flowOf(true)
- it.isTransitioningSets(from = nonKeyguardContent) -> flowOf(false)
- it.isTransitioningSets(from = keyguardAgnosticContent) ->
- isDeviceNotEnteredDirectly
- else ->
- throw IllegalStateException(
- "Unknown content: ${it.fromContent}"
- )
+ private val lockscreenVisibilityWithScenes: Flow<Boolean> =
+ // The scene container visibility into account as that will be forced to false when the
+ // device isn't yet provisioned (e.g. still in the setup wizard).
+ sceneInteractor.get().isVisible.flatMapLatestConflated { isVisible ->
+ if (isVisible) {
+ combine(
+ sceneInteractor.get().transitionState.flatMapLatestConflated {
+ when (it) {
+ is Idle ->
+ when (it.currentScene) {
+ in keyguardContent -> flowOf(true)
+ in nonKeyguardContent -> flowOf(false)
+ in keyguardAgnosticContent -> isDeviceNotEnteredDirectly
+ else ->
+ throw IllegalStateException(
+ "Unknown scene: ${it.currentScene}"
+ )
+ }
+ is Transition ->
+ when {
+ it.isTransitioningSets(from = keyguardContent) ->
+ flowOf(true)
+ it.isTransitioningSets(from = nonKeyguardContent) ->
+ flowOf(false)
+ it.isTransitioningSets(from = keyguardAgnosticContent) ->
+ isDeviceNotEnteredDirectly
+ else ->
+ throw IllegalStateException(
+ "Unknown content: ${it.fromContent}"
+ )
+ }
}
- }
+ },
+ wakeToGoneInteractor.canWakeDirectlyToGone,
+ ::Pair,
+ )
+ .map { (lockscreenVisibilityByTransitionState, canWakeDirectlyToGone) ->
+ lockscreenVisibilityByTransitionState && !canWakeDirectlyToGone
}
- },
- wakeToGoneInteractor.canWakeDirectlyToGone,
- ::Pair,
- )
- .map { (lockscreenVisibilityByTransitionState, canWakeDirectlyToGone) ->
- lockscreenVisibilityByTransitionState && !canWakeDirectlyToGone
+ } else {
+ // Lockscreen is never visible when the scene container is invisible.
+ flowOf(false)
}
+ }
private val lockscreenVisibilityLegacy =
combine(
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt
index 70a827d5e45b..1ea47ec670af 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt
@@ -39,6 +39,7 @@ import com.android.systemui.plugins.FalsingManager
import com.android.systemui.res.R
import com.android.systemui.statusbar.VibratorHelper
import com.android.systemui.util.kotlin.DisposableHandles
+import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DisposableHandle
@@ -56,6 +57,7 @@ object DeviceEntryIconViewBinder {
@JvmStatic
fun bind(
applicationScope: CoroutineScope,
+ mainImmediateDispatcher: CoroutineDispatcher,
view: DeviceEntryIconView,
viewModel: DeviceEntryIconViewModel,
fgViewModel: DeviceEntryForegroundViewModel,
@@ -96,6 +98,32 @@ object DeviceEntryIconViewBinder {
}
disposables +=
+ view.repeatWhenAttached(mainImmediateDispatcher) {
+ repeatOnLifecycle(Lifecycle.State.CREATED) {
+ launch("$TAG#viewModel.useBackgroundProtection") {
+ viewModel.useBackgroundProtection.collect { useBackgroundProtection ->
+ if (useBackgroundProtection) {
+ bgView.visibility = View.VISIBLE
+ } else {
+ bgView.visibility = View.GONE
+ }
+ }
+ }
+ launch("$TAG#viewModel.burnInOffsets") {
+ viewModel.burnInOffsets.collect { burnInOffsets ->
+ view.translationX = burnInOffsets.x.toFloat()
+ view.translationY = burnInOffsets.y.toFloat()
+ view.aodFpDrawable.progress = burnInOffsets.progress
+ }
+ }
+
+ launch("$TAG#viewModel.deviceEntryViewAlpha") {
+ viewModel.deviceEntryViewAlpha.collect { alpha -> view.alpha = alpha }
+ }
+ }
+ }
+
+ disposables +=
view.repeatWhenAttached {
// Repeat on CREATED so that the view will always observe the entire
// GONE => AOD transition (even though the view may not be visible until the middle
@@ -152,26 +180,6 @@ object DeviceEntryIconViewBinder {
}
}
}
- launch("$TAG#viewModel.useBackgroundProtection") {
- viewModel.useBackgroundProtection.collect { useBackgroundProtection ->
- if (useBackgroundProtection) {
- bgView.visibility = View.VISIBLE
- } else {
- bgView.visibility = View.GONE
- }
- }
- }
- launch("$TAG#viewModel.burnInOffsets") {
- viewModel.burnInOffsets.collect { burnInOffsets ->
- view.translationX = burnInOffsets.x.toFloat()
- view.translationY = burnInOffsets.y.toFloat()
- view.aodFpDrawable.progress = burnInOffsets.progress
- }
- }
-
- launch("$TAG#viewModel.deviceEntryViewAlpha") {
- viewModel.deviceEntryViewAlpha.collect { alpha -> view.alpha = alpha }
- }
}
}
@@ -212,7 +220,7 @@ object DeviceEntryIconViewBinder {
}
disposables +=
- bgView.repeatWhenAttached {
+ bgView.repeatWhenAttached(mainImmediateDispatcher) {
repeatOnLifecycle(Lifecycle.State.CREATED) {
launch("$TAG#bgViewModel.alpha") {
bgViewModel.alpha.collect { alpha -> bgView.alpha = alpha }
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 aeb327035c79..60460bf68c12 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
@@ -135,7 +135,10 @@ object KeyguardRootViewBinder {
} else if (
event.action == MotionEvent.ACTION_UP && !event.isTouchscreenSource()
) {
- statusBarKeyguardViewManager?.showBouncer(true)
+ statusBarKeyguardViewManager?.showBouncer(
+ true,
+ "KeyguardRootViewBinder: click on lockscreen",
+ )
consumed = true
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySection.kt
index 58d482b8a66f..9c8f04b419fb 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySection.kt
@@ -28,6 +28,7 @@ import androidx.constraintlayout.widget.ConstraintSet
import com.android.systemui.biometrics.AuthController
import com.android.systemui.customization.R as customR
import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags
import com.android.systemui.keyguard.shared.model.KeyguardSection
@@ -48,6 +49,7 @@ import com.android.systemui.shade.ShadeDisplayAware
import com.android.systemui.statusbar.VibratorHelper
import dagger.Lazy
import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DisposableHandle
@@ -56,6 +58,7 @@ class DefaultDeviceEntrySection
@Inject
constructor(
@Application private val applicationScope: CoroutineScope,
+ @Main private val mainDispatcher: CoroutineDispatcher,
private val authController: AuthController,
private val windowManager: WindowManager,
@ShadeDisplayAware private val context: Context,
@@ -91,6 +94,7 @@ constructor(
disposableHandle =
DeviceEntryIconViewBinder.bind(
applicationScope,
+ mainDispatcher,
it,
deviceEntryIconViewModel.get(),
deviceEntryForegroundViewModel.get(),
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerUdfpsIconViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerUdfpsIconViewModel.kt
index 9038922466df..803e2c0b0f96 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerUdfpsIconViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerUdfpsIconViewModel.kt
@@ -106,7 +106,10 @@ constructor(
}
fun onTapped() {
- statusBarKeyguardViewManager.showPrimaryBouncer(/* scrimmed */ true)
+ statusBarKeyguardViewManager.showPrimaryBouncer(
+ /* scrimmed */ true,
+ "AlternateBouncerUdfpsIconViewModel#onTapped",
+ )
}
val bgColor: Flow<Int> = deviceEntryBackgroundViewModel.color
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModel.kt
index cff651114c93..45f43bb484c8 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModel.kt
@@ -47,7 +47,9 @@ constructor(
/** Reports the alternate bouncer visible state if the scene container flag is enabled. */
val isVisible: Flow<Boolean> =
- alternateBouncerInteractor.get().isVisible.onEach { SceneContainerFlag.unsafeAssertInNewMode() }
+ alternateBouncerInteractor.get().isVisible.onEach {
+ SceneContainerFlag.unsafeAssertInNewMode()
+ }
/** Progress to a fully transitioned alternate bouncer. 1f represents fully transitioned. */
val transitionToAlternateBouncerProgress: Flow<Float> =
@@ -63,7 +65,10 @@ constructor(
transitionToAlternateBouncerProgress.map { it == 1f }.distinctUntilChanged()
fun onTapped() {
- statusBarKeyguardViewManager.showPrimaryBouncer(/* scrimmed */ true)
+ statusBarKeyguardViewManager.showPrimaryBouncer(
+ /* scrimmed */ true,
+ "AlternateBouncerViewModel#onTapped",
+ )
}
fun onRemovedFromWindow() {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModel.kt
index 13cd5839e1c8..9b4bd67f227e 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModel.kt
@@ -257,7 +257,9 @@ constructor(
if (SceneContainerFlag.isEnabled) {
deviceEntryInteractor.attemptDeviceEntry()
} else {
- keyguardViewController.get().showPrimaryBouncer(/* scrim */ true)
+ keyguardViewController
+ .get()
+ .showPrimaryBouncer(/* scrim */ true, "DeviceEntryIconViewModel#onUserInteraction")
}
deviceEntrySourceInteractor.attemptEnterDeviceFromDeviceEntryIcon()
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModel.kt
index 9312bca04994..a0458f0172f5 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModel.kt
@@ -64,7 +64,7 @@ constructor(
val shortcutsAlpha: Flow<Float> =
transitionAnimation.sharedFlow(
- duration = FromLockscreenTransitionInteractor.TO_PRIMARY_BOUNCER_DURATION,
+ duration = 200.milliseconds,
onStep = alphaForAnimationStep,
// Rapid swipes to bouncer, and may end up skipping intermediate values that would've
// caused a complete fade out of lockscreen elements. Ensure it goes to 0f.
diff --git a/packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/DismissibleHorizontalPager.kt b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/DismissibleHorizontalPager.kt
deleted file mode 100644
index 8df916fe6969..000000000000
--- a/packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/DismissibleHorizontalPager.kt
+++ /dev/null
@@ -1,160 +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.media.remedia.ui.compose
-
-import androidx.compose.animation.core.Animatable
-import androidx.compose.animation.core.AnimationVector1D
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.BoxScope
-import androidx.compose.foundation.pager.HorizontalPager
-import androidx.compose.foundation.pager.PagerScope
-import androidx.compose.foundation.pager.PagerState
-import androidx.compose.foundation.pager.rememberPagerState
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.rememberCoroutineScope
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.graphics.graphicsLayer
-import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
-import androidx.compose.ui.input.nestedscroll.NestedScrollSource
-import androidx.compose.ui.input.nestedscroll.nestedScroll
-import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.Velocity
-import androidx.compose.ui.unit.dp
-import com.android.compose.modifiers.thenIf
-import kotlinx.coroutines.launch
-
-/** State for a [DismissibleHorizontalPager] */
-class DismissibleHorizontalPagerState(
- val isDismissible: Boolean,
- val isScrollingEnabled: Boolean,
- val pagerState: PagerState,
- val offset: Animatable<Float, AnimationVector1D>,
-)
-
-/**
- * Returns a remembered [DismissibleHorizontalPagerState] that starts at [initialPage] and has
- * [pageCount] total pages.
- */
-@Composable
-fun rememberDismissibleHorizontalPagerState(
- isDismissible: Boolean = true,
- isScrollingEnabled: Boolean = true,
- initialPage: Int = 0,
- pageCount: () -> Int,
-): DismissibleHorizontalPagerState {
- val pagerState = rememberPagerState(initialPage = initialPage, pageCount = pageCount)
- val offset = remember { Animatable(0f) }
-
- return remember(isDismissible, isScrollingEnabled, pagerState, offset) {
- DismissibleHorizontalPagerState(
- isDismissible = isDismissible,
- isScrollingEnabled = isScrollingEnabled,
- pagerState = pagerState,
- offset = offset,
- )
- }
-}
-
-/**
- * A [HorizontalPager] that can be swiped-away to dismiss by the user when swiped farther left or
- * right once fully scrolled to the left-most or right-most page, respectively.
- */
-@Composable
-fun DismissibleHorizontalPager(
- state: DismissibleHorizontalPagerState,
- onDismissed: () -> Unit,
- modifier: Modifier = Modifier,
- key: ((Int) -> Any)? = null,
- pageSpacing: Dp = 0.dp,
- isFalseTouchDetected: Boolean,
- indicator: @Composable BoxScope.() -> Unit,
- pageContent: @Composable PagerScope.(page: Int) -> Unit,
-) {
- val scope = rememberCoroutineScope()
-
- val nestedScrollConnection = remember {
- object : NestedScrollConnection {
- override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
- return if (state.offset.value > 0f && available.x < 0f) {
- scope.launch { state.offset.snapTo(state.offset.value + available.x) }
- Offset(available.x, 0f)
- } else if (state.offset.value < 0f && available.x > 0f) {
- scope.launch { state.offset.snapTo(state.offset.value + available.x) }
- Offset(available.x, 0f)
- } else {
- Offset.Zero
- }
- }
-
- override fun onPostScroll(
- consumed: Offset,
- available: Offset,
- source: NestedScrollSource,
- ): Offset {
- return if (available.x > 0f) {
- scope.launch { state.offset.snapTo(state.offset.value + available.x) }
- Offset(available.x, 0f)
- } else if (available.x < 0f) {
- scope.launch { state.offset.snapTo(state.offset.value + available.x) }
- Offset(available.x, 0f)
- } else {
- Offset.Zero
- }
- }
-
- override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
- scope.launch {
- state.offset.animateTo(
- if (state.offset.value >= state.pagerState.layoutInfo.pageSize / 2f) {
- state.pagerState.layoutInfo.pageSize * 2f
- } else if (
- state.offset.value <= -state.pagerState.layoutInfo.pageSize / 2f
- ) {
- -state.pagerState.layoutInfo.pageSize * 2f
- } else {
- 0f
- }
- )
- if (state.offset.value != 0f) {
- onDismissed()
- }
- }
- return super.onPostFling(consumed, available)
- }
- }
- }
-
- Box(modifier = modifier) {
- HorizontalPager(
- state = state.pagerState,
- userScrollEnabled = state.isScrollingEnabled && !isFalseTouchDetected,
- key = key,
- pageSpacing = pageSpacing,
- pageContent = pageContent,
- modifier =
- Modifier.thenIf(state.isDismissible) {
- Modifier.nestedScroll(nestedScrollConnection).graphicsLayer {
- translationX = state.offset.value
- }
- },
- )
-
- indicator()
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/Media.kt b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/Media.kt
index f07238895aa5..d6d185195c51 100644
--- a/packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/Media.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/Media.kt
@@ -33,6 +33,7 @@ import androidx.compose.animation.graphics.vector.AnimatedImageVector
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.Image
+import androidx.compose.foundation.OverscrollEffect
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable
@@ -54,6 +55,8 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.pager.HorizontalPager
+import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ButtonDefaults
@@ -107,7 +110,9 @@ import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
+import androidx.compose.ui.util.fastCoerceIn
import androidx.compose.ui.util.fastForEach
import androidx.compose.ui.util.fastForEachIndexed
import androidx.compose.ui.util.fastRoundToInt
@@ -120,6 +125,7 @@ import com.android.compose.animation.scene.SceneKey
import com.android.compose.animation.scene.SceneTransitionLayout
import com.android.compose.animation.scene.rememberMutableSceneTransitionLayoutState
import com.android.compose.animation.scene.transitions
+import com.android.compose.gesture.effect.rememberOffsetOverscrollEffect
import com.android.compose.ui.graphics.painter.rememberDrawablePainter
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.common.ui.compose.Icon
@@ -137,7 +143,9 @@ import com.android.systemui.media.remedia.ui.viewmodel.MediaNavigationViewModel
import com.android.systemui.media.remedia.ui.viewmodel.MediaOutputSwitcherChipViewModel
import com.android.systemui.media.remedia.ui.viewmodel.MediaPlayPauseActionViewModel
import com.android.systemui.media.remedia.ui.viewmodel.MediaSecondaryActionViewModel
+import com.android.systemui.media.remedia.ui.viewmodel.MediaSettingsButtonViewModel
import com.android.systemui.media.remedia.ui.viewmodel.MediaViewModel
+import kotlin.math.abs
import kotlin.math.max
import kotlin.math.min
@@ -208,43 +216,16 @@ private fun CardCarouselContent(
onDismissed: () -> Unit,
modifier: Modifier = Modifier,
) {
- val pagerState =
- rememberDismissibleHorizontalPagerState(
- isDismissible = behavior.isCarouselDismissible,
- isScrollingEnabled = behavior.isCarouselScrollingEnabled,
- ) {
- viewModel.cards.size
- }
+ val pagerState = rememberPagerState { viewModel.cards.size }
+ LaunchedEffect(pagerState.currentPage) { viewModel.onCardSelected(pagerState.currentPage) }
+
var isFalseTouchDetected: Boolean by
remember(behavior.isCarouselScrollFalseTouch) { mutableStateOf(false) }
+ val isSwipingEnabled = behavior.isCarouselScrollingEnabled && !isFalseTouchDetected
val roundedCornerShape = RoundedCornerShape(32.dp)
- LaunchedEffect(pagerState.pagerState.currentPage) {
- viewModel.onCardSelected(pagerState.pagerState.currentPage)
- }
-
- DismissibleHorizontalPager(
- state = pagerState,
- onDismissed = onDismissed,
- pageSpacing = 8.dp,
- key = { index -> viewModel.cards[index].key },
- indicator = {
- if (pagerState.pagerState.pageCount > 1) {
- PagerDots(
- pagerState = pagerState.pagerState,
- activeColor = Color(0xffdee0ff),
- nonActiveColor = Color(0xffa7a9ca),
- dotSize = 6.dp,
- spaceSize = 6.dp,
- modifier =
- Modifier.align(Alignment.BottomCenter).padding(8.dp).graphicsLayer {
- translationX = pagerState.offset.value
- },
- )
- }
- },
- isFalseTouchDetected = isFalseTouchDetected,
+ Box(
modifier =
modifier.padding(8.dp).clip(roundedCornerShape).pointerInput(behavior) {
if (behavior.isCarouselScrollFalseTouch != null) {
@@ -253,13 +234,54 @@ private fun CardCarouselContent(
isFalseTouchDetected = behavior.isCarouselScrollFalseTouch.invoke()
}
}
- },
- ) { index ->
- Card(
- viewModel = viewModel.cards[index],
- presentationStyle = presentationStyle,
- modifier = Modifier.clip(roundedCornerShape),
- )
+ }
+ ) {
+ @Composable
+ fun PagerContent(overscrollEffect: OverscrollEffect? = null) {
+ Box {
+ HorizontalPager(
+ state = pagerState,
+ userScrollEnabled = isSwipingEnabled,
+ pageSpacing = 8.dp,
+ key = { index: Int -> viewModel.cards[index].key },
+ overscrollEffect = overscrollEffect ?: rememberOffsetOverscrollEffect(),
+ ) { pageIndex: Int ->
+ Card(
+ viewModel = viewModel.cards[pageIndex],
+ presentationStyle = presentationStyle,
+ modifier = Modifier.clip(roundedCornerShape),
+ )
+ }
+
+ if (pagerState.pageCount > 1) {
+ PagerDots(
+ pagerState = pagerState,
+ activeColor = Color(0xffdee0ff),
+ nonActiveColor = Color(0xffa7a9ca),
+ dotSize = 6.dp,
+ spaceSize = 6.dp,
+ modifier = Modifier.align(Alignment.BottomCenter).padding(8.dp),
+ )
+ }
+ }
+ }
+
+ if (behavior.isCarouselDismissible) {
+ SwipeToDismiss(content = { PagerContent() }, onDismissed = onDismissed)
+ } else {
+ val overscrollEffect = rememberOffsetOverscrollEffect()
+ SwipeToReveal(
+ foregroundContent = { PagerContent(overscrollEffect) },
+ foregroundContentEffect = overscrollEffect,
+ revealedContent = { revealAmount ->
+ RevealedContent(
+ viewModel = viewModel.settingsButtonViewModel,
+ revealAmount = revealAmount,
+ )
+ },
+ isSwipingEnabled = isSwipingEnabled,
+ )
+ }
}
}
@@ -496,16 +518,19 @@ private fun ContentScope.CardForegroundContent(
modifier = Modifier.weight(1f).padding(end = 8.dp),
)
+ val playPauseSize = DpSize(width = 48.dp, height = 48.dp)
if (viewModel.actionButtonLayout == MediaCardActionButtonLayout.WithPlayPause) {
AnimatedVisibility(visible = viewModel.playPauseAction != null) {
PlayPauseAction(
viewModel = checkNotNull(viewModel.playPauseAction),
- buttonWidth = 48.dp,
+ buttonSize = playPauseSize,
buttonColor = colorScheme.primary,
iconColor = colorScheme.onPrimary,
buttonCornerRadius = { isPlaying -> if (isPlaying) 16.dp else 48.dp },
)
}
+ } else {
+ Spacer(Modifier.size(playPauseSize))
}
}
@@ -565,14 +590,19 @@ private fun ContentScope.CardForegroundContent(
}
}
- AnimatedVisibility(visible = viewModel.playPauseAction != null) {
- PlayPauseAction(
- viewModel = checkNotNull(viewModel.playPauseAction),
- buttonWidth = 48.dp,
- buttonColor = colorScheme.primary,
- iconColor = colorScheme.onPrimary,
- buttonCornerRadius = { isPlaying -> if (isPlaying) 16.dp else 48.dp },
- )
+ val playPauseSize = DpSize(width = 48.dp, height = 48.dp)
+ if (viewModel.actionButtonLayout == MediaCardActionButtonLayout.WithPlayPause) {
+ AnimatedVisibility(visible = viewModel.playPauseAction != null) {
+ PlayPauseAction(
+ viewModel = checkNotNull(viewModel.playPauseAction),
+ buttonSize = playPauseSize,
+ buttonColor = colorScheme.primary,
+ iconColor = colorScheme.onPrimary,
+ buttonCornerRadius = { isPlaying -> if (isPlaying) 16.dp else 48.dp },
+ )
+ }
+ } else {
+ Spacer(Modifier.size(playPauseSize))
}
}
}
@@ -628,7 +658,7 @@ private fun ContentScope.CompactCardForeground(
AnimatedVisibility(visible = viewModel.playPauseAction != null) {
PlayPauseAction(
viewModel = checkNotNull(viewModel.playPauseAction),
- buttonWidth = 72.dp,
+ buttonSize = DpSize(width = 72.dp, height = 48.dp),
buttonColor = MaterialTheme.colorScheme.primaryContainer,
iconColor = MaterialTheme.colorScheme.onPrimaryContainer,
buttonCornerRadius = { isPlaying -> if (isPlaying) 16.dp else 24.dp },
@@ -1084,7 +1114,7 @@ private fun OutputSwitcherChip(
@Composable
private fun ContentScope.PlayPauseAction(
viewModel: MediaPlayPauseActionViewModel,
- buttonWidth: Dp,
+ buttonSize: DpSize,
buttonColor: Color,
iconColor: Color,
buttonCornerRadius: (isPlaying: Boolean) -> Dp,
@@ -1102,7 +1132,7 @@ private fun ContentScope.PlayPauseAction(
enabled = viewModel.onClick != null,
colors = ButtonDefaults.buttonColors(containerColor = buttonColor),
shape = RoundedCornerShape(cornerRadius),
- modifier = Modifier.size(width = buttonWidth, height = 48.dp),
+ modifier = Modifier.size(buttonSize),
) {
when (viewModel.state) {
is MediaSessionState.Playing,
@@ -1186,6 +1216,65 @@ private fun SecondaryActionContent(
}
}
+/**
+ * Renders the revealed content on the sides of the horizontal pager.
+ *
+ * @param revealAmount A callback that can return the amount of revealing done. This value will be
+ * in a range slightly larger than `-1` to `+1` where `1` is fully revealed on the left-hand side,
+ * `-1` is fully revealed on the right-hand side, and `0` is not revealed at all. Numbers lower
+ * than `-1` or greater than `1` are possible when the overscroll effect adds additional pixels of
+ * offset.
+ */
+@Composable
+private fun RevealedContent(
+ viewModel: MediaSettingsButtonViewModel,
+ revealAmount: () -> Float,
+ modifier: Modifier = Modifier,
+) {
+ val horizontalPadding = 18.dp
+
+ // This custom layout's purpose is only to place the icon in the center of the revealed content,
+ // taking into account the amount of reveal.
+ Layout(
+ content = {
+ Icon(
+ icon = viewModel.icon,
+ modifier =
+ Modifier.size(48.dp)
+ .padding(12.dp)
+ .graphicsLayer {
+ alpha = abs(revealAmount()).fastCoerceIn(0f, 1f)
+ rotationZ = revealAmount() * 90
+ }
+ .clickable { viewModel.onClick() },
+ )
+ },
+ modifier = modifier,
+ ) { measurables, constraints ->
+ check(measurables.size == 1)
+ val placeable = measurables[0].measure(constraints)
+ val totalWidth =
+ min(horizontalPadding.roundToPx() * 2 + placeable.measuredWidth, constraints.maxWidth)
+
+ layout(totalWidth, constraints.maxHeight) {
+ coordinates?.size?.let { size ->
+ val reveal = revealAmount()
+ val x =
+ if (reveal >= 0f) {
+ ((size.width * abs(reveal)) - placeable.measuredWidth) / 2
+ } else {
+ size.width * (1 - abs(reveal) / 2) - placeable.measuredWidth / 2
+ }
+
+ placeable.place(
+ x = x.fastRoundToInt(),
+ y = (size.height - placeable.measuredHeight) / 2,
+ )
+ }
+ }
+ }
+}
+
/** Enumerates all supported media presentation styles. */
enum class MediaPresentationStyle {
/** The "normal" 3-row carousel look. */
diff --git a/packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/SwipeToDismiss.kt b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/SwipeToDismiss.kt
new file mode 100644
index 000000000000..b80bf4143252
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/SwipeToDismiss.kt
@@ -0,0 +1,115 @@
+/*
+ * 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.media.remedia.ui.compose
+
+import androidx.compose.animation.core.Animatable
+import androidx.compose.foundation.OverscrollEffect
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.offset
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
+import androidx.compose.ui.input.nestedscroll.NestedScrollSource
+import androidx.compose.ui.input.nestedscroll.nestedScroll
+import androidx.compose.ui.layout.onSizeChanged
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.Velocity
+import androidx.compose.ui.util.fastRoundToInt
+import kotlinx.coroutines.launch
+
+/** Swipe to dismiss that supports nested scrolling. */
+@Composable
+fun SwipeToDismiss(
+ content: @Composable (overscrollEffect: OverscrollEffect?) -> Unit,
+ onDismissed: () -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ val scope = rememberCoroutineScope()
+ val offsetAnimatable = remember { Animatable(0f) }
+
+ // This is the width of the revealed content UI box. It's not a state because it's not
+ // observed in any composition and is an object with a value to avoid the extra cost
+ // associated with boxing and unboxing an int.
+ val revealedContentBoxWidth = remember {
+ object {
+ var value = 0
+ }
+ }
+
+ val nestedScrollConnection = remember {
+ object : NestedScrollConnection {
+ override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
+ return if (offsetAnimatable.value > 0f && available.x < 0f) {
+ scope.launch { offsetAnimatable.snapTo(offsetAnimatable.value + available.x) }
+ Offset(available.x, 0f)
+ } else if (offsetAnimatable.value < 0f && available.x > 0f) {
+ scope.launch { offsetAnimatable.snapTo(offsetAnimatable.value + available.x) }
+ Offset(available.x, 0f)
+ } else {
+ Offset.Zero
+ }
+ }
+
+ override fun onPostScroll(
+ consumed: Offset,
+ available: Offset,
+ source: NestedScrollSource,
+ ): Offset {
+ return if (available.x > 0f) {
+ scope.launch { offsetAnimatable.snapTo(offsetAnimatable.value + available.x) }
+ Offset(available.x, 0f)
+ } else if (available.x < 0f) {
+ scope.launch { offsetAnimatable.snapTo(offsetAnimatable.value + available.x) }
+ Offset(available.x, 0f)
+ } else {
+ Offset.Zero
+ }
+ }
+
+ override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
+ scope.launch {
+ offsetAnimatable.animateTo(
+ if (offsetAnimatable.value >= revealedContentBoxWidth.value / 2f) {
+ revealedContentBoxWidth.value * 2f
+ } else if (offsetAnimatable.value <= -revealedContentBoxWidth.value / 2f) {
+ -revealedContentBoxWidth.value * 2f
+ } else {
+ 0f
+ }
+ )
+ if (offsetAnimatable.value != 0f) {
+ onDismissed()
+ }
+ }
+ return super.onPostFling(consumed, available)
+ }
+ }
+ }
+
+ Box(
+ modifier =
+ modifier
+ .onSizeChanged { revealedContentBoxWidth.value = it.width }
+ .nestedScroll(nestedScrollConnection)
+ .offset { IntOffset(x = offsetAnimatable.value.fastRoundToInt(), y = 0) }
+ ) {
+ content(null)
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/SwipeToReveal.kt b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/SwipeToReveal.kt
new file mode 100644
index 000000000000..770762c7a29f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/SwipeToReveal.kt
@@ -0,0 +1,264 @@
+/*
+ * 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.media.remedia.ui.compose
+
+import androidx.compose.animation.core.Animatable
+import androidx.compose.foundation.OverscrollEffect
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxScope
+import androidx.compose.foundation.layout.absoluteOffset
+import androidx.compose.foundation.layout.offset
+import androidx.compose.foundation.overscroll
+import androidx.compose.foundation.withoutVisualEffect
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.input.pointer.PointerType
+import androidx.compose.ui.layout.Layout
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.util.fastCoerceIn
+import androidx.compose.ui.util.fastRoundToInt
+import com.android.compose.gesture.NestedDraggable
+import com.android.compose.gesture.effect.OffsetOverscrollEffect
+import com.android.compose.gesture.effect.rememberOffsetOverscrollEffect
+import com.android.compose.gesture.nestedDraggable
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+
+/**
+ * Swipe to reveal that supports nested scrolling and an overscroll effect.
+ *
+ * @param foregroundContent The content to show above all else; this is the content that can be
+ * swiped sideways to reveal the [revealedContent]. This may contain a horizontally-scrollable
+ * component (for example a `HorizontalPager`).
+ * @param revealedContent The content that is shown below the [foregroundContent]; this is the
+ * content that can be revealed by swiping the [foregroundContent] sideways.
+ */
+@Composable
+fun SwipeToReveal(
+ foregroundContent: @Composable (overscrollEffect: OverscrollEffect?) -> Unit,
+ foregroundContentEffect: OffsetOverscrollEffect,
+ revealedContent: @Composable BoxScope.(revealAmount: () -> Float) -> Unit,
+ isSwipingEnabled: Boolean,
+ modifier: Modifier = Modifier,
+) {
+ // This composable supports an overscroll effect, to make it possible for the user to
+ // "stretch" the UI when the side is fully revealed but the user keeps trying to reveal it
+ // further.
+ val revealedContentEffect = rememberOffsetOverscrollEffect()
+
+ // This is the width of the revealed content UI box. It's not a state because it's not
+ // observed in any composition and is an object with a value to avoid the extra cost
+ // associated with boxing and unboxing an int.
+ val revealedContentBoxWidth = remember {
+ object {
+ var value = 0
+ }
+ }
+
+ // In order to support the drag to reveal, infrastructure has to be put in place where a
+ // NestedDraggable helps by consuming the unconsumed drags and flings and applying the
+ // overscroll visual effect.
+ //
+ // This is the NestedDraggalbe controller.
+ val revealedContentDragController = rememberRevealedContentDragController {
+ revealedContentBoxWidth.value.toFloat()
+ }
+
+ Box(
+ modifier =
+ modifier
+ .nestedDraggable(
+ enabled = isSwipingEnabled,
+ draggable =
+ remember {
+ object : NestedDraggable {
+ override fun onDragStarted(
+ position: Offset,
+ sign: Float,
+ pointersDown: Int,
+ pointerType: PointerType?,
+ ): NestedDraggable.Controller {
+ return revealedContentDragController
+ }
+
+ override fun shouldConsumeNestedPostScroll(sign: Float): Boolean {
+ return revealedContentDragController.shouldConsumePostScrolls(
+ sign
+ )
+ }
+
+ override fun shouldConsumeNestedPreScroll(sign: Float): Boolean {
+ return revealedContentDragController.shouldConsumePreScrolls(
+ sign
+ )
+ }
+ }
+ },
+ orientation = Orientation.Horizontal,
+ overscrollEffect = revealedContentEffect.withoutVisualEffect(),
+ )
+ .overscroll(revealedContentEffect)
+ ) {
+ val density = LocalDensity.current
+
+ /**
+ * Returns the amount of visual offset, in pixels, that is comprised of both the offset from
+ * dragging and the overscroll effect's additional pixels after applying its animation curve
+ * on the raw distance.
+ */
+ fun offsetWithOverscroll(): Float {
+ return revealedContentDragController.offset +
+ OffsetOverscrollEffect.computeOffset(
+ density,
+ foregroundContentEffect.overscrollDistance,
+ ) +
+ OffsetOverscrollEffect.computeOffset(
+ density,
+ revealedContentEffect.overscrollDistance,
+ )
+ }
+
+ /**
+ * Returns the ratio of the amount by which the revealed content is revealed, where:
+ * - `0` means none of it is revealed
+ * - `+1` means all of it is revealed to the start of the foreground content
+ * - `-1` means all of it is revealed to the end of the foreground content
+ *
+ * The number could be smaller than `-1` or larger than `+1` to model overscrolling.
+ */
+ fun revealAmount(): Float {
+ return (offsetWithOverscroll() / revealedContentBoxWidth.value)
+ }
+
+ Layout(
+ content = { revealedContent { revealAmount() } },
+ modifier = Modifier.matchParentSize(),
+ ) { measurables, constraints ->
+ check(measurables.size == 1)
+ val placeable = measurables[0].measure(constraints.copy(minWidth = 0, minHeight = 0))
+ // Keep revealedContentBoxWidth up to date with the latest value.
+ revealedContentBoxWidth.value = placeable.measuredWidth
+
+ // Place the revealed content on the correct side, depending on the direction of the
+ // reveal.
+ val alignedToStart = revealAmount() >= 0f
+ layout(constraints.maxWidth, constraints.maxHeight) {
+ coordinates?.size?.let { size ->
+ placeable.place(
+ x = if (alignedToStart) 0 else size.width - placeable.measuredWidth,
+ y = 0,
+ )
+ }
+ }
+ }
+
+ Box(
+ modifier =
+ Modifier.absoluteOffset {
+ IntOffset(revealedContentDragController.offset.fastRoundToInt(), y = 0)
+ }
+ ) {
+ foregroundContent(foregroundContentEffect)
+ }
+ }
+}
+
+@Composable
+private fun rememberRevealedContentDragController(
+ maxBound: () -> Float
+): RevealedContentDragController {
+ val scope = rememberCoroutineScope()
+ return remember { RevealedContentDragController(scope = scope, maxBound = maxBound) }
+}
+
+private class RevealedContentDragController(
+ private val scope: CoroutineScope,
+ private val maxBound: () -> Float,
+) : NestedDraggable.Controller {
+ private val offsetAnimatable = Animatable(0f)
+ private var lastTarget = 0f
+ private var range = 0f..1f
+ private var shouldConsumePreScrolls by mutableStateOf(false)
+
+ override val autoStopNestedDrags: Boolean
+ get() = true
+
+ val offset: Float
+ get() = offsetAnimatable.value
+
+ fun shouldConsumePreScrolls(sign: Float): Boolean {
+ if (!shouldConsumePreScrolls) return false
+
+ if (lastTarget > 0f && sign < 0f) {
+ range = 0f..maxBound()
+ return true
+ }
+
+ if (lastTarget < 0f && sign > 0f) {
+ range = -maxBound()..0f
+ return true
+ }
+
+ return false
+ }
+
+ fun shouldConsumePostScrolls(sign: Float): Boolean {
+ val max = maxBound()
+ if (sign > 0f && lastTarget < max) {
+ range = 0f..maxBound()
+ return true
+ }
+
+ if (sign < 0f && lastTarget > -max) {
+ range = -maxBound()..0f
+ return true
+ }
+
+ return false
+ }
+
+ override fun onDrag(delta: Float): Float {
+ val previousTarget = lastTarget
+ lastTarget = (lastTarget + delta).fastCoerceIn(range.start, range.endInclusive)
+ val newTarget = lastTarget
+ scope.launch { offsetAnimatable.snapTo(newTarget) }
+ return lastTarget - previousTarget
+ }
+
+ override suspend fun onDragStopped(velocity: Float, awaitFling: suspend () -> Unit): Float {
+ val rangeMiddle = range.start + (range.endInclusive - range.start) / 2f
+ lastTarget =
+ when {
+ lastTarget >= rangeMiddle -> range.endInclusive
+ else -> range.start
+ }
+
+ shouldConsumePreScrolls = lastTarget != 0f
+ val newTarget = lastTarget
+
+ scope.launch { offsetAnimatable.animateTo(newTarget) }
+ return velocity
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaSettingsButtonViewModel.kt b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaSettingsButtonViewModel.kt
new file mode 100644
index 000000000000..4f4c25c3c74f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaSettingsButtonViewModel.kt
@@ -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.
+ */
+
+package com.android.systemui.media.remedia.ui.viewmodel
+
+import com.android.systemui.common.shared.model.Icon
+
+data class MediaSettingsButtonViewModel(val icon: Icon.Resource, val onClick: () -> Unit)
diff --git a/packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaViewModel.kt b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaViewModel.kt
index 19b08fa212db..a57da63a7a81 100644
--- a/packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaViewModel.kt
@@ -252,6 +252,21 @@ constructor(
}
}
+ val settingsButtonViewModel =
+ MediaSettingsButtonViewModel(
+ icon =
+ Icon.Resource(
+ res = R.drawable.ic_settings,
+ contentDescription =
+ ContentDescription.Resource(res = R.string.controls_media_settings_button),
+ ),
+ onClick = {
+ falsingSystem.runIfNotFalseTap(FalsingManager.LOW_PENALTY) {
+ interactor.openMediaSettings()
+ }
+ },
+ )
+
/** Whether the carousel should be visible. */
val isCarouselVisible: Boolean
get() =
diff --git a/packages/SystemUI/src/com/android/systemui/model/SceneContainerPlugin.kt b/packages/SystemUI/src/com/android/systemui/model/SceneContainerPlugin.kt
index 4559a7aea1a2..7b3f4c61088b 100644
--- a/packages/SystemUI/src/com/android/systemui/model/SceneContainerPlugin.kt
+++ b/packages/SystemUI/src/com/android/systemui/model/SceneContainerPlugin.kt
@@ -79,6 +79,7 @@ constructor(
SceneContainerPluginState(
scene = idleState.currentScene,
overlays = idleState.currentOverlays,
+ isVisible = sceneInteractor.get().isVisible.value,
invisibleDueToOcclusion = invisibleDueToOcclusion,
)
)
@@ -100,12 +101,17 @@ constructor(
mapOf<Long, (SceneContainerPluginState) -> Boolean>(
SYSUI_STATE_NOTIFICATION_PANEL_VISIBLE to
{
- it.scene != Scenes.Gone || it.overlays.isNotEmpty()
+ when {
+ !it.isVisible -> false
+ it.scene != Scenes.Gone -> true
+ it.overlays.isNotEmpty() -> true
+ else -> false
+ }
},
SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED to
{
when {
- it.invisibleDueToOcclusion -> false
+ !it.isVisible -> false
it.scene == Scenes.Lockscreen -> true
it.scene == Scenes.Shade -> true
Overlays.NotificationsShade in it.overlays -> true
@@ -114,19 +120,23 @@ constructor(
},
SYSUI_STATE_QUICK_SETTINGS_EXPANDED to
{
- it.scene == Scenes.QuickSettings ||
- Overlays.QuickSettingsShade in it.overlays
+ when {
+ !it.isVisible -> false
+ it.scene == Scenes.QuickSettings -> true
+ Overlays.QuickSettingsShade in it.overlays -> true
+ else -> false
+ }
},
- SYSUI_STATE_BOUNCER_SHOWING to { Overlays.Bouncer in it.overlays },
+ SYSUI_STATE_BOUNCER_SHOWING to { it.isVisible && Overlays.Bouncer in it.overlays },
SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING to
{
- it.scene == Scenes.Lockscreen && !it.invisibleDueToOcclusion
+ it.isVisible && it.scene == Scenes.Lockscreen
},
SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING_OCCLUDED to
{
it.scene == Scenes.Lockscreen && it.invisibleDueToOcclusion
},
- SYSUI_STATE_COMMUNAL_HUB_SHOWING to { it.scene == Scenes.Communal },
+ SYSUI_STATE_COMMUNAL_HUB_SHOWING to { it.isVisible && it.scene == Scenes.Communal },
)
}
@@ -134,5 +144,6 @@ constructor(
val scene: SceneKey,
val overlays: Set<OverlayKey>,
val invisibleDueToOcclusion: Boolean,
+ val isVisible: Boolean,
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/model/SysUiStateExt.kt b/packages/SystemUI/src/com/android/systemui/model/SysUiStateExt.kt
index 1e18f24c9e65..195535669c7e 100644
--- a/packages/SystemUI/src/com/android/systemui/model/SysUiStateExt.kt
+++ b/packages/SystemUI/src/com/android/systemui/model/SysUiStateExt.kt
@@ -16,8 +16,6 @@
package com.android.systemui.model
-import com.android.systemui.dagger.qualifiers.DisplayId
-
/**
* In-bulk updates multiple flag values and commits the update.
*
@@ -32,16 +30,8 @@ import com.android.systemui.dagger.qualifiers.DisplayId
* SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING to (sceneKey == Scenes.Lockscreen),
* )
* ```
- *
- * You can inject [displayId] by injecting it using:
- * ```
- * @DisplayId private val displayId: Int`,
- * ```
*/
-fun SysUiState.updateFlags(
- @DisplayId displayId: Int,
- vararg flagValuePairs: Pair<Long, Boolean>,
-) {
+fun SysUiState.updateFlags(vararg flagValuePairs: Pair<Long, Boolean>) {
flagValuePairs.forEach { (flag, enabled) -> setFlag(flag, enabled) }
- commitUpdate(displayId)
+ commitUpdate()
}
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt
index 8dc27bf4ac3e..080940169e46 100644
--- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt
+++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt
@@ -86,7 +86,10 @@ constructor(
*/
private fun initializeKeyGestureEventHandler() {
if (useKeyGestureEventHandler()) {
- inputManager.registerKeyGestureEventHandler(callbacks)
+ inputManager.registerKeyGestureEventHandler(
+ listOf(KeyGestureEvent.KEY_GESTURE_TYPE_OPEN_NOTES),
+ callbacks,
+ )
}
}
@@ -156,11 +159,8 @@ constructor(
controller.updateNoteTaskForCurrentUserAndManagedProfiles()
}
- override fun handleKeyGestureEvent(
- event: KeyGestureEvent,
- focusedToken: IBinder?,
- ): Boolean {
- return this@NoteTaskInitializer.handleKeyGestureEvent(event)
+ override fun handleKeyGestureEvent(event: KeyGestureEvent, focusedToken: IBinder?) {
+ this@NoteTaskInitializer.handleKeyGestureEvent(event)
}
}
@@ -202,23 +202,19 @@ constructor(
return !isMultiPress && !isLongPress
}
- private fun handleKeyGestureEvent(event: KeyGestureEvent): Boolean {
- // This method is on input hot path and should be kept lightweight. Shift all complex
- // processing onto background executor wherever possible.
+ private fun handleKeyGestureEvent(event: KeyGestureEvent) {
if (event.keyGestureType != KeyGestureEvent.KEY_GESTURE_TYPE_OPEN_NOTES) {
- return false
+ return
}
debugLog {
"handleKeyGestureEvent: Received OPEN_NOTES gesture event from keycodes: " +
event.keycodes.contentToString()
}
if (event.keycodes.size == 1 && event.keycodes[0] == KEYCODE_STYLUS_BUTTON_TAIL) {
- debugLog { "Note task triggered by stylus tail button" }
backgroundExecutor.execute { controller.showNoteTask(TAIL_BUTTON) }
- return true
+ } else {
+ backgroundExecutor.execute { controller.showNoteTask(KEYBOARD_SHORTCUT) }
}
- backgroundExecutor.execute { controller.showNoteTask(KEYBOARD_SHORTCUT) }
- return true
}
companion object {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt b/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt
index 05a60a6db31e..9f04f69bd0bf 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt
@@ -18,6 +18,9 @@ package com.android.systemui.qs.composefragment
import android.annotation.SuppressLint
import android.content.Context
+import android.content.res.Configuration
+import android.graphics.Canvas
+import android.graphics.Path
import android.graphics.PointF
import android.graphics.Rect
import android.os.Bundle
@@ -35,6 +38,7 @@ import androidx.activity.setViewTreeOnBackPressedDispatcherOwner
import androidx.annotation.VisibleForTesting
import androidx.compose.animation.core.tween
import androidx.compose.foundation.ScrollState
+import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Arrangement.spacedBy
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -48,6 +52,7 @@ import androidx.compose.foundation.layout.requiredHeightIn
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@@ -70,6 +75,8 @@ import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.layout.positionInRoot
import androidx.compose.ui.layout.positionOnScreen
import androidx.compose.ui.platform.ComposeView
+import androidx.compose.ui.platform.LocalConfiguration
+import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.CustomAccessibilityAction
@@ -102,6 +109,7 @@ import com.android.compose.theme.PlatformTheme
import com.android.mechanics.GestureContext
import com.android.systemui.Dumpable
import com.android.systemui.Flags
+import com.android.systemui.Flags.notificationShadeBlur
import com.android.systemui.brightness.ui.compose.BrightnessSliderContainer
import com.android.systemui.brightness.ui.compose.ContainerColors
import com.android.systemui.compose.modifiers.sysuiResTag
@@ -119,7 +127,6 @@ import com.android.systemui.qs.composefragment.SceneKeys.debugName
import com.android.systemui.qs.composefragment.SceneKeys.toIdleSceneKey
import com.android.systemui.qs.composefragment.ui.GridAnchor
import com.android.systemui.qs.composefragment.ui.NotificationScrimClipParams
-import com.android.systemui.qs.composefragment.ui.notificationScrimClip
import com.android.systemui.qs.composefragment.ui.quickQuickSettingsToQuickSettings
import com.android.systemui.qs.composefragment.ui.toEditMode
import com.android.systemui.qs.composefragment.viewmodel.QSFragmentComposeViewModel
@@ -235,7 +242,7 @@ constructor(
FrameLayoutTouchPassthrough(
context,
{ notificationScrimClippingParams.isEnabled },
- { notificationScrimClippingParams.params.top },
+ snapshotFlow { notificationScrimClippingParams.params },
// Only allow scrolling when we are fully expanded. That way, we don't intercept
// swipes in lockscreen (when somehow QS is receiving touches).
{ (scrollState.canScrollForward && viewModel.isQsFullyExpanded) || isCustomizing },
@@ -251,7 +258,7 @@ constructor(
@Composable
private fun Content() {
- PlatformTheme {
+ PlatformTheme(isDarkTheme = if (notificationShadeBlur()) isSystemInDarkTheme() else true) {
ProvideShortcutHelperIndication(interactionsConfig = interactionsConfig()) {
// TODO(b/389985793): Make sure that there is no coroutine work or recompositions
// happening when alwaysCompose is true but isQsVisibleAndAnyShadeExpanded is false.
@@ -270,11 +277,6 @@ constructor(
}
}
.graphicsLayer { alpha = viewModel.viewAlpha }
- .thenIf(notificationScrimClippingParams.isEnabled) {
- Modifier.notificationScrimClip {
- notificationScrimClippingParams.params
- }
- }
.thenIf(!Flags.notificationShadeBlur()) {
Modifier.offset {
IntOffset(
@@ -746,19 +748,31 @@ constructor(
Box(
Modifier.systemGestureExclusionInShade(
enabled = {
- layoutState.transitionState is TransitionState.Idle
+ /*
+ * While we are transitioning into QS (either from QQS
+ * or from gone), the global position of the brightness
+ * slider will change in every frame. This causes
+ * the modifier to send a new gesture exclusion
+ * rectangle on every frame. Instead, only apply the
+ * modifier when this is settled.
+ */
+ layoutState.transitionState is TransitionState.Idle &&
+ viewModel.isNotTransitioning
}
)
) {
- BrightnessSliderContainer(
- viewModel = containerViewModel.brightnessSliderViewModel,
- containerColors =
- ContainerColors(
- Color.Transparent,
- ContainerColors.defaultContainerColor,
- ),
- modifier = Modifier.fillMaxWidth(),
- )
+ AlwaysDarkMode {
+ BrightnessSliderContainer(
+ viewModel =
+ containerViewModel.brightnessSliderViewModel,
+ containerColors =
+ ContainerColors(
+ Color.Transparent,
+ ContainerColors.defaultContainerColor,
+ ),
+ modifier = Modifier.fillMaxWidth(),
+ )
+ }
}
}
val TileGrid =
@@ -1043,17 +1057,75 @@ private const val EDIT_MODE_TIME_MILLIS = 500
private class FrameLayoutTouchPassthrough(
context: Context,
private val clippingEnabledProvider: () -> Boolean,
- private val clippingTopProvider: () -> Int,
+ private val clippingParams: Flow<NotificationScrimClipParams>,
private val canScrollForwardQs: () -> Boolean,
private val emitMotionEventForFalsing: () -> Unit,
) : FrameLayout(context) {
+
+ init {
+ repeatWhenAttached {
+ repeatOnLifecycle(Lifecycle.State.STARTED) {
+ clippingParams.collect { currentClipParams = it }
+ }
+ }
+ }
+
+ private val currentClippingPath = Path()
+ private var lastWidth = -1
+ set(value) {
+ if (field != value) {
+ field = value
+ updateClippingPath()
+ }
+ }
+
+ private var currentClipParams = NotificationScrimClipParams()
+ set(value) {
+ if (field != value) {
+ field = value
+ updateClippingPath()
+ }
+ }
+
+ private fun updateClippingPath() {
+ currentClippingPath.rewind()
+ if (clippingEnabledProvider()) {
+ val right = width + currentClipParams.rightInset
+ val left = -currentClipParams.leftInset
+ val top = currentClipParams.top
+ val bottom = currentClipParams.bottom
+ currentClippingPath.addRoundRect(
+ left.toFloat(),
+ top.toFloat(),
+ right.toFloat(),
+ bottom.toFloat(),
+ currentClipParams.radius.toFloat(),
+ currentClipParams.radius.toFloat(),
+ Path.Direction.CW,
+ )
+ }
+ invalidate()
+ }
+
+ override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
+ super.onLayout(changed, left, top, right, bottom)
+ lastWidth = right - left
+ }
+
+ override fun dispatchDraw(canvas: Canvas) {
+ if (!currentClippingPath.isEmpty) {
+ canvas.clipOutPath(currentClippingPath)
+ }
+ super.dispatchDraw(canvas)
+ }
+
override fun isTransformedTouchPointInView(
x: Float,
y: Float,
child: View?,
outLocalPoint: PointF?,
): Boolean {
- return if (clippingEnabledProvider() && y + translationY > clippingTopProvider()) {
+ return if (clippingEnabledProvider() && y + translationY > currentClipParams.top) {
false
} else {
super.isTransformedTouchPointInView(x, y, child, outLocalPoint)
@@ -1236,3 +1308,31 @@ private fun interactionsConfig() =
private inline val alwaysCompose
get() = Flags.alwaysComposeQsUiFragment()
+
+/**
+ * Forces the configuration and themes to be dark theme. This is needed in order to have
+ * [colorResource] retrieve the dark mode colors.
+ *
+ * This should be removed when [notificationShadeBlur] is removed
+ */
+@Composable
+private fun AlwaysDarkMode(content: @Composable () -> Unit) {
+ if (notificationShadeBlur()) {
+ content()
+ } else {
+ val currentConfig = LocalConfiguration.current
+ val darkConfig =
+ Configuration(currentConfig).apply {
+ uiMode =
+ (uiMode and (Configuration.UI_MODE_NIGHT_MASK.inv())) or
+ Configuration.UI_MODE_NIGHT_YES
+ }
+ val newContext = LocalContext.current.createConfigurationContext(darkConfig)
+ CompositionLocalProvider(
+ LocalConfiguration provides darkConfig,
+ LocalContext provides newContext,
+ ) {
+ content()
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/composefragment/ui/NotificationScrimClip.kt b/packages/SystemUI/src/com/android/systemui/qs/composefragment/ui/NotificationScrimClip.kt
deleted file mode 100644
index 3049a40f18c4..000000000000
--- a/packages/SystemUI/src/com/android/systemui/qs/composefragment/ui/NotificationScrimClip.kt
+++ /dev/null
@@ -1,67 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.qs.composefragment.ui
-
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.drawWithContent
-import androidx.compose.ui.geometry.CornerRadius
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.geometry.Size
-import androidx.compose.ui.graphics.BlendMode
-import androidx.compose.ui.graphics.ClipOp
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.CompositingStrategy
-import androidx.compose.ui.graphics.drawscope.clipRect
-import androidx.compose.ui.graphics.graphicsLayer
-
-/**
- * Clipping modifier for clipping out the notification scrim as it slides over QS. It will clip out
- * ([ClipOp.Difference]) a `RoundRect(-leftInset, top, width + rightInset, bottom, radius, radius)`
- * from the QS container.
- */
-fun Modifier.notificationScrimClip(clipParams: () -> NotificationScrimClipParams): Modifier {
- return this.graphicsLayer { compositingStrategy = CompositingStrategy.Offscreen }
- .drawWithContent {
- drawContent()
- val params = clipParams()
- val left = -params.leftInset.toFloat()
- val right = size.width + params.rightInset.toFloat()
- val top = params.top.toFloat()
- val bottom = params.bottom.toFloat()
- val clipSize = Size(right - left, bottom - top)
- if (!clipSize.isEmpty()) {
- clipRect {
- drawRoundRect(
- color = Color.Black,
- cornerRadius = CornerRadius(params.radius.toFloat()),
- blendMode = BlendMode.Clear,
- topLeft = Offset(left, top),
- size = Size(right - left, bottom - top),
- )
- }
- }
- }
-}
-
-/** Params for [notificationScrimClip]. */
-data class NotificationScrimClipParams(
- val top: Int = 0,
- val bottom: Int = 0,
- val leftInset: Int = 0,
- val rightInset: Int = 0,
- val radius: Int = 0,
-)
diff --git a/packages/SystemUI/src/com/android/systemui/qs/composefragment/ui/NotificationScrimClipParams.kt b/packages/SystemUI/src/com/android/systemui/qs/composefragment/ui/NotificationScrimClipParams.kt
new file mode 100644
index 000000000000..db320d3b9f1c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/composefragment/ui/NotificationScrimClipParams.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.composefragment.ui
+
+/** Params for [notificationScrimClip]. */
+data class NotificationScrimClipParams(
+ val top: Int = 0,
+ val bottom: Int = 0,
+ val leftInset: Int = 0,
+ val rightInset: Int = 0,
+ val radius: Int = 0,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModel.kt
index b61fa9cfe264..b7e9c5296bc9 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModel.kt
@@ -312,6 +312,11 @@ constructor(
source = containerViewModel.editModeViewModel.isEditing,
)
+ /** True if we are not in an expansion (from Gone to QQS/QS) animation. */
+ val isNotTransitioning by derivedStateOf {
+ viewTranslationY == 0f && viewAlpha == 1f && constrainedSquishinessFraction == 1f
+ }
+
private val inFirstPage: Boolean
get() = inFirstPageViewModel.inFirstPage
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/DragAndDropState.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/DragAndDropState.kt
index 405ce8a8e5e0..005c8b26b0d8 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/DragAndDropState.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/DragAndDropState.kt
@@ -35,7 +35,6 @@ import androidx.compose.ui.draganddrop.mimeTypes
import androidx.compose.ui.draganddrop.toAndroidDragEvent
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.unit.IntRect
-import androidx.compose.ui.unit.center
import androidx.compose.ui.unit.toRect
import com.android.systemui.qs.panels.shared.model.SizedTile
import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel
@@ -44,6 +43,7 @@ import com.android.systemui.qs.pipeline.shared.TileSpec
/** Holds the [TileSpec] of the tile being moved and receives drag and drop events. */
interface DragAndDropState {
val draggedCell: SizedTile<EditTileViewModel>?
+ val isDraggedCellRemovable: Boolean
val draggedPosition: Offset
val dragInProgress: Boolean
val dragType: DragType?
@@ -76,7 +76,7 @@ enum class DragType {
@Composable
fun Modifier.dragAndDropRemoveZone(
dragAndDropState: DragAndDropState,
- onDrop: (TileSpec) -> Unit,
+ onDrop: (TileSpec, removalEnabled: Boolean) -> Unit,
): Modifier {
val target =
remember(dragAndDropState) {
@@ -87,13 +87,15 @@ fun Modifier.dragAndDropRemoveZone(
override fun onDrop(event: DragAndDropEvent): Boolean {
return dragAndDropState.draggedCell?.let {
- onDrop(it.tile.tileSpec)
+ onDrop(it.tile.tileSpec, dragAndDropState.isDraggedCellRemovable)
dragAndDropState.onDrop()
true
} ?: false
}
override fun onEntered(event: DragAndDropEvent) {
+ if (!dragAndDropState.isDraggedCellRemovable) return
+
dragAndDropState.movedOutOfBounds()
}
}
@@ -168,10 +170,10 @@ private fun DragAndDropEvent.toOffset(): Offset {
}
private fun insertAfter(item: LazyGridItemInfo, offset: Offset): Boolean {
- // We want to insert the tile after the target if we're aiming at the right side of a large tile
+ // We want to insert the tile after the target if we're aiming at the end of a large tile
// TODO(ostonge): Verify this behavior in RTL
- val itemCenter = item.offset + item.size.center
- return item.span != 1 && offset.x > itemCenter.x
+ val itemCenter = item.offset.x + item.size.width * .75
+ return item.span != 1 && offset.x > itemCenter
}
@OptIn(ExperimentalFoundationApi::class)
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/EditTileListState.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/EditTileListState.kt
index 868855840922..70f1674acd3b 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/EditTileListState.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/EditTileListState.kt
@@ -25,6 +25,7 @@ import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.runtime.toMutableStateList
import androidx.compose.ui.geometry.Offset
import com.android.systemui.qs.panels.shared.model.SizedTile
+import com.android.systemui.qs.panels.ui.compose.selection.PlacementEvent
import com.android.systemui.qs.panels.ui.model.GridCell
import com.android.systemui.qs.panels.ui.model.TileGridCell
import com.android.systemui.qs.panels.ui.model.toGridCells
@@ -60,6 +61,11 @@ class EditTileListState(
override var dragType by mutableStateOf<DragType?>(null)
private set
+ // A dragged cell can be removed if it was added in the drag movement OR if it's marked as
+ // removable
+ override val isDraggedCellRemovable: Boolean
+ get() = dragType == DragType.Add || draggedCell?.tile?.isRemovable ?: false
+
override val dragInProgress: Boolean
get() = draggedCell != null
@@ -76,10 +82,16 @@ class EditTileListState(
return _tiles.indexOfFirst { it is TileGridCell && it.tile.tileSpec == tileSpec }
}
+ fun isRemovable(tileSpec: TileSpec): Boolean {
+ return _tiles.find {
+ it is TileGridCell && it.tile.tileSpec == tileSpec && it.tile.isRemovable
+ } != null
+ }
+
/** Resize the tile corresponding to the [TileSpec] to [toIcon] */
fun resizeTile(tileSpec: TileSpec, toIcon: Boolean) {
val fromIndex = indexOf(tileSpec)
- if (fromIndex != -1) {
+ if (fromIndex != INVALID_INDEX) {
val cell = _tiles[fromIndex] as TileGridCell
if (cell.isIcon == toIcon) return
@@ -97,9 +109,6 @@ class EditTileListState(
override fun onStarted(cell: SizedTile<EditTileViewModel>, dragType: DragType) {
draggedCell = cell
this.dragType = dragType
-
- // Add spacers to the grid to indicate where the user can move a tile
- regenerateGrid()
}
override fun onTargeting(target: Int, insertAfter: Boolean) {
@@ -111,7 +120,7 @@ class EditTileListState(
}
val insertionIndex = if (insertAfter) target + 1 else target
- if (fromIndex != -1) {
+ if (fromIndex != INVALID_INDEX) {
val cell = _tiles.removeAt(fromIndex)
regenerateGrid()
_tiles.add(insertionIndex.coerceIn(0, _tiles.size), cell)
@@ -149,6 +158,43 @@ class EditTileListState(
regenerateGrid()
}
+ /**
+ * Return the appropriate index to move the tile to for the placement [event]
+ *
+ * The grid includes spacers. As a result, indexes from the grid need to be translated to the
+ * corresponding index from [currentTileSpecs].
+ */
+ fun targetIndexForPlacement(event: PlacementEvent): Int {
+ val currentTileSpecs = tileSpecs()
+ return when (event) {
+ is PlacementEvent.PlaceToTileSpec -> {
+ currentTileSpecs.indexOf(event.targetSpec)
+ }
+ is PlacementEvent.PlaceToIndex -> {
+ if (event.targetIndex >= _tiles.size) {
+ currentTileSpecs.size
+ } else if (event.targetIndex <= 0) {
+ 0
+ } else {
+ // The index may point to a spacer, so first find the first tile located
+ // after index, then use its position as a target
+ val targetTile =
+ _tiles.subList(event.targetIndex, _tiles.size).firstOrNull {
+ it is TileGridCell
+ } as? TileGridCell
+
+ if (targetTile == null) {
+ currentTileSpecs.size
+ } else {
+ val targetIndex = currentTileSpecs.indexOf(targetTile.tile.tileSpec)
+ val fromIndex = currentTileSpecs.indexOf(event.movingSpec)
+ if (fromIndex < targetIndex) targetIndex - 1 else targetIndex
+ }
+ }
+ }
+ }
+ }
+
/** Regenerate the list of [GridCell] with their new potential rows */
private fun regenerateGrid() {
_tiles.filterIsInstance<TileGridCell>().toGridCells(columns).let {
@@ -170,4 +216,8 @@ class EditTileListState(
_tiles.addAll(it)
}
}
+
+ companion object {
+ const val INVALID_INDEX = -1
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/TileDetails.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/TileDetails.kt
index 1176095fbb1c..f3ed07a8a753 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/TileDetails.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/TileDetails.kt
@@ -46,6 +46,8 @@ import com.android.systemui.bluetooth.qsdialog.BluetoothDetailsViewModel
import com.android.systemui.plugins.qs.TileDetailsViewModel
import com.android.systemui.qs.flags.QsDetailedView
import com.android.systemui.qs.panels.ui.viewmodel.DetailsViewModel
+import com.android.systemui.qs.tiles.dialog.CastDetailsContent
+import com.android.systemui.qs.tiles.dialog.CastDetailsViewModel
import com.android.systemui.qs.tiles.dialog.InternetDetailsContent
import com.android.systemui.qs.tiles.dialog.InternetDetailsViewModel
import com.android.systemui.qs.tiles.dialog.ModesDetailsContent
@@ -155,6 +157,7 @@ private fun MapTileDetailsContent(tileDetailsViewModel: TileDetailsViewModel) {
is BluetoothDetailsViewModel ->
BluetoothDetailsContent(tileDetailsViewModel.detailsContentViewModel)
is ModesDetailsViewModel -> ModesDetailsContent(tileDetailsViewModel)
+ is CastDetailsViewModel -> CastDetailsContent(tileDetailsViewModel)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt
index 46f05d0ac895..f8eaa6c3bcfb 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt
@@ -19,6 +19,7 @@
package com.android.systemui.qs.panels.ui.compose.infinitegrid
import androidx.compose.animation.AnimatedContent
+import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.animateContentSize
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.animateDpAsState
@@ -34,6 +35,7 @@ import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.clipScrollableContainer
import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Arrangement.spacedBy
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
@@ -78,6 +80,7 @@ import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.State
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.key
@@ -96,6 +99,7 @@ import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.isSpecified
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.MeasureScope
import androidx.compose.ui.layout.layout
import androidx.compose.ui.layout.onGloballyPositioned
@@ -125,6 +129,7 @@ import com.android.systemui.qs.panels.shared.model.SizedTileImpl
import com.android.systemui.qs.panels.ui.compose.DragAndDropState
import com.android.systemui.qs.panels.ui.compose.DragType
import com.android.systemui.qs.panels.ui.compose.EditTileListState
+import com.android.systemui.qs.panels.ui.compose.EditTileListState.Companion.INVALID_INDEX
import com.android.systemui.qs.panels.ui.compose.dragAndDropRemoveZone
import com.android.systemui.qs.panels.ui.compose.dragAndDropTileList
import com.android.systemui.qs.panels.ui.compose.dragAndDropTileSource
@@ -152,7 +157,6 @@ import com.android.systemui.qs.panels.ui.model.AvailableTileGridCell
import com.android.systemui.qs.panels.ui.model.GridCell
import com.android.systemui.qs.panels.ui.model.SpacerGridCell
import com.android.systemui.qs.panels.ui.model.TileGridCell
-import com.android.systemui.qs.panels.ui.viewmodel.AvailableEditActions
import com.android.systemui.qs.panels.ui.viewmodel.BounceableTileViewModel
import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel
import com.android.systemui.qs.pipeline.shared.TileSpec
@@ -161,7 +165,6 @@ import com.android.systemui.res.R
import kotlin.math.abs
import kotlin.math.roundToInt
import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
object TileType
@@ -225,7 +228,7 @@ fun DefaultEditTileGrid(
columns: Int,
largeTilesSpan: Int,
modifier: Modifier,
- onAddTile: (TileSpec) -> Unit,
+ onAddTile: (TileSpec, Int) -> Unit,
onRemoveTile: (TileSpec) -> Unit,
onSetTiles: (List<TileSpec>) -> Unit,
onResize: (TileSpec, toIcon: Boolean) -> Unit,
@@ -243,6 +246,15 @@ fun DefaultEditTileGrid(
null
}
+ LaunchedEffect(selectionState.placementEvent) {
+ selectionState.placementEvent?.let { event ->
+ listState
+ .targetIndexForPlacement(event)
+ .takeIf { it != INVALID_INDEX }
+ ?.let { onAddTile(event.movingSpec, it) }
+ }
+ }
+
Scaffold(
containerColor = Color.Transparent,
topBar = { EditModeTopBar(onStopEditing = onStopEditing, onReset = reset) },
@@ -272,29 +284,23 @@ fun DefaultEditTileGrid(
.padding(top = innerPadding.calculateTopPadding())
.clipScrollableContainer(Orientation.Vertical)
.verticalScroll(scrollState)
- .dragAndDropRemoveZone(listState, onRemoveTile),
+ .dragAndDropRemoveZone(listState) { spec, removalEnabled ->
+ if (removalEnabled) {
+ // If removal is enabled, remove the tile
+ onRemoveTile(spec)
+ } else {
+ // Otherwise submit the new tile ordering
+ onSetTiles(listState.tileSpecs())
+ selectionState.select(spec)
+ }
+ },
) {
- AnimatedContent(
- targetState = listState.dragInProgress || selectionState.selected,
- label = "QSEditHeader",
- contentAlignment = Alignment.Center,
+ CurrentTilesGridHeader(
+ listState,
+ selectionState,
+ onRemoveTile,
modifier = Modifier.fillMaxWidth().heightIn(min = 48.dp),
- ) { showRemoveTarget ->
- EditGridHeader {
- if (showRemoveTarget) {
- RemoveTileTarget {
- selectionState.selection?.let {
- selectionState.unSelect()
- onRemoveTile(it)
- }
- }
- } else {
- EditGridCenteredText(
- text = stringResource(id = R.string.drag_to_rearrange_tiles)
- )
- }
- }
- }
+ )
CurrentTilesGrid(
listState,
@@ -315,7 +321,7 @@ fun DefaultEditTileGrid(
// Using the fully qualified name here as a workaround for AnimatedVisibility
// not being available from a Box
androidx.compose.animation.AnimatedVisibility(
- visible = !listState.dragInProgress,
+ visible = !listState.dragInProgress && !selectionState.placementEnabled,
enter = fadeIn(),
exit = fadeOut(),
) {
@@ -340,7 +346,7 @@ fun DefaultEditTileGrid(
availableTiles,
selectionState,
columns,
- onAddTile,
+ { onAddTile(it, listState.tileSpecs().size) }, // Add to the end
listState,
)
}
@@ -398,6 +404,76 @@ private fun AutoScrollGrid(
}
}
+private enum class EditModeHeaderState {
+ Remove,
+ Place,
+ Idle,
+}
+
+@Composable
+private fun rememberEditModeState(
+ listState: EditTileListState,
+ selectionState: MutableSelectionState,
+): State<EditModeHeaderState> {
+ val editGridHeaderState = remember { mutableStateOf(EditModeHeaderState.Idle) }
+ LaunchedEffect(
+ listState.dragInProgress,
+ selectionState.selected,
+ selectionState.placementEnabled,
+ ) {
+ val canRemove =
+ listState.isDraggedCellRemovable ||
+ selectionState.selection?.let { listState.isRemovable(it) } ?: false
+
+ editGridHeaderState.value =
+ when {
+ selectionState.placementEnabled -> EditModeHeaderState.Place
+ canRemove -> EditModeHeaderState.Remove
+ else -> EditModeHeaderState.Idle
+ }
+ }
+
+ return editGridHeaderState
+}
+
+@Composable
+private fun CurrentTilesGridHeader(
+ listState: EditTileListState,
+ selectionState: MutableSelectionState,
+ onRemoveTile: (TileSpec) -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ val editGridHeaderState by rememberEditModeState(listState, selectionState)
+
+ AnimatedContent(
+ targetState = editGridHeaderState,
+ label = "QSEditHeader",
+ contentAlignment = Alignment.Center,
+ modifier = modifier,
+ ) { state ->
+ EditGridHeader {
+ when (state) {
+ EditModeHeaderState.Remove -> {
+ RemoveTileTarget {
+ selectionState.selection?.let {
+ selectionState.unSelect()
+ onRemoveTile(it)
+ }
+ }
+ }
+ EditModeHeaderState.Place -> {
+ EditGridCenteredText(text = stringResource(id = R.string.tap_to_position_tile))
+ }
+ EditModeHeaderState.Idle -> {
+ EditGridCenteredText(
+ text = stringResource(id = R.string.drag_to_rearrange_tiles)
+ )
+ }
+ }
+ }
+ }
+}
+
@Composable
private fun EditGridHeader(
modifier: Modifier = Modifier,
@@ -484,8 +560,14 @@ private fun CurrentTilesGrid(
}
.testTag(CURRENT_TILES_GRID_TEST_TAG),
) {
- EditTiles(cells, listState, selectionState, coroutineScope, largeTilesSpan, onRemoveTile) {
- resizingOperation ->
+ EditTiles(
+ cells,
+ listState,
+ selectionState,
+ coroutineScope,
+ largeTilesSpan,
+ onRemoveTile = onRemoveTile,
+ ) { resizingOperation ->
when (resizingOperation) {
is TemporaryResizeOperation -> {
currentListState.resizeTile(resizingOperation.spec, resizingOperation.toIcon)
@@ -585,6 +667,7 @@ private fun GridCell.key(index: Int): Any {
* @param selectionState the [MutableSelectionState] for this grid
* @param coroutineScope the [CoroutineScope] to be used for the tiles
* @param largeTilesSpan the width used for large tiles
+ * @param onRemoveTile the callback when a tile is removed from this grid
* @param onResize the callback when a tile has a new [ResizeOperation]
*/
fun LazyGridScope.EditTiles(
@@ -628,12 +711,33 @@ fun LazyGridScope.EditTiles(
modifier = Modifier.animateItem(),
)
}
- is SpacerGridCell -> SpacerGridCell()
+ is SpacerGridCell ->
+ SpacerGridCell(
+ Modifier.pointerInput(Unit) {
+ detectTapGestures(onTap = { selectionState.onTap(index) })
+ }
+ )
}
}
}
@Composable
+private fun rememberTileState(
+ tile: EditTileViewModel,
+ selectionState: MutableSelectionState,
+): State<TileState> {
+ val tileState = remember { mutableStateOf(TileState.None) }
+ val canShowRemovalBadge = tile.isRemovable
+
+ LaunchedEffect(selectionState.selection, selectionState.placementEnabled, canShowRemovalBadge) {
+ tileState.value =
+ selectionState.tileStateFor(tile.tileSpec, tileState.value, canShowRemovalBadge)
+ }
+
+ return tileState
+}
+
+@Composable
private fun TileGridCell(
cell: TileGridCell,
index: Int,
@@ -646,29 +750,7 @@ private fun TileGridCell(
modifier: Modifier = Modifier,
) {
val stateDescription = stringResource(id = R.string.accessibility_qs_edit_position, index + 1)
- val canShowRemovalBadge = cell.tile.availableEditActions.contains(AvailableEditActions.REMOVE)
- var tileState by remember { mutableStateOf(TileState.None) }
-
- LaunchedEffect(selectionState.selection, canShowRemovalBadge) {
- tileState =
- when {
- selectionState.selection == cell.tile.tileSpec -> {
- if (tileState == TileState.None && canShowRemovalBadge) {
- // The tile decoration is None if a tile is newly composed OR the removal
- // badge can't be shown.
- // For newly composed and selected tiles, such as dragged tiles or moved
- // tiles from resizing, introduce a short delay. This avoids clipping issues
- // on the border and resizing handle, as well as letting the selection
- // animation play correctly.
- delay(250)
- }
- TileState.Selected
- }
- canShowRemovalBadge -> TileState.Removable
- else -> TileState.None
- }
- }
-
+ val tileState by rememberTileState(cell.tile, selectionState)
val resizingState = rememberResizingState(cell.tile.tileSpec, cell.isIcon)
val progress: () -> Float = {
if (tileState == TileState.Selected) {
@@ -696,12 +778,16 @@ private fun TileGridCell(
with(LocalDensity.current) { (largeTilesSpan - 1) * TileArrangementPadding.roundToPx() }
val colors = EditModeTileDefaults.editTileColors()
val toggleSizeLabel = stringResource(R.string.accessibility_qs_edit_toggle_tile_size_action)
- val clickLabel =
+ val togglePlacementModeLabel =
+ stringResource(R.string.accessibility_qs_edit_toggle_placement_mode)
+ val decorationClickLabel =
when (tileState) {
- TileState.None -> null
TileState.Removable ->
stringResource(id = R.string.accessibility_qs_edit_remove_tile_action)
TileState.Selected -> toggleSizeLabel
+ TileState.None,
+ TileState.Placeable,
+ TileState.GreyedOut -> null
}
InteractiveTileContainer(
tileState = tileState,
@@ -720,8 +806,13 @@ private fun TileGridCell(
coroutineScope.launch { resizingState.toggleCurrentValue() }
}
},
- onClickLabel = clickLabel,
+ onClickLabel = decorationClickLabel,
) {
+ val placeableColor = MaterialTheme.colorScheme.primary.copy(alpha = .4f)
+ val backgroundColor by
+ animateColorAsState(
+ if (tileState == TileState.Placeable) placeableColor else colors.background
+ )
Box(
modifier
.fillMaxSize()
@@ -734,7 +825,11 @@ private fun TileGridCell(
CustomAccessibilityAction(toggleSizeLabel) {
onResize(FinalResizeOperation(cell.tile.tileSpec, !cell.isIcon))
true
- }
+ },
+ CustomAccessibilityAction(togglePlacementModeLabel) {
+ selectionState.togglePlacementMode(cell.tile.tileSpec)
+ true
+ },
)
}
.selectableTile(cell.tile.tileSpec, selectionState)
@@ -744,9 +839,14 @@ private fun TileGridCell(
DragType.Move,
selectionState::unSelect,
)
- .tileBackground(colors.background)
+ .tileBackground { backgroundColor }
) {
- EditTile(tile = cell.tile, state = resizingState, progress = progress)
+ EditTile(
+ tile = cell.tile,
+ tileState = tileState,
+ state = resizingState,
+ progress = progress,
+ )
}
}
}
@@ -791,7 +891,7 @@ private fun AvailableTileGridCell(
} else {
Modifier
}
- Box(draggableModifier.fillMaxSize().tileBackground(colors.background)) {
+ Box(draggableModifier.fillMaxSize().tileBackground { colors.background }) {
// Icon
SmallTileContent(
iconProvider = { cell.tile.icon },
@@ -834,11 +934,13 @@ private fun SpacerGridCell(modifier: Modifier = Modifier) {
@Composable
fun EditTile(
tile: EditTileViewModel,
+ tileState: TileState,
state: ResizingState,
progress: () -> Float,
colors: TileColors = EditModeTileDefaults.editTileColors(),
) {
val iconSizeDiff = CommonTileDefaults.IconSize - CommonTileDefaults.LargeTileIconSize
+ val alpha by animateFloatAsState(if (tileState == TileState.GreyedOut) .4f else 1f)
Row(
horizontalArrangement = spacedBy(6.dp),
verticalAlignment = Alignment.CenterVertically,
@@ -871,7 +973,8 @@ fun EditTile(
placeable.place(startPadding.roundToInt(), 0)
}
}
- .largeTilePadding(),
+ .largeTilePadding()
+ .graphicsLayer { this.alpha = alpha },
) {
// Icon
Box(Modifier.size(ToggleTargetSize)) {
@@ -889,7 +992,7 @@ fun EditTile(
label = tile.label.text,
secondaryLabel = tile.appName?.text,
colors = colors,
- modifier = Modifier.weight(1f).graphicsLayer { alpha = progress() },
+ modifier = Modifier.weight(1f).graphicsLayer { this.alpha = progress() },
)
}
}
@@ -908,9 +1011,9 @@ private fun MeasureScope.iconHorizontalCenter(containerSize: Int): Float {
CommonTileDefaults.TileStartPadding.toPx()
}
-private fun Modifier.tileBackground(color: Color): Modifier {
+private fun Modifier.tileBackground(color: () -> Color): Modifier {
// Clip tile contents from overflowing past the tile
- return clip(RoundedCornerShape(InactiveCornerRadius)).drawBehind { drawRect(color) }
+ return clip(RoundedCornerShape(InactiveCornerRadius)).drawBehind { drawRect(color()) }
}
private object EditModeTileDefaults {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt
index 984343a45797..233af548fff2 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt
@@ -42,7 +42,6 @@ import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel
import com.android.systemui.qs.panels.ui.viewmodel.IconTilesViewModel
import com.android.systemui.qs.panels.ui.viewmodel.InfiniteGridViewModel
import com.android.systemui.qs.panels.ui.viewmodel.TileViewModel
-import com.android.systemui.qs.pipeline.domain.interactor.CurrentTilesInteractor.Companion.POSITION_AT_END
import com.android.systemui.qs.pipeline.shared.TileSpec
import com.android.systemui.qs.shared.ui.ElementKeys.toElementKey
import com.android.systemui.res.R
@@ -171,7 +170,7 @@ constructor(
otherTiles = otherTiles,
columns = columns,
modifier = modifier,
- onAddTile = { onAddTile(it, POSITION_AT_END) },
+ onAddTile = onAddTile,
onRemoveTile = onRemoveTile,
onSetTiles = onSetTiles,
onResize = iconTilesViewModel::resize,
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/MutableSelectionState.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/MutableSelectionState.kt
index 3dfde86bf8d9..50b29557683d 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/MutableSelectionState.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/MutableSelectionState.kt
@@ -16,15 +16,17 @@
package com.android.systemui.qs.panels.ui.compose.selection
-import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.pointer.pointerInput
+import com.android.systemui.common.ui.compose.gestures.detectEagerTapGestures
import com.android.systemui.qs.pipeline.shared.TileSpec
+import kotlinx.coroutines.delay
/** Creates the state of the current selected tile that is remembered across compositions. */
@Composable
@@ -38,6 +40,17 @@ class MutableSelectionState {
var selection by mutableStateOf<TileSpec?>(null)
private set
+ /**
+ * Whether the current selection is in placement mode or not.
+ *
+ * A tile in placement mode can be positioned by tapping at the desired location in the grid.
+ */
+ var placementEnabled by mutableStateOf(false)
+ private set
+
+ /** Latest event from coming from placement mode. */
+ var placementEvent by mutableStateOf<PlacementEvent?>(null)
+
val selected: Boolean
get() = selection != null
@@ -47,37 +60,122 @@ class MutableSelectionState {
fun unSelect() {
selection = null
+ exitPlacementMode()
}
-}
-/**
- * Listens for click events to select/unselect the given [TileSpec]. Use this on current tiles as
- * they can be selected.
- */
-fun Modifier.selectableTile(
- tileSpec: TileSpec,
- selectionState: MutableSelectionState,
- onClick: () -> Unit = {},
-): Modifier {
- return pointerInput(Unit) {
- detectTapGestures(
- onTap = {
- if (selectionState.selection == tileSpec) {
- selectionState.unSelect()
- } else {
- selectionState.select(tileSpec)
+ /** Selects [tileSpec] and enable placement mode. */
+ fun enterPlacementMode(tileSpec: TileSpec) {
+ selection = tileSpec
+ placementEnabled = true
+ }
+
+ /** Disable placement mode but maintains current selection. */
+ private fun exitPlacementMode() {
+ placementEnabled = false
+ }
+
+ fun togglePlacementMode(tileSpec: TileSpec) {
+ if (placementEnabled) exitPlacementMode() else enterPlacementMode(tileSpec)
+ }
+
+ suspend fun tileStateFor(
+ tileSpec: TileSpec,
+ previousState: TileState,
+ canShowRemovalBadge: Boolean,
+ ): TileState {
+ return when {
+ placementEnabled && selection == tileSpec -> TileState.Placeable
+ placementEnabled -> TileState.GreyedOut
+ selection == tileSpec -> {
+ if (previousState == TileState.None && canShowRemovalBadge) {
+ // The tile decoration is None if a tile is newly composed OR the removal
+ // badge can't be shown.
+ // For newly composed and selected tiles, such as dragged tiles or moved
+ // tiles from resizing, introduce a short delay. This avoids clipping issues
+ // on the border and resizing handle, as well as letting the selection
+ // animation play correctly.
+ delay(250)
}
- onClick()
+ TileState.Selected
}
- )
+ canShowRemovalBadge -> TileState.Removable
+ else -> TileState.None
+ }
+ }
+
+ /**
+ * Tap callback on a tile.
+ *
+ * Tiles can be selected and placed using placement mode.
+ */
+ fun onTap(tileSpec: TileSpec) {
+ when {
+ placementEnabled && selection == tileSpec -> {
+ exitPlacementMode()
+ }
+ placementEnabled -> {
+ selection?.let { placementEvent = PlacementEvent.PlaceToTileSpec(it, tileSpec) }
+ exitPlacementMode()
+ }
+ selection == tileSpec -> {
+ unSelect()
+ }
+ else -> {
+ select(tileSpec)
+ }
+ }
+ }
+
+ /**
+ * Tap on a position.
+ *
+ * Use on grid items not associated with a [TileSpec], such as a spacer. Spacers can't be
+ * selected, but selections can be moved to their position.
+ */
+ fun onTap(index: Int) {
+ when {
+ placementEnabled -> {
+ selection?.let { placementEvent = PlacementEvent.PlaceToIndex(it, index) }
+ exitPlacementMode()
+ }
+ selected -> {
+ unSelect()
+ }
+ }
}
}
+// Not using data classes here as distinct placement events may have the same moving spec and target
+@Stable
+sealed interface PlacementEvent {
+ val movingSpec: TileSpec
+
+ /** Placement event corresponding to [movingSpec] moving to [targetSpec]'s position */
+ class PlaceToTileSpec(override val movingSpec: TileSpec, val targetSpec: TileSpec) :
+ PlacementEvent
+
+ /** Placement event corresponding to [movingSpec] moving to [targetIndex] */
+ class PlaceToIndex(override val movingSpec: TileSpec, val targetIndex: Int) : PlacementEvent
+}
+
/**
- * Listens for click events to unselect any tile. Use this on available tiles as they can't be
- * selected.
+ * Listens for click events on selectable tiles.
+ *
+ * Use this on current tiles as they can be selected.
+ *
+ * @param tileSpec the [TileSpec] of the tile this modifier is applied to
+ * @param selectionState the [MutableSelectionState] representing the grid's selection
*/
@Composable
-fun Modifier.clearSelectionTile(selectionState: MutableSelectionState): Modifier {
- return pointerInput(Unit) { detectTapGestures(onTap = { selectionState.unSelect() }) }
+fun Modifier.selectableTile(tileSpec: TileSpec, selectionState: MutableSelectionState): Modifier {
+ return pointerInput(Unit) {
+ detectEagerTapGestures(
+ doubleTapEnabled = {
+ // Double tap enabled if where not in placement mode already
+ !selectionState.placementEnabled
+ },
+ onDoubleTap = { selectionState.enterPlacementMode(tileSpec) },
+ onTap = { selectionState.onTap(tileSpec) },
+ )
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/Selection.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/Selection.kt
index 57f63c755b43..8ffc4be88e7c 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/Selection.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/Selection.kt
@@ -17,6 +17,7 @@
package com.android.systemui.qs.panels.ui.compose.selection
import androidx.compose.animation.animateColor
+import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.Transition
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.animateFloatAsState
@@ -37,9 +38,13 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.LocalMinimumInteractiveComponentSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State
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.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
@@ -73,7 +78,9 @@ import com.android.systemui.qs.panels.ui.compose.selection.SelectionDefaults.RES
import com.android.systemui.qs.panels.ui.compose.selection.SelectionDefaults.ResizingPillHeight
import com.android.systemui.qs.panels.ui.compose.selection.SelectionDefaults.ResizingPillWidth
import com.android.systemui.qs.panels.ui.compose.selection.SelectionDefaults.SelectedBorderWidth
+import com.android.systemui.qs.panels.ui.compose.selection.TileState.GreyedOut
import com.android.systemui.qs.panels.ui.compose.selection.TileState.None
+import com.android.systemui.qs.panels.ui.compose.selection.TileState.Placeable
import com.android.systemui.qs.panels.ui.compose.selection.TileState.Removable
import com.android.systemui.qs.panels.ui.compose.selection.TileState.Selected
import kotlin.math.cos
@@ -104,10 +111,11 @@ fun InteractiveTileContainer(
) {
val transition: Transition<TileState> = updateTransition(tileState)
val decorationColor by transition.animateColor()
- val decorationAngle by transition.animateAngle()
+ val decorationAngle by animateAngle(tileState)
val decorationSize by transition.animateSize()
val decorationOffset by transition.animateOffset()
- val decorationAlpha by transition.animateFloat { state -> if (state == None) 0f else 1f }
+ val decorationAlpha by
+ transition.animateFloat { state -> if (state == Removable || state == Selected) 1f else 0f }
val badgeIconAlpha by transition.animateFloat { state -> if (state == Removable) 1f else 0f }
val selectionBorderAlpha by
transition.animateFloat { state -> if (state == Selected) 1f else 0f }
@@ -282,27 +290,61 @@ private fun Modifier.resizable(selected: Boolean, state: ResizingState): Modifie
}
enum class TileState {
+ /** Tile is displayed as-is, no additional decoration needed. */
None,
+ /** Tile can be removed by the user. This is displayed by a badge in the upper end corner. */
Removable,
+ /**
+ * Tile is selected and resizable. One tile can be selected at a time in the grid. This is when
+ * we display the resizing handle and a highlighted border around the tile.
+ */
Selected,
+ /**
+ * Tile placeable. This state means that the grid is in placement mode and this tile is
+ * selected. It should be highlighted to stand out in the grid.
+ */
+ Placeable,
+ /**
+ * Tile is faded out. This state means that the grid is in placement mode and this tile isn't
+ * selected. It serves as a target to place the selected tile.
+ */
+ GreyedOut,
}
@Composable
private fun Transition<TileState>.animateColor(): State<Color> {
return animateColor { state ->
when (state) {
- None -> Color.Transparent
+ None,
+ GreyedOut -> Color.Transparent
Removable -> MaterialTheme.colorScheme.primaryContainer
- Selected -> MaterialTheme.colorScheme.primary
+ Selected,
+ Placeable -> MaterialTheme.colorScheme.primary
}
}
}
+/**
+ * Animate the angle of the tile decoration based on the previous state
+ *
+ * Some [TileState] don't have a visible decoration, and the angle should only animate when going
+ * between visible states.
+ */
@Composable
-private fun Transition<TileState>.animateAngle(): State<Float> {
- return animateFloat { state ->
- if (state == Removable) BADGE_ANGLE_RAD else RESIZING_PILL_ANGLE_RAD
+private fun animateAngle(tileState: TileState): State<Float> {
+ val animatable = remember { Animatable(0f) }
+ var animate by remember { mutableStateOf(false) }
+ LaunchedEffect(tileState) {
+ val targetAngle = tileState.decorationAngle()
+
+ if (targetAngle == null) {
+ animate = false
+ } else {
+ if (animate) animatable.animateTo(targetAngle) else animatable.snapTo(targetAngle)
+ animate = true
+ }
}
+ return animatable.asState()
}
@Composable
@@ -310,7 +352,9 @@ private fun Transition<TileState>.animateSize(): State<Size> {
return animateSize { state ->
with(LocalDensity.current) {
when (state) {
- None -> Size.Zero
+ None,
+ Placeable,
+ GreyedOut -> Size.Zero
Removable -> Size(BadgeSize.toPx())
Selected -> Size(ResizingPillWidth.toPx(), ResizingPillHeight.toPx())
}
@@ -323,7 +367,9 @@ private fun Transition<TileState>.animateOffset(): State<Offset> {
return animateOffset { state ->
with(LocalDensity.current) {
when (state) {
- None -> Offset.Zero
+ None,
+ Placeable,
+ GreyedOut -> Offset.Zero
Removable -> Offset(BadgeXOffset.toPx(), BadgeYOffset.toPx())
Selected -> Offset(-SelectedBorderWidth.toPx(), 0f)
}
@@ -331,6 +377,16 @@ private fun Transition<TileState>.animateOffset(): State<Offset> {
}
}
+private fun TileState.decorationAngle(): Float? {
+ return when (this) {
+ Removable -> BADGE_ANGLE_RAD
+ Selected -> RESIZING_PILL_ANGLE_RAD
+ None,
+ Placeable,
+ GreyedOut -> null // No visible decoration
+ }
+}
+
private fun Size(size: Float) = Size(size, size)
private fun offsetForAngle(angle: Float, radius: Float, center: Offset): Offset {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/EditTileViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/EditTileViewModel.kt
index be6ce5c5b4f4..cf325f531c38 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/EditTileViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/EditTileViewModel.kt
@@ -66,6 +66,9 @@ data class EditTileViewModel(
) : CategoryAndName {
override val name
get() = label.text
+
+ val isRemovable
+ get() = availableEditActions.contains(AvailableEditActions.REMOVE)
}
enum class AvailableEditActions {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/CastTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/CastTile.java
index c60e3da9d833..49ec1baeeeee 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/CastTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/CastTile.java
@@ -48,11 +48,13 @@ import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.plugins.FalsingManager;
import com.android.systemui.plugins.qs.QSTile.BooleanState;
+import com.android.systemui.plugins.qs.TileDetailsViewModel;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.qs.QSHost;
import com.android.systemui.qs.QsEventLogger;
import com.android.systemui.qs.logging.QSLogger;
import com.android.systemui.qs.tileimpl.QSTileImpl;
+import com.android.systemui.qs.tiles.dialog.CastDetailsViewModel;
import com.android.systemui.res.R;
import com.android.systemui.shade.domain.interactor.ShadeDialogContextInteractor;
import com.android.systemui.statusbar.connectivity.NetworkController;
@@ -93,6 +95,7 @@ public class CastTile extends QSTileImpl<BooleanState> {
private final ShadeDialogContextInteractor mShadeDialogContextInteractor;
private boolean mCastTransportAllowed;
private boolean mHotspotConnected;
+ private final CastDetailsViewModel.Factory mCastDetailsViewModelFactory;
@Inject
public CastTile(
@@ -113,7 +116,8 @@ public class CastTile extends QSTileImpl<BooleanState> {
ConnectivityRepository connectivityRepository,
TileJavaAdapter javaAdapter,
FeatureFlags featureFlags,
- ShadeDialogContextInteractor shadeDialogContextInteractor
+ ShadeDialogContextInteractor shadeDialogContextInteractor,
+ CastDetailsViewModel.Factory castDetailsViewModelFactory
) {
super(host, uiEventLogger, backgroundLooper, mainHandler, falsingManager, metricsLogger,
statusBarStateController, activityStarter, qsLogger);
@@ -124,6 +128,7 @@ public class CastTile extends QSTileImpl<BooleanState> {
mJavaAdapter = javaAdapter;
mFeatureFlags = featureFlags;
mShadeDialogContextInteractor = shadeDialogContextInteractor;
+ mCastDetailsViewModelFactory = castDetailsViewModelFactory;
mController.observe(this, mCallback);
mKeyguard.observe(this, mCallback);
if (!mFeatureFlags.isEnabled(SIGNAL_CALLBACK_DEPRECATION)) {
@@ -172,12 +177,7 @@ public class CastTile extends QSTileImpl<BooleanState> {
@Override
protected void handleClick(@Nullable Expandable expandable) {
- if (getState().state == Tile.STATE_UNAVAILABLE) {
- return;
- }
-
- List<CastDevice> activeDevices = getActiveDevices();
- if (willPopDialog()) {
+ handleClick(() -> {
if (!mKeyguard.isShowing()) {
showDialog(expandable);
} else {
@@ -187,16 +187,44 @@ public class CastTile extends QSTileImpl<BooleanState> {
showDialog(null /* view */);
});
}
+ });
+ }
+
+ @Override
+ public boolean getDetailsViewModel(Consumer<TileDetailsViewModel> callback) {
+ CastDetailsViewModel viewModel = mCastDetailsViewModelFactory
+ .create(mShadeDialogContextInteractor.getContext(), ROUTE_TYPE_REMOTE_DISPLAY);
+ handleClick(() -> {
+ if (!mKeyguard.isShowing()) {
+ callback.accept(viewModel);
+ } else {
+ mActivityStarter.dismissKeyguardThenExecute(() -> {
+ callback.accept(viewModel);
+ return false;
+ }, null /* cancelAction */, true/* afterKeyguardGone */);
+ }
+ });
+ return true;
+ }
+
+ private void handleClick(Runnable showPromptCallback) {
+ if (getState().state == Tile.STATE_UNAVAILABLE) {
+ return;
+ }
+
+ List<CastDevice> activeDevices = getActiveDevices();
+ if (willShowPrompt()) {
+ showPromptCallback.run();
} else {
mController.stopCasting(activeDevices.get(0), StopReason.STOP_QS_TILE);
}
}
- // We want to pop up the media route selection dialog if we either have no active devices
- // (neither routes nor projection), or if we have an active route. In other cases, we assume
- // that a projection is active. This is messy, but this tile never correctly handled the
- // case where multiple devices were active :-/.
- private boolean willPopDialog() {
+ // We want to pop up the media route selection dialog (or show the cast details view) if we
+ // either have no active devices (neither routes nor projection), or if we have an active
+ // route. In other cases, we assume that a projection is active. This is messy, but this tile
+ // never correctly handled the case where multiple devices were active :-/.
+ private boolean willShowPrompt() {
List<CastDevice> activeDevices = getActiveDevices();
return activeDevices.isEmpty() || (activeDevices.get(0).getTag() instanceof RouteInfo);
}
@@ -303,7 +331,7 @@ public class CastTile extends QSTileImpl<BooleanState> {
state.secondaryLabel = "";
}
state.expandedAccessibilityClassName = Button.class.getName();
- state.forceExpandIcon = willPopDialog();
+ state.forceExpandIcon = willShowPrompt();
} else {
state.state = Tile.STATE_UNAVAILABLE;
String noWifi = mContext.getString(R.string.quick_settings_cast_no_network);
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/CastDetailsContent.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/CastDetailsContent.kt
new file mode 100644
index 000000000000..cd33c964ce1b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/CastDetailsContent.kt
@@ -0,0 +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.
+ */
+
+package com.android.systemui.qs.tiles.dialog
+
+import android.view.LayoutInflater
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.viewinterop.AndroidView
+import com.android.internal.R
+import com.android.internal.app.MediaRouteControllerContentManager
+
+@Composable
+fun CastDetailsContent(castDetailsViewModel: CastDetailsViewModel) {
+ if (castDetailsViewModel.shouldShowChooserDialog()) {
+ // TODO(b/378514236): Show the chooser UI here.
+ return
+ }
+
+ val contentManager: MediaRouteControllerContentManager = remember {
+ castDetailsViewModel.createControllerContentManager()
+ }
+
+ AndroidView(
+ modifier = Modifier.fillMaxWidth().fillMaxHeight(),
+ factory = { context ->
+ // Inflate with the existing dialog xml layout
+ val view =
+ LayoutInflater.from(context).inflate(R.layout.media_route_controller_dialog, null)
+ contentManager.bindViews(view)
+ contentManager.onAttachedToWindow()
+
+ view
+ },
+ onRelease = { contentManager.onDetachedFromWindow() },
+ )
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/CastDetailsViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/CastDetailsViewModel.kt
new file mode 100644
index 000000000000..72322eff8bed
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/CastDetailsViewModel.kt
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles.dialog
+
+import android.content.Context
+import android.content.Intent
+import android.graphics.drawable.Drawable
+import android.provider.Settings
+import com.android.internal.app.MediaRouteControllerContentManager
+import com.android.internal.app.MediaRouteDialogPresenter
+import com.android.systemui.plugins.qs.TileDetailsViewModel
+import com.android.systemui.qs.tiles.base.domain.actions.QSTileIntentUserInputHandler
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+
+/** The view model used for the screen record details view in the Quick Settings */
+class CastDetailsViewModel
+@AssistedInject
+constructor(
+ private val qsTileIntentUserActionHandler: QSTileIntentUserInputHandler,
+ @Assisted private val context: Context,
+ @Assisted private val routeTypes: Int,
+) : MediaRouteControllerContentManager.Delegate, TileDetailsViewModel {
+ @AssistedFactory
+ fun interface Factory {
+ fun create(context: Context, routeTypes: Int): CastDetailsViewModel
+ }
+
+ fun shouldShowChooserDialog(): Boolean {
+ return MediaRouteDialogPresenter.shouldShowChooserDialog(context, routeTypes)
+ }
+
+ fun createControllerContentManager(): MediaRouteControllerContentManager {
+ return MediaRouteControllerContentManager(context, this)
+ }
+
+ override fun clickOnSettingsButton() {
+ qsTileIntentUserActionHandler.handle(
+ /* expandable= */ null,
+ Intent(Settings.ACTION_CAST_SETTINGS),
+ )
+ }
+
+ // TODO(b/388321032): Replace this string with a string in a translatable xml file,
+ override val title: String
+ get() = "Cast screen to device"
+
+ // TODO(b/388321032): Replace this string with a string in a translatable xml file,
+ override val subTitle: String
+ get() = "Searching for devices..."
+
+ override fun setMediaRouteDeviceTitle(title: CharSequence?) {
+ // TODO(b/378514236): Finish implementing this function.
+ }
+
+ override fun setMediaRouteDeviceIcon(icon: Drawable?) {
+ // TODO(b/378514236): Finish implementing this function.
+ }
+
+ override fun dismissView() {
+ // TODO(b/378514236): Finish implementing this function.
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/reardisplay/RearDisplayCoreStartable.kt b/packages/SystemUI/src/com/android/systemui/reardisplay/RearDisplayCoreStartable.kt
index 263ef09ea767..5d4a774d77f9 100644
--- a/packages/SystemUI/src/com/android/systemui/reardisplay/RearDisplayCoreStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/reardisplay/RearDisplayCoreStartable.kt
@@ -19,14 +19,18 @@ package com.android.systemui.reardisplay
import android.content.Context
import android.hardware.devicestate.DeviceStateManager
import android.hardware.devicestate.feature.flags.Flags
+import android.os.Handler
+import android.view.accessibility.AccessibilityManager
import androidx.annotation.VisibleForTesting
import com.android.keyguard.KeyguardUpdateMonitor
import com.android.keyguard.KeyguardUpdateMonitorCallback
import com.android.systemui.CoreStartable
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.display.domain.interactor.RearDisplayStateInteractor
import com.android.systemui.statusbar.phone.SystemUIDialog
+import java.util.concurrent.atomic.AtomicBoolean
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
@@ -52,6 +56,8 @@ internal constructor(
private val rearDisplayInnerDialogDelegateFactory: RearDisplayInnerDialogDelegate.Factory,
@Application private val scope: CoroutineScope,
private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
+ private val accessibilityManager: AccessibilityManager,
+ @Background private val handler: Handler,
) : CoreStartable, AutoCloseable {
companion object {
@@ -77,6 +83,12 @@ internal constructor(
override fun start() {
if (Flags.deviceStateRdmV2()) {
var dialog: SystemUIDialog? = null
+ var touchExplorationEnabled = AtomicBoolean(false)
+
+ accessibilityManager.addTouchExplorationStateChangeListener(
+ { enabled -> touchExplorationEnabled.set(enabled) },
+ handler,
+ )
keyguardUpdateMonitor.registerCallback(keyguardCallback)
@@ -99,6 +111,7 @@ internal constructor(
rearDisplayInnerDialogDelegateFactory.create(
rearDisplayContext,
deviceStateManager::cancelStateRequest,
+ touchExplorationEnabled.get(),
)
dialog = delegate.createDialog().apply { show() }
}
diff --git a/packages/SystemUI/src/com/android/systemui/reardisplay/RearDisplayInnerDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/reardisplay/RearDisplayInnerDialogDelegate.kt
index f5facf42ee67..96f1bd270239 100644
--- a/packages/SystemUI/src/com/android/systemui/reardisplay/RearDisplayInnerDialogDelegate.kt
+++ b/packages/SystemUI/src/com/android/systemui/reardisplay/RearDisplayInnerDialogDelegate.kt
@@ -20,7 +20,10 @@ import android.annotation.SuppressLint
import android.content.Context
import android.os.Bundle
import android.view.MotionEvent
+import android.view.View
+import android.widget.Button
import android.widget.SeekBar
+import android.widget.TextView
import com.android.systemui.haptics.slider.HapticSlider
import com.android.systemui.haptics.slider.HapticSliderPlugin
import com.android.systemui.haptics.slider.HapticSliderViewBinder
@@ -45,6 +48,7 @@ class RearDisplayInnerDialogDelegate
internal constructor(
private val systemUIDialogFactory: SystemUIDialog.Factory,
@Assisted private val rearDisplayContext: Context,
+ @Assisted private val touchExplorationEnabled: Boolean,
private val vibratorHelper: VibratorHelper,
private val msdlPlayer: MSDLPlayer,
private val systemClock: SystemClock,
@@ -82,6 +86,7 @@ internal constructor(
fun create(
rearDisplayContext: Context,
onCanceledRunnable: Runnable,
+ touchExplorationEnabled: Boolean,
): RearDisplayInnerDialogDelegate
}
@@ -95,11 +100,32 @@ internal constructor(
@SuppressLint("ClickableViewAccessibility")
override fun onCreate(dialog: SystemUIDialog, savedInstanceState: Bundle?) {
+
dialog.apply {
setContentView(R.layout.activity_rear_display_enabled)
setCanceledOnTouchOutside(false)
+ requireViewById<Button>(R.id.cancel_button).let { it ->
+ if (!touchExplorationEnabled) {
+ return@let
+ }
+
+ it.visibility = View.VISIBLE
+ it.setOnClickListener { onCanceledRunnable.run() }
+ }
+
+ requireViewById<TextView>(R.id.seekbar_instructions).let { it ->
+ if (touchExplorationEnabled) {
+ it.visibility = View.GONE
+ }
+ }
+
requireViewById<SeekBar>(R.id.seekbar).let { it ->
+ if (touchExplorationEnabled) {
+ it.visibility = View.GONE
+ return@let
+ }
+
// Create and bind the HapticSliderPlugin
val hapticSliderPlugin =
HapticSliderPlugin(
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 3ad0867192d3..06fc8610c97b 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
@@ -33,10 +33,8 @@ import com.android.systemui.bouncer.domain.interactor.SimBouncerInteractor
import com.android.systemui.bouncer.shared.logging.BouncerUiEvent
import com.android.systemui.classifier.FalsingCollector
import com.android.systemui.classifier.FalsingCollectorActual
-import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.dagger.qualifiers.DisplayId
import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor
import com.android.systemui.deviceentry.domain.interactor.DeviceEntryHapticsInteractor
import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor
@@ -82,6 +80,7 @@ import com.android.systemui.util.kotlin.pairwise
import com.android.systemui.util.kotlin.sample
import com.android.systemui.util.printSection
import com.android.systemui.util.println
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
import com.google.android.msdl.data.model.MSDLToken
import com.google.android.msdl.domain.MSDLPlayer
import dagger.Lazy
@@ -123,7 +122,6 @@ constructor(
private val bouncerInteractor: BouncerInteractor,
private val keyguardInteractor: KeyguardInteractor,
private val sysUiState: SysUiState,
- @DisplayId private val displayId: Int,
private val sceneLogger: SceneLogger,
@FalsingCollectorActual private val falsingCollector: FalsingCollector,
private val falsingManager: FalsingManager,
@@ -197,7 +195,8 @@ constructor(
return
}
- printSection("Scene state") {
+ printSection("Framework state") {
+ println("isVisible", sceneInteractor.isVisible.value)
println("currentScene", sceneInteractor.currentScene.value.debugName)
println(
"currentOverlays",
@@ -732,21 +731,26 @@ constructor(
sceneInteractor.transitionState
.mapNotNull { it as? ObservableTransitionState.Idle }
.distinctUntilChanged(),
+ sceneInteractor.isVisible,
occlusionInteractor.invisibleDueToOcclusion,
- ) { idleState, invisibleDueToOcclusion ->
+ ) { idleState, isVisible, invisibleDueToOcclusion ->
SceneContainerPlugin.SceneContainerPluginState(
scene = idleState.currentScene,
overlays = idleState.currentOverlays,
+ isVisible = isVisible,
invisibleDueToOcclusion = invisibleDueToOcclusion,
)
}
- .collect { sceneContainerPluginState ->
+ .map { sceneContainerPluginState ->
+ SceneContainerPlugin.EvaluatorByFlag.map { (flag, evaluator) ->
+ flag to evaluator(sceneContainerPluginState)
+ }
+ .toMap()
+ }
+ .distinctUntilChanged()
+ .collect { flags ->
sysUiState.updateFlags(
- displayId,
- *SceneContainerPlugin.EvaluatorByFlag.map { (flag, evaluator) ->
- flag to evaluator.invoke(sceneContainerPluginState)
- }
- .toTypedArray(),
+ *(flags.entries.map { (key, value) -> key to value }).toTypedArray()
)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index 24e7976011f4..b90624245cc5 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
@@ -206,6 +206,7 @@ import com.google.android.msdl.data.model.MSDLToken;
import com.google.android.msdl.domain.MSDLPlayer;
import dagger.Lazy;
+
import kotlin.Unit;
import kotlinx.coroutines.CoroutineDispatcher;
@@ -4267,7 +4268,8 @@ public final class NotificationPanelViewController implements
== AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_FORWARD.getId()
|| action
== AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_UP.getId()) {
- mStatusBarKeyguardViewManager.showPrimaryBouncer(true);
+ mStatusBarKeyguardViewManager.showPrimaryBouncer(true,
+ "NotificationPanelViewController#performAccessibilityAction");
return true;
}
return super.performAccessibilityAction(host, action, args);
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt
index e6834ad6cdda..b211f0729318 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt
@@ -97,6 +97,11 @@ constructor(
loggingReason = "ShadeControllerSceneImpl.instantCollapseShade",
transitionKey = Instant,
)
+
+ shadeInteractor.collapseQuickSettingsShade(
+ loggingReason = "ShadeControllerSceneImpl.instantCollapseShade",
+ transitionKey = Instant,
+ )
}
override fun animateCollapseShade(
diff --git a/packages/SystemUI/src/com/android/systemui/shade/carrier/ShadeCarrierGroupController.java b/packages/SystemUI/src/com/android/systemui/shade/carrier/ShadeCarrierGroupController.java
index 4b8cc00e1c28..421b5ea23278 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/carrier/ShadeCarrierGroupController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/carrier/ShadeCarrierGroupController.java
@@ -39,9 +39,13 @@ import androidx.annotation.VisibleForTesting;
import com.android.keyguard.CarrierTextManager;
import com.android.settingslib.AccessibilityContentDescriptions;
import com.android.settingslib.mobile.TelephonyIcons;
+import com.android.systemui.Flags;
import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.dagger.qualifiers.Application;
import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.dagger.qualifiers.Main;
+import com.android.systemui.kairos.ExperimentalKairosApi;
+import com.android.systemui.kairos.KairosNetwork;
import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.res.R;
import com.android.systemui.shade.ShadeDisplayAware;
@@ -52,17 +56,30 @@ import com.android.systemui.statusbar.connectivity.ui.MobileContextProvider;
import com.android.systemui.statusbar.phone.StatusBarLocation;
import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags;
import com.android.systemui.statusbar.pipeline.mobile.ui.MobileUiAdapter;
+import com.android.systemui.statusbar.pipeline.mobile.ui.MobileUiAdapterKairos;
import com.android.systemui.statusbar.pipeline.mobile.ui.binder.MobileIconsBinder;
import com.android.systemui.statusbar.pipeline.mobile.ui.view.ModernShadeCarrierGroupMobileView;
import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel;
+import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModelKairos;
import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.ShadeCarrierGroupMobileIconViewModel;
import com.android.systemui.util.CarrierConfigTracker;
+import dagger.Lazy;
+
+import kotlin.OptIn;
+import kotlin.Pair;
+
+import kotlinx.coroutines.CoroutineScope;
+import kotlinx.coroutines.Job;
+
+import java.util.ArrayList;
import java.util.List;
+import java.util.concurrent.CancellationException;
import java.util.function.Consumer;
import javax.inject.Inject;
+@OptIn(markerClass = ExperimentalKairosApi.class)
public class ShadeCarrierGroupController {
private static final String TAG = "ShadeCarrierGroup";
@@ -100,38 +117,43 @@ public class ShadeCarrierGroupController {
private final ShadeCarrierGroupControllerLogger mLogger;
+ private final Lazy<MobileUiAdapterKairos> mMobileUiAdapterKairos;
+ private final CoroutineScope mAppScope;
+ private final KairosNetwork mKairosNetwork;
+
private final SignalCallback mSignalCallback = new SignalCallback() {
- @Override
- public void setMobileDataIndicators(@NonNull MobileDataIndicators indicators) {
- int slotIndex = getSlotIndex(indicators.subId);
- if (slotIndex >= SIM_SLOTS) {
- Log.w(TAG, "setMobileDataIndicators - slot: " + slotIndex);
- return;
- }
- if (slotIndex == INVALID_SIM_SLOT_INDEX) {
- Log.e(TAG, "Invalid SIM slot index for subscription: " + indicators.subId);
- return;
- }
- mInfos[slotIndex] = new CellSignalState(
- indicators.statusIcon.visible,
- indicators.statusIcon.icon,
- indicators.statusIcon.contentDescription,
- indicators.typeContentDescription.toString(),
- indicators.roaming
- );
- mMainHandler.obtainMessage(H.MSG_UPDATE_STATE).sendToTarget();
- }
+ @Override
+ public void setMobileDataIndicators(@NonNull MobileDataIndicators indicators) {
+ int slotIndex = getSlotIndex(indicators.subId);
+ if (slotIndex >= SIM_SLOTS) {
+ Log.w(TAG, "setMobileDataIndicators - slot: " + slotIndex);
+ return;
+ }
+ if (slotIndex == INVALID_SIM_SLOT_INDEX) {
+ Log.e(TAG, "Invalid SIM slot index for subscription: " + indicators.subId);
+ return;
+ }
+ mInfos[slotIndex] = new CellSignalState(
+ indicators.statusIcon.visible,
+ indicators.statusIcon.icon,
+ indicators.statusIcon.contentDescription,
+ indicators.typeContentDescription.toString(),
+ indicators.roaming
+ );
+ mMainHandler.obtainMessage(H.MSG_UPDATE_STATE).sendToTarget();
+ }
- @Override
- public void setNoSims(boolean hasNoSims, boolean simDetected) {
- if (hasNoSims) {
- for (int i = 0; i < SIM_SLOTS; i++) {
- mInfos[i] = mInfos[i].changeVisibility(false);
- }
- }
- mMainHandler.obtainMessage(H.MSG_UPDATE_STATE).sendToTarget();
+ @Override
+ public void setNoSims(boolean hasNoSims, boolean simDetected) {
+ if (hasNoSims) {
+ for (int i = 0; i < SIM_SLOTS; i++) {
+ mInfos[i] = mInfos[i].changeVisibility(false);
}
- };
+ }
+ mMainHandler.obtainMessage(H.MSG_UPDATE_STATE).sendToTarget();
+ }
+ };
+ private final ArrayList<Job> mBindingJobs = new ArrayList<>();
private static class Callback implements CarrierTextManager.CarrierTextCallback {
private H mHandler;
@@ -159,7 +181,10 @@ public class ShadeCarrierGroupController {
SlotIndexResolver slotIndexResolver,
MobileUiAdapter mobileUiAdapter,
MobileContextProvider mobileContextProvider,
- StatusBarPipelineFlags statusBarPipelineFlags
+ StatusBarPipelineFlags statusBarPipelineFlags,
+ Lazy<MobileUiAdapterKairos> mobileUiAdapterKairos,
+ CoroutineScope appScope,
+ KairosNetwork kairosNetwork
) {
mContext = context;
mActivityStarter = activityStarter;
@@ -174,6 +199,9 @@ public class ShadeCarrierGroupController {
.build();
mCarrierConfigTracker = carrierConfigTracker;
mSlotIndexResolver = slotIndexResolver;
+ mMobileUiAdapterKairos = mobileUiAdapterKairos;
+ mAppScope = appScope;
+ mKairosNetwork = kairosNetwork;
View.OnClickListener onClickListener = v -> {
if (!v.isVisibleToUser()) {
return;
@@ -195,8 +223,12 @@ public class ShadeCarrierGroupController {
mMobileIconsViewModel = mobileUiAdapter.getMobileIconsViewModel();
if (mStatusBarPipelineFlags.useNewShadeCarrierGroupMobileIcons()) {
- mobileUiAdapter.setShadeCarrierGroupController(this);
- MobileIconsBinder.bind(view, mMobileIconsViewModel);
+ if (Flags.statusBarMobileIconKairos()) {
+ mobileUiAdapterKairos.get().setShadeCarrierGroupController(this);
+ } else {
+ mobileUiAdapter.setShadeCarrierGroupController(this);
+ MobileIconsBinder.bind(view, mMobileIconsViewModel);
+ }
}
mCarrierDividers[0] = view.getCarrierDivider1();
@@ -243,21 +275,52 @@ public class ShadeCarrierGroupController {
List<IconData> iconDataList = processSubIdList(subIds);
- for (IconData iconData : iconDataList) {
- ShadeCarrier carrier = mCarrierGroups[iconData.slotIndex];
-
- Context mobileContext =
- mMobileContextProvider.getMobileContextForSub(iconData.subId, mContext);
- ModernShadeCarrierGroupMobileView modernMobileView = ModernShadeCarrierGroupMobileView
- .constructAndBind(
- mobileContext,
- mMobileIconsViewModel.getLogger(),
- "mobile_carrier_shade_group",
- (ShadeCarrierGroupMobileIconViewModel) mMobileIconsViewModel
- .viewModelForSub(iconData.subId,
- StatusBarLocation.SHADE_CARRIER_GROUP)
- );
- carrier.addModernMobileView(modernMobileView);
+ if (Flags.statusBarMobileIconKairos()) {
+ for (Job job : mBindingJobs) {
+ job.cancel(new CancellationException());
+ }
+ mBindingJobs.clear();
+ MobileIconsViewModelKairos mobileIconsViewModel =
+ mMobileUiAdapterKairos.get().getMobileIconsViewModel();
+ for (IconData iconData : iconDataList) {
+ ShadeCarrier carrier = mCarrierGroups[iconData.slotIndex];
+
+ Context mobileContext =
+ mMobileContextProvider.getMobileContextForSub(iconData.subId, mContext);
+
+ Pair<ModernShadeCarrierGroupMobileView, Job> viewAndJob =
+ ModernShadeCarrierGroupMobileView.constructAndBind(
+ mobileContext,
+ mobileIconsViewModel.getLogger(),
+ "mobile_carrier_shade_group",
+ mobileIconsViewModel.shadeCarrierGroupIcon(iconData.subId),
+ mAppScope,
+ iconData.subId,
+ StatusBarLocation.SHADE_CARRIER_GROUP,
+ mKairosNetwork
+ );
+ mBindingJobs.add(viewAndJob.getSecond());
+ carrier.addModernMobileView(viewAndJob.getFirst());
+ }
+ } else {
+ for (IconData iconData : iconDataList) {
+ ShadeCarrier carrier = mCarrierGroups[iconData.slotIndex];
+
+ Context mobileContext =
+ mMobileContextProvider.getMobileContextForSub(iconData.subId, mContext);
+
+ ModernShadeCarrierGroupMobileView modernMobileView =
+ ModernShadeCarrierGroupMobileView
+ .constructAndBind(
+ mobileContext,
+ mMobileIconsViewModel.getLogger(),
+ "mobile_carrier_shade_group",
+ (ShadeCarrierGroupMobileIconViewModel) mMobileIconsViewModel
+ .viewModelForSub(iconData.subId,
+ StatusBarLocation.SHADE_CARRIER_GROUP)
+ );
+ carrier.addModernMobileView(modernMobileView);
+ }
}
}
@@ -288,7 +351,6 @@ public class ShadeCarrierGroupController {
* Sets a {@link OnSingleCarrierChangedListener}.
*
* This will get notified when the number of carriers changes between 1 and "not one".
- * @param listener
*/
public void setOnSingleCarrierChangedListener(
@Nullable OnSingleCarrierChangedListener listener) {
@@ -489,6 +551,9 @@ public class ShadeCarrierGroupController {
private final MobileUiAdapter mMobileUiAdapter;
private final MobileContextProvider mMobileContextProvider;
private final StatusBarPipelineFlags mStatusBarPipelineFlags;
+ private final CoroutineScope mAppScope;
+ private final KairosNetwork mKairosNetwork;
+ private final Lazy<MobileUiAdapterKairos> mMobileUiAdapterKairos;
@Inject
public Builder(
@@ -503,7 +568,10 @@ public class ShadeCarrierGroupController {
SlotIndexResolver slotIndexResolver,
MobileUiAdapter mobileUiAdapter,
MobileContextProvider mobileContextProvider,
- StatusBarPipelineFlags statusBarPipelineFlags
+ StatusBarPipelineFlags statusBarPipelineFlags,
+ @Application CoroutineScope appScope,
+ KairosNetwork kairosNetwork,
+ Lazy<MobileUiAdapterKairos> mobileUiAdapterKairos
) {
mActivityStarter = activityStarter;
mHandler = handler;
@@ -517,6 +585,9 @@ public class ShadeCarrierGroupController {
mMobileUiAdapter = mobileUiAdapter;
mMobileContextProvider = mobileContextProvider;
mStatusBarPipelineFlags = statusBarPipelineFlags;
+ mAppScope = appScope;
+ mKairosNetwork = kairosNetwork;
+ mMobileUiAdapterKairos = mobileUiAdapterKairos;
}
public Builder setShadeCarrierGroup(ShadeCarrierGroup view) {
@@ -538,8 +609,10 @@ public class ShadeCarrierGroupController {
mSlotIndexResolver,
mMobileUiAdapter,
mMobileContextProvider,
- mStatusBarPipelineFlags
- );
+ mStatusBarPipelineFlags,
+ mMobileUiAdapterKairos,
+ mAppScope,
+ mKairosNetwork);
}
}
@@ -571,7 +644,8 @@ public class ShadeCarrierGroupController {
public static class SubscriptionManagerSlotIndexResolver implements SlotIndexResolver {
@Inject
- public SubscriptionManagerSlotIndexResolver() {}
+ public SubscriptionManagerSlotIndexResolver() {
+ }
@Override
public int getSlotIndex(int subscriptionId) {
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImpl.kt
index 52de0abf7d3c..043742245227 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImpl.kt
@@ -187,12 +187,18 @@ constructor(
override fun collapseNotificationsShade(loggingReason: String, transitionKey: TransitionKey?) {
if (shadeModeInteractor.isDualShade) {
- // TODO(b/356596436): Hide without animation if transitionKey is Instant.
- sceneInteractor.hideOverlay(
- overlay = Overlays.NotificationsShade,
- loggingReason = loggingReason,
- transitionKey = transitionKey,
- )
+ if (transitionKey == Instant) {
+ sceneInteractor.instantlyHideOverlay(
+ overlay = Overlays.NotificationsShade,
+ loggingReason = loggingReason,
+ )
+ } else {
+ sceneInteractor.hideOverlay(
+ overlay = Overlays.NotificationsShade,
+ loggingReason = loggingReason,
+ transitionKey = transitionKey,
+ )
+ }
} else if (transitionKey == Instant) {
// TODO(b/356596436): Define instant transition instead of snapToScene().
sceneInteractor.snapToScene(
@@ -215,12 +221,18 @@ constructor(
bypassNotificationsShade: Boolean,
) {
if (shadeModeInteractor.isDualShade) {
- // TODO(b/356596436): Hide without animation if transitionKey is Instant.
- sceneInteractor.hideOverlay(
- overlay = Overlays.QuickSettingsShade,
- loggingReason = loggingReason,
- transitionKey = transitionKey,
- )
+ if (transitionKey == Instant) {
+ sceneInteractor.instantlyHideOverlay(
+ overlay = Overlays.QuickSettingsShade,
+ loggingReason = loggingReason,
+ )
+ } else {
+ sceneInteractor.hideOverlay(
+ overlay = Overlays.QuickSettingsShade,
+ loggingReason = loggingReason,
+ transitionKey = transitionKey,
+ )
+ }
return
}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt
index 5609326362fc..88242762da78 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt
@@ -14,6 +14,8 @@
* limitations under the License.
*/
+@file:OptIn(ExperimentalKairosApi::class)
+
package com.android.systemui.shade.ui.viewmodel
import android.content.Context
@@ -28,6 +30,8 @@ import androidx.compose.ui.graphics.Color
import com.android.app.tracing.coroutines.launchTraced as launch
import com.android.compose.animation.scene.OverlayKey
import com.android.systemui.battery.BatteryMeterViewController
+import com.android.systemui.kairos.ExperimentalKairosApi
+import com.android.systemui.kairos.KairosNetwork
import com.android.systemui.lifecycle.ExclusiveActivatable
import com.android.systemui.lifecycle.Hydrator
import com.android.systemui.plugins.ActivityStarter
@@ -47,6 +51,7 @@ import com.android.systemui.statusbar.phone.ui.StatusBarIconController
import com.android.systemui.statusbar.phone.ui.TintedIconManager
import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor
import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel
+import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModelKairos
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import java.util.Locale
@@ -76,6 +81,8 @@ constructor(
private val tintedIconManagerFactory: TintedIconManager.Factory,
private val batteryMeterViewControllerFactory: BatteryMeterViewController.Factory,
val statusBarIconController: StatusBarIconController,
+ val kairosNetwork: KairosNetwork,
+ val mobileIconsViewModelKairos: dagger.Lazy<MobileIconsViewModelKairos>,
) : ExclusiveActivatable() {
private val hydrator = Hydrator("ShadeHeaderViewModel.hydrator")
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
index e44701dba87c..4daf61a895c7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
@@ -64,6 +64,7 @@ import androidx.annotation.VisibleForTesting;
import com.android.internal.annotations.KeepForWeakReference;
import com.android.internal.os.SomeArgs;
+import com.android.internal.statusbar.DisableStates;
import com.android.internal.statusbar.IAddTileResultCallback;
import com.android.internal.statusbar.IStatusBar;
import com.android.internal.statusbar.IUndoMediaTransferCallback;
@@ -85,6 +86,7 @@ import java.io.FileOutputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.util.ArrayList;
+import java.util.Map;
/**
* This class takes the functions from IStatusBar that come in on
@@ -184,6 +186,8 @@ public class CommandQueue extends IStatusBar.Stub implements
private static final int MSG_TOGGLE_QUICK_SETTINGS_PANEL = 82 << MSG_SHIFT;
private static final int MSG_WALLET_ACTION_LAUNCH_GESTURE = 83 << MSG_SHIFT;
private static final int MSG_DISPLAY_REMOVE_SYSTEM_DECORATIONS = 85 << MSG_SHIFT;
+ private static final int MSG_DISABLE_ALL = 86 << MSG_SHIFT;
+
public static final int FLAG_EXCLUDE_NONE = 0;
public static final int FLAG_EXCLUDE_SEARCH_PANEL = 1 << 0;
public static final int FLAG_EXCLUDE_RECENTS_PANEL = 1 << 1;
@@ -654,7 +658,8 @@ public class CommandQueue extends IStatusBar.Stub implements
/**
* Called to notify that disable flags are updated.
- * @see Callbacks#disable(int, int, int, boolean).
+ * @see Callbacks#disable(int, int, int, boolean)
+ * @see Callbacks#disableForAllDisplays(DisableStates)
*/
public void disable(int displayId, @DisableFlags int state1, @Disable2Flags int state2,
boolean animate) {
@@ -682,6 +687,27 @@ public class CommandQueue extends IStatusBar.Stub implements
disable(displayId, state1, state2, true);
}
+ @Override
+ public void disableForAllDisplays(DisableStates disableStates) throws RemoteException {
+ synchronized (mLock) {
+ for (Map.Entry<Integer, Pair<Integer, Integer>> displaysWithStates :
+ disableStates.displaysWithStates.entrySet()) {
+ int displayId = displaysWithStates.getKey();
+ Pair<Integer, Integer> states = displaysWithStates.getValue();
+ setDisabled(displayId, states.first, states.second);
+ }
+ mHandler.removeMessages(MSG_DISABLE_ALL);
+ Message msg = mHandler.obtainMessage(MSG_DISABLE_ALL, disableStates);
+ if (Looper.myLooper() == mHandler.getLooper()) {
+ // If its the right looper execute immediately so hides can be handled quickly.
+ mHandler.handleMessage(msg);
+ msg.recycle();
+ } else {
+ msg.sendToTarget();
+ }
+ }
+ }
+
/**
* Apply current disable flags by {@link CommandQueue#disable(int, int, int, boolean)}.
*
@@ -1552,6 +1578,21 @@ public class CommandQueue extends IStatusBar.Stub implements
args.argi4 != 0 /* animate */);
}
break;
+ case MSG_DISABLE_ALL:
+ DisableStates disableStates = (DisableStates) msg.obj;
+ boolean animate = disableStates.animate;
+ Map<Integer, Pair<Integer, Integer>> displaysWithDisableStates =
+ disableStates.displaysWithStates;
+ for (Map.Entry<Integer, Pair<Integer, Integer>> displayWithDisableStates :
+ displaysWithDisableStates.entrySet()) {
+ int displayId = displayWithDisableStates.getKey();
+ Pair<Integer, Integer> states = displayWithDisableStates.getValue();
+ for (int i = 0; i < mCallbacks.size(); i++) {
+ mCallbacks.get(i).disable(displayId, states.first, states.second,
+ animate);
+ }
+ }
+ break;
case MSG_EXPAND_NOTIFICATIONS:
for (int i = 0; i < mCallbacks.size(); i++) {
mCallbacks.get(i).animateExpandNotificationsPanel();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationGroupingUtil.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationGroupingUtil.java
index 03c191e40ccf..2030606e4274 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationGroupingUtil.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationGroupingUtil.java
@@ -38,6 +38,7 @@ import com.android.internal.R;
import com.android.internal.widget.CachingIconView;
import com.android.internal.widget.ConversationLayout;
import com.android.internal.widget.ImageFloatingTextView;
+import com.android.systemui.statusbar.notification.icon.IconPack;
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
import com.android.systemui.statusbar.notification.row.NotificationContentView;
import com.android.systemui.statusbar.notification.row.shared.AsyncGroupHeaderViewInflation;
@@ -61,10 +62,19 @@ public class NotificationGroupingUtil {
private static final VisibilityApplicator VISIBILITY_APPLICATOR = new VisibilityApplicator();
private static final VisibilityApplicator APP_NAME_APPLICATOR = new AppNameApplicator();
private static final ResultApplicator LEFT_ICON_APPLICATOR = new LeftIconApplicator();
- private static final DataExtractor ICON_EXTRACTOR = new DataExtractor() {
+
+ @VisibleForTesting
+ static final DataExtractor ICON_EXTRACTOR = new DataExtractor() {
@Override
public Object extractData(ExpandableNotificationRow row) {
- return row.getEntry().getSbn().getNotification();
+ if (NotificationBundleUi.isEnabled()) {
+ if (row.getEntryAdapter().getSbn() != null) {
+ return row.getEntryAdapter().getSbn().getNotification();
+ }
+ return null;
+ } else {
+ return row.getEntry().getSbn().getNotification();
+ }
}
};
@@ -253,7 +263,7 @@ public class NotificationGroupingUtil {
if (NotificationBundleUi.isEnabled()) {
sbn = row.getEntryAdapter() != null ? row.getEntryAdapter().getSbn() : null;
} else {
- sbn = row.getEntry().getSbn();
+ sbn = row.getEntryLegacy().getSbn();
}
return (sbn != null && sbn.getNotification().showsTime());
}
@@ -357,7 +367,8 @@ public class NotificationGroupingUtil {
boolean isEmpty(View view);
}
- private interface DataExtractor {
+ @VisibleForTesting
+ interface DataExtractor {
Object extractData(ExpandableNotificationRow row);
}
@@ -395,13 +406,17 @@ public class NotificationGroupingUtil {
}
}
- private abstract static class IconComparator implements ViewComparator {
+ @VisibleForTesting
+ static class IconComparator implements ViewComparator {
@Override
public boolean compare(View parent, View child, Object parentData, Object childData) {
return false;
}
protected boolean hasSameIcon(Object parentData, Object childData) {
+ if (parentData == null || childData == null) {
+ return false;
+ }
Icon parentIcon = ((Notification) parentData).getSmallIcon();
Icon childIcon = ((Notification) childData).getSmallIcon();
return parentIcon.sameAs(childIcon);
@@ -411,6 +426,10 @@ public class NotificationGroupingUtil {
* @return whether two ImageViews have the same colorFilterSet or none at all
*/
protected boolean hasSameColor(Object parentData, Object childData) {
+ if ((parentData == null && childData != null)
+ || (parentData != null && childData == null)) {
+ return false;
+ }
int parentColor = ((Notification) parentData).color;
int childColor = ((Notification) childData).color;
return parentColor == childColor;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt
index 472dc823423e..e292bcf1f7a8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt
@@ -24,6 +24,7 @@ import android.util.IndentingPrintWriter
import android.util.Log
import android.util.MathUtils
import android.view.Choreographer
+import android.view.Display
import android.view.View
import androidx.annotation.VisibleForTesting
import androidx.dynamicanimation.animation.FloatPropertyCompat
@@ -42,7 +43,9 @@ import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
import com.android.systemui.plugins.statusbar.StatusBarStateController
import com.android.systemui.shade.ShadeExpansionChangeEvent
import com.android.systemui.shade.ShadeExpansionListener
+import com.android.systemui.shade.data.repository.ShadeDisplaysRepository
import com.android.systemui.shade.domain.interactor.ShadeModeInteractor
+import com.android.systemui.shade.shared.flag.ShadeWindowGoesAround
import com.android.systemui.statusbar.phone.BiometricUnlockController
import com.android.systemui.statusbar.phone.BiometricUnlockController.MODE_WAKE_AND_UNLOCK
import com.android.systemui.statusbar.phone.DozeParameters
@@ -52,6 +55,7 @@ import com.android.systemui.util.WallpaperController
import com.android.systemui.wallpapers.domain.interactor.WallpaperInteractor
import com.android.systemui.window.domain.interactor.WindowRootViewBlurInteractor
import com.android.wm.shell.appzoomout.AppZoomOut
+import dagger.Lazy
import java.io.PrintWriter
import java.util.Optional
import javax.inject.Inject
@@ -83,6 +87,7 @@ constructor(
private val appZoomOutOptional: Optional<AppZoomOut>,
@Application private val applicationScope: CoroutineScope,
dumpManager: DumpManager,
+ private val shadeDisplaysRepository: Lazy<ShadeDisplaysRepository>,
) : ShadeExpansionListener, Dumpable {
companion object {
private const val WAKE_UP_ANIMATION_ENABLED = true
@@ -228,6 +233,14 @@ constructor(
private data class WakeAndUnlockBlurData(val radius: Float, val useZoom: Boolean = true)
+ private val isShadeOnDefaultDisplay: Boolean
+ get() =
+ if (ShadeWindowGoesAround.isEnabled) {
+ shadeDisplaysRepository.get().displayId.value == Display.DEFAULT_DISPLAY
+ } else {
+ true
+ }
+
/** Blur radius of the wake and unlock animation on this frame, and whether to zoom out. */
private var wakeAndUnlockBlurData = WakeAndUnlockBlurData(0f)
set(value) {
@@ -265,9 +278,14 @@ constructor(
var blur = shadeRadius.toInt()
// If the blur comes from waking up, we don't want to zoom out the background
val zoomOut =
- if (shadeRadius != wakeAndUnlockBlurData.radius|| wakeAndUnlockBlurData.useZoom)
- blurRadiusToZoomOut(blurRadius = shadeRadius)
- else 0f
+ when {
+ // When the shade is in another display, we don't want to zoom out the background.
+ // Only the default display is supported right now.
+ !isShadeOnDefaultDisplay -> 0f
+ shadeRadius != wakeAndUnlockBlurData.radius || wakeAndUnlockBlurData.useZoom ->
+ blurRadiusToZoomOut(blurRadius = shadeRadius)
+ else -> 0f
+ }
// Make blur be 0 if it is necessary to stop blur effect.
if (scrimsVisible) {
if (!Flags.notificationShadeBlur()) {
@@ -353,7 +371,9 @@ constructor(
interpolator = Interpolators.FAST_OUT_SLOW_IN
addUpdateListener { animation: ValueAnimator ->
wakeAndUnlockBlurData =
- WakeAndUnlockBlurData(blurUtils.blurRadiusOfRatio(animation.animatedValue as Float))
+ WakeAndUnlockBlurData(
+ blurUtils.blurRadiusOfRatio(animation.animatedValue as Float)
+ )
}
addListener(
object : AnimatorListenerAdapter() {
@@ -428,8 +448,10 @@ constructor(
applicationScope.launch {
wallpaperInteractor.wallpaperSupportsAmbientMode.collect { supported ->
wallpaperSupportsAmbientMode = supported
- if (getNewWakeBlurRadius(prevDozeAmount) == wakeAndUnlockBlurData.radius
- && !wakeAndUnlockBlurData.useZoom) {
+ if (
+ getNewWakeBlurRadius(prevDozeAmount) == wakeAndUnlockBlurData.radius &&
+ !wakeAndUnlockBlurData.useZoom
+ ) {
// Update wake and unlock radius only if the previous value comes from wake-up.
updateWakeBlurRadius(prevDozeAmount)
}
@@ -452,6 +474,21 @@ constructor(
scheduleUpdate()
}
}
+
+ applicationScope.launch {
+ windowRootViewBlurInteractor.isBlurCurrentlySupported.collect { supported ->
+ if (supported) {
+ // when battery saver changes, try scheduling an update.
+ scheduleUpdate()
+ } else {
+ // when blur becomes unsupported, no more updates will be scheduled,
+ // reset updateScheduled state.
+ updateScheduled = false
+ // reset blur and internal state to 0
+ onBlurApplied(0, 0.0f)
+ }
+ }
+ }
}
fun addListener(listener: DepthListener) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModel.kt
index 6ce350cb95f5..c862460ad6f7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModel.kt
@@ -194,6 +194,7 @@ constructor(
): OngoingActivityChipModel.Active {
return OngoingActivityChipModel.Active.Timer(
key = KEY,
+ isImportantForPrivacy = true,
icon =
OngoingActivityChipModel.ChipIcon.SingleColorIcon(
Icon.Resource(
@@ -232,6 +233,7 @@ constructor(
private fun createIconOnlyCastChip(deviceName: String?): OngoingActivityChipModel.Active {
return OngoingActivityChipModel.Active.IconOnly(
key = KEY,
+ isImportantForPrivacy = true,
icon =
OngoingActivityChipModel.ChipIcon.SingleColorIcon(
Icon.Resource(
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 1f2079d83e6f..356731cb3777 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
@@ -17,7 +17,7 @@
package com.android.systemui.statusbar.chips.notification.domain.model
import com.android.systemui.statusbar.StatusBarIconView
-import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel
+import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModels
/** Modeling all the data needed to render a status bar notification chip. */
data class NotificationChipModel(
@@ -25,7 +25,7 @@ data class NotificationChipModel(
/** The user-readable name of the app that posted this notification. */
val appName: String,
val statusBarChipIconView: StatusBarIconView?,
- val promotedContent: PromotedNotificationContentModel,
+ val promotedContent: PromotedNotificationContentModels,
/** 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. */
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 1a802d634894..dfbd12d5a215 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
@@ -72,6 +72,8 @@ constructor(
headsUpState: TopPinnedState
): OngoingActivityChipModel.Active {
StatusBarNotifChips.unsafeAssertInNewMode()
+ // Chips are never shown when locked, so it's safe to use the version with sensitive content
+ val chipContent = promotedContent.privateVersion
val contentDescription = getContentDescription(this.appName)
val icon =
if (this.statusBarChipIconView != null) {
@@ -115,65 +117,62 @@ constructor(
// If the user tapped this chip to show the HUN, we want to just show the icon because
// the HUN will show the rest of the information.
return OngoingActivityChipModel.Active.IconOnly(
- this.key,
- icon,
- colors,
- onClickListenerLegacy,
- clickBehavior,
+ key = this.key,
+ icon = icon,
+ colors = colors,
+ onClickListenerLegacy = onClickListenerLegacy,
+ clickBehavior = clickBehavior,
)
}
- if (this.promotedContent.shortCriticalText != null) {
+ if (chipContent.shortCriticalText != null) {
return OngoingActivityChipModel.Active.Text(
- this.key,
- icon,
- colors,
- this.promotedContent.shortCriticalText,
- onClickListenerLegacy,
- clickBehavior,
+ key = this.key,
+ icon = icon,
+ colors = colors,
+ text = chipContent.shortCriticalText,
+ onClickListenerLegacy = onClickListenerLegacy,
+ clickBehavior = clickBehavior,
)
}
- if (
- Flags.promoteNotificationsAutomatically() &&
- this.promotedContent.wasPromotedAutomatically
- ) {
+ if (Flags.promoteNotificationsAutomatically() && chipContent.wasPromotedAutomatically) {
// When we're promoting notifications automatically, the `when` time set on the
// notification will likely just be set to the current time, which would cause the chip
// to always show "now". We don't want early testers to get that experience since it's
// not what will happen at launch, so just don't show any time.onometerstate
return OngoingActivityChipModel.Active.IconOnly(
- this.key,
- icon,
- colors,
- onClickListenerLegacy,
- clickBehavior,
+ key = this.key,
+ icon = icon,
+ colors = colors,
+ onClickListenerLegacy = onClickListenerLegacy,
+ clickBehavior = clickBehavior,
)
}
- if (this.promotedContent.time == null) {
+ if (chipContent.time == null) {
return OngoingActivityChipModel.Active.IconOnly(
- this.key,
- icon,
- colors,
- onClickListenerLegacy,
- clickBehavior,
+ key = this.key,
+ icon = icon,
+ colors = colors,
+ onClickListenerLegacy = onClickListenerLegacy,
+ clickBehavior = clickBehavior,
)
}
- when (this.promotedContent.time) {
+ when (chipContent.time) {
is PromotedNotificationContentModel.When.Time -> {
return if (
- this.promotedContent.time.currentTimeMillis >=
+ chipContent.time.currentTimeMillis >=
systemClock.currentTimeMillis() + FUTURE_TIME_THRESHOLD_MILLIS
) {
OngoingActivityChipModel.Active.ShortTimeDelta(
- this.key,
- icon,
- colors,
- time = this.promotedContent.time.currentTimeMillis,
- onClickListenerLegacy,
- clickBehavior,
+ key = this.key,
+ icon = icon,
+ colors = colors,
+ time = chipContent.time.currentTimeMillis,
+ onClickListenerLegacy = onClickListenerLegacy,
+ clickBehavior = clickBehavior,
)
} else {
// Don't show a `when` time that's close to now or in the past because it's
@@ -185,21 +184,21 @@ constructor(
// 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,
+ key = this.key,
+ icon = icon,
+ colors = colors,
+ onClickListenerLegacy = onClickListenerLegacy,
+ clickBehavior = clickBehavior,
)
}
}
is PromotedNotificationContentModel.When.Chronometer -> {
return OngoingActivityChipModel.Active.Timer(
- this.key,
- icon,
- colors,
- startTimeMs = this.promotedContent.time.elapsedRealtimeMillis,
- isEventInFuture = this.promotedContent.time.isCountDown,
+ key = this.key,
+ icon = icon,
+ colors = colors,
+ startTimeMs = chipContent.time.elapsedRealtimeMillis,
+ isEventInFuture = chipContent.time.isCountDown,
onClickListenerLegacy = onClickListenerLegacy,
clickBehavior = clickBehavior,
)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractor.kt
index 572c7faf19e6..fce428dcb7e1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractor.kt
@@ -94,9 +94,11 @@ constructor(
TAG,
LogLevel.INFO,
{},
- { "State: Recording(taskPackage=null) due to force-start" },
+ {
+ "State: Recording(hostPackage=null, taskPackage=null) due to force-start"
+ },
)
- ScreenRecordChipModel.Recording(recordedTask = null)
+ ScreenRecordChipModel.Recording(hostPackage = null, recordedTask = null)
} else {
when (screenRecordState) {
is ScreenRecordModel.DoingNothing -> {
@@ -124,13 +126,25 @@ constructor(
} else {
null
}
+ val hostPackage =
+ if (mediaProjectionState is MediaProjectionState.Projecting) {
+ mediaProjectionState.hostPackage
+ } else {
+ null
+ }
logger.log(
TAG,
LogLevel.INFO,
- { str1 = recordedTask?.baseIntent?.component?.packageName },
- { "State: Recording(taskPackage=$str1)" },
+ {
+ str1 = hostPackage
+ str2 = recordedTask?.baseIntent?.component?.packageName
+ },
+ { "State: Recording(hostPackage=$str1, taskPackage=$str2)" },
+ )
+ ScreenRecordChipModel.Recording(
+ hostPackage = hostPackage,
+ recordedTask = recordedTask,
)
- ScreenRecordChipModel.Recording(recordedTask)
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/domain/model/ScreenRecordChipModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/domain/model/ScreenRecordChipModel.kt
index ba6cd4df8d47..0a7278792e70 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/domain/model/ScreenRecordChipModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/domain/model/ScreenRecordChipModel.kt
@@ -29,10 +29,13 @@ sealed interface ScreenRecordChipModel {
/**
* There's an active screen recording happening.
*
+ * @property hostPackage the package name of the app that is receiving the content of the media
+ * projection (aka which app the phone screen contents are being sent to).
* @property recordedTask the task being recorded if the user is recording only a single app.
* Null if the user is recording the entire screen or we don't have the task info yet.
*/
data class Recording(
+ val hostPackage: String?,
val recordedTask: ActivityManager.RunningTaskInfo?,
) : ScreenRecordChipModel
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModel.kt
index 55c89a96422f..363b8beab2d7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModel.kt
@@ -77,6 +77,7 @@ constructor(
is ScreenRecordChipModel.Starting -> {
OngoingActivityChipModel.Active.Countdown(
key = KEY,
+ isImportantForPrivacy = true,
colors = ColorsModel.Red,
secondsUntilStarted = state.millisUntilStarted.toCountdownSeconds(),
)
@@ -84,6 +85,7 @@ constructor(
is ScreenRecordChipModel.Recording -> {
OngoingActivityChipModel.Active.Timer(
key = KEY,
+ isImportantForPrivacy = true,
icon =
OngoingActivityChipModel.ChipIcon.SingleColorIcon(
Icon.Resource(
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModel.kt
index 92e17bdd511a..0defa531d18d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModel.kt
@@ -222,6 +222,7 @@ constructor(
): OngoingActivityChipModel.Active {
return OngoingActivityChipModel.Active.Timer(
key = KEY,
+ isImportantForPrivacy = true,
icon =
OngoingActivityChipModel.ChipIcon.SingleColorIcon(
Icon.Resource(
@@ -257,6 +258,7 @@ constructor(
private fun createIconOnlyShareToAppChip(): OngoingActivityChipModel.Active {
return OngoingActivityChipModel.Active.IconOnly(
key = KEY,
+ isImportantForPrivacy = true,
icon =
OngoingActivityChipModel.ChipIcon.SingleColorIcon(
Icon.Resource(
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChip.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChip.kt
index b94e7b249233..104c2b546200 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChip.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChip.kt
@@ -107,11 +107,15 @@ fun OngoingActivityChip(
}
}
.thenIf(isClickable) { Modifier.widthIn(min = minWidth) }
- .layout { measurable, constraints ->
- val placeable = measurable.measure(constraints)
- layout(placeable.width, placeable.height) {
- if (constraints.maxWidth >= minWidth.roundToPx()) {
- placeable.place(0, 0)
+ // For non-privacy-related chips, only show the chip if there's enough space for at
+ // least the minimum width.
+ .thenIf(!model.isImportantForPrivacy) {
+ Modifier.layout { measurable, constraints ->
+ val placeable = measurable.measure(constraints)
+ layout(placeable.width, placeable.height) {
+ if (constraints.maxWidth >= minWidth.roundToPx()) {
+ placeable.place(0, 0)
+ }
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/OngoingActivityChipModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/OngoingActivityChipModel.kt
index 364e6656ee9d..e28f3684b0fa 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/OngoingActivityChipModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/OngoingActivityChipModel.kt
@@ -57,6 +57,11 @@ sealed class OngoingActivityChipModel {
* A key that uniquely identifies this chip. Used for better visual effects, like animation.
*/
open val key: String,
+ /**
+ * True if this chip is critical for privacy so we should keep it visible at all times, and
+ * false otherwise.
+ */
+ open val isImportantForPrivacy: Boolean = false,
/** The icon to show on the chip. If null, no icon will be shown. */
open val icon: ChipIcon?,
/** What colors to use for the chip. */
@@ -81,6 +86,7 @@ sealed class OngoingActivityChipModel {
/** This chip shows only an icon and nothing else. */
data class IconOnly(
override val key: String,
+ override val isImportantForPrivacy: Boolean = false,
override val icon: ChipIcon,
override val colors: ColorsModel,
override val onClickListenerLegacy: View.OnClickListener?,
@@ -91,6 +97,7 @@ sealed class OngoingActivityChipModel {
) :
Active(
key,
+ isImportantForPrivacy,
icon,
colors,
onClickListenerLegacy,
@@ -105,6 +112,7 @@ sealed class OngoingActivityChipModel {
/** The chip shows a timer, counting up from [startTimeMs]. */
data class Timer(
override val key: String,
+ override val isImportantForPrivacy: Boolean = false,
override val icon: ChipIcon,
override val colors: ColorsModel,
/**
@@ -138,6 +146,7 @@ sealed class OngoingActivityChipModel {
) :
Active(
key,
+ isImportantForPrivacy,
icon,
colors,
onClickListenerLegacy,
@@ -155,6 +164,7 @@ sealed class OngoingActivityChipModel {
*/
data class ShortTimeDelta(
override val key: String,
+ override val isImportantForPrivacy: Boolean = false,
override val icon: ChipIcon,
override val colors: ColorsModel,
/**
@@ -175,6 +185,7 @@ sealed class OngoingActivityChipModel {
) :
Active(
key,
+ isImportantForPrivacy,
icon,
colors,
onClickListenerLegacy,
@@ -196,6 +207,7 @@ sealed class OngoingActivityChipModel {
*/
data class Countdown(
override val key: String,
+ override val isImportantForPrivacy: Boolean = false,
override val colors: ColorsModel,
/** The number of seconds until an event is started. */
val secondsUntilStarted: Long,
@@ -205,6 +217,7 @@ sealed class OngoingActivityChipModel {
) :
Active(
key,
+ isImportantForPrivacy,
icon = null,
colors,
onClickListenerLegacy = null,
@@ -219,6 +232,7 @@ sealed class OngoingActivityChipModel {
/** This chip shows the specified [text] in the chip. */
data class Text(
override val key: String,
+ override val isImportantForPrivacy: Boolean = false,
override val icon: ChipIcon,
override val colors: ColorsModel,
// TODO(b/361346412): Enforce a max length requirement?
@@ -231,6 +245,7 @@ sealed class OngoingActivityChipModel {
) :
Active(
key,
+ isImportantForPrivacy,
icon,
colors,
onClickListenerLegacy,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModel.kt
index 8228b5533fca..76d2af86e239 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModel.kt
@@ -265,11 +265,12 @@ constructor(
// [shouldSquish] returns false for that model, but protect against it just in case.)
val currentIcon = icon ?: return this
return OngoingActivityChipModel.Active.IconOnly(
- key,
- currentIcon,
- colors,
- onClickListenerLegacy,
- clickBehavior,
+ key = key,
+ isImportantForPrivacy = isImportantForPrivacy,
+ icon = currentIcon,
+ colors = colors,
+ onClickListenerLegacy = onClickListenerLegacy,
+ clickBehavior = clickBehavior,
)
}
@@ -374,6 +375,9 @@ constructor(
* Sort the given chip [bundle] in order of priority, and divide the chips between active,
* overflow, and inactive (see [MultipleOngoingActivityChipsModel] for a description of each).
*/
+ // IMPORTANT: PromotedNotificationsInteractor re-implements this same ordering scheme. Any
+ // changes here should also be made in PromotedNotificationsInteractor.
+ // TODO(b/402471288): Create a single source of truth for the ordering.
private fun rankChips(bundle: ChipBundle): MultipleOngoingActivityChipsModel {
val activeChips = mutableListOf<OngoingActivityChipModel.Active>()
val overflowChips = mutableListOf<OngoingActivityChipModel.Active>()
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 4558017a98c8..b5ab0920a470 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
@@ -68,6 +68,7 @@ import com.android.systemui.statusbar.notification.collection.notifcollection.No
import com.android.systemui.statusbar.notification.headsup.PinnedStatus;
import com.android.systemui.statusbar.notification.icon.IconPack;
import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel;
+import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModels;
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRowController;
import com.android.systemui.statusbar.notification.row.NotificationGuts;
@@ -198,7 +199,7 @@ public final class NotificationEntry extends ListEntry {
// TODO(b/377565433): Move into NotificationContentModel during/after
// NotificationRowContentBinderRefactor.
- private PromotedNotificationContentModel mPromotedNotificationContentModel;
+ private PromotedNotificationContentModels mPromotedNotificationContentModels;
/**
* True if both
@@ -1106,9 +1107,9 @@ public final class NotificationEntry extends ListEntry {
* Gets the content needed to render this notification as a promoted notification on various
* surfaces (like status bar chips and AOD).
*/
- public PromotedNotificationContentModel getPromotedNotificationContentModel() {
+ public PromotedNotificationContentModels getPromotedNotificationContentModels() {
if (PromotedNotificationContentModel.featureFlagEnabled()) {
- return mPromotedNotificationContentModel;
+ return mPromotedNotificationContentModels;
} else {
Log.wtf(TAG, "getting promoted content without feature flag enabled", new Throwable());
return null;
@@ -1127,10 +1128,10 @@ public final class NotificationEntry extends ListEntry {
* Sets the content needed to render this notification as a promoted notification on various
* surfaces (like status bar chips and AOD).
*/
- public void setPromotedNotificationContentModel(
- @Nullable PromotedNotificationContentModel promotedNotificationContentModel) {
+ public void setPromotedNotificationContentModels(
+ @Nullable PromotedNotificationContentModels promotedNotificationContentModels) {
if (PromotedNotificationContentModel.featureFlagEnabled()) {
- this.mPromotedNotificationContentModel = promotedNotificationContentModel;
+ this.mPromotedNotificationContentModels = promotedNotificationContentModels;
} else {
Log.wtf(TAG, "setting promoted content without feature flag enabled", new Throwable());
}
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 afba85b49c30..c8ec85d0a227 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
@@ -24,10 +24,9 @@ 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.PipelineEntry;
import com.android.systemui.statusbar.notification.collection.NotifPipeline;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
+import com.android.systemui.statusbar.notification.collection.PipelineEntry;
import com.android.systemui.statusbar.notification.collection.coordinator.dagger.CoordinatorScope;
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifComparator;
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifPromoter;
@@ -100,7 +99,7 @@ public class ColorizedFgsCoordinator implements Coordinator {
public boolean isInSection(PipelineEntry entry) {
NotificationEntry notificationEntry = entry.getRepresentativeEntry();
if (notificationEntry != null) {
- return isRichOngoing(notificationEntry);
+ return isRichOngoing(notificationEntry) || isPromotedNotifChip(notificationEntry);
}
return false;
}
@@ -159,4 +158,10 @@ public class ColorizedFgsCoordinator implements Coordinator {
return entry.getImportance() > IMPORTANCE_MIN
&& notification.isStyle(Notification.CallStyle.class);
}
+
+ private boolean isPromotedNotifChip(NotificationEntry entry) {
+ return PromotedNotificationUi.isEnabled()
+ && entry.getImportance() > IMPORTANCE_MIN
+ && mOrderedPromotedNotifKeys.contains(entry.getKey());
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt
index a0eab43f854b..26b86f9ed74d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt
@@ -15,6 +15,8 @@
*/
package com.android.systemui.statusbar.notification.collection.coordinator
+import com.android.systemui.Flags.notificationSkipSilentUpdates
+
import android.app.Notification
import android.app.Notification.GROUP_ALERT_SUMMARY
import android.util.ArrayMap
@@ -465,15 +467,32 @@ constructor(
}
hunMutator.updateNotification(posted.key, pinnedStatus)
}
- } else {
+ } else { // shouldHeadsUpEver = false
if (posted.isHeadsUpEntry) {
- // We don't want this to be interrupting anymore, let's remove it
- // If the notification is pinned by the user, the only way a user can un-pin
- // it is by tapping the status bar notification chip. Since that's a clear
- // user action, we should remove the HUN immediately instead of waiting for
- // any sort of minimum timeout.
- val shouldRemoveImmediately = posted.isPinnedByUser
- hunMutator.removeNotification(posted.key, shouldRemoveImmediately)
+ if (notificationSkipSilentUpdates()) {
+ if (posted.isPinnedByUser) {
+ // We don't want this to be interrupting anymore, let's remove it
+ // If the notification is pinned by the user, the only way a user
+ // can un-pin it by tapping the status bar notification chip. Since
+ // that's a clear user action, we should remove the HUN immediately
+ // instead of waiting for any sort of minimum timeout.
+ // TODO(b/401068530) Ensure that status bar chip HUNs are not
+ // removed for silent update
+ hunMutator.removeNotification(posted.key,
+ /* releaseImmediately= */ true)
+ } else {
+ // Do NOT remove HUN for non-user update.
+ // Let the HUN show for its remaining duration.
+ }
+ } else {
+ // We don't want this to be interrupting anymore, let's remove it
+ // If the notification is pinned by the user, the only way a user can
+ // un-pin it is by tapping the status bar notification chip. Since
+ // that's a clear user action, we should remove the HUN immediately
+ // instead of waiting for any sort of minimum timeout.
+ val shouldRemoveImmediately = posted.isPinnedByUser
+ hunMutator.removeNotification(posted.key, shouldRemoveImmediately)
+ }
} else {
// Don't let the bind finish
cancelHeadsUpBind(posted.entry)
@@ -573,24 +592,34 @@ constructor(
isBinding = isBinding,
)
}
- // Handle cancelling heads up here, rather than in the OnBeforeFinalizeFilter, so
- // that
- // work can be done before the ShadeListBuilder is run. This prevents re-entrant
- // behavior between this Coordinator, HeadsUpManager, and VisualStabilityManager.
- if (posted?.shouldHeadsUpEver == false) {
- if (posted.isHeadsUpEntry) {
- // We don't want this to be interrupting anymore, let's remove it
- mHeadsUpManager.removeNotification(
- posted.key,
- /* removeImmediately= */ false,
- "onEntryUpdated",
- )
- } else if (posted.isBinding) {
+ if (notificationSkipSilentUpdates()) {
+ // TODO(b/403703828) Move canceling to OnBeforeFinalizeFilter, since we are not
+ // removing from HeadsUpManager and don't need to deal with re-entrant behavior
+ // between HeadsUpCoordinator, HeadsUpManager, and VisualStabilityManager.
+ if (posted?.shouldHeadsUpEver == false
+ && !posted.isHeadsUpEntry && posted.isBinding) {
// Don't let the bind finish
cancelHeadsUpBind(posted.entry)
}
+ } else {
+ // Handle cancelling heads up here, rather than in the OnBeforeFinalizeFilter,
+ // so that work can be done before the ShadeListBuilder is run. This prevents
+ // re-entrant behavior between this Coordinator, HeadsUpManager, and
+ // VisualStabilityManager.
+ if (posted?.shouldHeadsUpEver == false) {
+ if (posted.isHeadsUpEntry) {
+ // We don't want this to be interrupting anymore, let's remove it
+ mHeadsUpManager.removeNotification(
+ posted.key,
+ /* removeImmediately= */ false,
+ "onEntryUpdated",
+ )
+ } else if (posted.isBinding) {
+ // Don't let the bind finish
+ cancelHeadsUpBind(posted.entry)
+ }
+ }
}
-
// Update last updated time for this entry
setUpdateTime(entry, mSystemClock.currentTimeMillis())
}
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 1e5aa01714be..6042bff4bb97 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
@@ -83,7 +83,6 @@ import com.android.systemui.statusbar.notification.logging.dagger.NotificationsL
import com.android.systemui.statusbar.notification.promoted.PromotedNotificationContentExtractor;
import com.android.systemui.statusbar.notification.promoted.PromotedNotificationContentExtractorImpl;
import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel;
-import com.android.systemui.statusbar.notification.row.NotificationActionClickManager;
import com.android.systemui.statusbar.notification.row.NotificationEntryProcessorFactory;
import com.android.systemui.statusbar.notification.row.NotificationEntryProcessorFactoryLooperImpl;
import com.android.systemui.statusbar.notification.row.NotificationGutsManager;
@@ -325,7 +324,7 @@ public interface NotificationsModule {
if (PromotedNotificationContentModel.featureFlagEnabled()) {
return implProvider.get();
} else {
- return (entry, recoveredBuilder, imageModelProvider) -> null;
+ return (entry, recoveredBuilder, redactionType, imageModelProvider) -> null;
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationListInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationListInteractor.kt
index adc049e7cdf1..e7cc342ab65c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationListInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationListInteractor.kt
@@ -20,6 +20,7 @@ import android.app.Notification.CallStyle.CALL_TYPE_ONGOING
import android.app.Notification.CallStyle.CALL_TYPE_SCREENING
import android.app.Notification.CallStyle.CALL_TYPE_UNKNOWN
import android.app.Notification.EXTRA_CALL_TYPE
+import android.app.Notification.FLAG_ONGOING_EVENT
import android.app.PendingIntent
import android.content.Context
import android.graphics.drawable.Icon
@@ -29,12 +30,13 @@ import com.android.app.tracing.traceSection
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.statusbar.StatusBarIconView
import com.android.systemui.statusbar.notification.collection.GroupEntry
-import com.android.systemui.statusbar.notification.collection.PipelineEntry
import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import com.android.systemui.statusbar.notification.collection.PipelineEntry
import com.android.systemui.statusbar.notification.collection.provider.SectionStyleProvider
import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationListRepository
import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationsStore
import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel
+import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModels
import com.android.systemui.statusbar.notification.shared.ActiveNotificationEntryModel
import com.android.systemui.statusbar.notification.shared.ActiveNotificationGroupModel
import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel
@@ -140,7 +142,7 @@ private class ActiveNotificationsStoreBuilder(
private fun NotificationEntry.toModel(): ActiveNotificationModel {
val promotedContent =
if (PromotedNotificationContentModel.featureFlagEnabled()) {
- promotedNotificationContentModel
+ promotedNotificationContentModels
} else {
null
}
@@ -149,6 +151,8 @@ private class ActiveNotificationsStoreBuilder(
key = key,
groupKey = sbn.groupKey,
whenTime = sbn.notification.`when`,
+ isForegroundService = sbn.notification.isForegroundService,
+ isOngoingEvent = (sbn.notification.flags and FLAG_ONGOING_EVENT) != 0,
isAmbient = sectionStyleProvider.isMinimized(this),
isRowDismissed = isRowDismissed,
isSilent = sectionStyleProvider.isSilent(this),
@@ -176,6 +180,8 @@ private fun ActiveNotificationsStore.createOrReuse(
key: String,
groupKey: String?,
whenTime: Long,
+ isForegroundService: Boolean,
+ isOngoingEvent: Boolean,
isAmbient: Boolean,
isRowDismissed: Boolean,
isSilent: Boolean,
@@ -194,13 +200,15 @@ private fun ActiveNotificationsStore.createOrReuse(
isGroupSummary: Boolean,
bucket: Int,
callType: CallType,
- promotedContent: PromotedNotificationContentModel?,
+ promotedContent: PromotedNotificationContentModels?,
): ActiveNotificationModel {
return individuals[key]?.takeIf {
it.isCurrent(
key = key,
groupKey = groupKey,
whenTime = whenTime,
+ isForegroundService = isForegroundService,
+ isOngoingEvent = isOngoingEvent,
isAmbient = isAmbient,
isRowDismissed = isRowDismissed,
isSilent = isSilent,
@@ -226,6 +234,8 @@ private fun ActiveNotificationsStore.createOrReuse(
key = key,
groupKey = groupKey,
whenTime = whenTime,
+ isForegroundService = isForegroundService,
+ isOngoingEvent = isOngoingEvent,
isAmbient = isAmbient,
isRowDismissed = isRowDismissed,
isSilent = isSilent,
@@ -252,6 +262,8 @@ private fun ActiveNotificationModel.isCurrent(
key: String,
groupKey: String?,
whenTime: Long,
+ isForegroundService: Boolean,
+ isOngoingEvent: Boolean,
isAmbient: Boolean,
isRowDismissed: Boolean,
isSilent: Boolean,
@@ -270,12 +282,14 @@ private fun ActiveNotificationModel.isCurrent(
isGroupSummary: Boolean,
bucket: Int,
callType: CallType,
- promotedContent: PromotedNotificationContentModel?,
+ promotedContent: PromotedNotificationContentModels?,
): Boolean {
return when {
key != this.key -> false
groupKey != this.groupKey -> false
whenTime != this.whenTime -> false
+ isForegroundService != this.isForegroundService -> false
+ isOngoingEvent != this.isOngoingEvent -> false
isAmbient != this.isAmbient -> false
isRowDismissed != this.isRowDismissed -> false
isSilent != this.isSilent -> false
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterView.java
index d09546fe80ca..3caaf542e787 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterView.java
@@ -67,6 +67,7 @@ public class FooterView extends StackScrollerDecorView {
private FooterViewButton mSettingsButton;
private FooterViewButton mHistoryButton;
private boolean mShouldBeHidden;
+ private boolean mIsBlurSupported;
// Footer label
private TextView mSeenNotifsFooterTextView;
@@ -390,15 +391,20 @@ public class FooterView extends StackScrollerDecorView {
if (!notificationFooterBackgroundTintOptimization()) {
if (notificationShadeBlur()) {
- Color backgroundColor = Color.valueOf(
- SurfaceEffectColors.surfaceEffect1(getContext()));
- scHigh = ColorUtils.setAlphaComponent(backgroundColor.toArgb(), 0xFF);
- // Apply alpha on background drawables.
- int backgroundAlpha = (int) (backgroundColor.alpha() * 0xFF);
- clearAllBg.setAlpha(backgroundAlpha);
- settingsBg.setAlpha(backgroundAlpha);
- if (historyBg != null) {
- historyBg.setAlpha(backgroundAlpha);
+ if (mIsBlurSupported) {
+ Color backgroundColor = Color.valueOf(
+ SurfaceEffectColors.surfaceEffect1(getContext()));
+ scHigh = ColorUtils.setAlphaComponent(backgroundColor.toArgb(), 0xFF);
+ // Apply alpha on background drawables.
+ int backgroundAlpha = (int) (backgroundColor.alpha() * 0xFF);
+ clearAllBg.setAlpha(backgroundAlpha);
+ settingsBg.setAlpha(backgroundAlpha);
+ if (historyBg != null) {
+ historyBg.setAlpha(backgroundAlpha);
+ }
+ } else {
+ scHigh = mContext.getColor(
+ com.android.internal.R.color.materialColorSurfaceContainer);
}
} else {
scHigh = mContext.getColor(
@@ -438,6 +444,16 @@ public class FooterView extends StackScrollerDecorView {
}
}
+ public void setIsBlurSupported(boolean isBlurSupported) {
+ if (notificationShadeBlur()) {
+ if (mIsBlurSupported == isBlurSupported) {
+ return;
+ }
+ mIsBlurSupported = isBlurSupported;
+ updateColors();
+ }
+ }
+
@Override
@NonNull
public ExpandableViewState createExpandableViewState() {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewbinder/FooterViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewbinder/FooterViewBinder.kt
index 3383ce98549f..3213754ca9a9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewbinder/FooterViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewbinder/FooterViewBinder.kt
@@ -20,6 +20,7 @@ import android.view.View
import androidx.lifecycle.lifecycleScope
import com.android.app.tracing.coroutines.launchTraced as launch
import com.android.internal.jank.InteractionJankMonitor
+import com.android.systemui.Flags.notificationShadeBlur
import com.android.systemui.lifecycle.repeatWhenAttached
import com.android.systemui.statusbar.notification.NotificationActivityStarter
import com.android.systemui.statusbar.notification.NotificationActivityStarter.SettingsIntent
@@ -81,6 +82,14 @@ object FooterViewBinder {
launch { bindHistoryButton(footer, viewModel, notificationActivityStarter) }
}
launch { bindMessage(footer, viewModel) }
+
+ if (notificationShadeBlur()) {
+ launch {
+ viewModel.isBlurSupported.collect { supported ->
+ footer.setIsBlurSupported(supported)
+ }
+ }
+ }
}
private suspend fun bindClearAllButton(
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModel.kt
index c895c41960d4..c1fc72c618ba 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModel.kt
@@ -31,6 +31,7 @@ import com.android.systemui.util.kotlin.sample
import com.android.systemui.util.ui.AnimatableEvent
import com.android.systemui.util.ui.AnimatedValue
import com.android.systemui.util.ui.toAnimatedValueFlow
+import com.android.systemui.window.domain.interactor.WindowRootViewBlurInteractor
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.flow.Flow
@@ -48,6 +49,7 @@ constructor(
notificationSettingsInteractor: NotificationSettingsInteractor,
seenNotificationsInteractor: SeenNotificationsInteractor,
shadeInteractor: ShadeInteractor,
+ windowRootViewBlurInteractor: WindowRootViewBlurInteractor,
) {
/** A message to show instead of the footer buttons. */
val message: FooterMessageViewModel =
@@ -119,6 +121,8 @@ constructor(
}
}
+ val isBlurSupported = windowRootViewBlurInteractor.isBlurCurrentlySupported
+
private val manageOrHistoryButtonText: Flow<Int> =
notificationSettingsInteractor.isNotificationHistoryEnabled.map { shouldLaunchHistory ->
if (shouldLaunchHistory) R.string.manage_notifications_history_text
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractor.kt
index 27b2788f0b08..a8a7e885d1f7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractor.kt
@@ -40,6 +40,8 @@ import android.graphics.drawable.Icon
import com.android.systemui.Flags
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.shade.ShadeDisplayAware
+import com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_NONE
+import com.android.systemui.statusbar.NotificationLockscreenUserManager.RedactionType
import com.android.systemui.statusbar.notification.collection.NotificationEntry
import com.android.systemui.statusbar.notification.promoted.AutomaticPromotionCoordinator.Companion.EXTRA_AUTOMATICALLY_EXTRACTED_SHORT_CRITICAL_TEXT
import com.android.systemui.statusbar.notification.promoted.AutomaticPromotionCoordinator.Companion.EXTRA_WAS_AUTOMATICALLY_PROMOTED
@@ -48,6 +50,7 @@ import com.android.systemui.statusbar.notification.promoted.shared.model.Promote
import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel.OldProgress
import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel.Style
import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel.When
+import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModels
import com.android.systemui.statusbar.notification.row.shared.ImageModel
import com.android.systemui.statusbar.notification.row.shared.ImageModelProvider
import com.android.systemui.statusbar.notification.row.shared.ImageModelProvider.ImageSizeClass.MediumSquare
@@ -60,8 +63,9 @@ interface PromotedNotificationContentExtractor {
fun extractContent(
entry: NotificationEntry,
recoveredBuilder: Notification.Builder,
+ @RedactionType redactionType: Int,
imageModelProvider: ImageModelProvider,
- ): PromotedNotificationContentModel?
+ ): PromotedNotificationContentModels?
}
@SysUISingleton
@@ -76,8 +80,9 @@ constructor(
override fun extractContent(
entry: NotificationEntry,
recoveredBuilder: Notification.Builder,
+ @RedactionType redactionType: Int,
imageModelProvider: ImageModelProvider,
- ): PromotedNotificationContentModel? {
+ ): PromotedNotificationContentModels? {
if (!PromotedNotificationContentModel.featureFlagEnabled()) {
logger.logExtractionSkipped(entry, "feature flags disabled")
return null
@@ -95,7 +100,55 @@ constructor(
return null
}
- val contentBuilder = PromotedNotificationContentModel.Builder(entry.key)
+ val privateVersion =
+ extractPrivateContent(
+ key = entry.key,
+ notification = notification,
+ recoveredBuilder = recoveredBuilder,
+ lastAudiblyAlertedMs = entry.lastAudiblyAlertedMs,
+ imageModelProvider = imageModelProvider,
+ )
+ val publicVersion =
+ if (redactionType == REDACTION_TYPE_NONE) {
+ privateVersion
+ } else {
+ if (notification.publicVersion == null) {
+ privateVersion.toDefaultPublicVersion()
+ } else {
+ // TODO(b/400991304): implement extraction for [Notification.publicVersion]
+ privateVersion.toDefaultPublicVersion()
+ }
+ }
+ return PromotedNotificationContentModels(
+ privateVersion = privateVersion,
+ publicVersion = publicVersion,
+ )
+ .also { logger.logExtractionSucceeded(entry, it) }
+ }
+
+ private fun PromotedNotificationContentModel.toDefaultPublicVersion():
+ PromotedNotificationContentModel =
+ PromotedNotificationContentModel.Builder(key = identity.key).let {
+ it.style = if (style == Style.Ineligible) Style.Ineligible else Style.Base
+ it.smallIcon = smallIcon
+ it.iconLevel = iconLevel
+ it.appName = appName
+ it.time = time
+ it.lastAudiblyAlertedMs = lastAudiblyAlertedMs
+ it.profileBadgeResId = profileBadgeResId
+ it.colors = colors
+ it.build()
+ }
+
+ private fun extractPrivateContent(
+ key: String,
+ notification: Notification,
+ recoveredBuilder: Notification.Builder,
+ lastAudiblyAlertedMs: Long,
+ imageModelProvider: ImageModelProvider,
+ ): PromotedNotificationContentModel {
+
+ val contentBuilder = PromotedNotificationContentModel.Builder(key)
// TODO: Pitch a fit if style is unsupported or mandatory fields are missing once
// FLAG_PROMOTED_ONGOING is set reliably and we're not testing status bar chips.
@@ -108,7 +161,7 @@ constructor(
contentBuilder.subText = notification.subText()
contentBuilder.time = notification.extractWhen()
contentBuilder.shortCriticalText = notification.shortCriticalText()
- contentBuilder.lastAudiblyAlertedMs = entry.lastAudiblyAlertedMs
+ contentBuilder.lastAudiblyAlertedMs = lastAudiblyAlertedMs
contentBuilder.profileBadgeResId = null // TODO
contentBuilder.title = notification.title(recoveredBuilder.style)
contentBuilder.text = notification.text(recoveredBuilder.style)
@@ -124,7 +177,7 @@ constructor(
recoveredBuilder.extractStyleContent(notification, contentBuilder, imageModelProvider)
- return contentBuilder.build().also { logger.logExtractionSucceeded(entry, it) }
+ return contentBuilder.build()
}
private fun Notification.smallIconModel(imageModelProvider: ImageModelProvider): ImageModel? =
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationLogger.kt
index 5f9678a06b59..6b6203d6ea4d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationLogger.kt
@@ -23,7 +23,7 @@ import com.android.systemui.log.core.LogLevel.ERROR
import com.android.systemui.log.core.LogLevel.INFO
import com.android.systemui.statusbar.notification.collection.NotificationEntry
import com.android.systemui.statusbar.notification.logKey
-import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel
+import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModels
import javax.inject.Inject
@OptIn(ExperimentalStdlibApi::class)
@@ -56,7 +56,7 @@ constructor(@PromotedNotificationLog private val buffer: LogBuffer) {
fun logExtractionSucceeded(
entry: NotificationEntry,
- content: PromotedNotificationContentModel,
+ content: PromotedNotificationContentModels,
) {
buffer.log(
EXTRACTION_TAG,
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 ec4ee4560ea1..d9778bdde0a5 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
@@ -22,6 +22,7 @@ import com.android.systemui.statusbar.notification.promoted.shared.model.Promote
import com.android.systemui.util.kotlin.FlowDumperImpl
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
@SysUISingleton
@@ -34,6 +35,16 @@ constructor(
/** The content to show as the promoted notification on AOD */
val content: Flow<PromotedNotificationContentModel?> =
promotedNotificationsInteractor.aodPromotedNotification
+ .map {
+ // TODO(b/400991304): show the private version when unlocked
+ it?.publicVersion
+ }
+ .distinctUntilNewInstance()
val isPresent: Flow<Boolean> = content.map { it != null }.dumpWhileCollecting("isPresent")
+
+ /**
+ * 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 }
}
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
index a99ca072b6c8..08e7528631c7 100644
--- 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
@@ -19,17 +19,24 @@ 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.mediaprojection.domain.interactor.MediaProjectionChipInteractor
+import com.android.systemui.statusbar.chips.mediaprojection.domain.model.ProjectionChipModel
import com.android.systemui.statusbar.chips.notification.domain.interactor.StatusBarNotificationChipsInteractor
+import com.android.systemui.statusbar.chips.screenrecord.domain.interactor.ScreenRecordChipInteractor
+import com.android.systemui.statusbar.chips.screenrecord.domain.model.ScreenRecordChipModel
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.Ineligible
+import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModels
import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel
import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallModel
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
@@ -38,52 +45,169 @@ import kotlinx.coroutines.flow.map
* presented order of current notification status bar chips.
*/
@SysUISingleton
+@OptIn(ExperimentalCoroutinesApi::class)
class PromotedNotificationsInteractor
@Inject
constructor(
- activeNotificationsInteractor: ActiveNotificationsInteractor,
+ private val activeNotificationsInteractor: ActiveNotificationsInteractor,
+ screenRecordChipInteractor: ScreenRecordChipInteractor,
+ mediaProjectionChipInteractor: MediaProjectionChipInteractor,
callChipInteractor: CallChipInteractor,
notifChipsInteractor: StatusBarNotificationChipsInteractor,
@Background backgroundDispatcher: CoroutineDispatcher,
) {
+ private val screenRecordChipNotification: Flow<NotifAndPromotedContent?> =
+ screenRecordChipInteractor.screenRecordState.flatMapLatest { screenRecordModel ->
+ when (screenRecordModel) {
+ is ScreenRecordChipModel.DoingNothing -> flowOf(null)
+ is ScreenRecordChipModel.Starting -> flowOf(null)
+ is ScreenRecordChipModel.Recording ->
+ createRecordingNotificationFlow(hostPackage = screenRecordModel.hostPackage)
+ }
+ }
+
+ private val mediaProjectionChipNotification: Flow<NotifAndPromotedContent?> =
+ mediaProjectionChipInteractor.projection.flatMapLatest { projectionModel ->
+ when (projectionModel) {
+ is ProjectionChipModel.NotProjecting -> flowOf(null)
+ is ProjectionChipModel.Projecting ->
+ createRecordingNotificationFlow(
+ hostPackage = projectionModel.projectionState.hostPackage
+ )
+ }
+ }
+
+ /**
+ * Creates a flow emitting the screen-recording-related notification corresponding to the given
+ * package name (if we can find it).
+ *
+ * @param hostPackage the package name of the app that is receiving the content of the media
+ * projection (aka which app the phone screen contents are being sent to).
+ */
+ private fun createRecordingNotificationFlow(
+ hostPackage: String?
+ ): Flow<NotifAndPromotedContent?> =
+ if (hostPackage == null) {
+ flowOf(null)
+ } else {
+ activeNotificationsInteractor.allRepresentativeNotifications
+ .map { allNotifs ->
+ findBestMatchingMediaProjectionNotif(allNotifs.values, hostPackage)
+ }
+ .distinctUntilChanged()
+ }
+
+ /**
+ * Finds the best notification that matches the given [hostPackage] that looks like a recording
+ * notification, or null if we couldn't find a uniquely good match.
+ */
+ private fun findBestMatchingMediaProjectionNotif(
+ allNotifs: Collection<ActiveNotificationModel>,
+ hostPackage: String,
+ ): NotifAndPromotedContent? {
+ val candidates = allNotifs.filter { it.packageName == hostPackage }
+ if (candidates.isEmpty()) {
+ return null
+ }
+
+ candidates
+ .findOnlyOrNull { it.isForegroundService }
+ ?.let {
+ return it.toNotifAndPromotedContent()
+ }
+ candidates
+ .findOnlyOrNull { it.isOngoingEvent }
+ ?.let {
+ return it.toNotifAndPromotedContent()
+ }
+ candidates
+ .findOnlyOrNull { it.isForegroundService && it.isOngoingEvent }
+ ?.let {
+ return it.toNotifAndPromotedContent()
+ }
+ // We weren't able to find exactly 1 match for the given [hostPackage], so just don't match
+ // at all.
+ return null
+ }
+
+ /**
+ * Returns the single notification matching the given [predicate] if there's only 1 match, or
+ * null if there's 0 or 2+ matches.
+ */
+ private fun List<ActiveNotificationModel>.findOnlyOrNull(
+ predicate: (ActiveNotificationModel) -> Boolean
+ ): ActiveNotificationModel? {
+ val list = this.filter(predicate)
+ return if (list.size == 1) {
+ list.first()
+ } else {
+ null
+ }
+ }
+
+ private fun ActiveNotificationModel.toNotifAndPromotedContent(): NotifAndPromotedContent {
+ return NotifAndPromotedContent(this.key, this.promotedContent)
+ }
+
+ private val callNotification: Flow<NotifAndPromotedContent?> =
+ callChipInteractor.ongoingCallState
+ .map {
+ when (it) {
+ is OngoingCallModel.InCall ->
+ NotifAndPromotedContent(it.notificationKey, it.promotedContent)
+ is OngoingCallModel.NoCall -> null
+ }
+ }
+ .distinctUntilChanged()
+
+ private val promotedChipNotifications: Flow<List<NotifAndPromotedContent>> =
+ notifChipsInteractor.allNotificationChips
+ .map { chips -> chips.map { NotifAndPromotedContent(it.key, it.promotedContent) } }
+ .distinctUntilChanged()
+
/**
* 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)
- }
- }
- )
+ combine(
+ screenRecordChipNotification,
+ mediaProjectionChipNotification,
+ callNotification,
+ promotedChipNotifications,
+ ) { screenRecordNotif, mediaProjectionNotif, callNotif, promotedNotifs ->
+ val chipNotifications = mutableListOf<NotifAndPromotedContent>()
+ val usedKeys = mutableListOf<String>()
+
+ fun addToList(item: NotifAndPromotedContent?) {
+ if (item != null && !usedKeys.contains(item.key)) {
+ chipNotifications.add(item)
+ usedKeys.add(item.key)
+ }
}
- }
- private fun OngoingCallModel.getNotifData(): NotifAndPromotedContent? =
- when (this) {
- is OngoingCallModel.InCall -> NotifAndPromotedContent(notificationKey, promotedContent)
- is OngoingCallModel.NoCall -> null
+ // IMPORTANT: This ordering is prescribed by OngoingActivityChipsViewModel. Be sure to
+ // always keep this ordering in sync with that view model.
+ // TODO(b/402471288): Create a single source of truth for the ordering.
+ addToList(screenRecordNotif)
+ addToList(mediaProjectionNotif)
+ addToList(callNotif)
+ promotedNotifs.forEach { addToList(it) }
+
+ chipNotifications
}
/**
* 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?> =
+ private val topPromotedChipNotification: Flow<PromotedNotificationContentModels?> =
orderedChipNotifications
.map { list -> list.firstNotNullOfOrNull { it.promotedContent } }
.distinctUntilNewInstance()
/** This is the AOD promoted notification, which should avoid regular changing. */
- val aodPromotedNotification: Flow<PromotedNotificationContentModel?> =
+ val aodPromotedNotification: Flow<PromotedNotificationContentModels?> =
combine(
topPromotedChipNotification,
activeNotificationsInteractor.topLevelRepresentativeNotifications,
@@ -105,13 +229,13 @@ constructor(
.flowOn(backgroundDispatcher)
private fun List<ActiveNotificationModel>.firstAodEligibleOrNull():
- PromotedNotificationContentModel? {
+ PromotedNotificationContentModels? {
return this.firstNotNullOfOrNull { it.promotedContent?.takeIfAodEligible() }
}
- private fun PromotedNotificationContentModel.takeIfAodEligible():
- PromotedNotificationContentModel? {
- return this.takeUnless { it.style == Ineligible }
+ private fun PromotedNotificationContentModels.takeIfAodEligible():
+ PromotedNotificationContentModels? {
+ return this.takeUnless { it.privateVersion.style == Ineligible }
}
/**
@@ -127,7 +251,7 @@ constructor(
*/
private data class NotifAndPromotedContent(
val key: String,
- val promotedContent: PromotedNotificationContentModel?,
+ val promotedContent: PromotedNotificationContentModels?,
) {
/**
* Define the equals of this object to only check the reference equality of the promoted
@@ -145,7 +269,7 @@ constructor(
/** 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)
+ result = 31 * result + (promotedContent?.key?.hashCode() ?: 0)
return result
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/shared/model/PromotedNotificationContentModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/shared/model/PromotedNotificationContentModel.kt
index 57b07204fc6a..ffacf62fccce 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/shared/model/PromotedNotificationContentModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/shared/model/PromotedNotificationContentModel.kt
@@ -29,6 +29,31 @@ import com.android.systemui.statusbar.notification.row.ImageResult
import com.android.systemui.statusbar.notification.row.LazyImage
import com.android.systemui.statusbar.notification.row.shared.ImageModel
+data class PromotedNotificationContentModels(
+ /** The potentially redacted version of the content that will be exposed to the public */
+ val publicVersion: PromotedNotificationContentModel,
+ /** The unredacted version of the content that will be kept private */
+ val privateVersion: PromotedNotificationContentModel,
+) {
+ val key: String
+ get() = privateVersion.identity.key
+
+ init {
+ check(publicVersion.identity.key == privateVersion.identity.key) {
+ "public and private models must have the same key"
+ }
+ }
+
+ fun toRedactedString(): String {
+ val publicVersionString =
+ "==privateVersion".takeIf { privateVersion === publicVersion }
+ ?: publicVersion.toRedactedString()
+ return ("PromotedNotificationContentModels(" +
+ "privateVersion=${privateVersion.toRedactedString()}, " +
+ "publicVersion=$publicVersionString)")
+ }
+}
+
/**
* The content needed to render a promoted notification to surfaces besides the notification stack,
* like the skeleton view on AOD or the status bar chip.
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 2a3b266c8d10..bef3c691cb4d 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
@@ -976,7 +976,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
return mEntry;
}
- @Nullable
+ @NonNull
public EntryAdapter getEntryAdapter() {
NotificationBundleUi.unsafeAssertInNewMode();
return mEntryAdapter;
@@ -1005,7 +1005,9 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
} else if (isAboveShelf() != wasAboveShelf) {
mAboveShelfChangedListener.onAboveShelfStateChanged(!wasAboveShelf);
}
- updateBackgroundTint();
+ if (notificationRowTransparency()) {
+ updateBackgroundTint();
+ }
}
/**
@@ -2684,30 +2686,58 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
}
/**
+ * Whether to allow dismissal with the whole-row translation animation.
+ *
+ * If true, either animation is permissible.
+ * If false, usingRTX behavior is forbidden, only clipping animation should be used.
+ *
+ * Usually either is OK, except for promoted notifications, where we always need to
+ * dismiss with content clipping/partial translation animation instead, so that we
+ * can show the demotion options.
+ * @return
+ */
+ private boolean allowDismissUsingRowTranslationX() {
+ if (Flags.permissionHelperInlineUiRichOngoing()) {
+ return !isPromotedOngoing();
+ } else {
+ // Don't change behavior unless the flag is on.
+ return true;
+ }
+ }
+
+ /**
* Set the dismiss behavior of the view.
*
* @param usingRowTranslationX {@code true} if the view should translate using regular
* translationX, otherwise the contents will be
* translated.
+ * @param forceUpdateChildren {@code true} to force initialization, {@code false} if lazy
+ * behavior is OK.
*/
@Override
- public void setDismissUsingRowTranslationX(boolean usingRowTranslationX) {
- if (usingRowTranslationX != mDismissUsingRowTranslationX) {
+ public void setDismissUsingRowTranslationX(boolean usingRowTranslationX,
+ boolean forceUpdateChildren) {
+ // Before updating dismiss behavior, make sure this is an allowable configuration for this
+ // notification.
+ usingRowTranslationX = usingRowTranslationX && allowDismissUsingRowTranslationX();
+
+ if (forceUpdateChildren || (usingRowTranslationX != mDismissUsingRowTranslationX)) {
// In case we were already transitioning, let's switch over!
float previousTranslation = getTranslation();
if (previousTranslation != 0) {
setTranslation(0);
}
- super.setDismissUsingRowTranslationX(usingRowTranslationX);
+ super.setDismissUsingRowTranslationX(usingRowTranslationX, forceUpdateChildren);
if (previousTranslation != 0) {
setTranslation(previousTranslation);
}
+
if (mChildrenContainer != null) {
List<ExpandableNotificationRow> notificationChildren =
mChildrenContainer.getAttachedChildren();
for (int i = 0; i < notificationChildren.size(); i++) {
ExpandableNotificationRow child = notificationChildren.get(i);
- child.setDismissUsingRowTranslationX(usingRowTranslationX);
+ child.setDismissUsingRowTranslationX(usingRowTranslationX, forceUpdateChildren);
}
}
}
@@ -3164,7 +3194,9 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
mChildrenContainer.setOnKeyguard(onKeyguard);
}
}
- updateBackgroundTint();
+ if (notificationRowTransparency()) {
+ updateBackgroundTint();
+ }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableOutlineView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableOutlineView.java
index 80cf818e985f..6c990df5d05e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableOutlineView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableOutlineView.java
@@ -292,7 +292,8 @@ public abstract class ExpandableOutlineView extends ExpandableView {
* translationX, otherwise the contents will be
* translated.
*/
- public void setDismissUsingRowTranslationX(boolean usingRowTranslationX) {
+ public void setDismissUsingRowTranslationX(boolean usingRowTranslationX,
+ boolean forceUpdateChildren) {
mDismissUsingRowTranslationX = usingRowTranslationX;
}
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 d97e25fdfa22..57ceafcd15c6 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
@@ -60,6 +60,7 @@ import com.android.systemui.statusbar.notification.NotificationUtils;
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;
+import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModels;
import com.android.systemui.statusbar.notification.row.shared.AsyncGroupHeaderViewInflation;
import com.android.systemui.statusbar.notification.row.shared.AsyncHybridViewInflation;
import com.android.systemui.statusbar.notification.row.shared.ImageModelProvider;
@@ -1003,7 +1004,7 @@ public class NotificationContentInflater implements NotificationRowContentBinder
row.mImageModelIndex = result.mRowImageInflater.getNewImageIndex();
if (PromotedNotificationContentModel.featureFlagEnabled()) {
- entry.setPromotedNotificationContentModel(result.mPromotedContent);
+ entry.setPromotedNotificationContentModels(result.mPromotedContent);
}
boolean setRepliesAndActions = true;
@@ -1387,9 +1388,9 @@ public class NotificationContentInflater implements NotificationRowContentBinder
mLogger.logAsyncTaskProgress(logKey, "extracting promoted notification content");
final ImageModelProvider imageModelProvider =
result.mRowImageInflater.useForContentModel();
- final PromotedNotificationContentModel promotedContent =
+ final PromotedNotificationContentModels promotedContent =
mPromotedNotificationContentExtractor.extractContent(mEntry,
- recoveredBuilder, imageModelProvider);
+ recoveredBuilder, mBindParams.redactionType, imageModelProvider);
mLogger.logAsyncTaskProgress(logKey, "extracted promoted notification content: "
+ promotedContent);
@@ -1503,7 +1504,7 @@ public class NotificationContentInflater implements NotificationRowContentBinder
static class InflationProgress {
RowImageInflater mRowImageInflater;
- PromotedNotificationContentModel mPromotedContent;
+ PromotedNotificationContentModels mPromotedContent;
private RemoteViews newContentView;
private RemoteViews newHeadsUpView;
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 cdb78d99538b..f4e01bf718d9 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
@@ -29,6 +29,7 @@ import android.content.pm.ShortcutManager;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
+import android.os.RemoteException;
import android.os.UserHandle;
import android.os.UserManager;
import android.provider.Settings;
@@ -309,6 +310,7 @@ public class NotificationGutsManager implements NotifGutsViewManager, CoreStarta
});
View gutsView = item.getGutsView();
+
try {
if (gutsView instanceof NotificationSnooze) {
initializeSnoozeView(row, (NotificationSnooze) gutsView);
@@ -322,6 +324,8 @@ public class NotificationGutsManager implements NotifGutsViewManager, CoreStarta
(PartialConversationInfo) gutsView);
} else if (gutsView instanceof FeedbackInfo) {
initializeFeedbackInfo(row, (FeedbackInfo) gutsView);
+ } else if (gutsView instanceof PromotedPermissionGutsContent) {
+ initializeDemoteView(row, (PromotedPermissionGutsContent) gutsView);
}
return true;
} catch (Exception e) {
@@ -351,6 +355,31 @@ public class NotificationGutsManager implements NotifGutsViewManager, CoreStarta
}
/**
+ * Sets up the {@link NotificationSnooze} inside the notification row's guts.
+ *
+ * @param row view to set up the guts for
+ * @param demoteGuts view to set up/bind within {@code row}
+ */
+ private void initializeDemoteView(
+ final ExpandableNotificationRow row,
+ PromotedPermissionGutsContent demoteGuts) {
+ StatusBarNotification sbn = row.getEntry().getSbn();
+ demoteGuts.setStatusBarNotification(sbn);
+ demoteGuts.setOnDemoteAction(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ try {
+ // TODO(b/391661009): Signal AutomaticPromotionCoordinator here
+ mNotificationManager.setCanBePromoted(
+ sbn.getPackageName(), sbn.getUid(), false, true);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Couldn't revoke live update permission", e);
+ }
+ }
+ });
+ }
+
+ /**
* Sets up the {@link FeedbackInfo} inside the notification row's guts.
*
* @param row view to set up the guts for
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationMenuRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationMenuRow.java
index c03dc279888f..f494a4ce40dd 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationMenuRow.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationMenuRow.java
@@ -272,6 +272,7 @@ public class NotificationMenuRow implements NotificationMenuRowPlugin, View.OnCl
} else if (personNotifType >= PeopleNotificationIdentifier.TYPE_FULL_PERSON) {
mInfoItem = createConversationItem(mContext);
} else if (android.app.Flags.uiRichOngoing()
+ && android.app.Flags.apiRichOngoing()
&& Flags.permissionHelperUiRichOngoing()
&& sbn.getNotification().isPromotedOngoing()) {
mInfoItem = createPromotedItem(mContext);
@@ -284,6 +285,15 @@ public class NotificationMenuRow implements NotificationMenuRowPlugin, View.OnCl
}
mRightMenuItems.add(mInfoItem);
mRightMenuItems.add(mFeedbackItem);
+ boolean isPromotedOngoing = NotificationBundleUi.isEnabled()
+ ? mParent.getEntryAdapter().isPromotedOngoing()
+ : mParent.getEntryLegacy().isPromotedOngoing();
+ if (android.app.Flags.uiRichOngoing() && Flags.permissionHelperInlineUiRichOngoing()
+ && isPromotedOngoing) {
+ mRightMenuItems.add(createDemoteItem(mContext));
+ }
+
+
mLeftMenuItems.addAll(mRightMenuItems);
populateMenuViews();
@@ -305,15 +315,19 @@ public class NotificationMenuRow implements NotificationMenuRowPlugin, View.OnCl
} else {
mMenuContainer = new FrameLayout(mContext);
}
+
final int showDismissSetting = Settings.Global.getInt(mContext.getContentResolver(),
Settings.Global.SHOW_NEW_NOTIF_DISMISS, /* default = */ 1);
final boolean newFlowHideShelf = showDismissSetting == 1;
- if (newFlowHideShelf) {
- return;
- }
- List<MenuItem> menuItems = mOnLeft ? mLeftMenuItems : mRightMenuItems;
- for (int i = 0; i < menuItems.size(); i++) {
- addMenuView(menuItems.get(i), mMenuContainer);
+
+ // Populate menu items if we are using the new permission helper (U+) or if we are using
+ // the very old dismiss setting (SC-).
+ // TODO: SHOW_NEW_NOTIF_DISMISS==0 case can likely be removed.
+ if (Flags.permissionHelperInlineUiRichOngoing() || !newFlowHideShelf) {
+ List<MenuItem> menuItems = mOnLeft ? mLeftMenuItems : mRightMenuItems;
+ for (int i = 0; i < menuItems.size(); i++) {
+ addMenuView(menuItems.get(i), mMenuContainer);
+ }
}
}
@@ -679,6 +693,15 @@ public class NotificationMenuRow implements NotificationMenuRowPlugin, View.OnCl
return snooze;
}
+ static MenuItem createDemoteItem(Context context) {
+ PromotedPermissionGutsContent demoteContent =
+ (PromotedPermissionGutsContent) LayoutInflater.from(context).inflate(
+ R.layout.promoted_permission_guts, null, false);
+ MenuItem info = new NotificationMenuItem(context, null, demoteContent,
+ R.drawable.unpin_icon);
+ return info;
+ }
+
static NotificationMenuItem createConversationItem(Context context) {
Resources res = context.getResources();
String infoDescription = res.getString(R.string.notification_menu_gear_description);
@@ -686,7 +709,7 @@ public class NotificationMenuRow implements NotificationMenuRowPlugin, View.OnCl
(NotificationConversationInfo) LayoutInflater.from(context).inflate(
R.layout.notification_conversation_info, null, false);
return new NotificationMenuItem(context, infoDescription, infoContent,
- R.drawable.ic_settings);
+ NotificationMenuItem.OMIT_FROM_SWIPE_MENU);
}
static NotificationMenuItem createPromotedItem(Context context) {
@@ -696,7 +719,7 @@ public class NotificationMenuRow implements NotificationMenuRowPlugin, View.OnCl
(PromotedNotificationInfo) LayoutInflater.from(context).inflate(
R.layout.promoted_notification_info, null, false);
return new NotificationMenuItem(context, infoDescription, infoContent,
- R.drawable.ic_settings);
+ NotificationMenuItem.OMIT_FROM_SWIPE_MENU);
}
static NotificationMenuItem createPartialConversationItem(Context context) {
@@ -706,7 +729,7 @@ public class NotificationMenuRow implements NotificationMenuRowPlugin, View.OnCl
(PartialConversationInfo) LayoutInflater.from(context).inflate(
R.layout.partial_conversation_info, null, false);
return new NotificationMenuItem(context, infoDescription, infoContent,
- R.drawable.ic_settings);
+ NotificationMenuItem.OMIT_FROM_SWIPE_MENU);
}
static NotificationMenuItem createInfoItem(Context context) {
@@ -718,14 +741,14 @@ public class NotificationMenuRow implements NotificationMenuRowPlugin, View.OnCl
NotificationInfo infoContent = (NotificationInfo) LayoutInflater.from(context).inflate(
layoutId, null, false);
return new NotificationMenuItem(context, infoDescription, infoContent,
- R.drawable.ic_settings);
+ NotificationMenuItem.OMIT_FROM_SWIPE_MENU);
}
static MenuItem createFeedbackItem(Context context) {
FeedbackInfo feedbackContent = (FeedbackInfo) LayoutInflater.from(context).inflate(
R.layout.feedback_info, null, false);
MenuItem info = new NotificationMenuItem(context, null, feedbackContent,
- -1 /*don't show in slow swipe menu */);
+ NotificationMenuItem.OMIT_FROM_SWIPE_MENU);
return info;
}
@@ -762,6 +785,10 @@ public class NotificationMenuRow implements NotificationMenuRowPlugin, View.OnCl
@Override
public boolean isWithinSnapMenuThreshold() {
+ if (getSpaceForMenu() == 0) {
+ // don't snap open if there are no items
+ return false;
+ }
float translation = getTranslation();
float snapBackThreshold = getSnapBackThreshold();
float targetRight = getDismissThreshold();
@@ -803,6 +830,10 @@ public class NotificationMenuRow implements NotificationMenuRowPlugin, View.OnCl
}
public static class NotificationMenuItem implements MenuItem {
+
+ // Constant signaling that this MenuItem should not appear in slow swipe.
+ public static final int OMIT_FROM_SWIPE_MENU = -1;
+
View mMenuView;
GutsContent mGutsContent;
String mContentDescription;
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 ae52db88358a..4f1b90544403 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
@@ -53,6 +53,7 @@ import com.android.systemui.statusbar.notification.collection.NotificationEntry
import com.android.systemui.statusbar.notification.logKey
import com.android.systemui.statusbar.notification.promoted.PromotedNotificationContentExtractor
import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel
+import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModels
import com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_CONTRACTED
import com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_EXPANDED
import com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_HEADSUP
@@ -595,7 +596,7 @@ constructor(
val rowImageInflater: RowImageInflater,
val remoteViews: NewRemoteViews,
val contentModel: NotificationContentModel,
- val promotedContent: PromotedNotificationContentModel?,
+ val promotedContent: PromotedNotificationContentModels?,
) {
var inflatedContentView: View? = null
@@ -700,7 +701,12 @@ constructor(
)
val imageModelProvider = rowImageInflater.useForContentModel()
promotedNotificationContentExtractor
- .extractContent(entry, builder, imageModelProvider)
+ .extractContent(
+ entry,
+ builder,
+ bindParams.redactionType,
+ imageModelProvider,
+ )
.also {
logger.logAsyncTaskProgress(
entry.logKey,
@@ -1519,7 +1525,7 @@ constructor(
entry.setContentModel(result.contentModel)
if (PromotedNotificationContentModel.featureFlagEnabled()) {
- entry.promotedNotificationContentModel = result.promotedContent
+ entry.promotedNotificationContentModels = result.promotedContent
}
result.inflatedSmartReplyState?.let { row.privateLayout.setInflatedSmartReplyState(it) }
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 01ee788f7fd7..769f0b5a4fa4 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
@@ -80,6 +80,7 @@ public class PromotedNotificationInfo extends NotificationInfo {
assistantFeedbackController, metricsLogger, onCloseClick);
mNotificationManager = iNotificationManager;
+
mPackageDemotionInteractor = packageDemotionInteractor;
bindDemote(entry.getSbn(), pkg);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/PromotedPermissionGutsContent.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/PromotedPermissionGutsContent.java
new file mode 100644
index 000000000000..222a1f4d8adf
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/PromotedPermissionGutsContent.java
@@ -0,0 +1,173 @@
+/*
+ * 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.systemui.statusbar.notification.row;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.service.notification.StatusBarNotification;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.android.internal.logging.MetricsLogger;
+import com.android.systemui.res.R;
+
+/**
+ * This GutsContent shows an explanatory interstitial telling the user they've just revoked this
+ * app's permission to post Promoted/Live notifications.
+ * If the guts are dismissed without further action, the revocation is committed.
+ * If the user hits undo, the permission is not revoked.
+ */
+public class PromotedPermissionGutsContent extends LinearLayout
+ implements NotificationGuts.GutsContent, View.OnClickListener {
+
+ private static final String TAG = "SnoozyPromotedGuts";
+
+ private NotificationGuts mGutsContainer;
+ private StatusBarNotification mSbn;
+
+ private TextView mUndoButton;
+
+ private MetricsLogger mMetricsLogger = new MetricsLogger();
+ private OnClickListener mDemoteAction;
+
+ public PromotedPermissionGutsContent(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ mUndoButton = (TextView) findViewById(R.id.undo);
+ mUndoButton.setOnClickListener(this);
+ mUndoButton.setContentDescription(
+ getContext().getString(R.string.snooze_undo_content_description));
+
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ dispatchConfigurationChanged(getResources().getConfiguration());
+ }
+
+ /**
+ * Update the content description of the snooze view based on the snooze option and whether the
+ * snooze options are expanded or not.
+ * For example, this will be something like "Collapsed\u2029Snooze for 1 hour". The paragraph
+ * separator is added to introduce a break in speech, to match what TalkBack does by default
+ * when you e.g. press on a notification.
+ */
+ private void updateContentDescription() {
+ //
+ }
+
+
+ @Override
+ public boolean performAccessibilityActionInternal(int action, Bundle arguments) {
+ if (super.performAccessibilityActionInternal(action, arguments)) {
+ return true;
+ }
+ if (action == R.id.action_snooze_undo) {
+ undoDemote(mUndoButton);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * TODO docs
+ * @param sbn
+ */
+ public void setStatusBarNotification(StatusBarNotification sbn) {
+ mSbn = sbn;
+ TextView demoteExplanation = (TextView) findViewById(R.id.demote_explain);
+ demoteExplanation.setText(mContext.getResources().getString(R.string.demote_explain_text,
+ mSbn.getPackageName()));
+ }
+
+ @Override
+ public void onClick(View v) {
+ if (mGutsContainer != null) {
+ mGutsContainer.resetFalsingCheck();
+ }
+ final int id = v.getId();
+ if (id == R.id.undo) {
+ undoDemote(v);
+ }
+
+ }
+
+ private void undoDemote(View v) {
+ // Don't commit the demote action, instead log the undo and dismiss the view.
+ mGutsContainer.closeControls(v, /* save= */ false);
+ }
+
+ @Override
+ public int getActualHeight() {
+ return getHeight();
+ }
+
+ @Override
+ public boolean willBeRemoved() {
+ return false;
+ }
+
+ @Override
+ public View getContentView() {
+ return this;
+ }
+
+ @Override
+ public void setGutsParent(NotificationGuts guts) {
+ mGutsContainer = guts;
+ }
+
+ @Override
+ public boolean handleCloseControls(boolean save, boolean force) {
+ if (!save) {
+ // Undo changes and let the guts handle closing the view
+ return false;
+ } else {
+ // Commit demote action.
+ mDemoteAction.onClick(this);
+ return false;
+ }
+ }
+
+ @Override
+ public boolean isLeavebehind() {
+ return true;
+ }
+
+ @Override
+ public boolean shouldBeSavedOnClose() {
+ return true;
+ }
+
+ @Override
+ public boolean needsFalsingProtection() {
+ return false;
+ }
+
+ public void setOnDemoteAction(OnClickListener demoteAction) {
+ mDemoteAction = demoteAction;
+ }
+
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/ActiveNotificationModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/ActiveNotificationModel.kt
index 53728c7da62d..96527389b5fe 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/ActiveNotificationModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/ActiveNotificationModel.kt
@@ -20,6 +20,7 @@ import android.graphics.drawable.Icon
import android.util.Log
import com.android.systemui.statusbar.StatusBarIconView
import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel
+import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModels
import com.android.systemui.statusbar.notification.stack.PriorityBucket
/**
@@ -38,6 +39,10 @@ data class ActiveNotificationModel(
val groupKey: String?,
/** When this notification was posted. */
val whenTime: Long,
+ /** True if this is a foreground service notification. */
+ val isForegroundService: Boolean,
+ /** True if this notification is for an ongoing event. */
+ val isOngoingEvent: Boolean,
/** Is this entry in the ambient / minimized section (lowest priority)? */
val isAmbient: Boolean,
/**
@@ -84,7 +89,7 @@ data class ActiveNotificationModel(
* The content needed to render this as a promoted notification on various surfaces, or null if
* this notification cannot be rendered as a promoted notification.
*/
- val promotedContent: PromotedNotificationContentModel?,
+ val promotedContent: PromotedNotificationContentModels?,
) : ActiveNotificationEntryModel() {
init {
if (!PromotedNotificationContentModel.featureFlagEnabled()) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManagerImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManagerImpl.kt
index 7d489a97f853..5c52500b7f70 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManagerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManagerImpl.kt
@@ -169,8 +169,7 @@ constructor(
}
private fun pullDismissibleRow(translation: Float) {
- val targetTranslation = swipedRowMultiplier * translation
- val crossedThreshold = abs(targetTranslation) >= magneticDetachThreshold
+ val crossedThreshold = abs(translation) >= magneticDetachThreshold
if (crossedThreshold) {
snapNeighborsBack()
currentMagneticListeners.swipedListener()?.let { detach(it, translation) }
@@ -247,8 +246,7 @@ constructor(
}
private fun translateDetachedRow(translation: Float) {
- val targetTranslation = swipedRowMultiplier * translation
- val crossedThreshold = abs(targetTranslation) <= magneticAttachThreshold
+ val crossedThreshold = abs(translation) <= magneticAttachThreshold
if (crossedThreshold) {
translationOffset += translation
updateRoundness(translation = 0f, animate = true)
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 9fea75048e3e..503256accff0 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
@@ -3220,8 +3220,7 @@ public class NotificationStackScrollLayout
updateAnimationState(child);
updateChronometerForChild(child);
if (child instanceof ExpandableNotificationRow row) {
- row.setDismissUsingRowTranslationX(mDismissUsingRowTranslationX);
-
+ row.setDismissUsingRowTranslationX(mDismissUsingRowTranslationX, /* force= */ true);
}
}
@@ -6157,7 +6156,7 @@ public class NotificationStackScrollLayout
View child = getChildAt(i);
if (child instanceof ExpandableNotificationRow) {
((ExpandableNotificationRow) child).setDismissUsingRowTranslationX(
- dismissUsingRowTranslationX);
+ dismissUsingRowTranslationX, /* force= */ false);
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt
index 000b3f643e9a..12b48eba7a96 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt
@@ -89,6 +89,17 @@ constructor(
source = shadeModeInteractor.shadeMode.map { getQuickSettingsShadeContentKey(it) },
)
+ /**
+ * Whether the current touch gesture is overscroll. If true, it means the NSSL has already
+ * consumed part of the gesture.
+ */
+ val isCurrentGestureOverscroll: Boolean by
+ hydrator.hydratedStateOf(
+ traceName = "isCurrentGestureOverscroll",
+ initialValue = false,
+ source = interactor.isCurrentGestureOverscroll
+ )
+
/** DEBUG: whether the placeholder should be made slightly visible for positional debugging. */
val isVisualDebuggingEnabled: Boolean = featureFlags.isEnabled(Flags.NSSL_DEBUG_LINES)
@@ -157,13 +168,6 @@ constructor(
val syntheticScroll: Flow<Float> =
interactor.syntheticScroll.dumpWhileCollecting("syntheticScroll")
- /**
- * Whether the current touch gesture is overscroll. If true, it means the NSSL has already
- * consumed part of the gesture.
- */
- val isCurrentGestureOverscroll: Flow<Boolean> =
- interactor.isCurrentGestureOverscroll.dumpWhileCollecting("isCurrentGestureOverScroll")
-
/** Whether remote input is currently active for any notification. */
val isRemoteInputActive = remoteInputInteractor.isRemoteInputActive
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java
index e4e56c5de65b..8a5b22183563 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java
@@ -527,7 +527,8 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp
break;
case MODE_SHOW_BOUNCER:
Trace.beginSection("MODE_SHOW_BOUNCER");
- mKeyguardViewController.showPrimaryBouncer(true);
+ mKeyguardViewController.showPrimaryBouncer(true,
+ "BiometricUnlockController#MODE_SHOW_BOUNCER");
Trace.endSection();
break;
case MODE_WAKE_AND_UNLOCK_FROM_DREAM:
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
index 9d9f01b571a7..e617254fa288 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
@@ -2407,11 +2407,12 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces {
}
if (needsBouncer) {
- Log.d(TAG, "showBouncerOrLockScreenIfKeyguard, showingBouncer");
+ var reason = "CentralSurfacesImpl#showBouncerOrLockScreenIfKeyguard";
if (SceneContainerFlag.isEnabled()) {
- mStatusBarKeyguardViewManager.showPrimaryBouncer(true /* scrimmed */);
+ mStatusBarKeyguardViewManager.showPrimaryBouncer(true /* scrimmed */,
+ reason);
} else {
- mStatusBarKeyguardViewManager.showBouncer(true /* scrimmed */);
+ mStatusBarKeyguardViewManager.showBouncer(true /* scrimmed */, reason);
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DemoStatusIcons.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DemoStatusIcons.java
index 0ba4aabcb0e2..34c193d18814 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DemoStatusIcons.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DemoStatusIcons.java
@@ -27,13 +27,19 @@ import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;
+import androidx.collection.MutableIntObjectMap;
+
import com.android.internal.statusbar.StatusBarIcon;
+import com.android.systemui.Flags;
import com.android.systemui.demomode.DemoMode;
+import com.android.systemui.kairos.ExperimentalKairosApi;
+import com.android.systemui.kairos.KairosNetwork;
import com.android.systemui.plugins.DarkIconDispatcher;
import com.android.systemui.plugins.DarkIconDispatcher.DarkReceiver;
import com.android.systemui.res.R;
import com.android.systemui.statusbar.StatusBarIconView;
import com.android.systemui.statusbar.StatusIconDisplayable;
+import com.android.systemui.statusbar.pipeline.mobile.ui.MobileUiAdapterKairos;
import com.android.systemui.statusbar.pipeline.mobile.ui.MobileViewLogger;
import com.android.systemui.statusbar.pipeline.mobile.ui.view.ModernStatusBarMobileView;
import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel;
@@ -41,10 +47,20 @@ import com.android.systemui.statusbar.pipeline.shared.ui.view.ModernStatusBarVie
import com.android.systemui.statusbar.pipeline.wifi.ui.view.ModernStatusBarWifiView;
import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.LocationBasedWifiViewModel;
+import dagger.Lazy;
+
+import kotlin.OptIn;
+import kotlin.Pair;
+
+import kotlinx.coroutines.CoroutineScope;
+import kotlinx.coroutines.Job;
+
import java.util.ArrayList;
import java.util.List;
+import java.util.concurrent.CancellationException;
//TODO: This should be a controller, not its own view
+@OptIn(markerClass = ExperimentalKairosApi.class)
public class DemoStatusIcons extends StatusIconContainer implements DemoMode, DarkReceiver {
private static final String TAG = "DemoStatusIcons";
@@ -60,15 +76,27 @@ public class DemoStatusIcons extends StatusIconContainer implements DemoMode, Da
private final MobileIconsViewModel mMobileIconsViewModel;
private final StatusBarLocation mLocation;
+ private final Lazy<MobileUiAdapterKairos> mMobileUiAdapterKairos;
+ private final KairosNetwork mKairosNetwork;
+ private final CoroutineScope mAppScope;
+
+ private final MutableIntObjectMap<Job> mBindingJobs = new MutableIntObjectMap<>();
+
public DemoStatusIcons(
LinearLayout statusIcons,
MobileIconsViewModel mobileIconsViewModel,
StatusBarLocation location,
- int iconSize
+ int iconSize,
+ Lazy<MobileUiAdapterKairos> mobileUiAdapterKairos,
+ KairosNetwork kairosNetwork,
+ CoroutineScope appScope
) {
super(statusIcons.getContext());
mStatusIcons = statusIcons;
mIconSize = iconSize;
+ mMobileUiAdapterKairos = mobileUiAdapterKairos;
+ mKairosNetwork = kairosNetwork;
+ mAppScope = appScope;
mColor = DarkIconDispatcher.DEFAULT_ICON_TINT;
mContrastColor = DarkIconDispatcher.DEFAULT_INVERSE_ICON_TINT;
mMobileIconsViewModel = mobileIconsViewModel;
@@ -236,24 +264,46 @@ public class DemoStatusIcons extends StatusIconContainer implements DemoMode, Da
/**
* Add a {@link ModernStatusBarMobileView}
+ *
* @param mobileContext possibly mcc/mnc overridden mobile context
- * @param subId the subscriptionId for this mobile view
+ * @param subId the subscriptionId for this mobile view
*/
public void addModernMobileView(
Context mobileContext,
MobileViewLogger mobileViewLogger,
int subId) {
Log.d(TAG, "addModernMobileView (subId=" + subId + ")");
- ModernStatusBarMobileView view = ModernStatusBarMobileView.constructAndBind(
- mobileContext,
- mobileViewLogger,
- "mobile",
- mMobileIconsViewModel.viewModelForSub(subId, mLocation)
- );
-
- // mobile always goes at the end
- mModernMobileViews.add(view);
- addView(view, getChildCount(), createLayoutParams());
+ if (Flags.statusBarMobileIconKairos()) {
+ Pair<ModernStatusBarMobileView, Job> viewAndJob =
+ ModernStatusBarMobileView.constructAndBind(
+ mobileContext,
+ mobileViewLogger,
+ "mobile",
+ mMobileUiAdapterKairos.get().getMobileIconsViewModel().viewModelForSub(
+ subId, mLocation),
+ mAppScope,
+ subId,
+ mLocation,
+ mKairosNetwork
+ );
+ ModernStatusBarMobileView view = viewAndJob.getFirst();
+ mBindingJobs.put(subId, viewAndJob.getSecond());
+ // mobile always goes at the end
+ mModernMobileViews.add(view);
+ addView(view, getChildCount(), createLayoutParams());
+ } else {
+ ModernStatusBarMobileView view =
+ ModernStatusBarMobileView.constructAndBind(
+ mobileContext,
+ mobileViewLogger,
+ "mobile",
+ mMobileIconsViewModel.viewModelForSub(subId, mLocation)
+ );
+
+ // mobile always goes at the end
+ mModernMobileViews.add(view);
+ addView(view, getChildCount(), createLayoutParams());
+ }
}
/**
@@ -299,6 +349,12 @@ public class DemoStatusIcons extends StatusIconContainer implements DemoMode, Da
ModernStatusBarMobileView mobileView = matchingModernMobileView(
(ModernStatusBarMobileView) view);
if (mobileView != null) {
+ if (Flags.statusBarMobileIconKairos()) {
+ Job job = mBindingJobs.remove(mobileView.getSubId());
+ if (job != null) {
+ job.cancel(new CancellationException());
+ }
+ }
removeView(mobileView);
mModernMobileViews.remove(mobileView);
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
index f3d72027238f..d68f7df79cd5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
@@ -538,10 +538,7 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump
private void handleBlurSupportedChanged(boolean isBlurSupported) {
this.mIsBlurSupported = isBlurSupported;
if (Flags.bouncerUiRevamp()) {
- // TODO: animate blur fallback when the bouncer is pulled up.
- for (ScrimState state : ScrimState.values()) {
- state.setDefaultScrimAlpha(getDefaultScrimAlpha(true));
- }
+ updateDefaultScrimAlphas();
if (isBlurSupported) {
ScrimState.BOUNCER_SCRIMMED.setNotifBlurRadius(mBlurConfig.getMaxBlurRadiusPx());
} else {
@@ -549,17 +546,7 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump
}
}
if (Flags.notificationShadeBlur()) {
- float inFrontAlpha = mInFrontAlpha;
- float behindAlpha = mBehindAlpha;
- float notifAlpha = mNotificationsAlpha;
-
mState.prepare(mState);
- applyState();
- startScrimAnimation(mScrimBehind, behindAlpha);
- startScrimAnimation(mNotificationsScrim, notifAlpha);
- startScrimAnimation(mScrimInFront, inFrontAlpha);
- dispatchBackScrimState(mScrimBehind.getViewAlpha());
- } else if (Flags.bouncerUiRevamp()) {
applyAndDispatchState();
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
index 8c44fe56d269..512340913de2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
@@ -669,7 +669,8 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb
* show if any subsequent events are to be handled.
*/
if (!SceneContainerFlag.isEnabled() && beginShowingBouncer(event)) {
- mPrimaryBouncerInteractor.show(/* isScrimmed= */false);
+ mPrimaryBouncerInteractor.show(/* isScrimmed= */false,
+ TAG + "#onPanelExpansionChanged");
}
if (!primaryBouncerIsOrWillBeShowing()) {
@@ -714,7 +715,8 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb
* Shows the notification keyguard or the bouncer depending on
* {@link #needsFullscreenBouncer()}.
*/
- protected void showBouncerOrKeyguard(boolean hideBouncerWhenShowing, boolean isFalsingReset) {
+ protected void showBouncerOrKeyguard(boolean hideBouncerWhenShowing, boolean isFalsingReset,
+ String reason) {
boolean showBouncer = needsFullscreenBouncer() && !mDozing;
if (Flags.simPinRaceConditionOnRestart()) {
showBouncer = showBouncer && !mIsSleeping;
@@ -726,11 +728,11 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb
mCentralSurfaces.hideKeyguard();
mSceneInteractorLazy.get().showOverlay(
Overlays.Bouncer,
- "StatusBarKeyguardViewManager.showBouncerOrKeyguard"
+ TAG + "#showBouncerOrKeyguard"
);
} else {
if (Flags.simPinRaceConditionOnRestart()) {
- if (mPrimaryBouncerInteractor.show(/* isScrimmed= */ true)) {
+ if (mPrimaryBouncerInteractor.show(/* isScrimmed= */ true, reason)) {
mAttemptsToShowBouncer = 0;
mCentralSurfaces.hideKeyguard();
} else {
@@ -744,19 +746,19 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb
+ mAttemptsToShowBouncer++);
mExecutor.executeDelayed(() ->
showBouncerOrKeyguard(hideBouncerWhenShowing,
- isFalsingReset),
+ isFalsingReset, reason),
500);
}
}
} else {
mCentralSurfaces.hideKeyguard();
- mPrimaryBouncerInteractor.show(/* isScrimmed= */ true);
+ mPrimaryBouncerInteractor.show(/* isScrimmed= */ true, reason);
}
}
} else if (!isFalsingReset) {
// Falsing resets can cause this to flicker, so don't reset in this case
Log.i(TAG, "Sim bouncer is already showing, issuing a refresh");
- mPrimaryBouncerInteractor.show(/* isScrimmed= */ true);
+ mPrimaryBouncerInteractor.show(/* isScrimmed= */ true, reason);
}
} else {
@@ -776,7 +778,7 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb
* false when the user will be dragging it and translation should be deferred
* {@see KeyguardBouncer#show(boolean, boolean)}
*/
- public void showBouncer(boolean scrimmed) {
+ public void showBouncer(boolean scrimmed, String reason) {
if (SceneContainerFlag.isEnabled()) {
mDeviceEntryInteractorLazy.get().attemptDeviceEntry();
return;
@@ -787,7 +789,7 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb
mAlternateBouncerInteractor.forceShow();
updateAlternateBouncerShowing(mAlternateBouncerInteractor.isVisibleState());
} else {
- showPrimaryBouncer(scrimmed);
+ showPrimaryBouncer(scrimmed, reason);
}
}
@@ -810,8 +812,10 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb
*
* @param scrimmed true when the bouncer should show scrimmed, false when the user will be
* dragging it and translation should be deferred {@see KeyguardBouncer#show(boolean, boolean)}
+ * @param reason string description for what is causing the bouncer to be requested
*/
- public void showPrimaryBouncer(boolean scrimmed) {
+ @Override
+ public void showPrimaryBouncer(boolean scrimmed, String reason) {
hideAlternateBouncer(
/* updateScrim= */ false,
// When the scene framework is on, don't ever clear the pending dismiss action from
@@ -823,7 +827,7 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb
"primary bouncer requested"
);
} else {
- mPrimaryBouncerInteractor.show(scrimmed);
+ mPrimaryBouncerInteractor.show(scrimmed, reason);
}
}
updateStates();
@@ -870,7 +874,7 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb
);
}
- showBouncer(true);
+ showBouncer(true, TAG + "#dismissWithAction");
Trace.endSection();
return;
}
@@ -919,10 +923,11 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb
if (SceneContainerFlag.isEnabled()) {
mSceneInteractorLazy.get().showOverlay(
Overlays.Bouncer,
- "StatusBarKeyguardViewManager.dismissWithAction"
+ TAG + "#dismissWithAction"
);
} else {
- mPrimaryBouncerInteractor.show(/* isScrimmed= */ true);
+ mPrimaryBouncerInteractor.show(/* isScrimmed= */ true,
+ TAG + "#dismissWithAction, afterKeyguardGone");
}
} else {
// after authentication success, run dismiss action with the option to defer
@@ -932,10 +937,11 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb
if (SceneContainerFlag.isEnabled()) {
mSceneInteractorLazy.get().showOverlay(
Overlays.Bouncer,
- "StatusBarKeyguardViewManager.dismissWithAction"
+ TAG + "#dismissWithAction"
);
} else {
- mPrimaryBouncerInteractor.show(/* isScrimmed= */ true);
+ mPrimaryBouncerInteractor.show(/* isScrimmed= */ true,
+ TAG + "#dismissWithAction");
}
// bouncer will handle the dismiss action, so we no longer need to track it here
mAfterKeyguardGoneAction = null;
@@ -992,7 +998,7 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb
}
}
} else {
- showBouncerOrKeyguard(hideBouncerWhenShowing, isFalsingReset);
+ showBouncerOrKeyguard(hideBouncerWhenShowing, isFalsingReset, "reset");
}
if (!SceneContainerFlag.isEnabled() && hideBouncerWhenShowing && isBouncerShowing()) {
hideAlternateBouncer(true);
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 8389aab4aac8..85fc9d4589c0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallback.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallback.java
@@ -156,7 +156,8 @@ public class StatusBarRemoteInputCallback implements Callback, Callbacks,
if (!row.isPinned()) {
mStatusBarStateController.setLeaveOpenOnKeyguardHide(true);
}
- mStatusBarKeyguardViewManager.showBouncer(true /* scrimmed */);
+ mStatusBarKeyguardViewManager.showBouncer(true /* scrimmed */,
+ "StatusBarRemoteInputCallback#onLockedRemoteInput");
mPendingRemoteInputView = clicked;
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt
index 61b7d80a8c85..b7eada1c6804 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt
@@ -38,7 +38,7 @@ import com.android.systemui.statusbar.chips.ui.view.ChipChronometer
import com.android.systemui.statusbar.data.repository.StatusBarModeRepositoryStore
import com.android.systemui.statusbar.gesture.SwipeStatusBarAwayGestureHandler
import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor
-import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel
+import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModels
import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel
import com.android.systemui.statusbar.notification.shared.CallType
import com.android.systemui.statusbar.phone.ongoingcall.data.repository.OngoingCallRepository
@@ -347,7 +347,7 @@ constructor(
* If the call notification also meets promoted notification criteria, this field is filled
* in with the content related to promotion. Otherwise null.
*/
- val promotedContent: PromotedNotificationContentModel?,
+ val promotedContent: PromotedNotificationContentModels?,
/** True if the call is currently ongoing (as opposed to incoming, screening, etc.). */
val isOngoing: Boolean,
/** True if the user has swiped away the status bar while in this phone call. */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallModel.kt
index 322dfff8f144..9546d374b595 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallModel.kt
@@ -18,7 +18,7 @@ package com.android.systemui.statusbar.phone.ongoingcall.shared.model
import android.app.PendingIntent
import com.android.systemui.statusbar.StatusBarIconView
-import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel
+import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModels
/** Represents the state of any ongoing calls. */
sealed interface OngoingCallModel {
@@ -47,7 +47,7 @@ sealed interface OngoingCallModel {
val intent: PendingIntent?,
val notificationKey: String,
val appName: String,
- val promotedContent: PromotedNotificationContentModel?,
+ val promotedContent: PromotedNotificationContentModels?,
val isAppVisible: Boolean,
) : OngoingCallModel
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ui/DarkIconManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ui/DarkIconManager.java
index 8d314eeb8493..6dcc0ada8c65 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ui/DarkIconManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ui/DarkIconManager.java
@@ -19,6 +19,9 @@ package com.android.systemui.statusbar.phone.ui;
import android.widget.LinearLayout;
import com.android.internal.statusbar.StatusBarIcon;
+import com.android.systemui.dagger.qualifiers.Application;
+import com.android.systemui.kairos.ExperimentalKairosApi;
+import com.android.systemui.kairos.KairosNetwork;
import com.android.systemui.plugins.DarkIconDispatcher;
import com.android.systemui.statusbar.StatusIconDisplayable;
import com.android.systemui.statusbar.connectivity.ui.MobileContextProvider;
@@ -26,13 +29,20 @@ import com.android.systemui.statusbar.phone.DemoStatusIcons;
import com.android.systemui.statusbar.phone.StatusBarIconHolder;
import com.android.systemui.statusbar.phone.StatusBarLocation;
import com.android.systemui.statusbar.pipeline.mobile.ui.MobileUiAdapter;
+import com.android.systemui.statusbar.pipeline.mobile.ui.MobileUiAdapterKairos;
import com.android.systemui.statusbar.pipeline.wifi.ui.WifiUiAdapter;
+import dagger.Lazy;
import dagger.assisted.Assisted;
import dagger.assisted.AssistedFactory;
import dagger.assisted.AssistedInject;
+import kotlin.OptIn;
+
+import kotlinx.coroutines.CoroutineScope;
+
/** Version of {@link IconManager} that observes state from the {@link DarkIconDispatcher}. */
+@OptIn(markerClass = ExperimentalKairosApi.class)
public class DarkIconManager extends IconManager {
private final DarkIconDispatcher mDarkIconDispatcher;
private final int mIconHorizontalMargin;
@@ -43,13 +53,21 @@ public class DarkIconManager extends IconManager {
@Assisted StatusBarLocation location,
WifiUiAdapter wifiUiAdapter,
MobileUiAdapter mobileUiAdapter,
+ Lazy<MobileUiAdapterKairos> mobileUiAdapterKairos,
MobileContextProvider mobileContextProvider,
+ KairosNetwork kairosNetwork,
+ @Application CoroutineScope appScope,
@Assisted DarkIconDispatcher darkIconDispatcher) {
- super(linearLayout, location, wifiUiAdapter, mobileUiAdapter, mobileContextProvider);
- mIconHorizontalMargin =
- mContext.getResources()
- .getDimensionPixelSize(
- com.android.systemui.res.R.dimen.status_bar_icon_horizontal_margin);
+ super(linearLayout,
+ location,
+ wifiUiAdapter,
+ mobileUiAdapter,
+ mobileUiAdapterKairos,
+ mobileContextProvider,
+ kairosNetwork,
+ appScope);
+ mIconHorizontalMargin = mContext.getResources().getDimensionPixelSize(
+ com.android.systemui.res.R.dimen.status_bar_icon_horizontal_margin);
mDarkIconDispatcher = darkIconDispatcher;
}
@@ -104,7 +122,7 @@ public class DarkIconManager extends IconManager {
super.exitDemoMode();
}
- /** */
+ /** */
@AssistedFactory
public interface Factory {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ui/IconManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ui/IconManager.java
index fd16c6090cb1..862931af7504 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ui/IconManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ui/IconManager.java
@@ -24,12 +24,19 @@ import static com.android.systemui.statusbar.phone.StatusBarIconHolder.TYPE_WIFI
import android.annotation.Nullable;
import android.content.Context;
import android.os.Bundle;
+import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;
+import androidx.annotation.OptIn;
+import androidx.collection.MutableIntObjectMap;
+
import com.android.internal.statusbar.StatusBarIcon;
import com.android.internal.statusbar.StatusBarIcon.Shape;
+import com.android.systemui.Flags;
import com.android.systemui.demomode.DemoModeCommandReceiver;
+import com.android.systemui.kairos.ExperimentalKairosApi;
+import com.android.systemui.kairos.KairosNetwork;
import com.android.systemui.modes.shared.ModesUiIcons;
import com.android.systemui.statusbar.BaseStatusBarFrameLayout;
import com.android.systemui.statusbar.StatusBarIconView;
@@ -40,6 +47,7 @@ import com.android.systemui.statusbar.phone.StatusBarIconHolder;
import com.android.systemui.statusbar.phone.StatusBarIconHolder.BindableIconHolder;
import com.android.systemui.statusbar.phone.StatusBarLocation;
import com.android.systemui.statusbar.pipeline.mobile.ui.MobileUiAdapter;
+import com.android.systemui.statusbar.pipeline.mobile.ui.MobileUiAdapterKairos;
import com.android.systemui.statusbar.pipeline.mobile.ui.binder.MobileIconsBinder;
import com.android.systemui.statusbar.pipeline.mobile.ui.view.ModernStatusBarMobileView;
import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel;
@@ -49,20 +57,34 @@ import com.android.systemui.statusbar.pipeline.wifi.ui.view.ModernStatusBarWifiV
import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.LocationBasedWifiViewModel;
import com.android.systemui.util.Assert;
+import dagger.Lazy;
+
+import kotlin.Pair;
+
+import kotlinx.coroutines.CoroutineScope;
+import kotlinx.coroutines.Job;
+
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.concurrent.CancellationException;
/**
* Turns info from StatusBarIconController into ImageViews in a ViewGroup.
*/
+@OptIn(markerClass = ExperimentalKairosApi.class)
public class IconManager implements DemoModeCommandReceiver {
protected final ViewGroup mGroup;
private final MobileContextProvider mMobileContextProvider;
private final LocationBasedWifiViewModel mWifiViewModel;
private final MobileIconsViewModel mMobileIconsViewModel;
+ private final Lazy<MobileUiAdapterKairos> mMobileUiAdapterKairos;
+ private final KairosNetwork mKairosNetwork;
+ private final CoroutineScope mAppScope;
+ private final MutableIntObjectMap<Job> mBindingJobs = new MutableIntObjectMap<>();
+
/**
* Stores the list of bindable icons that have been added, keyed on slot name. This ensures
* we don't accidentally add the same bindable icon twice.
@@ -87,12 +109,17 @@ public class IconManager implements DemoModeCommandReceiver {
StatusBarLocation location,
WifiUiAdapter wifiUiAdapter,
MobileUiAdapter mobileUiAdapter,
- MobileContextProvider mobileContextProvider
+ Lazy<MobileUiAdapterKairos> mobileUiAdapterKairos,
+ MobileContextProvider mobileContextProvider,
+ KairosNetwork kairosNetwork,
+ CoroutineScope appScope
) {
mGroup = group;
mMobileContextProvider = mobileContextProvider;
mContext = group.getContext();
mLocation = location;
+ mKairosNetwork = kairosNetwork;
+ mAppScope = appScope;
reloadDimens();
@@ -101,6 +128,9 @@ public class IconManager implements DemoModeCommandReceiver {
mMobileIconsViewModel = mobileUiAdapter.getMobileIconsViewModel();
MobileIconsBinder.bind(mGroup, mMobileIconsViewModel);
+
+ mMobileUiAdapterKairos = mobileUiAdapterKairos;
+
mWifiViewModel = wifiUiAdapter.bindGroup(mGroup, mLocation);
}
@@ -150,7 +180,7 @@ public class IconManager implements DemoModeCommandReceiver {
case TYPE_MOBILE_NEW -> addNewMobileIcon(index, slot, holder.getTag());
case TYPE_BINDABLE ->
// Safe cast, since only BindableIconHolders can set this tag on themselves
- addBindableIcon((BindableIconHolder) holder, index);
+ addBindableIcon((BindableIconHolder) holder, index);
default -> null;
};
}
@@ -223,13 +253,30 @@ public class IconManager implements DemoModeCommandReceiver {
private ModernStatusBarMobileView onCreateModernStatusBarMobileView(
String slot, int subId) {
Context mobileContext = mMobileContextProvider.getMobileContextForSub(subId, mContext);
- return ModernStatusBarMobileView
- .constructAndBind(
- mobileContext,
- mMobileIconsViewModel.getLogger(),
- slot,
- mMobileIconsViewModel.viewModelForSub(subId, mLocation)
- );
+ if (Flags.statusBarMobileIconKairos()) {
+ Pair<ModernStatusBarMobileView, Job> viewAndJob =
+ ModernStatusBarMobileView.constructAndBind(
+ mobileContext,
+ mMobileUiAdapterKairos.get().getMobileIconsViewModel().getLogger(),
+ slot,
+ mMobileUiAdapterKairos.get().getMobileIconsViewModel()
+ .viewModelForSub(subId, mLocation),
+ mAppScope,
+ subId,
+ mLocation,
+ mKairosNetwork
+ );
+ mBindingJobs.put(subId, viewAndJob.getSecond());
+ return viewAndJob.getFirst();
+ } else {
+ return ModernStatusBarMobileView
+ .constructAndBind(
+ mobileContext,
+ mMobileIconsViewModel.getLogger(),
+ slot,
+ mMobileIconsViewModel.viewModelForSub(subId, mLocation)
+ );
+ }
}
protected LinearLayout.LayoutParams onCreateLayoutParams(Shape shape) {
@@ -253,6 +300,15 @@ public class IconManager implements DemoModeCommandReceiver {
if (mIsInDemoMode) {
mDemoStatusIcons.onRemoveIcon((StatusIconDisplayable) mGroup.getChildAt(viewIndex));
}
+ if (Flags.statusBarMobileIconKairos()) {
+ View view = mGroup.getChildAt(viewIndex);
+ if (view instanceof ModernStatusBarMobileView) {
+ Job bindingJob = mBindingJobs.remove(((ModernStatusBarMobileView) view).getSubId());
+ if (bindingJob != null) {
+ bindingJob.cancel(new CancellationException());
+ }
+ }
+ }
mGroup.removeViewAt(viewIndex);
}
@@ -326,7 +382,10 @@ public class IconManager implements DemoModeCommandReceiver {
(LinearLayout) mGroup,
mMobileIconsViewModel,
mLocation,
- mIconSize
+ mIconSize,
+ mMobileUiAdapterKairos,
+ mKairosNetwork,
+ mAppScope
);
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ui/TintedIconManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ui/TintedIconManager.java
index e520148e925a..da59fc073c86 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ui/TintedIconManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ui/TintedIconManager.java
@@ -20,19 +20,30 @@ import android.view.View;
import android.view.ViewGroup;
import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.dagger.qualifiers.Application;
+import com.android.systemui.kairos.ExperimentalKairosApi;
+import com.android.systemui.kairos.KairosNetwork;
import com.android.systemui.statusbar.StatusIconDisplayable;
import com.android.systemui.statusbar.connectivity.ui.MobileContextProvider;
import com.android.systemui.statusbar.phone.DemoStatusIcons;
import com.android.systemui.statusbar.phone.StatusBarIconHolder;
import com.android.systemui.statusbar.phone.StatusBarLocation;
import com.android.systemui.statusbar.pipeline.mobile.ui.MobileUiAdapter;
+import com.android.systemui.statusbar.pipeline.mobile.ui.MobileUiAdapterKairos;
import com.android.systemui.statusbar.pipeline.wifi.ui.WifiUiAdapter;
+import dagger.Lazy;
+
+import kotlin.OptIn;
+
+import kotlinx.coroutines.CoroutineScope;
+
import javax.inject.Inject;
/**
* Version of {@link IconManager} that can tint the icons to a particular color.
*/
+@OptIn(markerClass = ExperimentalKairosApi.class)
public class TintedIconManager extends IconManager {
// The main tint, used as the foreground in non layer drawables
private int mColor;
@@ -44,13 +55,17 @@ public class TintedIconManager extends IconManager {
StatusBarLocation location,
WifiUiAdapter wifiUiAdapter,
MobileUiAdapter mobileUiAdapter,
- MobileContextProvider mobileContextProvider
+ Lazy<MobileUiAdapterKairos> mobileUiAdapterKairos,
+ MobileContextProvider mobileContextProvider,
+ KairosNetwork kairosNetwork,
+ CoroutineScope appScope
) {
super(group,
location,
wifiUiAdapter,
mobileUiAdapter,
- mobileContextProvider);
+ mobileUiAdapterKairos,
+ mobileContextProvider, kairosNetwork, appScope);
}
@Override
@@ -99,16 +114,25 @@ public class TintedIconManager extends IconManager {
private final WifiUiAdapter mWifiUiAdapter;
private final MobileContextProvider mMobileContextProvider;
private final MobileUiAdapter mMobileUiAdapter;
+ private final Lazy<MobileUiAdapterKairos> mMobileUiAdapterKairos;
+ private final KairosNetwork mKairosNetwork;
+ private final CoroutineScope mAppScope;
@Inject
public Factory(
WifiUiAdapter wifiUiAdapter,
MobileUiAdapter mobileUiAdapter,
- MobileContextProvider mobileContextProvider
+ MobileContextProvider mobileContextProvider,
+ Lazy<MobileUiAdapterKairos> mobileUiAdapterKairos,
+ KairosNetwork kairosNetwork,
+ @Application CoroutineScope appScope
) {
mWifiUiAdapter = wifiUiAdapter;
mMobileUiAdapter = mobileUiAdapter;
mMobileContextProvider = mobileContextProvider;
+ mMobileUiAdapterKairos = mobileUiAdapterKairos;
+ mKairosNetwork = kairosNetwork;
+ mAppScope = appScope;
}
/** Creates a new {@link TintedIconManager} for the given view group and location. */
@@ -118,7 +142,10 @@ public class TintedIconManager extends IconManager {
location,
mWifiUiAdapter,
mMobileUiAdapter,
- mMobileContextProvider);
+ mMobileUiAdapterKairos,
+ mMobileContextProvider,
+ mKairosNetwork,
+ mAppScope);
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileUiAdapterKairos.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileUiAdapterKairos.kt
new file mode 100644
index 000000000000..9881b354d8d9
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileUiAdapterKairos.kt
@@ -0,0 +1,95 @@
+/*
+ * 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.pipeline.mobile.ui
+
+import com.android.systemui.Dumpable
+import com.android.systemui.KairosActivatable
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.kairos.BuildScope
+import com.android.systemui.kairos.ExperimentalKairosApi
+import com.android.systemui.kairos.awaitClose
+import com.android.systemui.kairos.combine
+import com.android.systemui.kairos.launchEffect
+import com.android.systemui.shade.carrier.ShadeCarrierGroupController
+import com.android.systemui.statusbar.phone.ui.StatusBarIconController
+import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractorKairos
+import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModelKairos
+import java.io.PrintWriter
+import javax.inject.Inject
+
+/**
+ * This class is intended to provide a context to collect on the
+ * [MobileIconsInteractorKairos.filteredSubscriptions] data source and supply a state flow that can
+ * control [StatusBarIconController] to keep the old UI in sync with the new data source.
+ *
+ * It also provides a mechanism to create a top-level view model for each IconManager to know about
+ * the list of available mobile lines of service for which we want to show icons.
+ */
+@ExperimentalKairosApi
+@SysUISingleton
+class MobileUiAdapterKairos
+@Inject
+constructor(
+ private val iconController: StatusBarIconController,
+ val mobileIconsViewModel: MobileIconsViewModelKairos,
+ private val logger: MobileViewLogger,
+ dumpManager: DumpManager,
+) : KairosActivatable, Dumpable {
+
+ init {
+ dumpManager.registerNormalDumpable(this)
+ }
+
+ private var isCollecting: Boolean = false
+ private var lastValue: List<Int>? = null
+
+ private var shadeCarrierGroupController: ShadeCarrierGroupController? = null
+
+ override fun BuildScope.activate() {
+ launchEffect {
+ isCollecting = true
+ awaitClose { isCollecting = false }
+ }
+ // Start notifying the icon controller of subscriptions
+ combine(mobileIconsViewModel.subscriptionIds, mobileIconsViewModel.isStackable) { a, b ->
+ Pair(a, b)
+ }
+ .observe { (subIds, isStackable) ->
+ logger.logUiAdapterSubIdsSentToIconController(subIds, isStackable)
+ lastValue = subIds
+ if (isStackable) {
+ // Passing an empty list to remove pre-existing mobile icons.
+ // StackedMobileBindableIcon will show the stacked icon instead.
+ iconController.setNewMobileIconSubIds(emptyList())
+ } else {
+ iconController.setNewMobileIconSubIds(subIds)
+ }
+ shadeCarrierGroupController?.updateModernMobileIcons(subIds)
+ }
+ }
+
+ /** Set the [ShadeCarrierGroupController] to notify of subscription updates */
+ fun setShadeCarrierGroupController(controller: ShadeCarrierGroupController) {
+ shadeCarrierGroupController = controller
+ }
+
+ override fun dump(pw: PrintWriter, args: Array<out String>) {
+ pw.println("isCollecting=$isCollecting")
+ pw.println("Last values sent to icon controller: $lastValue")
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileViewLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileViewLogger.kt
index 4c2849de34ee..dec26886e063 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileViewLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileViewLogger.kt
@@ -20,10 +20,12 @@ import android.view.View
import com.android.systemui.Dumpable
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dump.DumpManager
+import com.android.systemui.kairos.ExperimentalKairosApi
import com.android.systemui.log.LogBuffer
import com.android.systemui.log.core.LogLevel
import com.android.systemui.statusbar.pipeline.dagger.MobileViewLog
import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.LocationBasedMobileViewModel
+import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.LocationBasedMobileViewModelKairos
import java.io.PrintWriter
import javax.inject.Inject
@@ -52,14 +54,17 @@ constructor(@MobileViewLog private val buffer: LogBuffer, dumpManager: DumpManag
)
}
- fun logNewViewBinding(view: View, viewModel: LocationBasedMobileViewModel) {
+ fun logNewViewBinding(view: View, viewModel: LocationBasedMobileViewModel) =
+ logNewViewBinding(view, viewModel, viewModel.location.name)
+
+ fun logNewViewBinding(view: View, viewModel: Any, location: String) {
buffer.log(
TAG,
LogLevel.INFO,
{
str1 = view.getIdForLogging()
str2 = viewModel.getIdForLogging()
- str3 = viewModel.location.name
+ str3 = location
},
{ "New view binding. viewId=$str1, viewModelId=$str2, viewModelLocation=$str3" },
)
@@ -93,6 +98,36 @@ constructor(@MobileViewLog private val buffer: LogBuffer, dumpManager: DumpManag
)
}
+ @OptIn(ExperimentalKairosApi::class)
+ fun logCollectionStarted(view: View, viewModel: LocationBasedMobileViewModelKairos) {
+ collectionStatuses[view.getIdForLogging()] = true
+ buffer.log(
+ TAG,
+ LogLevel.INFO,
+ {
+ str1 = view.getIdForLogging()
+ str2 = viewModel.getIdForLogging()
+ str3 = viewModel.location.name
+ },
+ { "Collection started. viewId=$str1, viewModelId=$str2, viewModelLocation=$str3" },
+ )
+ }
+
+ @OptIn(ExperimentalKairosApi::class)
+ fun logCollectionStopped(view: View, viewModel: LocationBasedMobileViewModelKairos) {
+ collectionStatuses[view.getIdForLogging()] = false
+ buffer.log(
+ TAG,
+ LogLevel.INFO,
+ {
+ str1 = view.getIdForLogging()
+ str2 = viewModel.getIdForLogging()
+ str3 = viewModel.location.name
+ },
+ { "Collection stopped. viewId=$str1, viewModelId=$str2, viewModelLocation=$str3" },
+ )
+ }
+
override fun dump(pw: PrintWriter, args: Array<out String>) {
pw.println("Collection statuses per view:---")
collectionStatuses.forEach { viewId, isCollecting ->
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/StackedMobileBindableIcon.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/StackedMobileBindableIcon.kt
index 32ebe884062d..bda76b72c08a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/StackedMobileBindableIcon.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/StackedMobileBindableIcon.kt
@@ -19,15 +19,19 @@ package com.android.systemui.statusbar.pipeline.mobile.ui
import android.content.Context
import com.android.settingslib.flags.Flags
import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.kairos.ExperimentalKairosApi
+import com.android.systemui.kairos.KairosNetwork
import com.android.systemui.statusbar.core.StatusBarRootModernization
import com.android.systemui.statusbar.pipeline.icons.shared.model.BindableIcon
import com.android.systemui.statusbar.pipeline.icons.shared.model.ModernStatusBarViewCreator
import com.android.systemui.statusbar.pipeline.mobile.ui.binder.StackedMobileIconBinder
import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel
import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.StackedMobileIconViewModelImpl
+import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.StackedMobileIconViewModelKairos
import com.android.systemui.statusbar.pipeline.shared.ui.view.SingleBindableStatusBarComposeIconView
import javax.inject.Inject
+@OptIn(ExperimentalKairosApi::class)
@SysUISingleton
class StackedMobileBindableIcon
@Inject
@@ -35,6 +39,8 @@ constructor(
context: Context,
mobileIconsViewModel: MobileIconsViewModel,
viewModelFactory: StackedMobileIconViewModelImpl.Factory,
+ kairosViewModelFactory: StackedMobileIconViewModelKairos.Factory,
+ kairosNetwork: KairosNetwork,
) : BindableIcon {
override val slot: String =
context.getString(com.android.internal.R.string.status_bar_stacked_mobile)
@@ -42,7 +48,13 @@ constructor(
override val initializer = ModernStatusBarViewCreator { context ->
SingleBindableStatusBarComposeIconView.createView(context).also { view ->
view.initView(slot) {
- StackedMobileIconBinder.bind(view, mobileIconsViewModel, viewModelFactory)
+ StackedMobileIconBinder.bind(
+ view,
+ mobileIconsViewModel,
+ viewModelFactory,
+ kairosViewModelFactory,
+ kairosNetwork,
+ )
}
}
}
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 0eef2e1ca685..0abd6d8d66b3 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
@@ -50,7 +50,7 @@ import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
-private data class Colors(@ColorInt val tint: Int, @ColorInt val contrast: Int)
+data class MobileIconColors(@ColorInt val tint: Int, @ColorInt val contrast: Int)
object MobileIconBinder {
/** Binds the view to the view-model, continuing to update the former based on the latter */
@@ -80,9 +80,9 @@ object MobileIconBinder {
@StatusBarIconView.VisibleState
val visibilityState: MutableStateFlow<Int> = MutableStateFlow(initialVisibilityState)
- val iconTint: MutableStateFlow<Colors> =
+ val iconTint: MutableStateFlow<MobileIconColors> =
MutableStateFlow(
- Colors(
+ MobileIconColors(
tint = DarkIconDispatcher.DEFAULT_ICON_TINT,
contrast = DarkIconDispatcher.DEFAULT_INVERSE_ICON_TINT,
)
@@ -291,7 +291,7 @@ object MobileIconBinder {
}
override fun onIconTintChanged(newTint: Int, contrastTint: Int) {
- iconTint.value = Colors(tint = newTint, contrast = contrastTint)
+ iconTint.value = MobileIconColors(tint = newTint, contrast = contrastTint)
}
override fun onDecorTintChanged(newTint: Int) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinderKairos.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinderKairos.kt
new file mode 100644
index 000000000000..1078ae343572
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinderKairos.kt
@@ -0,0 +1,290 @@
+/*
+ * 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.pipeline.mobile.ui.binder
+
+import android.content.res.ColorStateList
+import android.graphics.Color
+import android.view.View
+import android.view.ViewGroup
+import android.widget.FrameLayout
+import android.widget.ImageView
+import android.widget.Space
+import androidx.core.view.isVisible
+import com.android.settingslib.graph.SignalDrawable
+import com.android.systemui.Flags
+import com.android.systemui.common.ui.binder.IconViewBinder
+import com.android.systemui.kairos.BuildScope
+import com.android.systemui.kairos.BuildSpec
+import com.android.systemui.kairos.ExperimentalKairosApi
+import com.android.systemui.kairos.KairosNetwork
+import com.android.systemui.kairos.MutableState
+import com.android.systemui.kairos.effect
+import com.android.systemui.lifecycle.repeatWhenAttachedToWindow
+import com.android.systemui.lifecycle.repeatWhenWindowIsVisible
+import com.android.systemui.plugins.DarkIconDispatcher
+import com.android.systemui.res.R
+import com.android.systemui.statusbar.StatusBarIconView
+import com.android.systemui.statusbar.pipeline.mobile.domain.model.SignalIconModel
+import com.android.systemui.statusbar.pipeline.mobile.ui.MobileViewLogger
+import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.LocationBasedMobileViewModelKairos
+import com.android.systemui.statusbar.pipeline.shared.ui.binder.ModernStatusBarViewBinding
+import com.android.systemui.statusbar.pipeline.shared.ui.binder.ModernStatusBarViewVisibilityHelper
+import com.android.systemui.statusbar.pipeline.shared.ui.binder.StatusBarViewBinderConstants
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.awaitCancellation
+import kotlinx.coroutines.launch
+
+object MobileIconBinderKairos {
+
+ @ExperimentalKairosApi
+ fun bind(
+ view: ViewGroup,
+ viewModel: BuildSpec<LocationBasedMobileViewModelKairos>,
+ @StatusBarIconView.VisibleState
+ initialVisibilityState: Int = StatusBarIconView.STATE_HIDDEN,
+ logger: MobileViewLogger,
+ scope: CoroutineScope,
+ kairosNetwork: KairosNetwork,
+ ): Pair<ModernStatusBarViewBinding, Job> {
+ val binding = ModernStatusBarViewBindingKairosImpl(kairosNetwork, initialVisibilityState)
+ return binding to
+ scope.launch {
+ view.repeatWhenAttachedToWindow {
+ kairosNetwork.activateSpec {
+ bind(
+ view = view,
+ viewModel = viewModel.applySpec(),
+ logger = logger,
+ binding = binding,
+ )
+ }
+ }
+ }
+ }
+
+ @ExperimentalKairosApi
+ private class ModernStatusBarViewBindingKairosImpl(
+ kairosNetwork: KairosNetwork,
+ initialVisibilityState: Int,
+ ) : ModernStatusBarViewBinding {
+
+ @JvmField var shouldIconBeVisible: Boolean = false
+ @JvmField var isCollecting: Boolean = false
+
+ // TODO(b/238425913): We should log this visibility state.
+ val visibility = MutableState(kairosNetwork, initialVisibilityState)
+ val iconTint =
+ MutableState(
+ kairosNetwork,
+ MobileIconColors(
+ tint = DarkIconDispatcher.DEFAULT_ICON_TINT,
+ contrast = DarkIconDispatcher.DEFAULT_INVERSE_ICON_TINT,
+ ),
+ )
+ val decorTint = MutableState(kairosNetwork, Color.WHITE)
+
+ override fun getShouldIconBeVisible(): Boolean = shouldIconBeVisible
+
+ override fun onVisibilityStateChanged(state: Int) {
+ visibility.setValue(state)
+ }
+
+ override fun onIconTintChanged(newTint: Int, contrastTint: Int) {
+ iconTint.setValue(MobileIconColors(tint = newTint, contrast = contrastTint))
+ }
+
+ override fun onDecorTintChanged(newTint: Int) {
+ decorTint.setValue(newTint)
+ }
+
+ override fun isCollecting(): Boolean = isCollecting
+ }
+
+ @ExperimentalKairosApi
+ private fun BuildScope.bind(
+ view: ViewGroup,
+ viewModel: LocationBasedMobileViewModelKairos,
+ logger: MobileViewLogger,
+ binding: ModernStatusBarViewBindingKairosImpl,
+ ) {
+ viewModel.isVisible.observe { binding.shouldIconBeVisible = it }
+
+ val mobileGroupView = view.requireViewById<ViewGroup>(R.id.mobile_group)
+ val activityContainer = view.requireViewById<View>(R.id.inout_container)
+ val activityIn = view.requireViewById<ImageView>(R.id.mobile_in)
+ val activityOut = view.requireViewById<ImageView>(R.id.mobile_out)
+ val networkTypeView = view.requireViewById<ImageView>(R.id.mobile_type)
+ val networkTypeContainer = view.requireViewById<FrameLayout>(R.id.mobile_type_container)
+ val iconView = view.requireViewById<ImageView>(R.id.mobile_signal)
+ val mobileDrawable = SignalDrawable(view.context)
+ val roamingView = view.requireViewById<ImageView>(R.id.mobile_roaming)
+ val roamingSpace = view.requireViewById<Space>(R.id.mobile_roaming_space)
+ val dotView = view.requireViewById<StatusBarIconView>(R.id.status_bar_dot)
+
+ effect {
+ view.isVisible = viewModel.isVisible.sample()
+ iconView.isVisible = true
+ launch {
+ view.repeatWhenAttachedToWindow {
+ // isVisible controls the visibility state of the outer group, and thus it needs
+ // to run in the CREATED lifecycle so it can continue to watch while invisible
+ // See (b/291031862) for details
+ kairosNetwork.activateSpec {
+ viewModel.isVisible.observe { isVisible ->
+ viewModel.verboseLogger?.logBinderReceivedVisibility(
+ view,
+ viewModel.subscriptionId,
+ isVisible,
+ )
+ view.isVisible = isVisible
+ // [StatusIconContainer] can get out of sync sometimes. Make sure to
+ // request another layout when this changes.
+ view.requestLayout()
+ }
+ }
+ }
+ }
+ launch {
+ view.repeatWhenWindowIsVisible {
+ logger.logCollectionStarted(view, viewModel)
+ binding.isCollecting = true
+ kairosNetwork.activateSpec {
+ binding.visibility.observe { state ->
+ ModernStatusBarViewVisibilityHelper.setVisibilityState(
+ state,
+ mobileGroupView,
+ dotView,
+ )
+ view.requestLayout()
+ }
+
+ // Set the icon for the triangle
+ viewModel.icon.observe { 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.contentDescription.observe {
+ MobileContentDescriptionViewBinder.bind(it, view)
+ }
+
+ // Set the network type icon
+ viewModel.networkTypeIcon.observe { dataTypeId ->
+ viewModel.verboseLogger?.logBinderReceivedNetworkTypeIcon(
+ view,
+ viewModel.subscriptionId,
+ dataTypeId,
+ )
+ dataTypeId?.let { IconViewBinder.bind(dataTypeId, networkTypeView) }
+ val prevVis = networkTypeContainer.visibility
+ networkTypeContainer.visibility =
+ if (dataTypeId != null) View.VISIBLE else View.GONE
+
+ if (prevVis != networkTypeContainer.visibility) {
+ view.requestLayout()
+ }
+ }
+
+ // Set the network type background
+ viewModel.networkTypeBackground.observe { background ->
+ networkTypeContainer.setBackgroundResource(background?.res ?: 0)
+
+ // Tint will invert when this bit changes
+ if (background?.res != null) {
+ networkTypeContainer.backgroundTintList =
+ ColorStateList.valueOf(binding.iconTint.sample().tint)
+ networkTypeView.imageTintList =
+ ColorStateList.valueOf(binding.iconTint.sample().contrast)
+ } else {
+ networkTypeView.imageTintList =
+ ColorStateList.valueOf(binding.iconTint.sample().tint)
+ }
+ }
+
+ // Set the roaming indicator
+ viewModel.roaming.observe { isRoaming ->
+ roamingView.isVisible = isRoaming
+ roamingSpace.isVisible = isRoaming
+ }
+
+ if (Flags.statusBarStaticInoutIndicators()) {
+ // Set the opacity of the activity indicators
+ viewModel.activityInVisible.observe { visible ->
+ activityIn.imageAlpha =
+ (if (visible) StatusBarViewBinderConstants.ALPHA_ACTIVE
+ else StatusBarViewBinderConstants.ALPHA_INACTIVE)
+ }
+ viewModel.activityOutVisible.observe { visible ->
+ activityOut.imageAlpha =
+ (if (visible) StatusBarViewBinderConstants.ALPHA_ACTIVE
+ else StatusBarViewBinderConstants.ALPHA_INACTIVE)
+ }
+ } else {
+ // Set the activity indicators
+ viewModel.activityInVisible.observe { activityIn.isVisible = it }
+ viewModel.activityOutVisible.observe { activityOut.isVisible = it }
+ }
+
+ viewModel.activityContainerVisible.observe {
+ activityContainer.isVisible = it
+ }
+
+ // Set the tint
+ binding.iconTint.observe { colors ->
+ val tint = ColorStateList.valueOf(colors.tint)
+ val contrast = ColorStateList.valueOf(colors.contrast)
+
+ iconView.imageTintList = tint
+
+ // If the bg is visible, tint it and use the contrast for the fg
+ if (viewModel.networkTypeBackground.sample() != null) {
+ networkTypeContainer.backgroundTintList = tint
+ networkTypeView.imageTintList = contrast
+ } else {
+ networkTypeView.imageTintList = tint
+ }
+
+ roamingView.imageTintList = tint
+ activityIn.imageTintList = tint
+ activityOut.imageTintList = tint
+ dotView.setDecorColor(colors.tint)
+ }
+
+ binding.decorTint.observe { tint -> dotView.setDecorColor(tint) }
+ }
+
+ try {
+ awaitCancellation()
+ } finally {
+ binding.isCollecting = false
+ logger.logCollectionStopped(view, viewModel)
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/ShadeCarrierBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/ShadeCarrierBinder.kt
index 5c80fda72373..5238f3ec800a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/ShadeCarrierBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/ShadeCarrierBinder.kt
@@ -19,10 +19,10 @@ package com.android.systemui.statusbar.pipeline.mobile.ui.binder
import androidx.core.view.isVisible
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.repeatOnLifecycle
+import com.android.app.tracing.coroutines.launchTraced as launch
import com.android.systemui.lifecycle.repeatWhenAttached
import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.ShadeCarrierGroupMobileIconViewModel
import com.android.systemui.util.AutoMarqueeTextView
-import com.android.app.tracing.coroutines.launchTraced as launch
object ShadeCarrierBinder {
/** Binds the view to the view-model, continuing to update the former based on the latter */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/ShadeCarrierBinderKairos.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/ShadeCarrierBinderKairos.kt
new file mode 100644
index 000000000000..a782116e669c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/ShadeCarrierBinderKairos.kt
@@ -0,0 +1,42 @@
+/*
+ * 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.pipeline.mobile.ui.binder
+
+import androidx.core.view.isVisible
+import com.android.systemui.kairos.BuildSpec
+import com.android.systemui.kairos.ExperimentalKairosApi
+import com.android.systemui.kairos.KairosNetwork
+import com.android.systemui.lifecycle.repeatWhenWindowIsVisible
+import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.ShadeCarrierGroupMobileIconViewModelKairos
+import com.android.systemui.util.AutoMarqueeTextView
+
+object ShadeCarrierBinderKairos {
+ /** Binds the view to the view-model, continuing to update the former based on the latter */
+ @ExperimentalKairosApi
+ suspend fun bind(
+ carrierTextView: AutoMarqueeTextView,
+ viewModel: BuildSpec<ShadeCarrierGroupMobileIconViewModelKairos>,
+ kairosNetwork: KairosNetwork,
+ ) {
+ carrierTextView.isVisible = true
+ carrierTextView.repeatWhenWindowIsVisible {
+ kairosNetwork.activateSpec {
+ viewModel.applySpec().carrierName.observe { carrierTextView.text = it }
+ }
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/StackedMobileIconBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/StackedMobileIconBinder.kt
index fef5bfe2b7d8..54cd8e3c46e4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/StackedMobileIconBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/StackedMobileIconBinder.kt
@@ -22,19 +22,28 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.repeatOnLifecycle
+import com.android.systemui.Flags
+import com.android.systemui.kairos.ExperimentalKairosApi
+import com.android.systemui.kairos.KairosNetwork
import com.android.systemui.lifecycle.rememberViewModel
import com.android.systemui.lifecycle.repeatWhenAttached
import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel
+import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.StackedMobileIconViewModel
import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.StackedMobileIconViewModelImpl
+import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.StackedMobileIconViewModelKairos
import com.android.systemui.statusbar.pipeline.shared.ui.binder.ModernStatusBarViewBinding
import com.android.systemui.statusbar.pipeline.shared.ui.composable.StackedMobileIcon
import com.android.systemui.statusbar.pipeline.shared.ui.view.SingleBindableStatusBarComposeIconView
+import com.android.systemui.util.composable.kairos.rememberKairosActivatable
object StackedMobileIconBinder {
+ @OptIn(ExperimentalKairosApi::class)
fun bind(
view: SingleBindableStatusBarComposeIconView,
mobileIconsViewModel: MobileIconsViewModel,
viewModelFactory: StackedMobileIconViewModelImpl.Factory,
+ kairosViewModelFactory: StackedMobileIconViewModelKairos.Factory,
+ kairosNetwork: KairosNetwork,
): ModernStatusBarViewBinding {
return SingleBindableStatusBarComposeIconView.withDefaultBinding(
view = view,
@@ -47,9 +56,15 @@ object StackedMobileIconBinder {
ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed
)
setContent {
- val viewModel =
- rememberViewModel("StackedMobileIconBinder") {
- viewModelFactory.create()
+ val viewModel: StackedMobileIconViewModel =
+ if (Flags.statusBarMobileIconKairos()) {
+ rememberKairosActivatable(kairosNetwork) {
+ kairosViewModelFactory.create()
+ }
+ } else {
+ rememberViewModel("StackedMobileIconBinder") {
+ viewModelFactory.create()
+ }
}
if (viewModel.isIconVisible) {
CompositionLocalProvider(LocalContentColor provides Color(tint())) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernShadeCarrierGroupMobileView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernShadeCarrierGroupMobileView.kt
index fbd074d5b003..f1c5fee808cf 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernShadeCarrierGroupMobileView.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernShadeCarrierGroupMobileView.kt
@@ -20,22 +20,30 @@ import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import android.widget.LinearLayout
+import com.android.systemui.kairos.BuildSpec
+import com.android.systemui.kairos.ExperimentalKairosApi
+import com.android.systemui.kairos.KairosNetwork
import com.android.systemui.res.R
import com.android.systemui.statusbar.StatusBarIconView.STATE_ICON
+import com.android.systemui.statusbar.phone.StatusBarLocation
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.binder.MobileIconBinderKairos
import com.android.systemui.statusbar.pipeline.mobile.ui.binder.ShadeCarrierBinder
+import com.android.systemui.statusbar.pipeline.mobile.ui.binder.ShadeCarrierBinderKairos
import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.ShadeCarrierGroupMobileIconViewModel
+import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.ShadeCarrierGroupMobileIconViewModelKairos
import com.android.systemui.util.AutoMarqueeTextView
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.launch
/**
* ViewGroup containing a mobile carrier name and icon in the Shade Header. Can be multiple
* instances as children under [ShadeCarrierGroup]
*/
-class ModernShadeCarrierGroupMobileView(
- context: Context,
- attrs: AttributeSet?,
-) : LinearLayout(context, attrs) {
+class ModernShadeCarrierGroupMobileView(context: Context, attrs: AttributeSet?) :
+ LinearLayout(context, attrs) {
var subId: Int = -1
@@ -73,5 +81,49 @@ class ModernShadeCarrierGroupMobileView(
ShadeCarrierBinder.bind(textView, viewModel)
}
}
+
+ /**
+ * Inflates a new instance of [ModernShadeCarrierGroupMobileView], binds it to [viewModel],
+ * and returns it.
+ */
+ @ExperimentalKairosApi
+ @JvmStatic
+ fun constructAndBind(
+ context: Context,
+ logger: MobileViewLogger,
+ slot: String,
+ viewModel: BuildSpec<ShadeCarrierGroupMobileIconViewModelKairos>,
+ scope: CoroutineScope,
+ subscriptionId: Int,
+ location: StatusBarLocation,
+ kairosNetwork: KairosNetwork,
+ ): Pair<ModernShadeCarrierGroupMobileView, Job> {
+ val view =
+ (LayoutInflater.from(context).inflate(R.layout.shade_carrier_new, null)
+ as ModernShadeCarrierGroupMobileView)
+ .apply { subId = subscriptionId }
+ return view to
+ scope.launch {
+ val iconView =
+ view.requireViewById<ModernStatusBarMobileView>(R.id.mobile_combo)
+ iconView.initView(slot) {
+ val (binding, _) =
+ MobileIconBinderKairos.bind(
+ view = iconView,
+ viewModel = viewModel,
+ initialVisibilityState = STATE_ICON,
+ logger = logger,
+ scope = this,
+ kairosNetwork = kairosNetwork,
+ )
+ binding
+ }
+ logger.logNewViewBinding(view, viewModel, location.name)
+
+ val textView =
+ view.requireViewById<AutoMarqueeTextView>(R.id.mobile_carrier_text)
+ launch { ShadeCarrierBinderKairos.bind(textView, viewModel, kairosNetwork) }
+ }
+ }
}
}
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 7eda87f8418d..382af7e135ef 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
@@ -21,13 +21,22 @@ import android.util.AttributeSet
import android.view.LayoutInflater
import android.widget.FrameLayout
import android.widget.ImageView
+import com.android.settingslib.flags.Flags.newStatusBarIcons
+import com.android.systemui.kairos.BuildSpec
+import com.android.systemui.kairos.ExperimentalKairosApi
+import com.android.systemui.kairos.KairosNetwork
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.phone.StatusBarLocation
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.binder.MobileIconBinderKairos
import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.LocationBasedMobileViewModel
+import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.LocationBasedMobileViewModelKairos
import com.android.systemui.statusbar.pipeline.shared.ui.view.ModernStatusBarView
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
class ModernStatusBarMobileView(context: Context, attrs: AttributeSet?) :
ModernStatusBarView(context, attrs) {
@@ -98,5 +107,58 @@ class ModernStatusBarMobileView(context: Context, attrs: AttributeSet?) :
logger.logNewViewBinding(it, viewModel)
}
}
+
+ /**
+ * Inflates a new instance of [ModernStatusBarMobileView], binds it to [viewModel], and
+ * returns it.
+ */
+ @ExperimentalKairosApi
+ @JvmStatic
+ fun constructAndBind(
+ context: Context,
+ logger: MobileViewLogger,
+ slot: String,
+ viewModel: BuildSpec<LocationBasedMobileViewModelKairos>,
+ scope: CoroutineScope,
+ subscriptionId: Int,
+ location: StatusBarLocation,
+ kairosNetwork: KairosNetwork,
+ ): Pair<ModernStatusBarMobileView, Job> {
+ val view =
+ (LayoutInflater.from(context)
+ .inflate(R.layout.status_bar_mobile_signal_group_new, null)
+ as ModernStatusBarMobileView)
+ .apply {
+ // Flag-specific configuration
+ if (newStatusBarIcons()) {
+ // New icon (with no embedded whitespace) is slightly shorter
+ // (but actually taller)
+ val iconView = requireViewById<ImageView>(R.id.mobile_signal)
+ val lp = iconView.layoutParams
+ lp.height =
+ context.resources.getDimensionPixelSize(
+ R.dimen.status_bar_mobile_signal_size_updated
+ )
+ }
+
+ subId = subscriptionId
+ }
+
+ lateinit var jobResult: Job
+ view.initView(slot) {
+ val (binding, job) =
+ MobileIconBinderKairos.bind(
+ view = view,
+ viewModel = viewModel,
+ logger = logger,
+ scope = scope,
+ kairosNetwork = kairosNetwork,
+ )
+ jobResult = job
+ binding
+ }
+ logger.logNewViewBinding(view, viewModel, location.name)
+ return view to jobResult
+ }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelKairos.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelKairos.kt
index 0a0f9640a920..bd42d5bf401f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelKairos.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelKairos.kt
@@ -60,17 +60,12 @@ interface MobileIconViewModelKairosCommon {
}
/**
- * View model for the state of a single mobile icon. Each [MobileIconViewModel] will keep watch over
- * a single line of service via [MobileIconInteractorKairos] and update the UI based on that
- * subscription's information.
+ * View model for the state of a single mobile icon. Each [MobileIconViewModelKairos] will keep
+ * watch over a single line of service via [MobileIconInteractorKairos] and update the UI based on
+ * that subscription's information.
*
* There will be exactly one [MobileIconViewModelKairos] per filtered subscription offered from
* [MobileIconsInteractorKairos.filteredSubscriptions].
- *
- * For the sake of keeping log spam in check, every flow funding the
- * [MobileIconViewModelKairosCommon] interface is implemented as a [StateFlow]. This ensures that
- * each location-based mobile icon view model gets the exact same information, as well as allows us
- * to log that unified state only once per icon.
*/
@ExperimentalKairosApi
class MobileIconViewModelKairos(
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModelKairos.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModelKairos.kt
index ada5500a6f3c..41c72bee9e52 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModelKairos.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModelKairos.kt
@@ -17,7 +17,7 @@
package com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel
import androidx.compose.runtime.State as ComposeState
-import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.getValue
import com.android.systemui.Flags
import com.android.systemui.KairosActivatable
import com.android.systemui.KairosBuilder
@@ -31,7 +31,6 @@ import com.android.systemui.kairos.Incremental
import com.android.systemui.kairos.State as KairosState
import com.android.systemui.kairos.State
import com.android.systemui.kairos.buildSpec
-import com.android.systemui.kairos.changes
import com.android.systemui.kairos.combine
import com.android.systemui.kairos.flatten
import com.android.systemui.kairos.map
@@ -46,6 +45,7 @@ import com.android.systemui.statusbar.pipeline.mobile.ui.MobileViewLogger
import com.android.systemui.statusbar.pipeline.mobile.ui.VerboseMobileViewLogger
import com.android.systemui.statusbar.pipeline.mobile.ui.view.ModernStatusBarMobileView
import com.android.systemui.statusbar.pipeline.shared.ConnectivityConstants
+import com.android.systemui.util.composable.kairos.toComposeState
import dagger.Provides
import dagger.multibindings.ElementsIntoSet
import javax.inject.Inject
@@ -53,7 +53,7 @@ import javax.inject.Provider
/**
* View model for describing the system's current mobile cellular connections. The result is a list
- * of [MobileIconViewModel]s which describe the individual icons and can be bound to
+ * of [MobileIconViewModelKairos]s which describe the individual icons and can be bound to
* [ModernStatusBarMobileView].
*/
@ExperimentalKairosApi
@@ -145,20 +145,16 @@ constructor(
@ExperimentalKairosApi
class MobileIconsViewModelKairosComposeWrapper(
- val icons: ComposeState<Map<Int, MobileIconViewModelKairos>>
-)
-
-@ExperimentalKairosApi
-fun composeWrapper(
- viewModel: MobileIconsViewModelKairos
-): BuildSpec<MobileIconsViewModelKairosComposeWrapper> = buildSpec {
- MobileIconsViewModelKairosComposeWrapper(icons = toComposeState(viewModel.icons))
+ icons: ComposeState<Map<Int, MobileIconViewModelKairos>>,
+ val logger: MobileViewLogger,
+) {
+ val icons: Map<Int, MobileIconViewModelKairos> by icons
}
@ExperimentalKairosApi
-fun <T> BuildScope.toComposeState(state: KairosState<T>): ComposeState<T> {
- val initial = state.sample()
- val cState = mutableStateOf(initial)
- state.changes.observe { cState.value = it }
- return cState
+fun MobileIconsViewModelKairos.composeWrapper(): BuildSpec<MobileIconsViewModelKairosComposeWrapper> = buildSpec {
+ MobileIconsViewModelKairosComposeWrapper(
+ icons = toComposeState(icons),
+ logger = logger,
+ )
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/util/FakeMobileMappingsProxy.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/util/FakeMobileMappingsProxy.kt
index a052008d4832..cba06e3caa58 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/util/FakeMobileMappingsProxy.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/util/FakeMobileMappingsProxy.kt
@@ -33,17 +33,20 @@ class FakeMobileMappingsProxy : MobileMappingsProxy {
fun setIconMap(map: Map<String, MobileIconGroup>) {
iconMap = map
}
+
override fun mapIconSets(config: Config): Map<String, MobileIconGroup> {
if (useRealImpl) {
return realImpl.mapIconSets(config)
}
return iconMap
}
+
fun getIconMap() = iconMap
fun setDefaultIcons(group: MobileIconGroup) {
defaultIcons = group
}
+
override fun getDefaultIcons(config: Config): MobileIconGroup {
if (useRealImpl) {
return realImpl.getDefaultIcons(config)
@@ -72,3 +75,6 @@ class FakeMobileMappingsProxy : MobileMappingsProxy {
return toIconKey(networkType) + "_override"
}
}
+
+val MobileMappingsProxy.fake
+ get() = this as FakeMobileMappingsProxy
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseUserSwitcherAdapter.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseUserSwitcherAdapter.kt
index 4c6374b75f82..efab21fd9364 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseUserSwitcherAdapter.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseUserSwitcherAdapter.kt
@@ -37,7 +37,7 @@ protected constructor(
protected open val users: List<UserRecord>
get() = controller.users.filter {
(!controller.isKeyguardShowing || !it.isRestricted) &&
- (controller.isUserSwitcherEnabled || it.isCurrent)
+ (controller.isUserSwitcherEnabled || it.isCurrent || it.isSignOut)
}
init {
@@ -109,6 +109,7 @@ protected constructor(
item.isAddUser,
item.isGuest,
item.isAddSupervisedUser,
+ item.isSignOut,
isTablet,
item.isManageUsers,
)
diff --git a/packages/SystemUI/src/com/android/systemui/unfold/DisplaySwitchLatencyLogger.kt b/packages/SystemUI/src/com/android/systemui/unfold/DisplaySwitchLatencyLogger.kt
index 1cc7a3185a5d..5541c50fb650 100644
--- a/packages/SystemUI/src/com/android/systemui/unfold/DisplaySwitchLatencyLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/unfold/DisplaySwitchLatencyLogger.kt
@@ -50,7 +50,7 @@ class DisplaySwitchLatencyLogger {
onScreenTurningOnToOnDrawnMs,
onDrawnToOnScreenTurnedOnMs,
trackingResult,
- screenWakelockstatus
+ screenWakelockStatus,
)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/unfold/DisplaySwitchLatencyTracker.kt b/packages/SystemUI/src/com/android/systemui/unfold/DisplaySwitchLatencyTracker.kt
index 5800d5ed41c6..336e8d172ad4 100644
--- a/packages/SystemUI/src/com/android/systemui/unfold/DisplaySwitchLatencyTracker.kt
+++ b/packages/SystemUI/src/com/android/systemui/unfold/DisplaySwitchLatencyTracker.kt
@@ -36,11 +36,11 @@ import com.android.systemui.power.shared.model.WakeSleepReason
import com.android.systemui.power.shared.model.WakefulnessModel
import com.android.systemui.power.shared.model.WakefulnessState
import com.android.systemui.shared.system.SysUiStatsLog
-import com.android.systemui.unfold.DisplaySwitchLatencyTracker.DisplaySwitchLatencyEvent
import com.android.systemui.unfold.DisplaySwitchLatencyTracker.TrackingResult.CORRUPTED
import com.android.systemui.unfold.DisplaySwitchLatencyTracker.TrackingResult.SUCCESS
import com.android.systemui.unfold.DisplaySwitchLatencyTracker.TrackingResult.TIMED_OUT
import com.android.systemui.unfold.dagger.UnfoldSingleThreadBg
+import com.android.systemui.unfold.data.repository.ScreenTimeoutPolicyRepository
import com.android.systemui.unfold.data.repository.UnfoldTransitionStatus.TransitionStarted
import com.android.systemui.unfold.domain.interactor.UnfoldTransitionInteractor
import com.android.systemui.util.Compile
@@ -80,6 +80,7 @@ constructor(
private val context: Context,
private val deviceStateRepository: DeviceStateRepository,
private val powerInteractor: PowerInteractor,
+ private val screenTimeoutPolicyRepository: ScreenTimeoutPolicyRepository,
private val unfoldTransitionInteractor: UnfoldTransitionInteractor,
private val animationStatusRepository: AnimationStatusRepository,
private val keyguardInteractor: KeyguardInteractor,
@@ -287,7 +288,18 @@ constructor(
log { "fromFoldableDeviceState=$fromFoldableDeviceState" }
instantForTrack(TAG) { "fromFoldableDeviceState=$fromFoldableDeviceState" }
- return copy(fromFoldableDeviceState = fromFoldableDeviceState)
+ val screenTimeoutActive = screenTimeoutPolicyRepository.screenTimeoutActive.value
+ val screenWakelockStatus =
+ if (screenTimeoutActive) {
+ NO_SCREEN_WAKELOCKS
+ } else {
+ HAS_SCREEN_WAKELOCKS
+ }
+
+ return copy(
+ fromFoldableDeviceState = fromFoldableDeviceState,
+ screenWakelockStatus = screenWakelockStatus
+ )
}
private fun DisplaySwitchLatencyEvent.withAfterFields(
@@ -344,7 +356,7 @@ constructor(
val onDrawnToOnScreenTurnedOnMs: Int = VALUE_UNKNOWN,
val trackingResult: Int =
SysUiStatsLog.DISPLAY_SWITCH_LATENCY_TRACKED__TRACKING_RESULT__UNKNOWN_RESULT,
- val screenWakelockstatus: Int =
+ val screenWakelockStatus: Int =
SysUiStatsLog.DISPLAY_SWITCH_LATENCY_TRACKED__SCREEN_WAKELOCK_STATUS__SCREEN_WAKELOCK_STATUS_UNKNOWN,
)
@@ -372,5 +384,10 @@ constructor(
SysUiStatsLog.DISPLAY_SWITCH_LATENCY_TRACKED__FROM_FOLDABLE_DEVICE_STATE__STATE_OPENED
private const val FOLDABLE_DEVICE_STATE_FLIPPED =
SysUiStatsLog.DISPLAY_SWITCH_LATENCY_TRACKED__FROM_FOLDABLE_DEVICE_STATE__STATE_FLIPPED
+
+ private const val HAS_SCREEN_WAKELOCKS =
+ SysUiStatsLog.DISPLAY_SWITCH_LATENCY_TRACKED__SCREEN_WAKELOCK_STATUS__SCREEN_WAKELOCK_STATUS_HAS_SCREEN_WAKELOCKS
+ private const val NO_SCREEN_WAKELOCKS =
+ SysUiStatsLog.DISPLAY_SWITCH_LATENCY_TRACKED__SCREEN_WAKELOCK_STATUS__SCREEN_WAKELOCK_STATUS_NO_WAKELOCKS
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/unfold/data/repository/ScreenTimeoutPolicyRepository.kt b/packages/SystemUI/src/com/android/systemui/unfold/data/repository/ScreenTimeoutPolicyRepository.kt
new file mode 100644
index 000000000000..797939447464
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/unfold/data/repository/ScreenTimeoutPolicyRepository.kt
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.systemui.unfold.data.repository
+
+import android.os.PowerManager
+import android.view.Display
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
+import java.util.concurrent.Executor
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.stateIn
+
+/** Repository to get screen timeout updates */
+@SysUISingleton
+class ScreenTimeoutPolicyRepository
+@Inject
+constructor(
+ private val powerManager: PowerManager,
+ @Background private val executor: Executor,
+ @Background private val scope: CoroutineScope,
+) {
+
+ /** Stores true if there is an active screen timeout */
+ val screenTimeoutActive: StateFlow<Boolean> =
+ conflatedCallbackFlow {
+ val listener =
+ PowerManager.ScreenTimeoutPolicyListener { screenTimeoutPolicy ->
+ trySend(screenTimeoutPolicy == PowerManager.SCREEN_TIMEOUT_ACTIVE)
+ }
+ powerManager.addScreenTimeoutPolicyListener(
+ Display.DEFAULT_DISPLAY,
+ executor,
+ listener,
+ )
+ awaitClose {
+ powerManager.removeScreenTimeoutPolicyListener(
+ Display.DEFAULT_DISPLAY,
+ listener,
+ )
+ }
+ }
+ .stateIn(scope, started = SharingStarted.Eagerly, initialValue = true)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/user/data/source/UserRecord.kt b/packages/SystemUI/src/com/android/systemui/user/data/source/UserRecord.kt
index d4fb5634bd1d..e16a51a6f6fa 100644
--- a/packages/SystemUI/src/com/android/systemui/user/data/source/UserRecord.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/data/source/UserRecord.kt
@@ -42,6 +42,8 @@ data class UserRecord(
@JvmField val isSwitchToEnabled: Boolean = false,
/** Whether this record represents an option to add another supervised user to the device. */
@JvmField val isAddSupervisedUser: Boolean = false,
+ /** Whether this record represents an option to sign out of the current user. */
+ @JvmField val isSignOut: Boolean = false,
/**
* An enforcing admin, if the user action represented by this record is disabled by the admin.
* If not disabled, this is `null`.
@@ -49,7 +51,7 @@ data class UserRecord(
@JvmField val enforcedAdmin: RestrictedLockUtils.EnforcedAdmin? = null,
/** Whether this record is to go to the Settings page to manage users. */
- @JvmField val isManageUsers: Boolean = false
+ @JvmField val isManageUsers: Boolean = false,
) {
/** Returns a new instance of [UserRecord] with its [isCurrent] set to the given value. */
fun copyWithIsCurrent(isCurrent: Boolean): UserRecord {
diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractor.kt b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractor.kt
index 163288b25b28..b82aefc1ac1c 100644
--- a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractor.kt
@@ -38,6 +38,7 @@ import com.android.internal.util.UserIcons
import com.android.keyguard.KeyguardUpdateMonitor
import com.android.keyguard.KeyguardUpdateMonitorCallback
import com.android.systemui.Flags.switchUserOnBg
+import com.android.systemui.Flags.userSwitcherAddSignOutOption
import com.android.systemui.SystemUISecondaryUserService
import com.android.systemui.animation.Expandable
import com.android.systemui.broadcast.BroadcastDispatcher
@@ -110,6 +111,7 @@ constructor(
private val uiEventLogger: UiEventLogger,
private val userRestrictionChecker: UserRestrictionChecker,
private val processWrapper: ProcessWrapper,
+ private val userLogoutInteractor: UserLogoutInteractor,
) {
/**
* Defines interface for classes that can be notified when the state of users on the device is
@@ -242,6 +244,12 @@ constructor(
) {
add(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT)
}
+ if (
+ userSwitcherAddSignOutOption() &&
+ userLogoutInteractor.isLogoutEnabled.value
+ ) {
+ add(UserActionModel.SIGN_OUT)
+ }
}
}
.flowOn(backgroundDispatcher)
@@ -261,7 +269,8 @@ constructor(
action = it,
selectedUserId = selectedUserInfo.id,
isRestricted =
- it != UserActionModel.ENTER_GUEST_MODE &&
+ it != UserActionModel.SIGN_OUT &&
+ it != UserActionModel.ENTER_GUEST_MODE &&
it != UserActionModel.NAVIGATE_TO_USER_MANAGEMENT &&
!settings.isAddUsersFromLockscreen,
)
@@ -499,6 +508,10 @@ constructor(
Intent(Settings.ACTION_USER_SETTINGS),
/* dismissShade= */ true,
)
+ UserActionModel.SIGN_OUT -> {
+ dismissDialog()
+ applicationScope.launch { userLogoutInteractor.logOut() }
+ }
}
}
@@ -583,9 +596,10 @@ constructor(
actionType = action,
isRestricted = isRestricted,
isSwitchToEnabled =
- canSwitchUsers(selectedUserId = selectedUserId, isAction = true) &&
- // If the user is auto-created is must not be currently resetting.
- !(isGuestUserAutoCreated && isGuestUserResetting),
+ action == UserActionModel.SIGN_OUT ||
+ (canSwitchUsers(selectedUserId = selectedUserId, isAction = true) &&
+ // If the user is auto-created is must not be currently resetting.
+ !(isGuestUserAutoCreated && isGuestUserResetting)),
userRestrictionChecker = userRestrictionChecker,
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/user/legacyhelper/data/LegacyUserDataHelper.kt b/packages/SystemUI/src/com/android/systemui/user/legacyhelper/data/LegacyUserDataHelper.kt
index 80139bd6ac0c..23ca4ceda2de 100644
--- a/packages/SystemUI/src/com/android/systemui/user/legacyhelper/data/LegacyUserDataHelper.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/legacyhelper/data/LegacyUserDataHelper.kt
@@ -74,6 +74,7 @@ object LegacyUserDataHelper {
isGuest = actionType == UserActionModel.ENTER_GUEST_MODE,
isAddUser = actionType == UserActionModel.ADD_USER,
isAddSupervisedUser = actionType == UserActionModel.ADD_SUPERVISED_USER,
+ isSignOut = actionType == UserActionModel.SIGN_OUT,
isRestricted = isRestricted,
isSwitchToEnabled = isSwitchToEnabled,
enforcedAdmin =
@@ -94,6 +95,7 @@ object LegacyUserDataHelper {
record.isAddSupervisedUser -> UserActionModel.ADD_SUPERVISED_USER
record.isGuest -> UserActionModel.ENTER_GUEST_MODE
record.isManageUsers -> UserActionModel.NAVIGATE_TO_USER_MANAGEMENT
+ record.isSignOut -> UserActionModel.SIGN_OUT
else -> error("Not a known action: $record")
}
}
@@ -105,15 +107,14 @@ object LegacyUserDataHelper {
private fun getEnforcedAdmin(
context: Context,
selectedUserId: Int,
- userRestrictionChecker: UserRestrictionChecker
+ userRestrictionChecker: UserRestrictionChecker,
): EnforcedAdmin? {
val admin =
userRestrictionChecker.checkIfRestrictionEnforced(
context,
UserManager.DISALLOW_ADD_USER,
selectedUserId,
- )
- ?: return null
+ ) ?: return null
return if (
!userRestrictionChecker.hasBaseUserRestriction(
@@ -145,11 +146,6 @@ object LegacyUserDataHelper {
val unscaledOrNull = manager.getUserIcon(userInfo.id) ?: return null
val avatarSize = context.resources.getDimensionPixelSize(R.dimen.max_avatar_size)
- return Bitmap.createScaledBitmap(
- unscaledOrNull,
- avatarSize,
- avatarSize,
- /* filter= */ true,
- )
+ return Bitmap.createScaledBitmap(unscaledOrNull, avatarSize, avatarSize, /* filter= */ true)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/user/legacyhelper/ui/LegacyUserUiHelper.kt b/packages/SystemUI/src/com/android/systemui/user/legacyhelper/ui/LegacyUserUiHelper.kt
index 09cef1ed64fc..e7a3c23e9119 100644
--- a/packages/SystemUI/src/com/android/systemui/user/legacyhelper/ui/LegacyUserUiHelper.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/legacyhelper/ui/LegacyUserUiHelper.kt
@@ -41,6 +41,7 @@ object LegacyUserUiHelper {
isAddUser: Boolean,
isGuest: Boolean,
isAddSupervisedUser: Boolean,
+ isSignOut: Boolean,
isTablet: Boolean = false,
isManageUsers: Boolean,
): Int {
@@ -52,6 +53,8 @@ object LegacyUserUiHelper {
com.android.settingslib.R.drawable.ic_account_circle
} else if (isAddSupervisedUser) {
com.android.settingslib.R.drawable.ic_add_supervised_user
+ } else if (isSignOut) {
+ com.android.internal.R.drawable.ic_logout
} else if (isManageUsers) {
R.drawable.ic_manage_users
} else {
@@ -81,6 +84,7 @@ object LegacyUserUiHelper {
isGuestUserResetting = isGuestUserResetting,
isAddUser = record.isAddUser,
isAddSupervisedUser = record.isAddSupervisedUser,
+ isSignOut = record.isSignOut,
isTablet = isTablet,
isManageUsers = record.isManageUsers,
)
@@ -111,10 +115,11 @@ object LegacyUserUiHelper {
isGuestUserResetting: Boolean,
isAddUser: Boolean,
isAddSupervisedUser: Boolean,
+ isSignOut: Boolean,
isTablet: Boolean = false,
isManageUsers: Boolean,
): Int {
- check(isGuest || isAddUser || isAddSupervisedUser || isManageUsers)
+ check(isGuest || isAddUser || isAddSupervisedUser || isManageUsers || isSignOut)
return when {
isGuest && isGuestUserAutoCreated && isGuestUserResetting ->
@@ -124,6 +129,7 @@ object LegacyUserUiHelper {
isGuest -> com.android.internal.R.string.guest_name
isAddUser -> com.android.settingslib.R.string.user_add_user
isAddSupervisedUser -> R.string.add_user_supervised
+ isSignOut -> com.android.internal.R.string.global_action_logout
isManageUsers -> R.string.manage_users
else -> error("This should never happen!")
}
diff --git a/packages/SystemUI/src/com/android/systemui/user/shared/model/UserActionModel.kt b/packages/SystemUI/src/com/android/systemui/user/shared/model/UserActionModel.kt
index 823bf74dc0f0..7f67d7691bf5 100644
--- a/packages/SystemUI/src/com/android/systemui/user/shared/model/UserActionModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/shared/model/UserActionModel.kt
@@ -22,4 +22,5 @@ enum class UserActionModel {
ADD_USER,
ADD_SUPERVISED_USER,
NAVIGATE_TO_USER_MANAGEMENT,
+ SIGN_OUT,
}
diff --git a/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModel.kt b/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModel.kt
index 4089889f4b1e..2e3af1c7ad00 100644
--- a/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModel.kt
@@ -129,9 +129,7 @@ constructor(
cancelButtonClicked || executedActionFinish || userSwitched
}
- private fun toViewModel(
- model: UserModel,
- ): UserViewModel {
+ private fun toViewModel(model: UserModel): UserViewModel {
return UserViewModel(
viewKey = model.id,
name =
@@ -152,14 +150,13 @@ constructor(
)
}
- private fun toViewModel(
- model: UserActionModel,
- ): UserActionViewModel {
+ private fun toViewModel(model: UserActionModel): UserActionViewModel {
return UserActionViewModel(
viewKey = model.ordinal.toLong(),
iconResourceId =
LegacyUserUiHelper.getUserSwitcherActionIconResourceId(
isAddSupervisedUser = model == UserActionModel.ADD_SUPERVISED_USER,
+ isSignOut = model == UserActionModel.SIGN_OUT,
isAddUser = model == UserActionModel.ADD_USER,
isGuest = model == UserActionModel.ENTER_GUEST_MODE,
isManageUsers = model == UserActionModel.NAVIGATE_TO_USER_MANAGEMENT,
@@ -171,6 +168,7 @@ constructor(
isGuestUserAutoCreated = guestUserInteractor.isGuestUserAutoCreated,
isGuestUserResetting = guestUserInteractor.isGuestUserResetting,
isAddSupervisedUser = model == UserActionModel.ADD_SUPERVISED_USER,
+ isSignOut = model == UserActionModel.SIGN_OUT,
isAddUser = model == UserActionModel.ADD_USER,
isManageUsers = model == UserActionModel.NAVIGATE_TO_USER_MANAGEMENT,
isTablet = true,
diff --git a/packages/SystemUI/src/com/android/systemui/util/composable/kairos/ActivatedKairosSpec.kt b/packages/SystemUI/src/com/android/systemui/util/composable/kairos/ActivatedKairosSpec.kt
new file mode 100644
index 000000000000..a108b1081cd2
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/composable/kairos/ActivatedKairosSpec.kt
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.util.composable.kairos
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import com.android.systemui.kairos.BuildSpec
+import com.android.systemui.kairos.ExperimentalKairosApi
+import com.android.systemui.kairos.KairosNetwork
+import com.android.systemui.kairos.awaitClose
+import com.android.systemui.kairos.launchEffect
+
+/**
+ * Activates the Kairos [buildSpec] within [kairosNetwork], bound to the current composition.
+ *
+ * [block] will be invoked with the result of activating the [buildSpec]. [buildSpec] will be
+ * deactivated automatically when [ActivatedKairosSpec] leaves the composition.
+ */
+@ExperimentalKairosApi
+@Composable
+fun <T> ActivatedKairosSpec(
+ buildSpec: BuildSpec<T>,
+ kairosNetwork: KairosNetwork,
+ block: @Composable (T) -> Unit,
+) {
+ val uninit = Any()
+ var state by remember { mutableStateOf<Any?>(uninit) }
+ LaunchedEffect(key1 = Unit) {
+ kairosNetwork.activateSpec {
+ val v = buildSpec.applySpec()
+ launchEffect {
+ state = v
+ awaitClose { state = uninit }
+ }
+ }
+ }
+ state.let {
+ if (it !== uninit) {
+ @Suppress("UNCHECKED_CAST") block(it as T)
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/util/composable/kairos/RememberKairosActivatable.kt b/packages/SystemUI/src/com/android/systemui/util/composable/kairos/RememberKairosActivatable.kt
new file mode 100644
index 000000000000..03a60bc002e8
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/composable/kairos/RememberKairosActivatable.kt
@@ -0,0 +1,38 @@
+/*
+ * 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.composable.kairos
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.remember
+import com.android.systemui.KairosActivatable
+import com.android.systemui.kairos.ExperimentalKairosApi
+import com.android.systemui.kairos.KairosNetwork
+
+@ExperimentalKairosApi
+@Composable
+fun <T : KairosActivatable> rememberKairosActivatable(
+ kairosNetwork: KairosNetwork,
+ key: Any = Unit,
+ factory: () -> T,
+): T {
+ val instance = remember(key, factory) { factory() }
+ LaunchedEffect(instance, kairosNetwork) {
+ kairosNetwork.activateSpec { instance.run { activate() } }
+ }
+ return instance
+}
diff --git a/packages/SystemUI/src/com/android/systemui/util/composable/kairos/ToComposeState.kt b/packages/SystemUI/src/com/android/systemui/util/composable/kairos/ToComposeState.kt
new file mode 100644
index 000000000000..b3accb66c179
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/composable/kairos/ToComposeState.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.util.composable.kairos
+
+import androidx.compose.runtime.State as ComposeState
+import androidx.compose.runtime.mutableStateOf
+import com.android.systemui.kairos.BuildScope
+import com.android.systemui.kairos.ExperimentalKairosApi
+import com.android.systemui.kairos.State as KairosState
+import com.android.systemui.kairos.changes
+
+/**
+ * Returns a [ComposeState] that is kept hydrated with the current value of [state] within this
+ * [BuildScope].
+ */
+@ExperimentalKairosApi
+fun <T> BuildScope.toComposeState(state: KairosState<T>): ComposeState<T> {
+ val initial = state.sample()
+ val cState = mutableStateOf(initial)
+ state.changes.observe { cState.value = it }
+ return cState
+}
diff --git a/packages/SystemUI/src/com/android/systemui/window/data/repository/NoopWindowRootViewBlurRepository.kt b/packages/SystemUI/src/com/android/systemui/window/data/repository/NoopWindowRootViewBlurRepository.kt
index f1dd374d3e1d..1ae8bc9405dc 100644
--- a/packages/SystemUI/src/com/android/systemui/window/data/repository/NoopWindowRootViewBlurRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/window/data/repository/NoopWindowRootViewBlurRepository.kt
@@ -16,13 +16,12 @@
package com.android.systemui.window.data.repository
-import com.android.systemui.window.data.repository.WindowRootViewBlurRepository
import javax.inject.Inject
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
class NoopWindowRootViewBlurRepository @Inject constructor() : WindowRootViewBlurRepository {
- override val blurRadius: MutableStateFlow<Int> = MutableStateFlow(0)
+ override val blurRequestedByShade: MutableStateFlow<Int> = MutableStateFlow(0)
override val isBlurOpaque: MutableStateFlow<Boolean> = MutableStateFlow(true)
override val isBlurSupported: StateFlow<Boolean> = MutableStateFlow(false)
override var blurAppliedListener: BlurAppliedListener? = null
diff --git a/packages/SystemUI/src/com/android/systemui/window/data/repository/WindowRootViewBlurRepository.kt b/packages/SystemUI/src/com/android/systemui/window/data/repository/WindowRootViewBlurRepository.kt
index f98efe1e3c33..9b934bc6c866 100644
--- a/packages/SystemUI/src/com/android/systemui/window/data/repository/WindowRootViewBlurRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/window/data/repository/WindowRootViewBlurRepository.kt
@@ -39,7 +39,7 @@ typealias BlurAppliedListener = Consumer<Int>
/** Repository that maintains state for the window blur effect. */
interface WindowRootViewBlurRepository {
- val blurRadius: MutableStateFlow<Int>
+ val blurRequestedByShade: MutableStateFlow<Int>
val isBlurOpaque: MutableStateFlow<Boolean>
/** Is blur supported based on settings toggle and battery power saver mode. */
@@ -67,7 +67,7 @@ constructor(
@Main private val executor: Executor,
@Application private val scope: CoroutineScope,
) : WindowRootViewBlurRepository {
- override val blurRadius = MutableStateFlow(0)
+ override val blurRequestedByShade = MutableStateFlow(0)
override val isBlurOpaque = MutableStateFlow(false)
diff --git a/packages/SystemUI/src/com/android/systemui/window/domain/interactor/WindowRootViewBlurInteractor.kt b/packages/SystemUI/src/com/android/systemui/window/domain/interactor/WindowRootViewBlurInteractor.kt
index 5197242e8079..2994138f8181 100644
--- a/packages/SystemUI/src/com/android/systemui/window/domain/interactor/WindowRootViewBlurInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/window/domain/interactor/WindowRootViewBlurInteractor.kt
@@ -87,7 +87,7 @@ constructor(
val isBlurCurrentlySupported: StateFlow<Boolean> = repository.isBlurSupported
/** Radius of blur to be applied on the window root view. */
- val blurRadius: StateFlow<Int> = repository.blurRadius.asStateFlow()
+ val blurRadiusRequestedByShade: StateFlow<Int> = repository.blurRequestedByShade.asStateFlow()
/** Whether the blur applied is opaque or transparent. */
val isBlurOpaque: Flow<Boolean> =
@@ -128,7 +128,7 @@ constructor(
return false
}
Log.d(TAG, "requestingBlurForShade for $blurRadius $opaque")
- repository.blurRadius.value = blurRadius
+ repository.blurRequestedByShade.value = blurRadius
repository.isBlurOpaque.value = opaque
return true
}
diff --git a/packages/SystemUI/src/com/android/systemui/window/ui/viewmodel/WindowRootViewModel.kt b/packages/SystemUI/src/com/android/systemui/window/ui/viewmodel/WindowRootViewModel.kt
index e4b3ec7c40a3..89101c961031 100644
--- a/packages/SystemUI/src/com/android/systemui/window/ui/viewmodel/WindowRootViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/window/ui/viewmodel/WindowRootViewModel.kt
@@ -62,7 +62,9 @@ constructor(
listOf(
*bouncerBlurRadiusFlows.toTypedArray(),
*glanceableHubBlurRadiusFlows.toTypedArray(),
- blurInteractor.blurRadius.map { it.toFloat() }.logIfPossible("ShadeBlur"),
+ blurInteractor.blurRadiusRequestedByShade
+ .map { it.toFloat() }
+ .logIfPossible("ShadeBlur"),
)
.merge()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityTransitionAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityTransitionAnimatorTest.kt
index 60345a358bac..6cc8238f2d09 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityTransitionAnimatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityTransitionAnimatorTest.kt
@@ -249,10 +249,12 @@ class ActivityTransitionAnimatorTest : SysuiTestCase() {
var factory = controllerFactory(controller)
underTest.register(factory.cookie, factory, testScope)
assertEquals(2, testShellTransitions.remotes.size)
+ assertTrue(testShellTransitions.remotesForTakeover.isEmpty())
factory = controllerFactory(controller)
underTest.register(factory.cookie, factory, testScope)
assertEquals(4, testShellTransitions.remotes.size)
+ assertTrue(testShellTransitions.remotesForTakeover.isEmpty())
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
index 206654abcaaa..9b314f25e02b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
@@ -1500,6 +1500,7 @@ public class KeyguardViewMediatorTest extends SysuiTestCase {
mKosmos.getKeyguardInteractor(),
mKeyguardTransitionBootInteractor,
mKosmos::getCommunalSceneInteractor,
+ mKosmos::getCommunalSettingsInteractor,
mock(WindowManagerOcclusionManager.class));
mViewMediator.mUserChangedCallback = mUserTrackerCallback;
mViewMediator.start();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTestKt.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTestKt.kt
index 86f7966d4ada..d6b778fe2bc2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTestKt.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTestKt.kt
@@ -35,8 +35,14 @@ import com.android.systemui.SysuiTestCase
import com.android.systemui.animation.activityTransitionAnimator
import com.android.systemui.broadcast.broadcastDispatcher
import com.android.systemui.classifier.falsingCollector
+import com.android.systemui.common.data.repository.batteryRepository
+import com.android.systemui.common.data.repository.fake
+import com.android.systemui.communal.data.model.FEATURE_AUTO_OPEN
+import com.android.systemui.communal.data.model.SuppressionReason
import com.android.systemui.communal.data.repository.communalSceneRepository
import com.android.systemui.communal.domain.interactor.communalSceneInteractor
+import com.android.systemui.communal.domain.interactor.communalSettingsInteractor
+import com.android.systemui.communal.domain.interactor.setCommunalV2Enabled
import com.android.systemui.communal.shared.model.CommunalScenes
import com.android.systemui.communal.ui.viewmodel.communalTransitionViewModel
import com.android.systemui.concurrency.fakeExecutor
@@ -81,8 +87,11 @@ import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.any
import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.kotlin.anyOrNull
import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.eq
import org.mockito.kotlin.mock
+import org.mockito.kotlin.verify
/** Kotlin version of KeyguardViewMediatorTest to allow for coroutine testing. */
@SmallTest
@@ -152,6 +161,7 @@ class KeyguardViewMediatorTestKt : SysuiTestCase() {
keyguardInteractor,
keyguardTransitionBootInteractor,
{ communalSceneInteractor },
+ { communalSettingsInteractor },
mock<WindowManagerOcclusionManager>(),
)
}
@@ -164,6 +174,10 @@ class KeyguardViewMediatorTestKt : SysuiTestCase() {
@Test
fun doKeyguardTimeout_changesCommunalScene() =
kosmos.runTest {
+ // Hub is enabled and hub condition is active.
+ setCommunalV2Enabled(true)
+ enableHubOnCharging()
+
// doKeyguardTimeout message received.
val timeoutOptions = Bundle()
timeoutOptions.putBoolean(KeyguardViewMediator.EXTRA_TRIGGER_HUB, true)
@@ -174,4 +188,56 @@ class KeyguardViewMediatorTestKt : SysuiTestCase() {
assertThat(communalSceneRepository.currentScene.value)
.isEqualTo(CommunalScenes.Communal)
}
+
+ @Test
+ fun doKeyguardTimeout_communalNotAvailable_sleeps() =
+ kosmos.runTest {
+ // Hub disabled.
+ setCommunalV2Enabled(false)
+
+ // doKeyguardTimeout message received.
+ val timeoutOptions = Bundle()
+ timeoutOptions.putBoolean(KeyguardViewMediator.EXTRA_TRIGGER_HUB, true)
+ underTest.doKeyguardTimeout(timeoutOptions)
+ testableLooper.processAllMessages()
+
+ // Sleep is requested.
+ verify(powerManager)
+ .goToSleep(anyOrNull(), eq(PowerManager.GO_TO_SLEEP_REASON_POWER_BUTTON), eq(0))
+
+ // Hub scene is not changed.
+ assertThat(communalSceneRepository.currentScene.value).isEqualTo(CommunalScenes.Blank)
+ }
+
+ @Test
+ fun doKeyguardTimeout_hubConditionNotActive_sleeps() =
+ kosmos.runTest {
+ // Communal enabled, but hub condition set to never.
+ setCommunalV2Enabled(true)
+ disableHubShowingAutomatically()
+
+ // doKeyguardTimeout message received.
+ val timeoutOptions = Bundle()
+ timeoutOptions.putBoolean(KeyguardViewMediator.EXTRA_TRIGGER_HUB, true)
+ underTest.doKeyguardTimeout(timeoutOptions)
+ testableLooper.processAllMessages()
+
+ // Sleep is requested.
+ verify(powerManager)
+ .goToSleep(anyOrNull(), eq(PowerManager.GO_TO_SLEEP_REASON_POWER_BUTTON), eq(0))
+
+ // Hub scene is not changed.
+ assertThat(communalSceneRepository.currentScene.value).isEqualTo(CommunalScenes.Blank)
+ }
+
+ private fun Kosmos.enableHubOnCharging() {
+ communalSettingsInteractor.setSuppressionReasons(emptyList())
+ batteryRepository.fake.setDevicePluggedIn(true)
+ }
+
+ private fun Kosmos.disableHubShowingAutomatically() {
+ communalSettingsInteractor.setSuppressionReasons(
+ listOf(SuppressionReason.ReasonUnknown(FEATURE_AUTO_OPEN))
+ )
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySectionTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySectionTest.kt
index df24bff43c08..78a4fbecabe8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySectionTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySectionTest.kt
@@ -31,11 +31,13 @@ import com.android.systemui.flags.Flags
import com.android.systemui.keyguard.ui.viewmodel.DeviceEntryBackgroundViewModel
import com.android.systemui.keyguard.ui.viewmodel.DeviceEntryForegroundViewModel
import com.android.systemui.keyguard.ui.viewmodel.DeviceEntryIconViewModel
+import com.android.systemui.kosmos.testDispatcher
import com.android.systemui.log.logcatLogBuffer
import com.android.systemui.plugins.FalsingManager
import com.android.systemui.res.R
import com.android.systemui.shade.NotificationPanelView
import com.android.systemui.statusbar.VibratorHelper
+import com.android.systemui.testKosmos
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.flow.MutableStateFlow
@@ -68,6 +70,7 @@ class DefaultDeviceEntrySectionTest : SysuiTestCase() {
underTest =
DefaultDeviceEntrySection(
TestScope().backgroundScope,
+ testKosmos().testDispatcher,
authController,
windowManager,
context,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt
index 8a4f1ad13b78..5596cc7ee7da 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt
@@ -38,8 +38,6 @@ import com.android.systemui.notetask.NoteTaskEntryPoint.TAIL_BUTTON
import com.android.systemui.settings.FakeUserTracker
import com.android.systemui.statusbar.CommandQueue
import com.android.systemui.util.concurrency.FakeExecutor
-import com.android.systemui.util.mockito.any
-import com.android.systemui.util.mockito.eq
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
import com.android.systemui.util.mockito.withArgCaptor
@@ -52,11 +50,14 @@ import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
+import org.mockito.Mockito.anyList
import org.mockito.Mockito.never
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
import org.mockito.Mockito.verifyNoMoreInteractions
import org.mockito.MockitoAnnotations.initMocks
+import org.mockito.kotlin.any
+import org.mockito.kotlin.eq
/** atest SystemUITests:NoteTaskInitializerTest */
@OptIn(InternalNoteTaskApi::class)
@@ -180,6 +181,18 @@ internal class NoteTaskInitializerTest : SysuiTestCase() {
@Test
@EnableFlags(com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER)
+ fun initialize_keyGestureTypeOpenNotes_isRegistered() {
+ val underTest = createUnderTest(isEnabled = true, bubbles = bubbles)
+ underTest.initialize()
+ verify(inputManager)
+ .registerKeyGestureEventHandler(
+ eq(listOf(KeyGestureEvent.KEY_GESTURE_TYPE_OPEN_NOTES)),
+ any(),
+ )
+ }
+
+ @Test
+ @EnableFlags(com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER)
fun handlesShortcut_keyGestureTypeOpenNotes() {
val gestureEvent =
KeyGestureEvent.Builder()
@@ -189,12 +202,12 @@ internal class NoteTaskInitializerTest : SysuiTestCase() {
val underTest = createUnderTest(isEnabled = true, bubbles = bubbles)
underTest.initialize()
val callback = withArgCaptor {
- verify(inputManager).registerKeyGestureEventHandler(capture())
+ verify(inputManager).registerKeyGestureEventHandler(anyList(), capture())
}
- assertThat(callback.handleKeyGestureEvent(gestureEvent, null)).isTrue()
-
+ callback.handleKeyGestureEvent(gestureEvent, null)
executor.runAllReady()
+
verify(controller).showNoteTask(eq(KEYBOARD_SHORTCUT))
}
@@ -203,19 +216,19 @@ internal class NoteTaskInitializerTest : SysuiTestCase() {
fun handlesShortcut_stylusTailButton() {
val gestureEvent =
KeyGestureEvent.Builder()
- .setKeycodes(intArrayOf(KeyEvent.KEYCODE_STYLUS_BUTTON_TAIL))
+ .setKeycodes(intArrayOf(KEYCODE_STYLUS_BUTTON_TAIL))
.setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_OPEN_NOTES)
.setAction(KeyGestureEvent.ACTION_GESTURE_COMPLETE)
.build()
val underTest = createUnderTest(isEnabled = true, bubbles = bubbles)
underTest.initialize()
val callback = withArgCaptor {
- verify(inputManager).registerKeyGestureEventHandler(capture())
+ verify(inputManager).registerKeyGestureEventHandler(anyList(), capture())
}
- assertThat(callback.handleKeyGestureEvent(gestureEvent, null)).isTrue()
-
+ callback.handleKeyGestureEvent(gestureEvent, null)
executor.runAllReady()
+
verify(controller).showNoteTask(eq(TAIL_BUTTON))
}
@@ -224,19 +237,19 @@ internal class NoteTaskInitializerTest : SysuiTestCase() {
fun ignoresUnrelatedShortcuts() {
val gestureEvent =
KeyGestureEvent.Builder()
- .setKeycodes(intArrayOf(KeyEvent.KEYCODE_STYLUS_BUTTON_TAIL))
+ .setKeycodes(intArrayOf(KEYCODE_STYLUS_BUTTON_TAIL))
.setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_HOME)
.setAction(KeyGestureEvent.ACTION_GESTURE_COMPLETE)
.build()
val underTest = createUnderTest(isEnabled = true, bubbles = bubbles)
underTest.initialize()
val callback = withArgCaptor {
- verify(inputManager).registerKeyGestureEventHandler(capture())
+ verify(inputManager).registerKeyGestureEventHandler(anyList(), capture())
}
- assertThat(callback.handleKeyGestureEvent(gestureEvent, null)).isFalse()
-
+ callback.handleKeyGestureEvent(gestureEvent, null)
executor.runAllReady()
+
verify(controller, never()).showNoteTask(any())
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropTest.kt
index 7e42ec7e83b1..1551375f6879 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropTest.kt
@@ -37,6 +37,7 @@ import com.android.systemui.common.shared.model.Icon
import com.android.systemui.qs.panels.shared.model.SizedTile
import com.android.systemui.qs.panels.shared.model.SizedTileImpl
import com.android.systemui.qs.panels.ui.compose.infinitegrid.DefaultEditTileGrid
+import com.android.systemui.qs.panels.ui.viewmodel.AvailableEditActions
import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel
import com.android.systemui.qs.pipeline.shared.TileSpec
import com.android.systemui.qs.shared.model.TileCategory
@@ -63,7 +64,7 @@ class DragAndDropTest : SysuiTestCase() {
columns = 4,
largeTilesSpan = 4,
modifier = Modifier.fillMaxSize(),
- onAddTile = {},
+ onAddTile = { _, _ -> },
onRemoveTile = {},
onSetTiles = onSetTiles,
onResize = { _, _ -> },
@@ -84,7 +85,7 @@ class DragAndDropTest : SysuiTestCase() {
}
composeRule.waitForIdle()
- listState.onStarted(TestEditTiles[0], DragType.Add)
+ listState.onStarted(TestEditTiles[0], DragType.Move)
// Tile is being dragged, it should be replaced with a placeholder
composeRule.onNodeWithContentDescription("tileA").assertDoesNotExist()
@@ -103,6 +104,45 @@ class DragAndDropTest : SysuiTestCase() {
}
@Test
+ fun nonRemovableDraggedTile_removeHeaderShouldNotExist() {
+ val nonRemovableTile = createEditTile("tileA", isRemovable = false)
+ val listState = EditTileListState(listOf(nonRemovableTile), columns = 4, largeTilesSpan = 2)
+ composeRule.setContent { EditTileGridUnderTest(listState) {} }
+ composeRule.waitForIdle()
+
+ listState.onStarted(nonRemovableTile, DragType.Move)
+
+ // Tile is being dragged, it should be replaced with a placeholder
+ composeRule.onNodeWithContentDescription("tileA").assertDoesNotExist()
+
+ // Remove drop zone should not appear
+ composeRule.onNodeWithText("Remove").assertDoesNotExist()
+ }
+
+ @Test
+ fun droppedNonRemovableDraggedTile_shouldStayInGrid() {
+ val nonRemovableTile = createEditTile("tileA", isRemovable = false)
+ val listState = EditTileListState(listOf(nonRemovableTile), columns = 4, largeTilesSpan = 2)
+ composeRule.setContent { EditTileGridUnderTest(listState) {} }
+ composeRule.waitForIdle()
+
+ listState.onStarted(nonRemovableTile, DragType.Move)
+
+ // Tile is being dragged, it should be replaced with a placeholder
+ composeRule.onNodeWithContentDescription("tileA").assertDoesNotExist()
+
+ // Remove drop zone should not appear
+ composeRule.onNodeWithText("Remove").assertDoesNotExist()
+
+ // Drop tile outside of the grid
+ listState.movedOutOfBounds()
+ listState.onDrop()
+
+ // Tile A is still in the grid
+ composeRule.assertGridContainsExactly(CURRENT_TILES_GRID_TEST_TAG, listOf("tileA"))
+ }
+
+ @Test
fun draggedTile_shouldChangePosition() {
var tiles by mutableStateOf(TestEditTiles)
val listState = EditTileListState(tiles, columns = 4, largeTilesSpan = 2)
@@ -113,7 +153,11 @@ class DragAndDropTest : SysuiTestCase() {
}
composeRule.waitForIdle()
- listState.onStarted(TestEditTiles[0], DragType.Add)
+ listState.onStarted(TestEditTiles[0], DragType.Move)
+
+ // Remove drop zone should appear
+ composeRule.onNodeWithText("Remove").assertExists()
+
listState.onTargeting(1, false)
listState.onDrop()
@@ -141,7 +185,11 @@ class DragAndDropTest : SysuiTestCase() {
}
composeRule.waitForIdle()
- listState.onStarted(TestEditTiles[0], DragType.Add)
+ listState.onStarted(TestEditTiles[0], DragType.Move)
+
+ // Remove drop zone should appear
+ composeRule.onNodeWithText("Remove").assertExists()
+
listState.movedOutOfBounds()
listState.onDrop()
@@ -169,7 +217,11 @@ class DragAndDropTest : SysuiTestCase() {
}
composeRule.waitForIdle()
- listState.onStarted(createEditTile("tile_new"), DragType.Add)
+ listState.onStarted(createEditTile("tile_new", isRemovable = false), DragType.Add)
+
+ // Remove drop zone should appear
+ composeRule.onNodeWithText("Remove").assertExists()
+
// Insert after tileD, which is at index 4
// [ a ] [ b ] [ c ] [ empty ]
// [ tile d ] [ e ]
@@ -193,7 +245,10 @@ class DragAndDropTest : SysuiTestCase() {
private const val CURRENT_TILES_GRID_TEST_TAG = "CurrentTilesGrid"
private const val AVAILABLE_TILES_GRID_TEST_TAG = "AvailableTilesGrid"
- private fun createEditTile(tileSpec: String): SizedTile<EditTileViewModel> {
+ private fun createEditTile(
+ tileSpec: String,
+ isRemovable: Boolean = true,
+ ): SizedTile<EditTileViewModel> {
return SizedTileImpl(
EditTileViewModel(
tileSpec = TileSpec.create(tileSpec),
@@ -205,7 +260,8 @@ class DragAndDropTest : SysuiTestCase() {
label = AnnotatedString(tileSpec),
appName = null,
isCurrent = true,
- availableEditActions = emptySet(),
+ availableEditActions =
+ if (isRemovable) setOf(AvailableEditActions.REMOVE) else emptySet(),
category = TileCategory.UNKNOWN,
),
getWidth(tileSpec),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/EditModeTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/EditModeTest.kt
index 9d4a425c678b..acb441c3765d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/EditModeTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/EditModeTest.kt
@@ -23,6 +23,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
+import androidx.compose.ui.test.doubleClick
import androidx.compose.ui.test.junit4.ComposeContentTestRule
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onAllNodesWithText
@@ -30,6 +31,7 @@ import androidx.compose.ui.test.onFirst
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
+import androidx.compose.ui.test.performTouchInput
import androidx.compose.ui.text.AnnotatedString
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
@@ -40,6 +42,7 @@ import com.android.systemui.common.shared.model.Icon
import com.android.systemui.qs.panels.shared.model.SizedTile
import com.android.systemui.qs.panels.shared.model.SizedTileImpl
import com.android.systemui.qs.panels.ui.compose.infinitegrid.DefaultEditTileGrid
+import com.android.systemui.qs.panels.ui.viewmodel.AvailableEditActions
import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel
import com.android.systemui.qs.pipeline.shared.TileSpec
import com.android.systemui.qs.shared.model.TileCategory
@@ -53,8 +56,8 @@ class EditModeTest : SysuiTestCase() {
@get:Rule val composeRule = createComposeRule()
@Composable
- private fun EditTileGridUnderTest() {
- var tiles by remember { mutableStateOf(TestEditTiles) }
+ private fun EditTileGridUnderTest(sizedTiles: List<SizedTile<EditTileViewModel>>) {
+ var tiles by remember { mutableStateOf(sizedTiles) }
val (currentTiles, otherTiles) = tiles.partition { it.tile.isCurrent }
val listState = EditTileListState(currentTiles, columns = 4, largeTilesSpan = 2)
@@ -65,7 +68,7 @@ class EditModeTest : SysuiTestCase() {
columns = 4,
largeTilesSpan = 4,
modifier = Modifier.fillMaxSize(),
- onAddTile = { tiles = tiles.add(it) },
+ onAddTile = { spec, _ -> tiles = tiles.add(spec) },
onRemoveTile = { tiles = tiles.remove(it) },
onSetTiles = {},
onResize = { _, _ -> },
@@ -77,7 +80,7 @@ class EditModeTest : SysuiTestCase() {
@Test
fun clickAvailableTile_shouldAdd() {
- composeRule.setContent { EditTileGridUnderTest() }
+ composeRule.setContent { EditTileGridUnderTest(TestEditTiles) }
composeRule.waitForIdle()
composeRule.onNodeWithContentDescription("tileF").performClick() // Tap to add
@@ -93,7 +96,7 @@ class EditModeTest : SysuiTestCase() {
@Test
fun clickRemoveTarget_shouldRemoveSelection() {
- composeRule.setContent { EditTileGridUnderTest() }
+ composeRule.setContent { EditTileGridUnderTest(TestEditTiles) }
composeRule.waitForIdle()
// Selects first "tileA", i.e. the one in the current grid
@@ -110,6 +113,36 @@ class EditModeTest : SysuiTestCase() {
)
}
+ @Test
+ fun selectNonRemovableTile_removeTargetShouldHide() {
+ val nonRemovableTile = createEditTile("tileA", isRemovable = false)
+ composeRule.setContent { EditTileGridUnderTest(listOf(nonRemovableTile)) }
+ composeRule.waitForIdle()
+
+ // Selects first "tileA", i.e. the one in the current grid
+ composeRule.onAllNodesWithText("tileA").onFirst().performClick()
+
+ // Assert the remove target isn't shown
+ composeRule.onNodeWithText("Remove").assertDoesNotExist()
+ }
+
+ @Test
+ fun placementMode_shouldRepositionTile() {
+ composeRule.setContent { EditTileGridUnderTest(TestEditTiles) }
+ composeRule.waitForIdle()
+
+ // Double tap first "tileA", i.e. the one in the current grid
+ composeRule.onAllNodesWithText("tileA").onFirst().performTouchInput { doubleClick() }
+
+ // Tap on tileE to position tileA in its spot
+ composeRule.onAllNodesWithText("tileE").onFirst().performClick()
+
+ // Assert tileA moved to tileE's position
+ composeRule.assertCurrentTilesGridContainsExactly(
+ listOf("tileB", "tileC", "tileD_large", "tileE", "tileA")
+ )
+ }
+
private fun ComposeContentTestRule.assertCurrentTilesGridContainsExactly(specs: List<String>) =
assertGridContainsExactly(CURRENT_TILES_GRID_TEST_TAG, specs)
@@ -148,6 +181,7 @@ class EditModeTest : SysuiTestCase() {
private fun createEditTile(
tileSpec: String,
isCurrent: Boolean = true,
+ isRemovable: Boolean = true,
): SizedTile<EditTileViewModel> {
return SizedTileImpl(
EditTileViewModel(
@@ -160,7 +194,8 @@ class EditModeTest : SysuiTestCase() {
label = AnnotatedString(tileSpec),
appName = null,
isCurrent = isCurrent,
- availableEditActions = emptySet(),
+ availableEditActions =
+ if (isRemovable) setOf(AvailableEditActions.REMOVE) else emptySet(),
category = TileCategory.UNKNOWN,
),
getWidth(tileSpec),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/ResizingTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/ResizingTest.kt
index 5e76000cc7f0..274c44cef949 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/ResizingTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/ResizingTest.kt
@@ -69,7 +69,7 @@ class ResizingTest : SysuiTestCase() {
columns = 4,
largeTilesSpan = 4,
modifier = Modifier.fillMaxSize(),
- onAddTile = {},
+ onAddTile = { _, _ -> },
onRemoveTile = {},
onSetTiles = {},
onResize = onResize,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/reardisplay/RearDisplayCoreStartableTest.kt b/packages/SystemUI/tests/src/com/android/systemui/reardisplay/RearDisplayCoreStartableTest.kt
index 997cf417fe10..f4d0c26f12ee 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/reardisplay/RearDisplayCoreStartableTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/reardisplay/RearDisplayCoreStartableTest.kt
@@ -18,9 +18,11 @@ package com.android.systemui.reardisplay
import android.hardware.devicestate.feature.flags.Flags.FLAG_DEVICE_STATE_RDM_V2
import android.hardware.display.rearDisplay
+import android.os.fakeExecutorHandler
import android.platform.test.annotations.DisableFlags
import android.platform.test.annotations.EnableFlags
import android.view.Display
+import android.view.accessibility.accessibilityManager
import androidx.test.filters.SmallTest
import com.android.keyguard.keyguardUpdateMonitor
import com.android.systemui.SysuiTestCase
@@ -62,6 +64,8 @@ class RearDisplayCoreStartableTest : SysuiTestCase() {
kosmos.rearDisplayInnerDialogDelegateFactory,
kosmos.testScope,
kosmos.keyguardUpdateMonitor,
+ kosmos.accessibilityManager,
+ kosmos.fakeExecutorHandler,
)
@Before
@@ -69,7 +73,7 @@ class RearDisplayCoreStartableTest : SysuiTestCase() {
whenever(kosmos.rearDisplay.flags).thenReturn(Display.FLAG_REAR)
whenever(kosmos.rearDisplay.displayAdjustments)
.thenReturn(mContext.display.displayAdjustments)
- whenever(kosmos.rearDisplayInnerDialogDelegateFactory.create(any(), any()))
+ whenever(kosmos.rearDisplayInnerDialogDelegateFactory.create(any(), any(), any()))
.thenReturn(mockDelegate)
whenever(mockDelegate.createDialog()).thenReturn(mockDialog)
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/reardisplay/RearDisplayInnerDialogDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/reardisplay/RearDisplayInnerDialogDelegateTest.kt
index fc7661666825..477e5babdcc3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/reardisplay/RearDisplayInnerDialogDelegateTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/reardisplay/RearDisplayInnerDialogDelegateTest.kt
@@ -17,7 +17,10 @@
package com.android.systemui.reardisplay
import android.testing.TestableLooper
+import android.view.View
+import android.widget.Button
import android.widget.SeekBar
+import android.widget.TextView
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.haptics.msdl.msdlPlayer
@@ -28,6 +31,7 @@ import com.android.systemui.res.R
import com.android.systemui.statusbar.phone.systemUIDialogDotFactory
import com.android.systemui.testKosmos
import com.android.systemui.util.time.systemClock
+import com.google.common.truth.Truth.assertThat
import junit.framework.Assert.assertFalse
import junit.framework.Assert.assertTrue
import org.junit.Test
@@ -49,6 +53,7 @@ class RearDisplayInnerDialogDelegateTest : SysuiTestCase() {
RearDisplayInnerDialogDelegate(
kosmos.systemUIDialogDotFactory,
mContext,
+ false /* touchExplorationEnabled */,
kosmos.vibratorHelper,
kosmos.msdlPlayer,
kosmos.systemClock,
@@ -68,6 +73,7 @@ class RearDisplayInnerDialogDelegateTest : SysuiTestCase() {
RearDisplayInnerDialogDelegate(
kosmos.systemUIDialogDotFactory,
mContext,
+ false /* touchExplorationEnabled */,
kosmos.vibratorHelper,
kosmos.msdlPlayer,
kosmos.systemClock,
@@ -78,6 +84,9 @@ class RearDisplayInnerDialogDelegateTest : SysuiTestCase() {
.apply {
show()
val seekbar = findViewById<SeekBar>(R.id.seekbar)
+ assertThat(seekbar.visibility).isEqualTo(View.VISIBLE)
+ assertThat(findViewById<TextView>(R.id.seekbar_instructions).visibility)
+ .isEqualTo(View.VISIBLE)
seekbar.progress = 50
seekbar.progress = 100
verify(mockCallback).run()
@@ -90,6 +99,7 @@ class RearDisplayInnerDialogDelegateTest : SysuiTestCase() {
RearDisplayInnerDialogDelegate(
kosmos.systemUIDialogDotFactory,
mContext,
+ false /* touchExplorationEnabled */,
kosmos.vibratorHelper,
kosmos.msdlPlayer,
kosmos.systemClock,
@@ -118,4 +128,33 @@ class RearDisplayInnerDialogDelegateTest : SysuiTestCase() {
// Progress is reset
verify(mockSeekbar).setProgress(eq(0))
}
+
+ @Test
+ fun testTouchExplorationEnabled() {
+ val mockCallback = mock<Runnable>()
+
+ RearDisplayInnerDialogDelegate(
+ kosmos.systemUIDialogDotFactory,
+ mContext,
+ true /* touchExplorationEnabled */,
+ kosmos.vibratorHelper,
+ kosmos.msdlPlayer,
+ kosmos.systemClock,
+ ) {
+ mockCallback.run()
+ }
+ .createDialog()
+ .apply {
+ show()
+ assertThat(findViewById<SeekBar>(R.id.seekbar).visibility).isEqualTo(View.GONE)
+ assertThat(findViewById<TextView>(R.id.seekbar_instructions).visibility)
+ .isEqualTo(View.GONE)
+
+ val cancelButton = findViewById<Button>(R.id.cancel_button)
+ assertThat(cancelButton.visibility).isEqualTo(View.VISIBLE)
+
+ cancelButton.performClick()
+ verify(mockCallback).run()
+ }
+ }
}
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 2ea4e7f67b3c..bc7ab9d4fe3c 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
@@ -582,7 +582,7 @@ public class ExpandableNotificationRowTest extends SysuiTestCase {
public void testIconScrollXAfterTranslationAndReset() throws Exception {
ExpandableNotificationRow group = mNotificationTestHelper.createGroup();
- group.setDismissUsingRowTranslationX(false);
+ group.setDismissUsingRowTranslationX(false, false);
group.setTranslation(50);
assertEquals(50, -group.getEntry().getIcons().getShelfIcon().getScrollX());
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
index 0d99c0e8cab8..320a87e7db17 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
@@ -176,6 +176,7 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase {
private FakeKeyguardStateController mKeyguardStateController =
spy(new FakeKeyguardStateController());
private final FakeExecutor mExecutor = new FakeExecutor(new FakeSystemClock());
+ private final static String TEST_REASON = "reason";
@Mock
private ViewRootImpl mViewRootImpl;
@@ -272,14 +273,15 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase {
mStatusBarKeyguardViewManager.dismissWithAction(
action, cancelAction, false /* afterKeyguardGone */);
verify(mPrimaryBouncerInteractor).setDismissAction(eq(action), eq(cancelAction));
- verify(mPrimaryBouncerInteractor).show(eq(true));
+ verify(mPrimaryBouncerInteractor).show(eq(true),
+ eq("StatusBarKeyguardViewManager#dismissWithAction"));
}
@Test
public void showPrimaryBouncer_onlyWhenShowing() {
mStatusBarKeyguardViewManager.hide(0 /* startTime */, 0 /* fadeoutDuration */);
- mStatusBarKeyguardViewManager.showPrimaryBouncer(true /* scrimmed */);
- verify(mPrimaryBouncerInteractor, never()).show(anyBoolean());
+ mStatusBarKeyguardViewManager.showPrimaryBouncer(true /* scrimmed */, TEST_REASON);
+ verify(mPrimaryBouncerInteractor, never()).show(anyBoolean(), eq(TEST_REASON));
verify(mDeviceEntryInteractor, never()).attemptDeviceEntry();
verify(mSceneInteractor, never()).changeScene(any(), any());
}
@@ -289,8 +291,8 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase {
mStatusBarKeyguardViewManager.hide(0 /* startTime */, 0 /* fadeoutDuration */);
when(mKeyguardSecurityModel.getSecurityMode(anyInt())).thenReturn(
KeyguardSecurityModel.SecurityMode.Password);
- mStatusBarKeyguardViewManager.showPrimaryBouncer(true /* scrimmed */);
- verify(mPrimaryBouncerInteractor, never()).show(anyBoolean());
+ mStatusBarKeyguardViewManager.showPrimaryBouncer(true /* scrimmed */, TEST_REASON);
+ verify(mPrimaryBouncerInteractor, never()).show(anyBoolean(), eq(TEST_REASON));
verify(mDeviceEntryInteractor, never()).attemptDeviceEntry();
verify(mSceneInteractor, never()).changeScene(any(), any());
}
@@ -298,8 +300,8 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase {
@Test
@DisableSceneContainer
public void showBouncer_showsTheBouncer() {
- mStatusBarKeyguardViewManager.showPrimaryBouncer(true /* scrimmed */);
- verify(mPrimaryBouncerInteractor).show(eq(true));
+ mStatusBarKeyguardViewManager.showPrimaryBouncer(true /* scrimmed */, TEST_REASON);
+ verify(mPrimaryBouncerInteractor).show(eq(true), eq(TEST_REASON));
}
@Test
@@ -344,19 +346,20 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase {
public void onPanelExpansionChanged_showsBouncerWhenSwiping() {
mKeyguardStateController.setCanDismissLockScreen(false);
mStatusBarKeyguardViewManager.onPanelExpansionChanged(EXPANSION_EVENT);
- verify(mPrimaryBouncerInteractor).show(eq(false));
+ verify(mPrimaryBouncerInteractor).show(eq(false),
+ eq("StatusBarKeyguardViewManager#onPanelExpansionChanged"));
// But not when it's already visible
reset(mPrimaryBouncerInteractor);
when(mPrimaryBouncerInteractor.isFullyShowing()).thenReturn(true);
mStatusBarKeyguardViewManager.onPanelExpansionChanged(EXPANSION_EVENT);
- verify(mPrimaryBouncerInteractor, never()).show(eq(false));
+ verify(mPrimaryBouncerInteractor, never()).show(eq(false), eq(TEST_REASON));
// Or animating away
reset(mPrimaryBouncerInteractor);
when(mPrimaryBouncerInteractor.isAnimatingAway()).thenReturn(true);
mStatusBarKeyguardViewManager.onPanelExpansionChanged(EXPANSION_EVENT);
- verify(mPrimaryBouncerInteractor, never()).show(eq(false));
+ verify(mPrimaryBouncerInteractor, never()).show(eq(false), eq(TEST_REASON));
}
@Test
@@ -546,7 +549,7 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase {
when(mAlternateBouncerInteractor.isVisibleState()).thenReturn(true);
// WHEN showBouncer is called
- mStatusBarKeyguardViewManager.showPrimaryBouncer(true);
+ mStatusBarKeyguardViewManager.showPrimaryBouncer(true, TEST_REASON);
// THEN alt bouncer should be hidden
verify(mAlternateBouncerInteractor).hide();
@@ -571,10 +574,10 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase {
// WHEN showGenericBouncer is called
final boolean scrimmed = true;
- mStatusBarKeyguardViewManager.showBouncer(scrimmed);
+ mStatusBarKeyguardViewManager.showBouncer(scrimmed, TEST_REASON);
// THEN regular bouncer is shown
- verify(mPrimaryBouncerInteractor).show(eq(scrimmed));
+ verify(mPrimaryBouncerInteractor).show(eq(scrimmed), eq(TEST_REASON));
}
@Test
@@ -835,7 +838,7 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase {
when(mAlternateBouncerInteractor.isVisibleState()).thenReturn(false);
// WHEN request to show primary bouncer
- mStatusBarKeyguardViewManager.showPrimaryBouncer(true);
+ mStatusBarKeyguardViewManager.showPrimaryBouncer(true, TEST_REASON);
// THEN the scrim isn't updated from StatusBarKeyguardViewManager
verify(mCentralSurfaces, never()).updateScrimController();
@@ -847,9 +850,9 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase {
public void testShowBouncerOrKeyguard_needsFullScreen() {
when(mKeyguardSecurityModel.getSecurityMode(anyInt())).thenReturn(
KeyguardSecurityModel.SecurityMode.SimPin);
- mStatusBarKeyguardViewManager.showBouncerOrKeyguard(false, false);
+ mStatusBarKeyguardViewManager.showBouncerOrKeyguard(false, false, TEST_REASON);
verify(mCentralSurfaces).hideKeyguard();
- verify(mPrimaryBouncerInteractor).show(true);
+ verify(mPrimaryBouncerInteractor).show(true, TEST_REASON);
}
@Test
@@ -859,7 +862,7 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase {
when(mKeyguardSecurityModel.getSecurityMode(anyInt())).thenReturn(
KeyguardSecurityModel.SecurityMode.SimPin);
// Returning false means unable to show the bouncer
- when(mPrimaryBouncerInteractor.show(true)).thenReturn(false);
+ when(mPrimaryBouncerInteractor.show(true, TEST_REASON)).thenReturn(false);
when(mKeyguardTransitionInteractor.getTransitionState().getValue().getTo())
.thenReturn(KeyguardState.LOCKSCREEN);
mStatusBarKeyguardViewManager.onStartedWakingUp();
@@ -868,8 +871,8 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase {
// Advance past reattempts
mStatusBarKeyguardViewManager.setAttemptsToShowBouncer(10);
- mStatusBarKeyguardViewManager.showBouncerOrKeyguard(false, false);
- verify(mPrimaryBouncerInteractor).show(true);
+ mStatusBarKeyguardViewManager.showBouncerOrKeyguard(false, false, TEST_REASON);
+ verify(mPrimaryBouncerInteractor).show(true, TEST_REASON);
verify(mCentralSurfaces).showKeyguard();
}
@@ -884,7 +887,7 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase {
reset(mCentralSurfaces);
reset(mPrimaryBouncerInteractor);
mStatusBarKeyguardViewManager.showBouncerOrKeyguard(
- /* hideBouncerWhenShowing= */true, false);
+ /* hideBouncerWhenShowing= */true, false, TEST_REASON);
verify(mCentralSurfaces).showKeyguard();
verify(mPrimaryBouncerInteractor).hide();
}
@@ -897,9 +900,9 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase {
when(mKeyguardSecurityModel.getSecurityMode(anyInt())).thenReturn(
KeyguardSecurityModel.SecurityMode.SimPin);
when(mPrimaryBouncerInteractor.isFullyShowing()).thenReturn(true);
- mStatusBarKeyguardViewManager.showBouncerOrKeyguard(false, isFalsingReset);
+ mStatusBarKeyguardViewManager.showBouncerOrKeyguard(false, isFalsingReset, TEST_REASON);
verify(mCentralSurfaces, never()).hideKeyguard();
- verify(mPrimaryBouncerInteractor).show(true);
+ verify(mPrimaryBouncerInteractor).show(true, TEST_REASON);
}
@Test
@@ -909,24 +912,24 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase {
when(mKeyguardSecurityModel.getSecurityMode(anyInt())).thenReturn(
KeyguardSecurityModel.SecurityMode.SimPin);
when(mPrimaryBouncerInteractor.isFullyShowing()).thenReturn(true);
- mStatusBarKeyguardViewManager.showBouncerOrKeyguard(false, isFalsingReset);
+ mStatusBarKeyguardViewManager.showBouncerOrKeyguard(false, isFalsingReset, TEST_REASON);
verify(mCentralSurfaces, never()).hideKeyguard();
// Do not refresh the full screen bouncer if the call is from falsing
- verify(mPrimaryBouncerInteractor, never()).show(true);
+ verify(mPrimaryBouncerInteractor, never()).show(true, TEST_REASON);
}
@Test
@EnableSceneContainer
public void showBouncer_attemptDeviceEntry() {
- mStatusBarKeyguardViewManager.showBouncer(false);
+ mStatusBarKeyguardViewManager.showBouncer(false, TEST_REASON);
verify(mDeviceEntryInteractor).attemptDeviceEntry();
}
@Test
@EnableSceneContainer
public void showPrimaryBouncer() {
- mStatusBarKeyguardViewManager.showPrimaryBouncer(false);
+ mStatusBarKeyguardViewManager.showPrimaryBouncer(false, TEST_REASON);
verify(mSceneInteractor).showOverlay(eq(Overlays.Bouncer), anyString());
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestCase.java b/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestCase.java
index 846db6389d0c..2facc1c01ae1 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestCase.java
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestCase.java
@@ -283,6 +283,9 @@ public abstract class SysuiTestCase {
}
public FakeBroadcastDispatcher getFakeBroadcastDispatcher() {
+ if (mSysuiDependency == null) {
+ return null;
+ }
return mSysuiDependency.getFakeBroadcastDispatcher();
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestCaseExt.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestCaseExt.kt
index d3dccb021ff8..c86ba6ccf47f 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestCaseExt.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestCaseExt.kt
@@ -18,9 +18,22 @@ package com.android.systemui
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.testCase
+import com.android.systemui.kosmos.useStandardTestDispatcher
fun SysuiTestCase.testKosmos(): Kosmos = Kosmos().apply { testCase = this@testKosmos }
+/**
+ * This should not be called directly. Instead, you can use:
+ * - testKosmos() to use the default dispatcher (which will soon be unconfined, see go/thetiger)
+ * - testKosmos().useStandardTestDispatcher() to explicitly choose the standard dispatcher
+ * - testKosmos().useUnconfinedTestDispatcher() to explicitly choose the unconfined dispatcher
+ *
+ * For details, see go/thetiger
+ */
+@Deprecated("Do not call this directly. Use testKosmos() with dispatcher functions if needed.")
+fun SysuiTestCase.testKosmosLegacy(): Kosmos =
+ Kosmos().useStandardTestDispatcher().apply { testCase = this@testKosmosLegacy }
+
/** Run [f] on the main thread and return its result once completed. */
fun <T : Any> SysuiTestCase.runOnMainThreadAndWaitForIdleSync(f: () -> T): T {
lateinit var result: T
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractorKosmos.kt
index 511bede7349b..41dddce77a30 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractorKosmos.kt
@@ -33,6 +33,7 @@ var Kosmos.fromLockscreenTransitionInteractor by
transitionInteractor = keyguardTransitionInteractor,
internalTransitionInteractor = internalKeyguardTransitionInteractor,
scope = applicationCoroutineScope,
+ applicationScope = applicationCoroutineScope,
bgDispatcher = testDispatcher,
mainDispatcher = testDispatcher,
keyguardInteractor = keyguardInteractor,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/startable/SceneContainerStartableKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/startable/SceneContainerStartableKosmos.kt
index 7a9b052481cb..349e670a9af3 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/startable/SceneContainerStartableKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/startable/SceneContainerStartableKosmos.kt
@@ -46,7 +46,6 @@ import com.android.systemui.scene.domain.interactor.sceneContainerOcclusionInter
import com.android.systemui.scene.domain.interactor.sceneInteractor
import com.android.systemui.scene.session.shared.shadeSessionStorage
import com.android.systemui.scene.shared.logger.sceneLogger
-import com.android.systemui.settings.displayTracker
import com.android.systemui.shade.domain.interactor.shadeInteractor
import com.android.systemui.shade.domain.interactor.shadeModeInteractor
import com.android.systemui.statusbar.notification.stack.domain.interactor.headsUpNotificationInteractor
@@ -65,7 +64,6 @@ val Kosmos.sceneContainerStartable by Fixture {
bouncerInteractor = bouncerInteractor,
keyguardInteractor = keyguardInteractor,
sysUiState = sysUiState,
- displayId = displayTracker.defaultDisplayId,
sceneLogger = sceneLogger,
falsingCollector = falsingCollector,
falsingManager = falsingManager,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelKosmos.kt
index 34e5bfde43c9..9a9f3354a7ba 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelKosmos.kt
@@ -14,10 +14,14 @@
* limitations under the License.
*/
+@file:OptIn(ExperimentalKairosApi::class)
+
package com.android.systemui.shade.ui.viewmodel
import android.content.applicationContext
import com.android.systemui.battery.batteryMeterViewControllerFactory
+import com.android.systemui.kairos.ExperimentalKairosApi
+import com.android.systemui.kairos.kairos
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.plugins.activityStarter
import com.android.systemui.scene.domain.interactor.sceneInteractor
@@ -46,6 +50,8 @@ val Kosmos.shadeHeaderViewModel: ShadeHeaderViewModel by
tintedIconManagerFactory = tintedIconManagerFactory,
batteryMeterViewControllerFactory = batteryMeterViewControllerFactory,
statusBarIconController = mock<StatusBarIconController>(),
+ kairosNetwork = kairos,
+ mobileIconsViewModelKairos = mock(),
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/model/ActiveNotificationModelBuilder.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/model/ActiveNotificationModelBuilder.kt
index 63085e178e7d..4af4e804ff10 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/model/ActiveNotificationModelBuilder.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/model/ActiveNotificationModelBuilder.kt
@@ -19,7 +19,7 @@ package com.android.systemui.statusbar.notification.data.model
import android.app.PendingIntent
import android.graphics.drawable.Icon
import com.android.systemui.statusbar.StatusBarIconView
-import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel
+import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModels
import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel
import com.android.systemui.statusbar.notification.shared.CallType
import com.android.systemui.statusbar.notification.stack.BUCKET_UNKNOWN
@@ -29,6 +29,8 @@ fun activeNotificationModel(
key: String,
groupKey: String? = null,
whenTime: Long = 0L,
+ isForegroundService: Boolean = false,
+ isOngoingEvent: Boolean = false,
isAmbient: Boolean = false,
isRowDismissed: Boolean = false,
isSilent: Boolean = false,
@@ -47,12 +49,14 @@ fun activeNotificationModel(
contentIntent: PendingIntent? = null,
bucket: Int = BUCKET_UNKNOWN,
callType: CallType = CallType.None,
- promotedContent: PromotedNotificationContentModel? = null,
+ promotedContent: PromotedNotificationContentModels? = null,
) =
ActiveNotificationModel(
key = key,
groupKey = groupKey,
whenTime = whenTime,
+ isForegroundService = isForegroundService,
+ isOngoingEvent = isOngoingEvent,
isAmbient = isAmbient,
isRowDismissed = isRowDismissed,
isSilent = isSilent,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModelKosmos.kt
index 99323dbd7cce..ebe20af51c19 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModelKosmos.kt
@@ -22,6 +22,7 @@ import com.android.systemui.shade.domain.interactor.shadeInteractor
import com.android.systemui.shared.notifications.domain.interactor.notificationSettingsInteractor
import com.android.systemui.statusbar.notification.domain.interactor.activeNotificationsInteractor
import com.android.systemui.statusbar.notification.domain.interactor.seenNotificationsInteractor
+import com.android.systemui.window.domain.interactor.windowRootViewBlurInteractor
val Kosmos.footerViewModel by Fixture {
FooterViewModel(
@@ -29,6 +30,7 @@ val Kosmos.footerViewModel by Fixture {
notificationSettingsInteractor = notificationSettingsInteractor,
seenNotificationsInteractor = seenNotificationsInteractor,
shadeInteractor = shadeInteractor,
+ windowRootViewBlurInteractor = windowRootViewBlurInteractor,
)
}
val Kosmos.footerViewModelFactory: FooterViewModel.Factory by Fixture {
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/FakePromotedNotificationContentExtractor.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/FakePromotedNotificationContentExtractor.kt
index 8fdf5dbf2aeb..aaa86aaaedc6 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/FakePromotedNotificationContentExtractor.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/FakePromotedNotificationContentExtractor.kt
@@ -17,21 +17,23 @@
package com.android.systemui.statusbar.notification.promoted
import android.app.Notification
+import com.android.systemui.statusbar.NotificationLockscreenUserManager.RedactionType
import com.android.systemui.statusbar.notification.collection.NotificationEntry
-import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel
+import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModels
import com.android.systemui.statusbar.notification.row.shared.ImageModelProvider
import org.junit.Assert
class FakePromotedNotificationContentExtractor : PromotedNotificationContentExtractor {
@JvmField
- val contentForEntry = mutableMapOf<NotificationEntry, PromotedNotificationContentModel?>()
+ val contentForEntry = mutableMapOf<NotificationEntry, PromotedNotificationContentModels?>()
@JvmField val extractCalls = mutableListOf<Pair<NotificationEntry, Notification.Builder>>()
override fun extractContent(
entry: NotificationEntry,
recoveredBuilder: Notification.Builder,
+ @RedactionType redactionType: Int,
imageModelProvider: ImageModelProvider,
- ): PromotedNotificationContentModel? {
+ ): PromotedNotificationContentModels? {
extractCalls.add(entry to recoveredBuilder)
if (contentForEntry.isEmpty()) {
@@ -44,7 +46,7 @@ class FakePromotedNotificationContentExtractor : PromotedNotificationContentExtr
}
}
- fun resetForEntry(entry: NotificationEntry, content: PromotedNotificationContentModel?) {
+ fun resetForEntry(entry: NotificationEntry, content: PromotedNotificationContentModels?) {
contentForEntry.clear()
contentForEntry[entry] = content
extractCalls.clear()
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 2b3158da38f9..c4542c4e709b 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
@@ -19,6 +19,7 @@ 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.NotificationLockscreenUserManager.REDACTION_TYPE_NONE
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
@@ -39,9 +40,10 @@ fun Kosmos.setPromotedContent(entry: NotificationEntry) {
promotedNotificationContentExtractor.extractContent(
entry,
Notification.Builder.recoverBuilder(applicationContext, entry.sbn.notification),
+ REDACTION_TYPE_NONE,
RowImageInflater.newInstance(previousIndex = null, reinflating = false)
.useForContentModel(),
)
- entry.promotedNotificationContentModel =
+ entry.promotedNotificationContentModels =
requireNotNull(extractedContent) { "extractContent returned null" }
}
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
index 093ec10e2642..8b2c68aa04cd 100644
--- 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
@@ -19,13 +19,17 @@ 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.mediaprojection.domain.interactor.mediaProjectionChipInteractor
import com.android.systemui.statusbar.chips.notification.domain.interactor.statusBarNotificationChipsInteractor
+import com.android.systemui.statusbar.chips.screenrecord.domain.interactor.screenRecordChipInteractor
import com.android.systemui.statusbar.notification.domain.interactor.activeNotificationsInteractor
val Kosmos.promotedNotificationsInteractor by
Kosmos.Fixture {
PromotedNotificationsInteractor(
activeNotificationsInteractor = activeNotificationsInteractor,
+ screenRecordChipInteractor = screenRecordChipInteractor,
+ mediaProjectionChipInteractor = mediaProjectionChipInteractor,
callChipInteractor = callChipInteractor,
notifChipsInteractor = statusBarNotificationChipsInteractor,
backgroundDispatcher = testDispatcher,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/shared/model/PromotedNotificationContentBuilder.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/shared/model/PromotedNotificationContentBuilder.kt
new file mode 100644
index 000000000000..6916d560a7ad
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/shared/model/PromotedNotificationContentBuilder.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.shared.model
+
+class PromotedNotificationContentBuilder(val key: String) {
+ private val sharedBuilder = PromotedNotificationContentModel.Builder(key)
+
+ fun applyToShared(
+ block: PromotedNotificationContentModel.Builder.() -> Unit
+ ): PromotedNotificationContentBuilder {
+ sharedBuilder.apply(block)
+ return this
+ }
+
+ fun build(): PromotedNotificationContentModels {
+ val sharedModel = sharedBuilder.build()
+ return PromotedNotificationContentModels(sharedModel, sharedModel)
+ }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/icon/AppIconProviderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/icon/AppIconProviderKosmos.kt
index 0fd0f1469818..277fc62242b1 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/icon/AppIconProviderKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/icon/AppIconProviderKosmos.kt
@@ -19,6 +19,9 @@ package com.android.systemui.statusbar.notification.row.icon
import android.content.applicationContext
import com.android.systemui.dump.dumpManager
import com.android.systemui.kosmos.Kosmos
+import org.mockito.kotlin.mock
+
+val Kosmos.mockAppIconProvider by Kosmos.Fixture { mock<AppIconProvider>() }
val Kosmos.appIconProvider by
Kosmos.Fixture { AppIconProviderImpl(applicationContext, dumpManager) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/icon/NotificationIconStyleProviderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/icon/NotificationIconStyleProviderKosmos.kt
index b4fb7dd9d760..86ff722f99be 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/icon/NotificationIconStyleProviderKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/icon/NotificationIconStyleProviderKosmos.kt
@@ -19,6 +19,10 @@ package com.android.systemui.statusbar.notification.row.icon
import android.os.userManager
import com.android.systemui.dump.dumpManager
import com.android.systemui.kosmos.Kosmos
+import org.mockito.kotlin.mock
+
+val Kosmos.mockNotificationIconStyleProvider by
+ Kosmos.Fixture { mock<NotificationIconStyleProvider>() }
val Kosmos.notificationIconStyleProvider by
Kosmos.Fixture { NotificationIconStyleProviderImpl(userManager, dumpManager) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallTestHelper.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallTestHelper.kt
index 3e96fd7c729f..e5e1a830231e 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallTestHelper.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallTestHelper.kt
@@ -26,7 +26,7 @@ import com.android.systemui.statusbar.notification.data.model.activeNotification
import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository
import com.android.systemui.statusbar.notification.data.repository.addNotif
import com.android.systemui.statusbar.notification.data.repository.removeNotif
-import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel
+import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModels
import com.android.systemui.statusbar.notification.shared.CallType
import com.android.systemui.statusbar.phone.ongoingcall.StatusBarChipsModernization
import com.android.systemui.statusbar.phone.ongoingcall.data.repository.ongoingCallRepository
@@ -39,7 +39,7 @@ fun inCallModel(
intent: PendingIntent? = null,
notificationKey: String = "test",
appName: String = "",
- promotedContent: PromotedNotificationContentModel? = null,
+ promotedContent: PromotedNotificationContentModels? = null,
isAppVisible: Boolean = false,
) =
OngoingCallModel.InCall(
@@ -77,7 +77,7 @@ object OngoingCallTestHelper {
key: String = "notif",
startTimeMs: Long = 1000L,
statusBarChipIconView: StatusBarIconView? = createStatusBarIconViewOrNull(),
- promotedContent: PromotedNotificationContentModel? = null,
+ promotedContent: PromotedNotificationContentModels? = null,
contentIntent: PendingIntent? = null,
uid: Int = DEFAULT_UID,
appName: String = "Fake name",
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ui/TintedIconManagerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ui/TintedIconManagerKosmos.kt
index 8e13b624f5fa..fd63ce28a29d 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ui/TintedIconManagerKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ui/TintedIconManagerKosmos.kt
@@ -16,16 +16,24 @@
package com.android.systemui.statusbar.phone.ui
+import com.android.systemui.kairos.ExperimentalKairosApi
+import com.android.systemui.kairos.kairos
import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
import com.android.systemui.statusbar.connectivity.ui.mobileContextProvider
import com.android.systemui.statusbar.pipeline.mobile.ui.mobileUiAdapter
+import com.android.systemui.statusbar.pipeline.mobile.ui.mobileUiAdapterKairos
import com.android.systemui.statusbar.pipeline.wifi.ui.wifiUiAdapter
+@OptIn(ExperimentalKairosApi::class)
val Kosmos.tintedIconManagerFactory by
-Kosmos.Fixture {
- TintedIconManager.Factory(
- wifiUiAdapter,
- mobileUiAdapter,
- mobileContextProvider,
- )
-} \ No newline at end of file
+ Kosmos.Fixture {
+ TintedIconManager.Factory(
+ wifiUiAdapter,
+ mobileUiAdapter,
+ mobileContextProvider,
+ { mobileUiAdapterKairos },
+ kairos,
+ applicationCoroutineScope,
+ )
+ }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileUiAdapterKairosKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileUiAdapterKairosKosmos.kt
new file mode 100644
index 000000000000..3a3f18ad2ede
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileUiAdapterKairosKosmos.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.mobile.ui
+
+import com.android.systemui.dump.dumpManager
+import com.android.systemui.kairos.ActivatedKairosFixture
+import com.android.systemui.kairos.ExperimentalKairosApi
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.statusbar.phone.ui.statusBarIconController
+import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.mobileIconsViewModelKairos
+
+@ExperimentalKairosApi
+val Kosmos.mobileUiAdapterKairos by ActivatedKairosFixture {
+ MobileUiAdapterKairos(
+ statusBarIconController,
+ mobileIconsViewModelKairos,
+ mobileViewLogger,
+ dumpManager,
+ )
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractorKosmos.kt
index 1504df4ef6d0..6767300a22bc 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractorKosmos.kt
@@ -55,5 +55,6 @@ val Kosmos.userSwitcherInteractor by
uiEventLogger = uiEventLogger,
userRestrictionChecker = userRestrictionChecker,
processWrapper = processWrapper,
+ userLogoutInteractor = userLogoutInteractor,
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/window/data/repository/WindowRootViewBlurRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/window/data/repository/WindowRootViewBlurRepositoryKosmos.kt
index b619e2d70724..2b518182922d 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/window/data/repository/WindowRootViewBlurRepositoryKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/window/data/repository/WindowRootViewBlurRepositoryKosmos.kt
@@ -26,7 +26,7 @@ val Kosmos.windowRootViewBlurRepository: WindowRootViewBlurRepository by
Kosmos.Fixture { fakeWindowRootViewBlurRepository }
class FakeWindowRootViewBlurRepository : WindowRootViewBlurRepository {
- override val blurRadius: MutableStateFlow<Int> = MutableStateFlow(0)
+ override val blurRequestedByShade: MutableStateFlow<Int> = MutableStateFlow(0)
override val isBlurOpaque: MutableStateFlow<Boolean> = MutableStateFlow(false)
override val isBlurSupported: MutableStateFlow<Boolean> = MutableStateFlow(false)
override var blurAppliedListener: BlurAppliedListener? = null
diff --git a/services/accessibility/accessibility.aconfig b/services/accessibility/accessibility.aconfig
index 35db3c6f0a6d..a133131a1d3f 100644
--- a/services/accessibility/accessibility.aconfig
+++ b/services/accessibility/accessibility.aconfig
@@ -223,6 +223,16 @@ flag {
}
flag {
+ name: "manager_lifecycle_user_change"
+ namespace: "accessibility"
+ description: "Use A11yManagerService's Lifecycle to change users, instead of listening for user changed events."
+ bug: "393626471"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+flag {
name: "motion_event_injector_cancel_fix"
namespace: "accessibility"
description: "Fix the ACTION_CANCEL logic used in MotionEventInjector to avoid InputDispatcher inconsistency"
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
index 703e37fad5ad..39c1fa73b7ce 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
@@ -500,6 +500,12 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
mService = new AccessibilityManagerService(context);
}
+ @VisibleForTesting
+ public Lifecycle(Context context, AccessibilityManagerService service) {
+ super(context);
+ mService = service;
+ }
+
@Override
public void onStart() {
LocalServices.addService(AccessibilityManagerInternal.class,
@@ -511,17 +517,19 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
public void onBootPhase(int phase) {
mService.onBootPhase(phase);
}
+
+ @Override
+ public void onUserSwitching(@androidx.annotation.Nullable TargetUser from,
+ @androidx.annotation.NonNull TargetUser to) {
+ super.onUserSwitching(from, to);
+ if (Flags.managerLifecycleUserChange()) {
+ mService.switchUser(to.getUserIdentifier());
+ }
+ }
}
private InputManager.KeyGestureEventHandler mKeyGestureEventHandler =
- new InputManager.KeyGestureEventHandler() {
- @Override
- public boolean handleKeyGestureEvent(
- @NonNull KeyGestureEvent event,
- @Nullable IBinder focusedToken) {
- return AccessibilityManagerService.this.handleKeyGestureEvent(event);
- }
- };
+ (event, focusedToken) -> AccessibilityManagerService.this.handleKeyGestureEvent(event);
@VisibleForTesting
AccessibilityManagerService(
@@ -637,7 +645,11 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
new AccessibilityContentObserver(mMainHandler).register(
mContext.getContentResolver());
if (enableTalkbackAndMagnifierKeyGestures()) {
- mInputManager.registerKeyGestureEventHandler(mKeyGestureEventHandler);
+ List<Integer> supportedGestures = List.of(
+ KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_MAGNIFICATION,
+ KeyGestureEvent.KEY_GESTURE_TYPE_ACTIVATE_SELECT_TO_SPEAK);
+ mInputManager.registerKeyGestureEventHandler(supportedGestures,
+ mKeyGestureEventHandler);
}
if (com.android.settingslib.flags.Flags.hearingDevicesInputRoutingControl()) {
if (mHearingDeviceNotificationController != null) {
@@ -686,13 +698,13 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
}
@VisibleForTesting
- boolean handleKeyGestureEvent(KeyGestureEvent event) {
+ void handleKeyGestureEvent(KeyGestureEvent event) {
final boolean complete =
event.getAction() == KeyGestureEvent.ACTION_GESTURE_COMPLETE
&& !event.isCancelled();
final int gestureType = event.getKeyGestureType();
if (!complete) {
- return false;
+ return;
}
String targetName;
@@ -703,7 +715,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
case KeyGestureEvent.KEY_GESTURE_TYPE_ACTIVATE_SELECT_TO_SPEAK:
targetName = mContext.getString(R.string.config_defaultSelectToSpeakService);
if (targetName.isEmpty()) {
- return false;
+ return;
}
final ComponentName targetServiceComponent = TextUtils.isEmpty(targetName)
@@ -715,7 +727,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
userState.getInstalledServiceInfoLocked(targetServiceComponent);
}
if (accessibilityServiceInfo == null) {
- return false;
+ return;
}
// Skip enabling if a warning dialog is required for the feature.
@@ -725,11 +737,13 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
Slog.w(LOG_TAG,
"Accessibility warning is required before this service can be "
+ "activated automatically via KEY_GESTURE shortcut.");
- return false;
+ return;
}
break;
default:
- return false;
+ Slog.w(LOG_TAG, "Received a key gesture " + event
+ + " that was not registered by this handler");
+ return;
}
List<String> shortcutTargets = getAccessibilityShortcutTargets(
@@ -748,14 +762,12 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
// this will be a separate dialog that appears that requires the user to confirm
// which will resolve this race condition. For now, just require two presses the
// first time it is activated.
- return true;
+ return;
}
final int displayId = event.getDisplayId() != INVALID_DISPLAY
? event.getDisplayId() : getLastNonProxyTopFocusedDisplayId();
performAccessibilityShortcutInternal(displayId, KEY_GESTURE, targetName);
-
- return true;
}
@Override
@@ -1055,7 +1067,9 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
String action = intent.getAction();
if (Intent.ACTION_USER_SWITCHED.equals(action)) {
- switchUser(intent.getIntExtra(Intent.EXTRA_USER_HANDLE, 0));
+ if (!Flags.managerLifecycleUserChange()) {
+ switchUser(intent.getIntExtra(Intent.EXTRA_USER_HANDLE, 0));
+ }
} else if (Intent.ACTION_USER_UNLOCKED.equals(action)) {
unlockUser(intent.getIntExtra(Intent.EXTRA_USER_HANDLE, 0));
} else if (Intent.ACTION_USER_REMOVED.equals(action)) {
diff --git a/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java
index 0b9c45de6e40..60343e9e81e5 100644
--- a/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java
+++ b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java
@@ -152,9 +152,20 @@ public class AutoclickController extends BaseEventStreamTransformation {
if (direction == AutoclickScrollPanel.DIRECTION_EXIT) {
return;
}
- // For direction buttons, perform scroll action immediately.
- if (hovered && direction != AutoclickScrollPanel.DIRECTION_NONE) {
- handleScroll(direction);
+
+ // Handle all non-exit buttons when hovered.
+ if (hovered) {
+ // Clear the indicator.
+ if (mAutoclickIndicatorScheduler != null) {
+ mAutoclickIndicatorScheduler.cancel();
+ if (mAutoclickIndicatorView != null) {
+ mAutoclickIndicatorView.clearIndicator();
+ }
+ }
+ // Perform scroll action.
+ if (direction != DIRECTION_NONE) {
+ handleScroll(direction);
+ }
}
}
};
diff --git a/services/core/java/com/android/server/adb/AdbService.java b/services/core/java/com/android/server/adb/AdbService.java
index d12a0a2a1e00..c338a1ef15c9 100644
--- a/services/core/java/com/android/server/adb/AdbService.java
+++ b/services/core/java/com/android/server/adb/AdbService.java
@@ -32,6 +32,7 @@ import android.debug.IAdbTransport;
import android.debug.PairDevice;
import android.hardware.usb.UsbManager;
import android.net.Uri;
+import android.net.wifi.WifiManager;
import android.os.Binder;
import android.os.IBinder;
import android.os.ParcelFileDescriptor;
@@ -192,6 +193,7 @@ public class AdbService extends IAdbManager.Stub {
@Override
public void onChange(boolean selfChange, @NonNull Uri uri, @UserIdInt int userId) {
+ Slog.d("AdbSettingsObserver", "onChange " + uri.toString());
if (mAdbUsbUri.equals(uri)) {
boolean shouldEnable = (Settings.Global.getInt(mContentResolver,
Settings.Global.ADB_ENABLED, 0) > 0);
@@ -417,6 +419,28 @@ public class AdbService extends IAdbManager.Stub {
}
}
+ private WifiManager.MulticastLock mAdbMulticastLock = null;
+
+ private void acquireMulticastLock() {
+ if (mAdbMulticastLock == null) {
+ WifiManager wifiManager = (WifiManager)
+ mContext.getApplicationContext().getSystemService(Context.WIFI_SERVICE);
+ mAdbMulticastLock = wifiManager.createMulticastLock("AdbMulticastLock");
+ }
+
+ if (!mAdbMulticastLock.isHeld()) {
+ mAdbMulticastLock.acquire();
+ Slog.d(TAG, "Acquired multicast lock");
+ }
+ }
+
+ private void releaseMulticastLock() {
+ if (mAdbMulticastLock != null && mAdbMulticastLock.isHeld()) {
+ mAdbMulticastLock.release();
+ Slog.d(TAG, "Released multicast lock");
+ }
+ }
+
private void setAdbEnabled(boolean enable, byte transportType) {
Slog.d(TAG, "setAdbEnabled(" + enable + "), mIsAdbUsbEnabled=" + mIsAdbUsbEnabled
+ ", mIsAdbWifiEnabled=" + mIsAdbWifiEnabled + ", transportType=" + transportType);
@@ -428,9 +452,11 @@ public class AdbService extends IAdbManager.Stub {
if (mIsAdbWifiEnabled) {
// Start adb over WiFi.
SystemProperties.set(WIFI_PERSISTENT_CONFIG_PROPERTY, "1");
+ acquireMulticastLock();
} else {
// Stop adb over WiFi.
SystemProperties.set(WIFI_PERSISTENT_CONFIG_PROPERTY, "0");
+ releaseMulticastLock();
}
} else {
// No change
diff --git a/services/core/java/com/android/server/am/ProcessList.java b/services/core/java/com/android/server/am/ProcessList.java
index a61368c4bc36..ad47e67b9332 100644
--- a/services/core/java/com/android/server/am/ProcessList.java
+++ b/services/core/java/com/android/server/am/ProcessList.java
@@ -793,6 +793,7 @@ public final class ProcessList {
final ProcessMap<ProcessRecord> mDyingProcesses = new ProcessMap<>();
// Self locked with the inner lock within the RemoteCallbackList
+ @GuardedBy("mProcessObservers")
private final RemoteCallbackList<IProcessObserver> mProcessObservers =
new RemoteCallbackList<>();
@@ -4980,11 +4981,15 @@ public final class ProcessList {
}
void registerProcessObserver(IProcessObserver observer) {
- mProcessObservers.register(observer);
+ synchronized (mProcessObservers) {
+ mProcessObservers.register(observer);
+ }
}
void unregisterProcessObserver(IProcessObserver observer) {
- mProcessObservers.unregister(observer);
+ synchronized (mProcessObservers) {
+ mProcessObservers.unregister(observer);
+ }
}
void dispatchProcessesChanged() {
@@ -5002,38 +5007,41 @@ public final class ProcessList {
}
}
- int i = mProcessObservers.beginBroadcast();
- while (i > 0) {
- i--;
- final IProcessObserver observer = mProcessObservers.getBroadcastItem(i);
- if (observer != null) {
- try {
- for (int j = 0; j < numOfChanges; j++) {
- ProcessChangeItem item = mActiveProcessChanges[j];
- if ((item.changes & ProcessChangeItem.CHANGE_ACTIVITIES) != 0) {
- if (DEBUG_PROCESS_OBSERVERS) {
- Slog.i(TAG_PROCESS_OBSERVERS,
- "ACTIVITIES CHANGED pid=" + item.pid + " uid="
- + item.uid + ": " + item.foregroundActivities);
+ synchronized (mProcessObservers) {
+ int i = mProcessObservers.beginBroadcast();
+ while (i > 0) {
+ i--;
+ final IProcessObserver observer = mProcessObservers.getBroadcastItem(i);
+ if (observer != null) {
+ try {
+ for (int j = 0; j < numOfChanges; j++) {
+ ProcessChangeItem item = mActiveProcessChanges[j];
+ if ((item.changes & ProcessChangeItem.CHANGE_ACTIVITIES) != 0) {
+ if (DEBUG_PROCESS_OBSERVERS) {
+ Slog.i(TAG_PROCESS_OBSERVERS,
+ "ACTIVITIES CHANGED pid=" + item.pid + " uid="
+ + item.uid + ": " + item.foregroundActivities);
+ }
+ observer.onForegroundActivitiesChanged(item.pid, item.uid,
+ item.foregroundActivities);
}
- observer.onForegroundActivitiesChanged(item.pid, item.uid,
- item.foregroundActivities);
- }
- if ((item.changes & ProcessChangeItem.CHANGE_FOREGROUND_SERVICES) != 0) {
- if (DEBUG_PROCESS_OBSERVERS) {
- Slog.i(TAG_PROCESS_OBSERVERS,
- "FOREGROUND SERVICES CHANGED pid=" + item.pid + " uid="
- + item.uid + ": " + item.foregroundServiceTypes);
+ if ((item.changes & ProcessChangeItem.CHANGE_FOREGROUND_SERVICES)
+ != 0) {
+ if (DEBUG_PROCESS_OBSERVERS) {
+ Slog.i(TAG_PROCESS_OBSERVERS,
+ "FOREGROUND SERVICES CHANGED pid=" + item.pid + " uid="
+ + item.uid + ": " + item.foregroundServiceTypes);
+ }
+ observer.onForegroundServicesChanged(item.pid, item.uid,
+ item.foregroundServiceTypes);
}
- observer.onForegroundServicesChanged(item.pid, item.uid,
- item.foregroundServiceTypes);
}
+ } catch (RemoteException e) {
}
- } catch (RemoteException e) {
}
}
+ mProcessObservers.finishBroadcast();
}
- mProcessObservers.finishBroadcast();
synchronized (mProcessChangeLock) {
for (int j = 0; j < numOfChanges; j++) {
@@ -5122,22 +5130,42 @@ public final class ProcessList {
}
void dispatchProcessStarted(ProcessRecord app, int pid) {
- // TODO(b/323959187) Add the implementation.
+ if (!android.app.Flags.enableProcessObserverBroadcastOnProcessStarted()) {
+ Slog.i(TAG, "ProcessObserver broadcast disabled");
+ return;
+ }
+ synchronized (mProcessObservers) {
+ int i = mProcessObservers.beginBroadcast();
+ while (i > 0) {
+ i--;
+ final IProcessObserver observer = mProcessObservers.getBroadcastItem(i);
+ if (observer != null) {
+ try {
+ observer.onProcessStarted(pid, app.uid, app.info.uid,
+ app.info.packageName, app.processName);
+ } catch (RemoteException e) {
+ }
+ }
+ }
+ mProcessObservers.finishBroadcast();
+ }
}
void dispatchProcessDied(int pid, int uid) {
- int i = mProcessObservers.beginBroadcast();
- while (i > 0) {
- i--;
- final IProcessObserver observer = mProcessObservers.getBroadcastItem(i);
- if (observer != null) {
- try {
- observer.onProcessDied(pid, uid);
- } catch (RemoteException e) {
+ synchronized (mProcessObservers) {
+ int i = mProcessObservers.beginBroadcast();
+ while (i > 0) {
+ i--;
+ final IProcessObserver observer = mProcessObservers.getBroadcastItem(i);
+ if (observer != null) {
+ try {
+ observer.onProcessDied(pid, uid);
+ } catch (RemoteException e) {
+ }
}
}
+ mProcessObservers.finishBroadcast();
}
- mProcessObservers.finishBroadcast();
}
@GuardedBy(anyOf = {"mService", "mProcLock"})
diff --git a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
index cb4342f27bc8..c2ed4d557e69 100644
--- a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
+++ b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
@@ -256,6 +256,7 @@ public class SettingsToPropertiesMapper {
"pixel_vpn",
"pixel_watch",
"pixel_watch_debug_trace",
+ "pixel_watch_watchfaces",
"pixel_wifi",
"platform_compat",
"platform_security",
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index 6b3661a2a004..a8bb5231d8c0 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -186,6 +186,7 @@ import android.media.audiopolicy.AudioPolicyConfig;
import android.media.audiopolicy.AudioProductStrategy;
import android.media.audiopolicy.AudioVolumeGroup;
import android.media.audiopolicy.IAudioPolicyCallback;
+import android.media.audiopolicy.IAudioVolumeChangeDispatcher;
import android.media.permission.ClearCallingIdentityContext;
import android.media.permission.SafeCloseable;
import android.media.projection.IMediaProjection;
@@ -1388,6 +1389,7 @@ public class AudioService extends IAudioService.Stub
mUseVolumeGroupAliases = mContext.getResources().getBoolean(
com.android.internal.R.bool.config_handleVolumeAliasesUsingVolumeGroups);
+ mAudioVolumeChangeHandler = new AudioVolumeChangeHandler(mAudioSystem);
// Initialize volume
// Priority 1 - Android Property
// Priority 2 - Audio Policy Service
@@ -4452,6 +4454,27 @@ public class AudioService extends IAudioService.Stub
}
}
+ //================================
+ // Audio Volume Change Dispatcher
+ //================================
+ private final AudioVolumeChangeHandler mAudioVolumeChangeHandler;
+
+ /** @see AudioManager#registerVolumeGroupCallback(executor, callback) */
+ @android.annotation.EnforcePermission(
+ android.Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED)
+ public void registerAudioVolumeCallback(IAudioVolumeChangeDispatcher callback) {
+ super.registerAudioVolumeCallback_enforcePermission();
+ mAudioVolumeChangeHandler.registerListener(callback);
+ }
+
+ /** @see AudioManager#unregisterVolumeGroupCallback(callback) */
+ @android.annotation.EnforcePermission(
+ android.Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED)
+ public void unregisterAudioVolumeCallback(IAudioVolumeChangeDispatcher callback) {
+ super.unregisterAudioVolumeCallback_enforcePermission();
+ mAudioVolumeChangeHandler.unregisterListener(callback);
+ }
+
@Override
@android.annotation.EnforcePermission(anyOf = {
MODIFY_AUDIO_SETTINGS_PRIVILEGED, MODIFY_AUDIO_ROUTING })
diff --git a/services/core/java/com/android/server/audio/AudioSystemAdapter.java b/services/core/java/com/android/server/audio/AudioSystemAdapter.java
index a6267c156fb3..ced5faeeff27 100644
--- a/services/core/java/com/android/server/audio/AudioSystemAdapter.java
+++ b/services/core/java/com/android/server/audio/AudioSystemAdapter.java
@@ -23,6 +23,7 @@ import android.media.AudioDeviceAttributes;
import android.media.AudioMixerAttributes;
import android.media.AudioSystem;
import android.media.IDevicesForAttributesCallback;
+import android.media.INativeAudioVolumeGroupCallback;
import android.media.ISoundDose;
import android.media.ISoundDoseCallback;
import android.media.audiopolicy.AudioMix;
@@ -758,6 +759,29 @@ public class AudioSystemAdapter implements AudioSystem.RoutingUpdateCallback,
}
/**
+ * Same as {@link AudioSystem#registerAudioVolumeGroupCallback(INativeAudioVolumeGroupCallback)}
+ * @param callback to register
+ * @return {@link #SUCCESS} if successfully registered.
+ *
+ * @hide
+ */
+ public int registerAudioVolumeGroupCallback(INativeAudioVolumeGroupCallback callback) {
+ return AudioSystem.registerAudioVolumeGroupCallback(callback);
+ }
+
+ /**
+ * Same as
+ * {@link AudioSystem#unregisterAudioVolumeGroupCallback(INativeAudioVolumeGroupCallback)}.
+ * @param callback to register
+ * @return {@link #SUCCESS} if successfully registered.
+ *
+ * @hide
+ */
+ public int unregisterAudioVolumeGroupCallback(INativeAudioVolumeGroupCallback callback) {
+ return AudioSystem.unregisterAudioVolumeGroupCallback(callback);
+ }
+
+ /**
* Part of AudioService dump
* @param pw
*/
diff --git a/services/core/java/com/android/server/audio/AudioVolumeChangeHandler.java b/services/core/java/com/android/server/audio/AudioVolumeChangeHandler.java
new file mode 100644
index 000000000000..2bb4301bb8fd
--- /dev/null
+++ b/services/core/java/com/android/server/audio/AudioVolumeChangeHandler.java
@@ -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.
+ */
+
+package com.android.server.audio;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.media.INativeAudioVolumeGroupCallback;
+import android.media.audio.common.AudioVolumeGroupChangeEvent;
+import android.media.audiopolicy.IAudioVolumeChangeDispatcher;
+import android.os.RemoteCallbackList;
+import android.os.RemoteException;
+import android.util.Slog;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.util.Preconditions;
+
+/**
+ * The AudioVolumeChangeHandler handles AudioVolume callbacks invoked by native
+ * {@link INativeAudioVolumeGroupCallback} callback.
+ */
+/* private package */ class AudioVolumeChangeHandler {
+ private static final String TAG = "AudioVolumeChangeHandler";
+
+ private final Object mLock = new Object();
+ @GuardedBy("mLock")
+ private final RemoteCallbackList<IAudioVolumeChangeDispatcher> mListeners =
+ new RemoteCallbackList<>();
+ private final @NonNull AudioSystemAdapter mAudioSystem;
+ private @Nullable AudioVolumeGroupCallback mAudioVolumeGroupCallback;
+
+ AudioVolumeChangeHandler(@NonNull AudioSystemAdapter asa) {
+ mAudioSystem = asa;
+ }
+
+ @GuardedBy("mLock")
+ private void lazyInitLocked() {
+ mAudioVolumeGroupCallback = new AudioVolumeGroupCallback();
+ mAudioSystem.registerAudioVolumeGroupCallback(mAudioVolumeGroupCallback);
+ }
+
+ private void sendAudioVolumeGroupChangedToClients(int groupId, int index) {
+ RemoteCallbackList<IAudioVolumeChangeDispatcher> listeners;
+ int nbDispatchers;
+ synchronized (mLock) {
+ listeners = mListeners;
+ nbDispatchers = mListeners.beginBroadcast();
+ }
+ for (int i = 0; i < nbDispatchers; i++) {
+ try {
+ listeners.getBroadcastItem(i).onAudioVolumeGroupChanged(groupId, index);
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Failed to broadcast Volume Changed event");
+ }
+ }
+ synchronized (mLock) {
+ mListeners.finishBroadcast();
+ }
+ }
+
+ /**
+ * @param cb the {@link IAudioVolumeChangeDispatcher} to register
+ */
+ public void registerListener(@NonNull IAudioVolumeChangeDispatcher cb) {
+ Preconditions.checkNotNull(cb, "Volume group callback must not be null");
+ synchronized (mLock) {
+ if (mAudioVolumeGroupCallback == null) {
+ lazyInitLocked();
+ }
+ mListeners.register(cb);
+ }
+ }
+
+ /**
+ * @param cb the {@link IAudioVolumeChangeDispatcher} to unregister
+ */
+ public void unregisterListener(@NonNull IAudioVolumeChangeDispatcher cb) {
+ Preconditions.checkNotNull(cb, "Volume group callback must not be null");
+ synchronized (mLock) {
+ mListeners.unregister(cb);
+ }
+ }
+
+ private final class AudioVolumeGroupCallback extends INativeAudioVolumeGroupCallback.Stub {
+ public void onAudioVolumeGroupChanged(AudioVolumeGroupChangeEvent volumeEvent) {
+ Slog.v(TAG, "onAudioVolumeGroupChanged volumeEvent=" + volumeEvent);
+ sendAudioVolumeGroupChangedToClients(volumeEvent.groupId, volumeEvent.flags);
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java
index a28069bbf050..95e58e1a7300 100644
--- a/services/core/java/com/android/server/display/DisplayManagerService.java
+++ b/services/core/java/com/android/server/display/DisplayManagerService.java
@@ -684,8 +684,9 @@ public final class DisplayManagerService extends SystemService {
final var backupManager = new BackupManager(mContext);
Consumer<Pair<DisplayTopology, DisplayTopologyGraph>> topologyChangedCallback =
update -> {
- if (mInputManagerInternal != null) {
- mInputManagerInternal.setDisplayTopology(update.second);
+ DisplayTopologyGraph graph = update.second;
+ if (mInputManagerInternal != null && graph != null) {
+ mInputManagerInternal.setDisplayTopology(graph);
}
deliverTopologyUpdate(update.first);
};
@@ -3647,7 +3648,7 @@ public final class DisplayManagerService extends SystemService {
private void deliverTopologyUpdate(DisplayTopology topology) {
if (DEBUG) {
- Slog.d(TAG, "Delivering topology update");
+ Slog.d(TAG, "Delivering topology update: " + topology);
}
if (Trace.isTagEnabled(Trace.TRACE_TAG_POWER)) {
Trace.instant(Trace.TRACE_TAG_POWER, "deliverTopologyUpdate");
@@ -4209,13 +4210,18 @@ public final class DisplayManagerService extends SystemService {
public boolean mWifiDisplayScanRequested;
- // A single pending event.
+ // A single pending display event.
private record Event(int displayId, @DisplayEvent int event) { };
- // The list of pending events. This is null until there is a pending event to be saved.
- // This is only used if {@link deferDisplayEventsWhenFrozen()} is true.
+ // The list of pending display events. This is null until there is a pending event to be
+ // saved. This is only used if {@link deferDisplayEventsWhenFrozen()} is true.
+ @GuardedBy("mCallback")
+ @Nullable
+ private ArrayList<Event> mPendingDisplayEvents;
+
@GuardedBy("mCallback")
- private ArrayList<Event> mPendingEvents;
+ @Nullable
+ private DisplayTopology mPendingTopology;
// Process states: a process is ready to receive events if it is neither cached nor
// frozen.
@@ -4285,7 +4291,10 @@ public final class DisplayManagerService extends SystemService {
*/
@GuardedBy("mCallback")
private boolean hasPendingAndIsReadyLocked() {
- return isReadyLocked() && mPendingEvents != null && !mPendingEvents.isEmpty() && mAlive;
+ boolean pendingDisplayEvents = mPendingDisplayEvents != null
+ && !mPendingDisplayEvents.isEmpty();
+ boolean pendingTopology = mPendingTopology != null;
+ return isReadyLocked() && (pendingDisplayEvents || pendingTopology) && mAlive;
}
/**
@@ -4366,7 +4375,8 @@ public final class DisplayManagerService extends SystemService {
// occurs as the client is transitioning to ready but pending events have not
// been dispatched. The new event must be added to the pending list to
// preserve event ordering.
- if (!isReadyLocked() || (mPendingEvents != null && !mPendingEvents.isEmpty())) {
+ if (!isReadyLocked() || (mPendingDisplayEvents != null
+ && !mPendingDisplayEvents.isEmpty())) {
// The client is interested in the event but is not ready to receive it.
// Put the event on the pending list.
addDisplayEvent(displayId, event);
@@ -4453,13 +4463,13 @@ public final class DisplayManagerService extends SystemService {
// This is only used if {@link deferDisplayEventsWhenFrozen()} is true.
@GuardedBy("mCallback")
private void addDisplayEvent(int displayId, int event) {
- if (mPendingEvents == null) {
- mPendingEvents = new ArrayList<>();
+ if (mPendingDisplayEvents == null) {
+ mPendingDisplayEvents = new ArrayList<>();
}
- if (!mPendingEvents.isEmpty()) {
+ if (!mPendingDisplayEvents.isEmpty()) {
// Ignore redundant events. Further optimization is possible by merging adjacent
// events.
- Event last = mPendingEvents.get(mPendingEvents.size() - 1);
+ Event last = mPendingDisplayEvents.get(mPendingDisplayEvents.size() - 1);
if (last.displayId == displayId && last.event == event) {
if (DEBUG) {
Slog.d(TAG, "Ignore redundant display event " + displayId + "/" + event
@@ -4468,12 +4478,13 @@ public final class DisplayManagerService extends SystemService {
return;
}
}
- mPendingEvents.add(new Event(displayId, event));
+ mPendingDisplayEvents.add(new Event(displayId, event));
}
/**
* @return {@code false} if RemoteException happens; otherwise {@code true} for
- * success.
+ * success. This returns true even if the update was deferred because the remote client is
+ * cached or frozen.
*/
boolean notifyTopologyUpdateAsync(DisplayTopology topology) {
if ((mInternalEventFlagsMask.get()
@@ -4490,6 +4501,18 @@ public final class DisplayManagerService extends SystemService {
// The client is not interested in this event, so do nothing.
return true;
}
+
+ if (deferDisplayEventsWhenFrozen()) {
+ synchronized (mCallback) {
+ // Save the new update if the client frozen or cached (not ready).
+ if (!isReadyLocked()) {
+ // The client is interested in the update but is not ready to receive it.
+ mPendingTopology = topology;
+ return true;
+ }
+ }
+ }
+
return transmitTopologyUpdate(topology);
}
@@ -4514,37 +4537,54 @@ public final class DisplayManagerService extends SystemService {
// would be unusual to do so. The method returns true on success.
// This is only used if {@link deferDisplayEventsWhenFrozen()} is true.
public boolean dispatchPending() {
- Event[] pending;
+ Event[] pendingDisplayEvents = null;
+ DisplayTopology pendingTopology;
synchronized (mCallback) {
- if (mPendingEvents == null || mPendingEvents.isEmpty() || !mAlive) {
+ if (!mAlive) {
return true;
}
if (!isReadyLocked()) {
return false;
}
- pending = new Event[mPendingEvents.size()];
- pending = mPendingEvents.toArray(pending);
- mPendingEvents.clear();
+
+ if (mPendingDisplayEvents != null && !mPendingDisplayEvents.isEmpty()) {
+ pendingDisplayEvents = new Event[mPendingDisplayEvents.size()];
+ pendingDisplayEvents = mPendingDisplayEvents.toArray(pendingDisplayEvents);
+ mPendingDisplayEvents.clear();
+ }
+
+ pendingTopology = mPendingTopology;
+ mPendingTopology = null;
}
try {
- for (int i = 0; i < pending.length; i++) {
- Event displayEvent = pending[i];
- if (DEBUG) {
- Slog.d(TAG, "Send pending display event #" + i + " "
- + displayEvent.displayId + "/"
- + displayEvent.event + " to " + mUid + "/" + mPid);
- }
+ if (pendingDisplayEvents != null) {
+ for (int i = 0; i < pendingDisplayEvents.length; i++) {
+ Event displayEvent = pendingDisplayEvents[i];
+ if (DEBUG) {
+ Slog.d(TAG, "Send pending display event #" + i + " "
+ + displayEvent.displayId + "/"
+ + displayEvent.event + " to " + mUid + "/" + mPid);
+ }
+
+ if (!shouldReceiveRefreshRateWithChangeUpdate(displayEvent.event)) {
+ continue;
+ }
- if (!shouldReceiveRefreshRateWithChangeUpdate(displayEvent.event)) {
- continue;
+ transmitDisplayEvent(displayEvent.displayId, displayEvent.event);
}
+ }
- transmitDisplayEvent(displayEvent.displayId, displayEvent.event);
+ if (pendingTopology != null) {
+ if (DEBUG) {
+ Slog.d(TAG, "Send pending topology: " + pendingTopology
+ + " to " + mUid + "/" + mPid);
+ }
+ mCallback.onTopologyChanged(pendingTopology);
}
+
return true;
} catch (RemoteException ex) {
- Slog.w(TAG, "Failed to notify process "
- + mPid + " that display topology changed, assuming it died.", ex);
+ Slog.w(TAG, "Failed to notify process " + mPid + ", assuming it died.", ex);
binderDied();
return false;
@@ -4556,11 +4596,12 @@ public final class DisplayManagerService extends SystemService {
if (deferDisplayEventsWhenFrozen()) {
final String fmt =
"mPid=%d mUid=%d mWifiDisplayScanRequested=%s"
- + " cached=%s frozen=%s pending=%d";
+ + " cached=%s frozen=%s pendingDisplayEvents=%d pendingTopology=%b";
synchronized (mCallback) {
return formatSimple(fmt,
mPid, mUid, mWifiDisplayScanRequested, mCached, mFrozen,
- (mPendingEvents == null) ? 0 : mPendingEvents.size());
+ (mPendingDisplayEvents == null) ? 0 : mPendingDisplayEvents.size(),
+ mPendingTopology != null);
}
} else {
final String fmt =
diff --git a/services/core/java/com/android/server/display/mode/DisplayModeDirector.java b/services/core/java/com/android/server/display/mode/DisplayModeDirector.java
index c37733b05fba..2c90e1919123 100644
--- a/services/core/java/com/android/server/display/mode/DisplayModeDirector.java
+++ b/services/core/java/com/android/server/display/mode/DisplayModeDirector.java
@@ -156,6 +156,8 @@ public class DisplayModeDirector {
private SparseArray<Display.Mode> mDefaultModeByDisplay;
// a map from display id to display device config
private SparseArray<DisplayDeviceConfig> mDisplayDeviceConfigByDisplay = new SparseArray<>();
+ // set containing connected external display ids
+ private final Set<Integer> mExternalDisplaysConnected = new HashSet<>();
private SparseBooleanArray mHasArrSupport;
@@ -425,7 +427,7 @@ public class DisplayModeDirector {
// Some external displays physical refresh rate modes are slightly above 60hz.
// SurfaceFlinger will not enable these display modes unless it is configured to allow
// render rate at least at this frame rate.
- if (mDisplayObserver.isExternalDisplayLocked(displayId)) {
+ if (isExternalDisplayLocked(displayId)) {
primarySummary.maxRenderFrameRate = Math.max(baseMode.getRefreshRate(),
primarySummary.maxRenderFrameRate);
appRequestSummary.maxRenderFrameRate = Math.max(baseMode.getRefreshRate(),
@@ -653,6 +655,10 @@ public class DisplayModeDirector {
}
}
+ boolean isExternalDisplayLocked(int displayId) {
+ return mExternalDisplaysConnected.contains(displayId);
+ }
+
private static String switchingTypeToString(@DisplayManager.SwitchingType int type) {
switch (type) {
case DisplayManager.SWITCHING_TYPE_NONE:
@@ -694,6 +700,11 @@ public class DisplayModeDirector {
}
@VisibleForTesting
+ void addExternalDisplayId(int externalDisplayId) {
+ mExternalDisplaysConnected.add(externalDisplayId);
+ }
+
+ @VisibleForTesting
void injectBrightnessObserver(BrightnessObserver brightnessObserver) {
mBrightnessObserver = brightnessObserver;
}
@@ -1210,7 +1221,7 @@ public class DisplayModeDirector {
@GuardedBy("mLock")
private void updateRefreshRateSettingLocked(float minRefreshRate, float peakRefreshRate,
float defaultRefreshRate, int displayId) {
- if (mDisplayObserver.isExternalDisplayLocked(displayId)) {
+ if (isExternalDisplayLocked(displayId)) {
if (mLoggingEnabled) {
Slog.d(TAG, "skip updateRefreshRateSettingLocked for external display "
+ displayId);
@@ -1309,20 +1320,25 @@ public class DisplayModeDirector {
public void setAppRequest(int displayId, int modeId, float requestedRefreshRate,
float requestedMinRefreshRateRange, float requestedMaxRefreshRateRange) {
Display.Mode requestedMode;
+ boolean isExternalDisplay;
synchronized (mLock) {
requestedMode = findModeLocked(displayId, modeId, requestedRefreshRate);
+ isExternalDisplay = isExternalDisplayLocked(displayId);
}
Vote frameRateVote = getFrameRateVote(
requestedMinRefreshRateRange, requestedMaxRefreshRateRange);
Vote baseModeRefreshRateVote = getBaseModeVote(requestedMode, requestedRefreshRate);
- Vote sizeVote = getSizeVote(requestedMode);
mVotesStorage.updateVote(displayId, Vote.PRIORITY_APP_REQUEST_RENDER_FRAME_RATE_RANGE,
frameRateVote);
mVotesStorage.updateVote(displayId, Vote.PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE,
baseModeRefreshRateVote);
- mVotesStorage.updateVote(displayId, Vote.PRIORITY_APP_REQUEST_SIZE, sizeVote);
+
+ if (!isExternalDisplay) {
+ Vote sizeVote = getSizeVote(requestedMode);
+ mVotesStorage.updateVote(displayId, Vote.PRIORITY_APP_REQUEST_SIZE, sizeVote);
+ }
}
private Display.Mode findModeLocked(int displayId, int modeId, float requestedRefreshRate) {
@@ -1420,7 +1436,6 @@ public class DisplayModeDirector {
private int mExternalDisplayPeakHeight;
private int mExternalDisplayPeakRefreshRate;
private final boolean mRefreshRateSynchronizationEnabled;
- private final Set<Integer> mExternalDisplaysConnected = new HashSet<>();
DisplayObserver(Context context, Handler handler, VotesStorage votesStorage,
Injector injector) {
@@ -1541,10 +1556,6 @@ public class DisplayModeDirector {
}
}
- boolean isExternalDisplayLocked(int displayId) {
- return mExternalDisplaysConnected.contains(displayId);
- }
-
@Nullable
private DisplayInfo getDisplayInfo(int displayId) {
DisplayInfo info = new DisplayInfo();
diff --git a/services/core/java/com/android/server/display/mode/ModeChangeObserver.java b/services/core/java/com/android/server/display/mode/ModeChangeObserver.java
index 50782a2f22c8..debfb067b710 100644
--- a/services/core/java/com/android/server/display/mode/ModeChangeObserver.java
+++ b/services/core/java/com/android/server/display/mode/ModeChangeObserver.java
@@ -25,6 +25,8 @@ import android.view.Display;
import android.view.DisplayAddress;
import android.view.DisplayEventReceiver;
+import androidx.annotation.VisibleForTesting;
+
import java.util.HashSet;
import java.util.Set;
@@ -35,8 +37,10 @@ final class ModeChangeObserver {
private final DisplayModeDirector.Injector mInjector;
@SuppressWarnings("unused")
- private DisplayEventReceiver mModeChangeListener;
- private DisplayManager.DisplayListener mDisplayListener;
+ @VisibleForTesting
+ DisplayEventReceiver mModeChangeListener;
+ @VisibleForTesting
+ DisplayManager.DisplayListener mDisplayListener;
private final LongSparseArray<Set<Integer>> mRejectedModesMap =
new LongSparseArray<>();
private final LongSparseArray<Integer> mPhysicalIdToLogicalIdMap = new LongSparseArray<>();
diff --git a/services/core/java/com/android/server/hdmi/HdmiControlService.java b/services/core/java/com/android/server/hdmi/HdmiControlService.java
index 41b0b4dc716a..a2d065400045 100644
--- a/services/core/java/com/android/server/hdmi/HdmiControlService.java
+++ b/services/core/java/com/android/server/hdmi/HdmiControlService.java
@@ -252,17 +252,22 @@ public class HdmiControlService extends SystemService {
static final AudioDeviceAttributes AUDIO_OUTPUT_DEVICE_HDMI_EARC =
new AudioDeviceAttributes(AudioDeviceAttributes.ROLE_OUTPUT,
AudioDeviceInfo.TYPE_HDMI_EARC, "");
+ static final AudioDeviceAttributes AUDIO_OUTPUT_DEVICE_LINE_DIGITAL =
+ new AudioDeviceAttributes(AudioDeviceAttributes.ROLE_OUTPUT,
+ AudioDeviceInfo.TYPE_LINE_DIGITAL, "");
// Audio output devices used for absolute volume behavior
private static final List<AudioDeviceAttributes> AVB_AUDIO_OUTPUT_DEVICES =
List.of(AUDIO_OUTPUT_DEVICE_HDMI,
AUDIO_OUTPUT_DEVICE_HDMI_ARC,
- AUDIO_OUTPUT_DEVICE_HDMI_EARC);
+ AUDIO_OUTPUT_DEVICE_HDMI_EARC,
+ AUDIO_OUTPUT_DEVICE_LINE_DIGITAL);
// Audio output devices used for absolute volume behavior on TV panels
private static final List<AudioDeviceAttributes> TV_AVB_AUDIO_OUTPUT_DEVICES =
List.of(AUDIO_OUTPUT_DEVICE_HDMI_ARC,
- AUDIO_OUTPUT_DEVICE_HDMI_EARC);
+ AUDIO_OUTPUT_DEVICE_HDMI_EARC,
+ AUDIO_OUTPUT_DEVICE_LINE_DIGITAL);
// Audio output devices used for absolute volume behavior on Playback devices
private static final List<AudioDeviceAttributes> PLAYBACK_AVB_AUDIO_OUTPUT_DEVICES =
diff --git a/services/core/java/com/android/server/input/AppLaunchShortcutManager.java b/services/core/java/com/android/server/input/AppLaunchShortcutManager.java
index 8c028bc92841..eb102294ac32 100644
--- a/services/core/java/com/android/server/input/AppLaunchShortcutManager.java
+++ b/services/core/java/com/android/server/input/AppLaunchShortcutManager.java
@@ -111,7 +111,7 @@ final class AppLaunchShortcutManager {
mContext = context;
}
- public void systemRunning() {
+ public void init() {
loadShortcuts();
}
diff --git a/services/core/java/com/android/server/input/InputGestureManager.java b/services/core/java/com/android/server/input/InputGestureManager.java
index 67e1ccc6a850..e6d71900f106 100644
--- a/services/core/java/com/android/server/input/InputGestureManager.java
+++ b/services/core/java/com/android/server/input/InputGestureManager.java
@@ -94,9 +94,9 @@ final class InputGestureManager {
mContext = context;
}
- public void systemRunning() {
+ public void init(List<InputGestureData> bookmarks) {
initSystemShortcuts();
- blockListBookmarkedTriggers();
+ blockListBookmarkedTriggers(bookmarks);
}
private void initSystemShortcuts() {
@@ -263,10 +263,9 @@ final class InputGestureManager {
}
}
- private void blockListBookmarkedTriggers() {
+ private void blockListBookmarkedTriggers(List<InputGestureData> bookmarks) {
synchronized (mGestureLock) {
- InputManager im = Objects.requireNonNull(mContext.getSystemService(InputManager.class));
- for (InputGestureData bookmark : im.getAppLaunchBookmarks()) {
+ for (InputGestureData bookmark : bookmarks) {
mBlockListedTriggers.add(bookmark.getTrigger());
}
}
diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java
index 6e6d00d62819..29e04e744759 100644
--- a/services/core/java/com/android/server/input/InputManagerService.java
+++ b/services/core/java/com/android/server/input/InputManagerService.java
@@ -2751,18 +2751,23 @@ public class InputManagerService extends IInputManager.Stub
@SuppressLint("MissingPermission")
private void initKeyGestures() {
InputManager im = Objects.requireNonNull(mContext.getSystemService(InputManager.class));
- im.registerKeyGestureEventHandler(new InputManager.KeyGestureEventHandler() {
- @Override
- public boolean handleKeyGestureEvent(@NonNull KeyGestureEvent event,
- @Nullable IBinder focussedToken) {
- return InputManagerService.this.handleKeyGestureEvent(event);
- }
- });
+ List<Integer> supportedGestures = List.of(
+ KeyGestureEvent.KEY_GESTURE_TYPE_KEYBOARD_BACKLIGHT_UP,
+ KeyGestureEvent.KEY_GESTURE_TYPE_KEYBOARD_BACKLIGHT_DOWN,
+ KeyGestureEvent.KEY_GESTURE_TYPE_KEYBOARD_BACKLIGHT_TOGGLE,
+ KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_CAPS_LOCK,
+ KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_BOUNCE_KEYS,
+ KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_MOUSE_KEYS,
+ KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_STICKY_KEYS,
+ KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_SLOW_KEYS
+ );
+ im.registerKeyGestureEventHandler(supportedGestures,
+ (event, focusedToken) -> InputManagerService.this.handleKeyGestureEvent(event));
}
@SuppressLint("MissingPermission")
@VisibleForTesting
- boolean handleKeyGestureEvent(@NonNull KeyGestureEvent event) {
+ void handleKeyGestureEvent(@NonNull KeyGestureEvent event) {
int deviceId = event.getDeviceId();
boolean complete = event.getAction() == KeyGestureEvent.ACTION_GESTURE_COMPLETE
&& !event.isCancelled();
@@ -2771,20 +2776,20 @@ public class InputManagerService extends IInputManager.Stub
if (complete) {
mKeyboardBacklightController.incrementKeyboardBacklight(deviceId);
}
- return true;
+ break;
case KeyGestureEvent.KEY_GESTURE_TYPE_KEYBOARD_BACKLIGHT_DOWN:
if (complete) {
mKeyboardBacklightController.decrementKeyboardBacklight(deviceId);
}
- return true;
+ break;
case KeyGestureEvent.KEY_GESTURE_TYPE_KEYBOARD_BACKLIGHT_TOGGLE:
// TODO(b/367748270): Add functionality to turn keyboard backlight on/off.
- return true;
+ break;
case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_CAPS_LOCK:
if (complete) {
mNative.toggleCapsLock(deviceId);
}
- return true;
+ break;
case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_BOUNCE_KEYS:
if (complete) {
final boolean bounceKeysEnabled =
@@ -2792,7 +2797,6 @@ public class InputManagerService extends IInputManager.Stub
InputSettings.setAccessibilityBounceKeysThreshold(mContext,
bounceKeysEnabled ? 0
: InputSettings.DEFAULT_BOUNCE_KEYS_THRESHOLD_MILLIS);
- return true;
}
break;
case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_MOUSE_KEYS:
@@ -2800,7 +2804,6 @@ public class InputManagerService extends IInputManager.Stub
final boolean mouseKeysEnabled = InputSettings.isAccessibilityMouseKeysEnabled(
mContext);
InputSettings.setAccessibilityMouseKeysEnabled(mContext, !mouseKeysEnabled);
- return true;
}
break;
case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_STICKY_KEYS:
@@ -2808,7 +2811,6 @@ public class InputManagerService extends IInputManager.Stub
final boolean stickyKeysEnabled =
InputSettings.isAccessibilityStickyKeysEnabled(mContext);
InputSettings.setAccessibilityStickyKeysEnabled(mContext, !stickyKeysEnabled);
- return true;
}
break;
case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_SLOW_KEYS:
@@ -2817,14 +2819,13 @@ public class InputManagerService extends IInputManager.Stub
InputSettings.isAccessibilitySlowKeysEnabled(mContext);
InputSettings.setAccessibilitySlowKeysThreshold(mContext,
slowKeysEnabled ? 0 : InputSettings.DEFAULT_SLOW_KEYS_THRESHOLD_MILLIS);
- return true;
}
break;
default:
- return false;
-
+ Log.w(TAG, "Received a key gesture " + event
+ + " that was not registered by this handler");
+ break;
}
- return false;
}
// Native callback.
@@ -3147,11 +3148,14 @@ public class InputManagerService extends IInputManager.Stub
@Override
@PermissionManuallyEnforced
- public void registerKeyGestureHandler(@NonNull IKeyGestureHandler handler) {
+ public void registerKeyGestureHandler(int[] keyGesturesToHandle,
+ @NonNull IKeyGestureHandler handler) {
enforceManageKeyGesturePermission();
Objects.requireNonNull(handler);
- mKeyGestureController.registerKeyGestureHandler(handler, Binder.getCallingPid());
+ Objects.requireNonNull(keyGesturesToHandle);
+ mKeyGestureController.registerKeyGestureHandler(keyGesturesToHandle, handler,
+ Binder.getCallingPid());
}
@Override
diff --git a/services/core/java/com/android/server/input/KeyGestureController.java b/services/core/java/com/android/server/input/KeyGestureController.java
index 395c77322c04..5de432e5849b 100644
--- a/services/core/java/com/android/server/input/KeyGestureController.java
+++ b/services/core/java/com/android/server/input/KeyGestureController.java
@@ -58,6 +58,7 @@ import android.util.IndentingPrintWriter;
import android.util.Log;
import android.util.Slog;
import android.util.SparseArray;
+import android.util.SparseIntArray;
import android.view.Display;
import android.view.InputDevice;
import android.view.KeyCharacterMap;
@@ -79,11 +80,11 @@ import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayDeque;
+import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
-import java.util.TreeMap;
/**
* A thread-safe component of {@link InputManagerService} responsible for managing callbacks when a
@@ -166,11 +167,14 @@ final class KeyGestureController {
private final SparseArray<KeyGestureEventListenerRecord>
mKeyGestureEventListenerRecords = new SparseArray<>();
- // List of currently registered key gesture event handler keyed by process pid. The map sorts
- // in the order of preference of the handlers, and we prioritize handlers in system server
- // over external handlers..
+ // Map of currently registered key gesture event handlers keyed by pid.
@GuardedBy("mKeyGestureHandlerRecords")
- private final TreeMap<Integer, KeyGestureHandlerRecord> mKeyGestureHandlerRecords;
+ private final SparseArray<KeyGestureHandlerRecord> mKeyGestureHandlerRecords =
+ new SparseArray<>();
+
+ // Currently supported key gestures mapped to pid that registered the corresponding handler.
+ @GuardedBy("mKeyGestureHandlerRecords")
+ private final SparseIntArray mSupportedKeyGestureToPidMap = new SparseIntArray();
private final ArrayDeque<KeyGestureEvent> mLastHandledEvents = new ArrayDeque<>();
@@ -193,18 +197,6 @@ final class KeyGestureController {
mHandler = new Handler(looper, this::handleMessage);
mIoHandler = new Handler(ioLooper, this::handleIoMessage);
mSystemPid = Process.myPid();
- mKeyGestureHandlerRecords = new TreeMap<>((p1, p2) -> {
- if (Objects.equals(p1, p2)) {
- return 0;
- }
- if (p1 == mSystemPid) {
- return -1;
- } else if (p2 == mSystemPid) {
- return 1;
- } else {
- return Integer.compare(p1, p2);
- }
- });
mKeyCombinationManager = new KeyCombinationManager(mHandler);
mSettingsObserver = new SettingsObserver(mHandler);
mAppLaunchShortcutManager = new AppLaunchShortcutManager(mContext);
@@ -450,8 +442,8 @@ final class KeyGestureController {
public void systemRunning() {
mSettingsObserver.observe();
- mAppLaunchShortcutManager.systemRunning();
- mInputGestureManager.systemRunning();
+ mAppLaunchShortcutManager.init();
+ mInputGestureManager.init(mAppLaunchShortcutManager.getBookmarks());
initKeyGestures();
int userId;
@@ -465,22 +457,24 @@ final class KeyGestureController {
@SuppressLint("MissingPermission")
private void initKeyGestures() {
InputManager im = Objects.requireNonNull(mContext.getSystemService(InputManager.class));
- im.registerKeyGestureEventHandler((event, focusedToken) -> {
- switch (event.getKeyGestureType()) {
- case KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD:
- if (event.getAction() == KeyGestureEvent.ACTION_GESTURE_START) {
- mHandler.removeMessages(MSG_ACCESSIBILITY_SHORTCUT);
- mHandler.sendMessageDelayed(
- mHandler.obtainMessage(MSG_ACCESSIBILITY_SHORTCUT),
- getAccessibilityShortcutTimeout());
+ im.registerKeyGestureEventHandler(
+ List.of(KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD),
+ (event, focusedToken) -> {
+ if (event.getKeyGestureType()
+ == KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD) {
+ if (event.getAction() == KeyGestureEvent.ACTION_GESTURE_START) {
+ mHandler.removeMessages(MSG_ACCESSIBILITY_SHORTCUT);
+ mHandler.sendMessageDelayed(
+ mHandler.obtainMessage(MSG_ACCESSIBILITY_SHORTCUT),
+ getAccessibilityShortcutTimeout());
+ } else {
+ mHandler.removeMessages(MSG_ACCESSIBILITY_SHORTCUT);
+ }
} else {
- mHandler.removeMessages(MSG_ACCESSIBILITY_SHORTCUT);
+ Log.w(TAG, "Received a key gesture " + event
+ + " that was not registered by this handler");
}
- return true;
- default:
- return false;
- }
- });
+ });
}
public boolean interceptKeyBeforeQueueing(KeyEvent event, int policyFlags) {
@@ -590,10 +584,11 @@ final class KeyGestureController {
return true;
}
if (result.appLaunchData() != null) {
- return handleKeyGesture(deviceId, new int[]{keyCode}, metaState,
+ handleKeyGesture(deviceId, new int[]{keyCode}, metaState,
KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_APPLICATION,
- KeyGestureEvent.ACTION_GESTURE_COMPLETE, displayId,
- focusedToken, /* flags = */0, result.appLaunchData());
+ KeyGestureEvent.ACTION_GESTURE_COMPLETE, displayId, focusedToken, /* flags = */
+ 0, result.appLaunchData());
+ return true;
}
// Handle system shortcuts
@@ -601,11 +596,11 @@ final class KeyGestureController {
InputGestureData systemShortcut = mInputGestureManager.getSystemShortcutForKeyEvent(
event);
if (systemShortcut != null) {
- return handleKeyGesture(deviceId, new int[]{keyCode}, metaState,
+ handleKeyGesture(deviceId, new int[]{keyCode}, metaState,
systemShortcut.getAction().keyGestureType(),
- KeyGestureEvent.ACTION_GESTURE_COMPLETE,
- displayId, focusedToken, /* flags = */0,
- systemShortcut.getAction().appLaunchData());
+ KeyGestureEvent.ACTION_GESTURE_COMPLETE, displayId,
+ focusedToken, /* flags = */0, systemShortcut.getAction().appLaunchData());
+ return true;
}
}
@@ -687,11 +682,11 @@ final class KeyGestureController {
return true;
case KeyEvent.KEYCODE_SEARCH:
if (firstDown && mSearchKeyBehavior == SEARCH_KEY_BEHAVIOR_TARGET_ACTIVITY) {
- return handleKeyGesture(deviceId, new int[]{keyCode}, /* modifierState = */0,
+ handleKeyGesture(deviceId, new int[]{keyCode}, /* modifierState = */0,
KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_SEARCH,
KeyGestureEvent.ACTION_GESTURE_COMPLETE, displayId,
focusedToken, /* flags = */0, /* appLaunchData = */null);
-
+ return true;
}
break;
case KeyEvent.KEYCODE_SETTINGS:
@@ -782,11 +777,12 @@ final class KeyGestureController {
if (KeyEvent.metaStateHasModifiers(
shiftlessModifiers, KeyEvent.META_ALT_ON)) {
mPendingHideRecentSwitcher = true;
- return handleKeyGesture(deviceId, new int[]{keyCode},
+ handleKeyGesture(deviceId, new int[]{keyCode},
KeyEvent.META_ALT_ON,
KeyGestureEvent.KEY_GESTURE_TYPE_RECENT_APPS_SWITCHER,
KeyGestureEvent.ACTION_GESTURE_START, displayId,
focusedToken, /* flags = */0, /* appLaunchData = */null);
+ return true;
}
}
}
@@ -803,21 +799,23 @@ final class KeyGestureController {
} else {
if (mPendingHideRecentSwitcher) {
mPendingHideRecentSwitcher = false;
- return handleKeyGesture(deviceId, new int[]{KeyEvent.KEYCODE_TAB},
+ handleKeyGesture(deviceId, new int[]{KeyEvent.KEYCODE_TAB},
KeyEvent.META_ALT_ON,
KeyGestureEvent.KEY_GESTURE_TYPE_RECENT_APPS_SWITCHER,
KeyGestureEvent.ACTION_GESTURE_COMPLETE, displayId,
focusedToken, /* flags = */0, /* appLaunchData = */null);
+ return true;
}
// Toggle Caps Lock on META-ALT.
if (mPendingCapsLockToggle) {
mPendingCapsLockToggle = false;
- return handleKeyGesture(deviceId, new int[]{KeyEvent.KEYCODE_META_LEFT,
+ handleKeyGesture(deviceId, new int[]{KeyEvent.KEYCODE_META_LEFT,
KeyEvent.KEYCODE_ALT_LEFT}, /* modifierState = */0,
KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_CAPS_LOCK,
KeyGestureEvent.ACTION_GESTURE_COMPLETE, displayId,
focusedToken, /* flags = */0, /* appLaunchData = */null);
+ return true;
}
}
break;
@@ -885,11 +883,11 @@ final class KeyGestureController {
if (customGesture == null) {
return false;
}
- return handleKeyGesture(deviceId, new int[]{keyCode}, metaState,
+ handleKeyGesture(deviceId, new int[]{keyCode}, metaState,
customGesture.getAction().keyGestureType(),
- KeyGestureEvent.ACTION_GESTURE_COMPLETE,
- displayId, focusedToken, /* flags = */0,
- customGesture.getAction().appLaunchData());
+ KeyGestureEvent.ACTION_GESTURE_COMPLETE, displayId, focusedToken,
+ /* flags = */0, customGesture.getAction().appLaunchData());
+ return true;
}
return false;
}
@@ -908,7 +906,7 @@ final class KeyGestureController {
// Handle keyboard layout switching. (CTRL + SPACE)
if (KeyEvent.metaStateHasModifiers(metaState & ~KeyEvent.META_SHIFT_MASK,
KeyEvent.META_CTRL_ON)) {
- return handleKeyGesture(deviceId, new int[]{keyCode},
+ handleKeyGesture(deviceId, new int[]{keyCode},
KeyEvent.META_CTRL_ON | (event.isShiftPressed()
? KeyEvent.META_SHIFT_ON : 0),
KeyGestureEvent.KEY_GESTURE_TYPE_LANGUAGE_SWITCH,
@@ -921,7 +919,7 @@ final class KeyGestureController {
if (down && KeyEvent.metaStateHasModifiers(metaState,
KeyEvent.META_CTRL_ON | KeyEvent.META_ALT_ON)) {
// Intercept the Accessibility keychord (CTRL + ALT + Z) for keyboard users.
- return handleKeyGesture(deviceId, new int[]{keyCode},
+ handleKeyGesture(deviceId, new int[]{keyCode},
KeyEvent.META_CTRL_ON | KeyEvent.META_ALT_ON,
KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT,
KeyGestureEvent.ACTION_GESTURE_COMPLETE, displayId,
@@ -930,7 +928,7 @@ final class KeyGestureController {
break;
case KeyEvent.KEYCODE_SYSRQ:
if (down && repeatCount == 0) {
- return handleKeyGesture(deviceId, new int[]{keyCode}, /* modifierState = */0,
+ handleKeyGesture(deviceId, new int[]{keyCode}, /* modifierState = */0,
KeyGestureEvent.KEY_GESTURE_TYPE_TAKE_SCREENSHOT,
KeyGestureEvent.ACTION_GESTURE_COMPLETE, displayId,
focusedToken, /* flags = */0, /* appLaunchData = */null);
@@ -938,7 +936,7 @@ final class KeyGestureController {
break;
case KeyEvent.KEYCODE_ESCAPE:
if (down && KeyEvent.metaStateHasNoModifiers(metaState) && repeatCount == 0) {
- return handleKeyGesture(deviceId, new int[]{keyCode}, /* modifierState = */0,
+ handleKeyGesture(deviceId, new int[]{keyCode}, /* modifierState = */0,
KeyGestureEvent.KEY_GESTURE_TYPE_CLOSE_ALL_DIALOGS,
KeyGestureEvent.ACTION_GESTURE_COMPLETE, displayId,
focusedToken, /* flags = */0, /* appLaunchData = */null);
@@ -964,29 +962,31 @@ final class KeyGestureController {
}
@VisibleForTesting
- boolean handleKeyGesture(int deviceId, int[] keycodes, int modifierState,
+ void handleKeyGesture(int deviceId, int[] keycodes, int modifierState,
@KeyGestureEvent.KeyGestureType int gestureType, int action, int displayId,
@Nullable IBinder focusedToken, int flags, @Nullable AppLaunchData appLaunchData) {
- return handleKeyGesture(createKeyGestureEvent(deviceId, keycodes,
- modifierState, gestureType, action, displayId, flags, appLaunchData), focusedToken);
+ handleKeyGesture(
+ createKeyGestureEvent(deviceId, keycodes, modifierState, gestureType, action,
+ displayId, flags, appLaunchData), focusedToken);
}
- private boolean handleKeyGesture(AidlKeyGestureEvent event, @Nullable IBinder focusedToken) {
+ private void handleKeyGesture(AidlKeyGestureEvent event, @Nullable IBinder focusedToken) {
if (mVisibleBackgroundUsersEnabled && event.displayId != DEFAULT_DISPLAY
&& shouldIgnoreGestureEventForVisibleBackgroundUser(event.gestureType,
event.displayId)) {
- return false;
+ return;
}
synchronized (mKeyGestureHandlerRecords) {
- for (KeyGestureHandlerRecord handler : mKeyGestureHandlerRecords.values()) {
- if (handler.handleKeyGesture(event, focusedToken)) {
- Message msg = Message.obtain(mHandler, MSG_NOTIFY_KEY_GESTURE_EVENT, event);
- mHandler.sendMessage(msg);
- return true;
- }
+ int index = mSupportedKeyGestureToPidMap.indexOfKey(event.gestureType);
+ if (index < 0) {
+ Log.i(TAG, "Key gesture: " + event.gestureType + " is not supported");
+ return;
}
+ int pid = mSupportedKeyGestureToPidMap.valueAt(index);
+ mKeyGestureHandlerRecords.get(pid).handleKeyGesture(event, focusedToken);
+ Message msg = Message.obtain(mHandler, MSG_NOTIFY_KEY_GESTURE_EVENT, event);
+ mHandler.sendMessage(msg);
}
- return false;
}
private boolean shouldIgnoreGestureEventForVisibleBackgroundUser(
@@ -1285,12 +1285,23 @@ final class KeyGestureController {
/** Register the key gesture event handler for a process. */
@BinderThread
- public void registerKeyGestureHandler(IKeyGestureHandler handler, int pid) {
+ public void registerKeyGestureHandler(int[] keyGesturesToHandle, IKeyGestureHandler handler,
+ int pid) {
synchronized (mKeyGestureHandlerRecords) {
if (mKeyGestureHandlerRecords.get(pid) != null) {
throw new IllegalStateException("The calling process has already registered "
+ "a KeyGestureHandler.");
}
+ if (keyGesturesToHandle.length == 0) {
+ throw new IllegalArgumentException("No key gestures provided for pid = " + pid);
+ }
+ for (int gestureType : keyGesturesToHandle) {
+ if (mSupportedKeyGestureToPidMap.indexOfKey(gestureType) >= 0) {
+ throw new IllegalArgumentException(
+ "Key gesture " + gestureType + " is already registered by pid = "
+ + mSupportedKeyGestureToPidMap.get(gestureType));
+ }
+ }
KeyGestureHandlerRecord record = new KeyGestureHandlerRecord(pid, handler);
try {
handler.asBinder().linkToDeath(record, 0);
@@ -1298,6 +1309,9 @@ final class KeyGestureController {
throw new RuntimeException(ex);
}
mKeyGestureHandlerRecords.put(pid, record);
+ for (int gestureType : keyGesturesToHandle) {
+ mSupportedKeyGestureToPidMap.put(gestureType, pid);
+ }
}
}
@@ -1315,7 +1329,7 @@ final class KeyGestureController {
+ "KeyGestureHandler.");
}
record.mKeyGestureHandler.asBinder().unlinkToDeath(record, 0);
- mKeyGestureHandlerRecords.remove(pid);
+ onKeyGestureHandlerRemoved(pid);
}
}
@@ -1328,9 +1342,14 @@ final class KeyGestureController {
return mAppLaunchShortcutManager.getBookmarks();
}
- private void onKeyGestureHandlerDied(int pid) {
+ private void onKeyGestureHandlerRemoved(int pid) {
synchronized (mKeyGestureHandlerRecords) {
mKeyGestureHandlerRecords.remove(pid);
+ for (int i = mSupportedKeyGestureToPidMap.size() - 1; i >= 0; i--) {
+ if (mSupportedKeyGestureToPidMap.valueAt(i) == pid) {
+ mSupportedKeyGestureToPidMap.removeAt(i);
+ }
+ }
}
}
@@ -1369,18 +1388,17 @@ final class KeyGestureController {
if (DEBUG) {
Slog.d(TAG, "Key gesture event handler for pid " + mPid + " died.");
}
- onKeyGestureHandlerDied(mPid);
+ onKeyGestureHandlerRemoved(mPid);
}
- public boolean handleKeyGesture(AidlKeyGestureEvent event, IBinder focusedToken) {
+ public void handleKeyGesture(AidlKeyGestureEvent event, IBinder focusedToken) {
try {
- return mKeyGestureHandler.handleKeyGesture(event, focusedToken);
+ mKeyGestureHandler.handleKeyGesture(event, focusedToken);
} catch (RemoteException ex) {
Slog.w(TAG, "Failed to send key gesture to process " + mPid
+ ", assuming it died.", ex);
binderDied();
}
- return false;
}
}
@@ -1479,18 +1497,21 @@ final class KeyGestureController {
}
}
ipw.println("}");
- ipw.print("mKeyGestureHandlerRecords = {");
synchronized (mKeyGestureHandlerRecords) {
- int i = mKeyGestureHandlerRecords.size() - 1;
- for (int processId : mKeyGestureHandlerRecords.keySet()) {
- ipw.print(processId);
- if (i > 0) {
+ ipw.print("mKeyGestureHandlerRecords = {");
+ int size = mKeyGestureHandlerRecords.size();
+ for (int i = 0; i < size; i++) {
+ int pid = mKeyGestureHandlerRecords.keyAt(i);
+ ipw.print(pid);
+ if (i < size - 1) {
ipw.print(", ");
}
- i--;
}
+ ipw.println("}");
+ ipw.println("mSupportedKeyGestures = " + Arrays.toString(
+ mSupportedKeyGestureToPidMap.copyKeys()));
}
- ipw.println("}");
+
ipw.decreaseIndent();
ipw.println("Last handled KeyGestureEvents: ");
ipw.increaseIndent();
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index fde9165a84c6..2066dbc87a0d 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -1826,8 +1826,14 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl.
@NonNull UserData userData) {
final int userId = userData.mUserId;
if (userData.mCurClient == client) {
- hideCurrentInputLocked(userData.mImeBindingState.mFocusedWindow, 0 /* flags */,
- SoftInputShowHideReason.HIDE_REMOVE_CLIENT, userId);
+ if (Flags.refactorInsetsController()) {
+ final var statsToken = createStatsTokenForFocusedClient(false /* show */,
+ SoftInputShowHideReason.HIDE_REMOVE_CLIENT, userId);
+ setImeVisibilityOnFocusedWindowClient(false, userData, statsToken);
+ } else {
+ hideCurrentInputLocked(userData.mImeBindingState.mFocusedWindow, 0 /* flags */,
+ SoftInputShowHideReason.HIDE_REMOVE_CLIENT, userId);
+ }
if (userData.mBoundToMethod) {
userData.mBoundToMethod = false;
final var userBindingController = userData.mBindingController;
@@ -2097,8 +2103,14 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl.
}
if (visibilityStateComputer.getImePolicy().isImeHiddenByDisplayPolicy()) {
- hideCurrentInputLocked(userData.mImeBindingState.mFocusedWindow, 0 /* flags */,
- SoftInputShowHideReason.HIDE_DISPLAY_IME_POLICY_HIDE, userId);
+ if (Flags.refactorInsetsController()) {
+ final var statsToken = createStatsTokenForFocusedClient(false /* show */,
+ SoftInputShowHideReason.HIDE_DISPLAY_IME_POLICY_HIDE, userId);
+ setImeVisibilityOnFocusedWindowClient(false, userData, statsToken);
+ } else {
+ hideCurrentInputLocked(userData.mImeBindingState.mFocusedWindow, 0 /* flags */,
+ SoftInputShowHideReason.HIDE_DISPLAY_IME_POLICY_HIDE, userId);
+ }
return InputBindResult.NO_IME;
}
@@ -3855,8 +3867,17 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl.
Slog.w(TAG, "If you need to impersonate a foreground user/profile from"
+ " a background user, use EditorInfo.targetInputMethodUser with"
+ " INTERACT_ACROSS_USERS_FULL permission.");
- hideCurrentInputLocked(userData.mImeBindingState.mFocusedWindow,
- 0 /* flags */, SoftInputShowHideReason.HIDE_INVALID_USER, userId);
+
+ if (Flags.refactorInsetsController()) {
+ final var statsToken = createStatsTokenForFocusedClient(
+ false /* show */, SoftInputShowHideReason.HIDE_INVALID_USER,
+ userId);
+ setImeVisibilityOnFocusedWindowClient(false, userData, statsToken);
+ } else {
+ hideCurrentInputLocked(userData.mImeBindingState.mFocusedWindow,
+ 0 /* flags */, SoftInputShowHideReason.HIDE_INVALID_USER,
+ userId);
+ }
return InputBindResult.INVALID_USER;
}
@@ -4993,7 +5014,6 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl.
setImeVisibilityOnFocusedWindowClient(false, userData,
null /* TODO(b/353463205) check statsToken */);
} else {
-
hideCurrentInputLocked(userData.mImeBindingState.mFocusedWindow,
0 /* flags */, reason, userId);
}
@@ -6688,8 +6708,9 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl.
final InputMethodSettings settings = InputMethodSettingsRepository.get(userId);
final var userData = getUserData(userId);
if (Flags.refactorInsetsController()) {
- setImeVisibilityOnFocusedWindowClient(false, userData,
- null /* TODO(b329229469) initialize statsToken here? */);
+ final var statsToken = createStatsTokenForFocusedClient(false /* show */,
+ SoftInputShowHideReason.HIDE_RESET_SHELL_COMMAND, userId);
+ setImeVisibilityOnFocusedWindowClient(false, userData, statsToken);
} else {
hideCurrentInputLocked(userData.mImeBindingState.mFocusedWindow,
0 /* flags */,
diff --git a/services/core/java/com/android/server/location/contexthub/ContextHubEndpointBroker.java b/services/core/java/com/android/server/location/contexthub/ContextHubEndpointBroker.java
index ccb9e3ea5cbe..bbf7732c9596 100644
--- a/services/core/java/com/android/server/location/contexthub/ContextHubEndpointBroker.java
+++ b/services/core/java/com/android/server/location/contexthub/ContextHubEndpointBroker.java
@@ -33,6 +33,7 @@ import android.hardware.contexthub.MessageDeliveryStatus;
import android.hardware.contexthub.Reason;
import android.hardware.location.ContextHubTransaction;
import android.hardware.location.IContextHubTransactionCallback;
+import android.hardware.location.NanoAppState;
import android.os.Binder;
import android.os.IBinder;
import android.os.PowerManager;
@@ -48,6 +49,7 @@ import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
+import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
@@ -182,8 +184,11 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub
long expiryMillis = RELIABLE_MESSAGE_DUPLICATE_DETECTION_TIMEOUT.toMillis();
if (nowMillis >= nextEntry.getValue() + expiryMillis) {
iterator.remove();
+ } else {
+ // Safe to break since LinkedHashMap is insertion-ordered, so the next entry
+ // will have a later timestamp and will not be expired.
+ break;
}
- break;
}
return mRxMessageHistoryMap.containsKey(message.getMessageSequenceNumber());
@@ -276,6 +281,7 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub
int sessionId = mEndpointManager.reserveSessionId();
EndpointInfo halEndpointInfo = ContextHubServiceUtil.convertHalEndpointInfo(destination);
+ Log.d(TAG, "openSession: sessionId=" + sessionId);
synchronized (mOpenSessionLock) {
try {
@@ -301,6 +307,7 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub
throw new IllegalArgumentException(
"Unknown session ID in closeSession: id=" + sessionId);
}
+ Log.d(TAG, "closeSession: sessionId=" + sessionId + " reason=" + reason);
mEndpointManager.halCloseEndpointSession(
sessionId, ContextHubServiceUtil.toHalReason(reason));
}
@@ -373,12 +380,43 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub
try {
mHubInterface.sendMessageToEndpoint(sessionId, halMessage);
} catch (RemoteException e) {
- Log.w(TAG, "Exception while sending message on session " + sessionId, e);
+ Log.e(
+ TAG,
+ "Exception while sending message on session "
+ + sessionId
+ + ", closing session",
+ e);
+ notifySessionClosedToBoth(sessionId, Reason.UNSPECIFIED);
}
} else {
+ IContextHubTransactionCallback wrappedCallback =
+ new IContextHubTransactionCallback.Stub() {
+ @Override
+ public void onQueryResponse(int result, List<NanoAppState> appStates)
+ throws RemoteException {
+ Log.w(TAG, "Unexpected onQueryResponse callback");
+ }
+
+ @Override
+ public void onTransactionComplete(int result) throws RemoteException {
+ callback.onTransactionComplete(result);
+ if (result != ContextHubTransaction.RESULT_SUCCESS) {
+ Log.e(
+ TAG,
+ "Failed to send reliable message "
+ + message
+ + ", closing session");
+ notifySessionClosedToBoth(sessionId, Reason.UNSPECIFIED);
+ }
+ }
+ };
ContextHubServiceTransaction transaction =
mTransactionManager.createSessionMessageTransaction(
- mHubInterface, sessionId, halMessage, mPackageName, callback);
+ mHubInterface,
+ sessionId,
+ halMessage,
+ mPackageName,
+ wrappedCallback);
try {
mTransactionManager.addTransaction(transaction);
info.setReliableMessagePending(transaction.getMessageSequenceNumber());
@@ -445,10 +483,7 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub
int id = mSessionMap.keyAt(i);
HubEndpointInfo target = mSessionMap.get(id).getRemoteEndpointInfo();
if (!hasEndpointPermissions(target)) {
- mEndpointManager.halCloseEndpointSessionNoThrow(
- id, Reason.PERMISSION_DENIED);
- onCloseEndpointSession(id, Reason.PERMISSION_DENIED);
- // Resource cleanup is done in onCloseEndpointSession
+ notifySessionClosedToBoth(id, Reason.PERMISSION_DENIED);
}
}
}
@@ -532,8 +567,17 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub
/* package */ void onMessageReceived(int sessionId, HubMessage message) {
byte errorCode = onMessageReceivedInternal(sessionId, message);
- if (errorCode != ErrorCode.OK && message.isResponseRequired()) {
- sendMessageDeliveryStatus(sessionId, message.getMessageSequenceNumber(), errorCode);
+ if (errorCode != ErrorCode.OK) {
+ Log.e(TAG, "Failed to send message to endpoint: " + message + ", closing session");
+ if (message.isResponseRequired()) {
+ sendMessageDeliveryStatus(sessionId, message.getMessageSequenceNumber(), errorCode);
+ } else {
+ notifySessionClosedToBoth(
+ sessionId,
+ (errorCode == ErrorCode.PERMISSION_DENIED)
+ ? Reason.PERMISSION_DENIED
+ : Reason.UNSPECIFIED);
+ }
}
}
@@ -800,4 +844,16 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub
+ "-0x"
+ Long.toHexString(endpoint.getIdentifier().getEndpoint()));
}
+
+ /**
+ * Notifies to both the HAL and the app that a session has been closed.
+ *
+ * @param sessionId The ID of the session that was closed
+ * @param halReason The HAL reason for closing the session
+ */
+ private void notifySessionClosedToBoth(int sessionId, byte halReason) {
+ Log.d(TAG, "notifySessionClosedToBoth: sessionId=" + sessionId + ", reason=" + halReason);
+ mEndpointManager.halCloseEndpointSessionNoThrow(sessionId, halReason);
+ onCloseEndpointSession(sessionId, halReason);
+ }
}
diff --git a/services/core/java/com/android/server/media/MediaSessionService.java b/services/core/java/com/android/server/media/MediaSessionService.java
index 58cf29b59961..c174451e8f5b 100644
--- a/services/core/java/com/android/server/media/MediaSessionService.java
+++ b/services/core/java/com/android/server/media/MediaSessionService.java
@@ -192,9 +192,15 @@ public class MediaSessionService extends SystemService implements Monitor {
private final Map<Integer, Set<MediaSessionRecordImpl>> mUserEngagedSessionsForFgs =
new HashMap<>();
- /* Maps uid with all media notifications associated to it */
+ /**
+ * Maps UIDs to their associated media notifications: UID -> (Notification ID ->
+ * {@link android.service.notification.StatusBarNotification}).
+ * Each UID maps to a collection of notifications, identified by their
+ * {@link android.service.notification.StatusBarNotification#getId()}.
+ */
@GuardedBy("mLock")
- private final Map<Integer, Set<StatusBarNotification>> mMediaNotifications = new HashMap<>();
+ private final Map<Integer, Map<String, StatusBarNotification>> mMediaNotifications =
+ new HashMap<>();
// The FullUserRecord of the current users. (i.e. The foreground user that isn't a profile)
// It's always not null after the MediaSessionService is started.
@@ -737,7 +743,8 @@ public class MediaSessionService extends SystemService implements Monitor {
}
synchronized (mLock) {
int uid = mediaSessionRecord.getUid();
- for (StatusBarNotification sbn : mMediaNotifications.getOrDefault(uid, Set.of())) {
+ for (StatusBarNotification sbn : mMediaNotifications.getOrDefault(uid,
+ Map.of()).values()) {
if (mediaSessionRecord.isLinkedToNotification(sbn.getNotification())) {
setFgsActiveLocked(mediaSessionRecord, sbn);
return;
@@ -771,7 +778,7 @@ public class MediaSessionService extends SystemService implements Monitor {
int uid, MediaSessionRecordImpl record) {
synchronized (mLock) {
for (StatusBarNotification sbn :
- mMediaNotifications.getOrDefault(uid, Set.of())) {
+ mMediaNotifications.getOrDefault(uid, Map.of()).values()) {
if (record.isLinkedToNotification(sbn.getNotification())) {
return sbn;
}
@@ -794,7 +801,8 @@ public class MediaSessionService extends SystemService implements Monitor {
for (MediaSessionRecordImpl record :
mUserEngagedSessionsForFgs.getOrDefault(uid, Set.of())) {
for (StatusBarNotification sbn :
- mMediaNotifications.getOrDefault(uid, Set.of())) {
+ mMediaNotifications.getOrDefault(uid, Map.of()).values()) {
+ //
if (record.isLinkedToNotification(sbn.getNotification())) {
// A user engaged session linked with a media notification is found.
// We shouldn't call stop FGS in this case.
@@ -3262,8 +3270,12 @@ public class MediaSessionService extends SystemService implements Monitor {
return;
}
synchronized (mLock) {
- mMediaNotifications.putIfAbsent(uid, new HashSet<>());
- mMediaNotifications.get(uid).add(sbn);
+ Map<String, StatusBarNotification> notifications = mMediaNotifications.get(uid);
+ if (notifications == null) {
+ notifications = new HashMap<>();
+ mMediaNotifications.put(uid, notifications);
+ }
+ notifications.put(sbn.getKey(), sbn);
MediaSessionRecordImpl userEngagedRecord =
getUserEngagedMediaSessionRecordForNotification(uid, postedNotification);
if (userEngagedRecord != null) {
@@ -3287,10 +3299,10 @@ public class MediaSessionService extends SystemService implements Monitor {
return;
}
synchronized (mLock) {
- Set<StatusBarNotification> uidMediaNotifications = mMediaNotifications.get(uid);
- if (uidMediaNotifications != null) {
- uidMediaNotifications.remove(sbn);
- if (uidMediaNotifications.isEmpty()) {
+ Map<String, StatusBarNotification> notifications = mMediaNotifications.get(uid);
+ if (notifications != null) {
+ notifications.remove(sbn.getKey());
+ if (notifications.isEmpty()) {
mMediaNotifications.remove(uid);
}
}
diff --git a/services/core/java/com/android/server/media/projection/Android.bp b/services/core/java/com/android/server/media/projection/Android.bp
new file mode 100644
index 000000000000..114be7d20d5b
--- /dev/null
+++ b/services/core/java/com/android/server/media/projection/Android.bp
@@ -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.
+
+team {
+ name: "trendy_team_media_projection",
+
+ // go/trendy/manage/engineers/6362947212640256
+ trendy_team_id: "6362947212640256",
+}
diff --git a/services/core/java/com/android/server/media/quality/MediaQualityUtils.java b/services/core/java/com/android/server/media/quality/MediaQualityUtils.java
index cf8b703a2641..05aac5587c2c 100644
--- a/services/core/java/com/android/server/media/quality/MediaQualityUtils.java
+++ b/services/core/java/com/android/server/media/quality/MediaQualityUtils.java
@@ -18,13 +18,20 @@ package com.android.server.media.quality;
import android.content.ContentValues;
import android.database.Cursor;
+import android.hardware.tv.mediaquality.ColorRange;
+import android.hardware.tv.mediaquality.ColorSpace;
+import android.hardware.tv.mediaquality.ColorTemperature;
import android.hardware.tv.mediaquality.DolbyAudioProcessing;
import android.hardware.tv.mediaquality.DtsVirtualX;
+import android.hardware.tv.mediaquality.Gamma;
import android.hardware.tv.mediaquality.ParameterDefaultValue;
import android.hardware.tv.mediaquality.ParameterName;
import android.hardware.tv.mediaquality.ParameterRange;
import android.hardware.tv.mediaquality.PictureParameter;
+import android.hardware.tv.mediaquality.PictureQualityEventType;
+import android.hardware.tv.mediaquality.QualityLevel;
import android.hardware.tv.mediaquality.SoundParameter;
+import android.media.quality.MediaQualityContract;
import android.media.quality.MediaQualityContract.BaseParameters;
import android.media.quality.MediaQualityContract.PictureQuality;
import android.media.quality.MediaQualityContract.SoundQuality;
@@ -371,7 +378,7 @@ public final class MediaQualityUtils {
}
List<PictureParameter> pictureParams = new ArrayList<>();
if (params.containsKey(PictureQuality.PARAMETER_BRIGHTNESS)) {
- pictureParams.add(PictureParameter.brightness(params.getLong(
+ pictureParams.add(PictureParameter.brightness((float) params.getDouble(
PictureQuality.PARAMETER_BRIGHTNESS)));
params.remove(PictureQuality.PARAMETER_BRIGHTNESS);
}
@@ -441,28 +448,46 @@ public final class MediaQualityUtils {
params.remove(PictureQuality.PARAMETER_COLOR_TUNER_BLUE_GAIN);
}
if (params.containsKey(PictureQuality.PARAMETER_NOISE_REDUCTION)) {
- pictureParams.add(PictureParameter.noiseReduction(
- (byte) params.getInt(PictureQuality.PARAMETER_NOISE_REDUCTION)));
+ String noiseReductionString = params.getString(
+ PictureQuality.PARAMETER_NOISE_REDUCTION);
+ if (noiseReductionString != null) {
+ byte noiseReductionByte = mapQualityLevel(noiseReductionString);
+ pictureParams.add(PictureParameter.noiseReduction(noiseReductionByte));
+ }
params.remove(PictureQuality.PARAMETER_NOISE_REDUCTION);
}
if (params.containsKey(PictureQuality.PARAMETER_MPEG_NOISE_REDUCTION)) {
- pictureParams.add(PictureParameter.mpegNoiseReduction(
- (byte) params.getInt(PictureQuality.PARAMETER_MPEG_NOISE_REDUCTION)));
+ String mpegNoiseReductionString = params.getString(
+ PictureQuality.PARAMETER_MPEG_NOISE_REDUCTION);
+ if (mpegNoiseReductionString != null) {
+ byte mpegNoiseReductionByte = mapQualityLevel(mpegNoiseReductionString);
+ pictureParams.add(PictureParameter.mpegNoiseReduction(mpegNoiseReductionByte));
+ }
params.remove(PictureQuality.PARAMETER_MPEG_NOISE_REDUCTION);
}
if (params.containsKey(PictureQuality.PARAMETER_FLESH_TONE)) {
- pictureParams.add(PictureParameter.fleshTone(
- (byte) params.getInt(PictureQuality.PARAMETER_FLESH_TONE)));
+ String fleshToneString = params.getString(PictureQuality.PARAMETER_FLESH_TONE);
+ if (fleshToneString != null) {
+ byte fleshToneByte = mapQualityLevel(fleshToneString);
+ pictureParams.add(PictureParameter.fleshTone(fleshToneByte));
+ }
params.remove(PictureQuality.PARAMETER_FLESH_TONE);
}
if (params.containsKey(PictureQuality.PARAMETER_DECONTOUR)) {
- pictureParams.add(PictureParameter.deContour(
- (byte) params.getInt(PictureQuality.PARAMETER_DECONTOUR)));
+ String decontourString = params.getString(PictureQuality.PARAMETER_DECONTOUR);
+ if (decontourString != null) {
+ byte decontourByte = mapQualityLevel(decontourString);
+ pictureParams.add(PictureParameter.deContour(decontourByte));
+ }
params.remove(PictureQuality.PARAMETER_DECONTOUR);
}
if (params.containsKey(PictureQuality.PARAMETER_DYNAMIC_LUMA_CONTROL)) {
- pictureParams.add(PictureParameter.dynamicLumaControl(
- (byte) params.getInt(PictureQuality.PARAMETER_DYNAMIC_LUMA_CONTROL)));
+ String dynamicLunaControlString = params.getString(
+ PictureQuality.PARAMETER_DYNAMIC_LUMA_CONTROL);
+ if (dynamicLunaControlString != null) {
+ byte dynamicLunaControlByte = mapQualityLevel(dynamicLunaControlString);
+ pictureParams.add(PictureParameter.dynamicLumaControl(dynamicLunaControlByte));
+ }
params.remove(PictureQuality.PARAMETER_DYNAMIC_LUMA_CONTROL);
}
if (params.containsKey(PictureQuality.PARAMETER_FILM_MODE)) {
@@ -481,9 +506,48 @@ public final class MediaQualityUtils {
params.remove(PictureQuality.PARAMETER_COLOR_TUNE);
}
if (params.containsKey(PictureQuality.PARAMETER_COLOR_TEMPERATURE)) {
- pictureParams.add(PictureParameter.colorTemperature(
- (byte) params.getInt(
- PictureQuality.PARAMETER_COLOR_TEMPERATURE)));
+ String colorTemperatureString = params.getString(
+ PictureQuality.PARAMETER_COLOR_TEMPERATURE);
+ if (colorTemperatureString != null) {
+ byte colorTemperatureByte;
+ switch (colorTemperatureString) {
+ case MediaQualityContract.COLOR_TEMP_USER:
+ colorTemperatureByte = ColorTemperature.USER;
+ break;
+ case MediaQualityContract.COLOR_TEMP_COOL:
+ colorTemperatureByte = ColorTemperature.COOL;
+ break;
+ case MediaQualityContract.COLOR_TEMP_STANDARD:
+ colorTemperatureByte = ColorTemperature.STANDARD;
+ break;
+ case MediaQualityContract.COLOR_TEMP_WARM:
+ colorTemperatureByte = ColorTemperature.WARM;
+ break;
+ case MediaQualityContract.COLOR_TEMP_USER_HDR10PLUS:
+ colorTemperatureByte = ColorTemperature.USER_HDR10PLUS;
+ break;
+ case MediaQualityContract.COLOR_TEMP_COOL_HDR10PLUS:
+ colorTemperatureByte = ColorTemperature.COOL_HDR10PLUS;
+ break;
+ case MediaQualityContract.COLOR_TEMP_STANDARD_HDR10PLUS:
+ colorTemperatureByte = ColorTemperature.STANDARD_HDR10PLUS;
+ break;
+ case MediaQualityContract.COLOR_TEMP_WARM_HDR10PLUS:
+ colorTemperatureByte = ColorTemperature.WARM_HDR10PLUS;
+ break;
+ case MediaQualityContract.COLOR_TEMP_FMMSDR:
+ colorTemperatureByte = ColorTemperature.FMMSDR;
+ break;
+ case MediaQualityContract.COLOR_TEMP_FMMHDR:
+ colorTemperatureByte = ColorTemperature.FMMHDR;
+ break;
+ default:
+ colorTemperatureByte = ColorTemperature.STANDARD;
+ Log.e("PictureParams", "Invalid color_temp string: "
+ + colorTemperatureString);
+ }
+ pictureParams.add(PictureParameter.colorTemperature(colorTemperatureByte));
+ }
params.remove(PictureQuality.PARAMETER_COLOR_TEMPERATURE);
}
if (params.containsKey(PictureQuality.PARAMETER_GLOBAL_DIMMING)) {
@@ -517,8 +581,26 @@ public final class MediaQualityUtils {
params.remove(PictureQuality.PARAMETER_COLOR_TUNER_BLUE_GAIN);
}
if (params.containsKey(PictureQuality.PARAMETER_LEVEL_RANGE)) {
- pictureParams.add(PictureParameter.levelRange(
- (byte) params.getInt(PictureQuality.PARAMETER_LEVEL_RANGE)));
+ String levelRangeString = params.getString(PictureQuality.PARAMETER_LEVEL_RANGE);
+ if (levelRangeString != null) {
+ byte levelRangeByte;
+ switch (levelRangeString) {
+ case "AUTO":
+ levelRangeByte = ColorRange.AUTO;
+ break;
+ case "LIMITED":
+ levelRangeByte = ColorRange.LIMITED;
+ break;
+ case "FULL":
+ levelRangeByte = ColorRange.FULL;
+ break;
+ default:
+ levelRangeByte = ColorRange.AUTO;
+ Log.e("PictureParams", "Invalid color_range string: "
+ + levelRangeString);
+ }
+ pictureParams.add(PictureParameter.levelRange(levelRangeByte));
+ }
params.remove(PictureQuality.PARAMETER_LEVEL_RANGE);
}
if (params.containsKey(PictureQuality.PARAMETER_GAMUT_MAPPING)) {
@@ -547,13 +629,61 @@ public final class MediaQualityUtils {
params.remove(PictureQuality.PARAMETER_CVRR);
}
if (params.containsKey(PictureQuality.PARAMETER_HDMI_RGB_RANGE)) {
- pictureParams.add(PictureParameter.hdmiRgbRange(
- (byte) params.getInt(PictureQuality.PARAMETER_HDMI_RGB_RANGE)));
+ String hdmiRgbRangeString = params.getString(PictureQuality.PARAMETER_HDMI_RGB_RANGE);
+ if (hdmiRgbRangeString != null) {
+ byte hdmiRgbRangeByte;
+ switch (hdmiRgbRangeString) {
+ case "AUTO":
+ hdmiRgbRangeByte = ColorRange.AUTO;
+ break;
+ case "LIMITED":
+ hdmiRgbRangeByte = ColorRange.LIMITED;
+ break;
+ case "FULL":
+ hdmiRgbRangeByte = ColorRange.FULL;
+ break;
+ default:
+ hdmiRgbRangeByte = ColorRange.AUTO;
+ Log.e("PictureParams", "Invalid hdmi_rgb_range string: "
+ + hdmiRgbRangeByte);
+ }
+ pictureParams.add(PictureParameter.hdmiRgbRange(hdmiRgbRangeByte));
+ }
params.remove(PictureQuality.PARAMETER_HDMI_RGB_RANGE);
}
if (params.containsKey(PictureQuality.PARAMETER_COLOR_SPACE)) {
- pictureParams.add(PictureParameter.colorSpace(
- (byte) params.getInt(PictureQuality.PARAMETER_COLOR_SPACE)));
+ String colorSpaceString = params.getString(PictureQuality.PARAMETER_COLOR_SPACE);
+ if (colorSpaceString != null) {
+ byte colorSpaceByte;
+ switch (colorSpaceString) {
+ case "AUTO":
+ colorSpaceByte = ColorSpace.AUTO;
+ break;
+ case "S_RGB_BT_709":
+ colorSpaceByte = ColorSpace.S_RGB_BT_709;
+ break;
+ case "DCI":
+ colorSpaceByte = ColorSpace.DCI;
+ break;
+ case "ADOBE_RGB":
+ colorSpaceByte = ColorSpace.ADOBE_RGB;
+ break;
+ case "BT2020":
+ colorSpaceByte = ColorSpace.BT2020;
+ break;
+ case "ON":
+ colorSpaceByte = ColorSpace.ON;
+ break;
+ case "OFF":
+ colorSpaceByte = ColorSpace.OFF;
+ break;
+ default:
+ colorSpaceByte = ColorSpace.OFF;
+ Log.e("PictureParams", "Invalid color_space string: "
+ + colorSpaceString);
+ }
+ pictureParams.add(PictureParameter.colorSpace(colorSpaceByte));
+ }
params.remove(PictureQuality.PARAMETER_COLOR_SPACE);
}
if (params.containsKey(PictureQuality.PARAMETER_PANEL_INIT_MAX_LUMINCE_NITS)) {
@@ -567,8 +697,25 @@ public final class MediaQualityUtils {
params.remove(PictureQuality.PARAMETER_PANEL_INIT_MAX_LUMINCE_VALID);
}
if (params.containsKey(PictureQuality.PARAMETER_GAMMA)) {
- pictureParams.add(PictureParameter.gamma(
- (byte) params.getInt(PictureQuality.PARAMETER_GAMMA)));
+ String gammaString = params.getString(PictureQuality.PARAMETER_GAMMA);
+ if (gammaString != null) {
+ byte gammaByte;
+ switch (gammaString) {
+ case "DARK":
+ gammaByte = Gamma.DARK;
+ break;
+ case "MIDDLE":
+ gammaByte = Gamma.MIDDLE;
+ break;
+ case "BRIGHT":
+ gammaByte = Gamma.BRIGHT;
+ break;
+ default:
+ gammaByte = Gamma.DARK;
+ Log.e("PictureParams", "Invalid gamma string: " + gammaString);
+ }
+ pictureParams.add(PictureParameter.gamma(gammaByte));
+ }
params.remove(PictureQuality.PARAMETER_GAMMA);
}
if (params.containsKey(PictureQuality.PARAMETER_COLOR_TEMPERATURE_RED_OFFSET)) {
@@ -602,13 +749,19 @@ public final class MediaQualityUtils {
params.remove(PictureQuality.PARAMETER_ELEVEN_POINT_BLUE);
}
if (params.containsKey(PictureQuality.PARAMETER_LOW_BLUE_LIGHT)) {
- pictureParams.add(PictureParameter.lowBlueLight(
- (byte) params.getInt(PictureQuality.PARAMETER_LOW_BLUE_LIGHT)));
+ String lowBlueLightString = params.getString(PictureQuality.PARAMETER_LOW_BLUE_LIGHT);
+ if (lowBlueLightString != null) {
+ byte lowBlueLightByte = mapQualityLevel(lowBlueLightString);
+ pictureParams.add(PictureParameter.lowBlueLight(lowBlueLightByte));
+ }
params.remove(PictureQuality.PARAMETER_LOW_BLUE_LIGHT);
}
if (params.containsKey(PictureQuality.PARAMETER_LD_MODE)) {
- pictureParams.add(PictureParameter.LdMode(
- (byte) params.getInt(PictureQuality.PARAMETER_LD_MODE)));
+ String ldModeString = params.getString(PictureQuality.PARAMETER_LD_MODE);
+ if (ldModeString != null) {
+ byte ldModeByte = mapQualityLevel(ldModeString);
+ pictureParams.add(PictureParameter.LdMode(ldModeByte));
+ }
params.remove(PictureQuality.PARAMETER_LD_MODE);
}
if (params.containsKey(PictureQuality.PARAMETER_OSD_RED_GAIN)) {
@@ -767,8 +920,44 @@ public final class MediaQualityUtils {
params.remove(PictureQuality.PARAMETER_COLOR_TUNER_LUMINANCE_FLESH);
}
if (params.containsKey(PictureQuality.PARAMETER_PICTURE_QUALITY_EVENT_TYPE)) {
- pictureParams.add(PictureParameter.pictureQualityEventType(
- (byte) params.getInt(PictureQuality.PARAMETER_PICTURE_QUALITY_EVENT_TYPE)));
+ String pictureQualityEventTypeString = params.getString(
+ PictureQuality.PARAMETER_PICTURE_QUALITY_EVENT_TYPE);
+ if (pictureQualityEventTypeString != null) {
+ byte pictureQualityEventTypeByte;
+ switch (pictureQualityEventTypeString) {
+ case "NONE":
+ pictureQualityEventTypeByte = PictureQualityEventType.NONE;
+ break;
+ case "BBD_RESULT":
+ pictureQualityEventTypeByte = PictureQualityEventType.BBD_RESULT;
+ break;
+ case "VIDEO_DELAY_CHANGE":
+ pictureQualityEventTypeByte = PictureQualityEventType.VIDEO_DELAY_CHANGE;
+ break;
+ case "CAPTUREPOINT_INFO_CHANGE":
+ pictureQualityEventTypeByte =
+ PictureQualityEventType.CAPTUREPOINT_INFO_CHANGE;
+ break;
+ case "VIDEOPATH_CHANGE":
+ pictureQualityEventTypeByte = PictureQualityEventType.VIDEOPATH_CHANGE;
+ break;
+ case "EXTRA_FRAME_CHANGE":
+ pictureQualityEventTypeByte = PictureQualityEventType.EXTRA_FRAME_CHANGE;
+ break;
+ case "DOLBY_IQ_CHANGE":
+ pictureQualityEventTypeByte = PictureQualityEventType.DOLBY_IQ_CHANGE;
+ break;
+ case "DOLBY_APO_CHANGE":
+ pictureQualityEventTypeByte = PictureQualityEventType.DOLBY_APO_CHANGE;
+ break;
+ default:
+ pictureQualityEventTypeByte = PictureQualityEventType.NONE;
+ Log.e("PictureParams", "Invalid event type string: "
+ + pictureQualityEventTypeString);
+ }
+ pictureParams.add(
+ PictureParameter.pictureQualityEventType(pictureQualityEventTypeByte));
+ }
params.remove(PictureQuality.PARAMETER_PICTURE_QUALITY_EVENT_TYPE);
}
return pictureParams.toArray(new PictureParameter[0]);
@@ -1657,6 +1846,19 @@ public final class MediaQualityUtils {
return colIndex != -1 ? cursor.getString(colIndex) : null;
}
+ private static byte mapQualityLevel(String qualityLevel) {
+ return switch (qualityLevel) {
+ case MediaQualityContract.LEVEL_OFF -> QualityLevel.OFF;
+ case MediaQualityContract.LEVEL_LOW -> QualityLevel.LOW;
+ case MediaQualityContract.LEVEL_MEDIUM -> QualityLevel.MEDIUM;
+ case MediaQualityContract.LEVEL_HIGH -> QualityLevel.HIGH;
+ default -> {
+ Log.e("PictureParams", "Invalid noise_reduction string: " + qualityLevel);
+ yield QualityLevel.OFF;
+ }
+ };
+ }
+
private MediaQualityUtils() {
}
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index 78554bdcf6aa..06fc9b083086 100644
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -14544,23 +14544,23 @@ public class NotificationManagerService extends SystemService {
*/
private class NotificationTrampolineCallback implements BackgroundActivityStartCallback {
@Override
- public boolean isActivityStartAllowed(Collection<IBinder> tokens, int uid,
- String packageName) {
+ public BackgroundActivityStartCallbackResult isActivityStartAllowed(
+ Collection<IBinder> tokens, int uid, String packageName) {
checkArgument(!tokens.isEmpty());
for (IBinder token : tokens) {
if (token != ALLOWLIST_TOKEN) {
// We only block or warn if the start is exclusively due to notification
- return true;
+ return RESULT_TRUE;
}
}
String logcatMessage =
"Indirect notification activity start (trampoline) from " + packageName;
if (blockTrampoline(uid)) {
Slog.e(TAG, logcatMessage + " blocked");
- return false;
+ return RESULT_FALSE;
} else {
Slog.w(TAG, logcatMessage + ", this should be avoided for performance reasons");
- return true;
+ return new BackgroundActivityStartCallbackResult(true, ALLOWLIST_TOKEN);
}
}
diff --git a/services/core/java/com/android/server/pm/DeletePackageHelper.java b/services/core/java/com/android/server/pm/DeletePackageHelper.java
index 38aa57f785e5..bbee77ce58ad 100644
--- a/services/core/java/com/android/server/pm/DeletePackageHelper.java
+++ b/services/core/java/com/android/server/pm/DeletePackageHelper.java
@@ -425,7 +425,7 @@ final class DeletePackageHelper {
user == null || user.getIdentifier() == USER_ALL;
if ((!deleteSystem || deleteAllUsers) && disabledPs == null) {
Slog.w(TAG, "Attempt to delete unknown system package "
- + ps.getPkg().getPackageName());
+ + ps.getName());
return null;
}
// Confirmed if the system package has been updated
diff --git a/services/core/java/com/android/server/pm/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java
index acdc79fb9922..e02ec6a9e3b4 100644
--- a/services/core/java/com/android/server/pm/InstallPackageHelper.java
+++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java
@@ -3077,7 +3077,8 @@ final class InstallPackageHelper {
}
if (succeeded) {
- Slog.i(TAG, "installation completed:" + packageName);
+ Slog.i(TAG, "installation completed for package:" + packageName
+ + ". Final code path: " + pkgSetting.getPath().getPath());
if (Flags.aslInApkAppMetadataSource()
&& pkgSetting.getAppMetadataSource() == APP_METADATA_SOURCE_APK) {
diff --git a/services/core/java/com/android/server/pm/InstallRequest.java b/services/core/java/com/android/server/pm/InstallRequest.java
index 3361dbc2df07..72f2068800d2 100644
--- a/services/core/java/com/android/server/pm/InstallRequest.java
+++ b/services/core/java/com/android/server/pm/InstallRequest.java
@@ -869,6 +869,9 @@ final class InstallRequest {
public void setScannedPackageSettingFirstInstallTimeFromReplaced(
@Nullable PackageStateInternal replacedPkgSetting, int[] userId) {
assertScanResultExists();
+ if (replacedPkgSetting == null) {
+ return;
+ }
mScanResult.mPkgSetting.setFirstInstallTimeFromReplaced(replacedPkgSetting, userId);
}
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index 38d458767015..2744721c3a46 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -115,7 +115,6 @@ import static com.android.server.wm.WindowManagerPolicyProto.SCREEN_ON_FULLY;
import static com.android.server.wm.WindowManagerPolicyProto.WINDOW_MANAGER_DRAW_COMPLETE;
import android.accessibilityservice.AccessibilityService;
-import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SuppressLint;
import android.app.ActivityManager;
@@ -268,6 +267,7 @@ import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.PrintWriter;
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
@@ -311,6 +311,7 @@ public class PhoneWindowManager implements WindowManagerPolicy {
static final int SHORT_PRESS_POWER_LOCK_OR_SLEEP = 6;
static final int SHORT_PRESS_POWER_DREAM_OR_SLEEP = 7;
static final int SHORT_PRESS_POWER_HUB_OR_DREAM_OR_SLEEP = 8;
+ static final int SHORT_PRESS_POWER_DREAM_OR_AWAKE_OR_SLEEP = 9;
// must match: config_LongPressOnPowerBehavior in config.xml
// The config value can be overridden using Settings.Global.POWER_BUTTON_LONG_PRESS
@@ -1234,8 +1235,10 @@ public class PhoneWindowManager implements WindowManagerPolicy {
break;
}
case SHORT_PRESS_POWER_DREAM_OR_SLEEP: {
- attemptToDreamFromShortPowerButtonPress(
- true,
+ attemptToDreamOrAwakeFromShortPowerButtonPress(
+ /* isScreenOn */ true,
+ /* awakeWhenDream */ false,
+ /* noDreamAction */
() -> sleepDefaultDisplayFromPowerButton(eventTime, 0));
break;
}
@@ -1269,13 +1272,22 @@ public class PhoneWindowManager implements WindowManagerPolicy {
lockNow(options);
} else {
// If the hub cannot be run, attempt to dream instead.
- attemptToDreamFromShortPowerButtonPress(
+ attemptToDreamOrAwakeFromShortPowerButtonPress(
/* isScreenOn */ true,
+ /* awakeWhenDream */ false,
/* noDreamAction */
() -> sleepDefaultDisplayFromPowerButton(eventTime, 0));
}
break;
}
+ case SHORT_PRESS_POWER_DREAM_OR_AWAKE_OR_SLEEP: {
+ attemptToDreamOrAwakeFromShortPowerButtonPress(
+ /* isScreenOn */ true,
+ /* awakeWhenDream */ true,
+ /* noDreamAction */
+ () -> sleepDefaultDisplayFromPowerButton(eventTime, 0));
+ break;
+ }
}
}
}
@@ -1319,15 +1331,18 @@ public class PhoneWindowManager implements WindowManagerPolicy {
}
/**
- * Attempt to dream from a power button press.
+ * Attempt to dream, awake or sleep from a power button press.
*
* @param isScreenOn Whether the screen is currently on.
+ * @param awakeWhenDream When it's set to {@code true}, awake the device from dreaming.
+ * Otherwise, go to sleep.
* @param noDreamAction The action to perform if dreaming is not possible.
*/
- private void attemptToDreamFromShortPowerButtonPress(
- boolean isScreenOn, Runnable noDreamAction) {
+ private void attemptToDreamOrAwakeFromShortPowerButtonPress(
+ boolean isScreenOn, boolean awakeWhenDream, Runnable noDreamAction) {
if (mShortPressOnPowerBehavior != SHORT_PRESS_POWER_DREAM_OR_SLEEP
- && mShortPressOnPowerBehavior != SHORT_PRESS_POWER_HUB_OR_DREAM_OR_SLEEP) {
+ && mShortPressOnPowerBehavior != SHORT_PRESS_POWER_HUB_OR_DREAM_OR_SLEEP
+ && mShortPressOnPowerBehavior != SHORT_PRESS_POWER_DREAM_OR_AWAKE_OR_SLEEP) {
// If the power button behavior isn't one that should be able to trigger the dream, give
// up.
noDreamAction.run();
@@ -1335,9 +1350,24 @@ public class PhoneWindowManager implements WindowManagerPolicy {
}
final DreamManagerInternal dreamManagerInternal = getDreamManagerInternal();
- if (dreamManagerInternal == null || !dreamManagerInternal.canStartDreaming(isScreenOn)) {
- Slog.d(TAG, "Can't start dreaming when attempting to dream from short power"
- + " press (isScreenOn=" + isScreenOn + ")");
+ if (dreamManagerInternal == null) {
+ Slog.d(TAG,
+ "Can't access dream manager dreaming when attempting to start or stop dream "
+ + "from short power press (isScreenOn="
+ + isScreenOn + ", awakeWhenDream=" + awakeWhenDream + ")");
+ noDreamAction.run();
+ return;
+ }
+
+ if (!dreamManagerInternal.canStartDreaming(isScreenOn)) {
+ if (awakeWhenDream && dreamManagerInternal.isDreaming()) {
+ dreamManagerInternal.stopDream(false /*immediate*/, "short press power" /*reason*/);
+ return;
+ }
+ Slog.d(TAG,
+ "Can't start dreaming and the device is not dreaming when attempting to start "
+ + "or stop dream from short power press (isScreenOn="
+ + isScreenOn + ", awakeWhenDream=" + awakeWhenDream + ")");
noDreamAction.run();
return;
}
@@ -2312,6 +2342,10 @@ public class PhoneWindowManager implements WindowManagerPolicy {
return ActivityManager.getService();
}
+ LockPatternUtils getLockPatternUtils() {
+ return new LockPatternUtils(mContext);
+ }
+
ButtonOverridePermissionChecker getButtonOverridePermissionChecker() {
return new ButtonOverridePermissionChecker();
}
@@ -2360,7 +2394,7 @@ public class PhoneWindowManager implements WindowManagerPolicy {
mAccessibilityShortcutController = injector.getAccessibilityShortcutController(
mContext, new Handler(), mCurrentUserId);
mGlobalActionsFactory = injector.getGlobalActionsFactory();
- mLockPatternUtils = new LockPatternUtils(mContext);
+ mLockPatternUtils = injector.getLockPatternUtils();
mLogger = new MetricsLogger();
Resources res = mContext.getResources();
@@ -4240,19 +4274,51 @@ public class PhoneWindowManager implements WindowManagerPolicy {
if (!useKeyGestureEventHandler()) {
return;
}
- mInputManager.registerKeyGestureEventHandler((event, focusedToken) -> {
- boolean handled = PhoneWindowManager.this.handleKeyGestureEvent(event,
- focusedToken);
- if (handled && !event.isCancelled() && Arrays.stream(event.getKeycodes()).anyMatch(
- (keycode) -> keycode == KeyEvent.KEYCODE_POWER)) {
- mPowerKeyHandled = true;
- }
- return handled;
- });
+ List<Integer> supportedGestures = new ArrayList<>(List.of(
+ KeyGestureEvent.KEY_GESTURE_TYPE_RECENT_APPS,
+ KeyGestureEvent.KEY_GESTURE_TYPE_APP_SWITCH,
+ KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_ASSISTANT,
+ KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_VOICE_ASSISTANT,
+ KeyGestureEvent.KEY_GESTURE_TYPE_HOME,
+ KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_SYSTEM_SETTINGS,
+ KeyGestureEvent.KEY_GESTURE_TYPE_LOCK_SCREEN,
+ KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_NOTIFICATION_PANEL,
+ KeyGestureEvent.KEY_GESTURE_TYPE_TAKE_SCREENSHOT,
+ KeyGestureEvent.KEY_GESTURE_TYPE_TRIGGER_BUG_REPORT,
+ KeyGestureEvent.KEY_GESTURE_TYPE_BACK,
+ KeyGestureEvent.KEY_GESTURE_TYPE_MULTI_WINDOW_NAVIGATION,
+ KeyGestureEvent.KEY_GESTURE_TYPE_DESKTOP_MODE,
+ KeyGestureEvent.KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_LEFT,
+ KeyGestureEvent.KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_RIGHT,
+ KeyGestureEvent.KEY_GESTURE_TYPE_OPEN_SHORTCUT_HELPER,
+ KeyGestureEvent.KEY_GESTURE_TYPE_BRIGHTNESS_UP,
+ KeyGestureEvent.KEY_GESTURE_TYPE_BRIGHTNESS_DOWN,
+ KeyGestureEvent.KEY_GESTURE_TYPE_RECENT_APPS_SWITCHER,
+ KeyGestureEvent.KEY_GESTURE_TYPE_ALL_APPS,
+ KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_ALL_APPS,
+ KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_SEARCH,
+ KeyGestureEvent.KEY_GESTURE_TYPE_LANGUAGE_SWITCH,
+ KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT,
+ KeyGestureEvent.KEY_GESTURE_TYPE_CLOSE_ALL_DIALOGS,
+ KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_APPLICATION,
+ KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_DO_NOT_DISTURB,
+ KeyGestureEvent.KEY_GESTURE_TYPE_SCREENSHOT_CHORD,
+ KeyGestureEvent.KEY_GESTURE_TYPE_RINGER_TOGGLE_CHORD,
+ KeyGestureEvent.KEY_GESTURE_TYPE_GLOBAL_ACTIONS,
+ KeyGestureEvent.KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT
+ ));
+ if (enableTalkbackAndMagnifierKeyGestures()) {
+ supportedGestures.add(KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_TALKBACK);
+ }
+ if (enableVoiceAccessKeyGestures()) {
+ supportedGestures.add(KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_VOICE_ACCESS);
+ }
+ mInputManager.registerKeyGestureEventHandler(supportedGestures,
+ PhoneWindowManager.this::handleKeyGestureEvent);
}
@VisibleForTesting
- boolean handleKeyGestureEvent(KeyGestureEvent event, IBinder focusedToken) {
+ void handleKeyGestureEvent(KeyGestureEvent event, IBinder focusedToken) {
boolean start = event.getAction() == KeyGestureEvent.ACTION_GESTURE_START;
boolean complete = event.getAction() == KeyGestureEvent.ACTION_GESTURE_COMPLETE
&& !event.isCancelled();
@@ -4262,12 +4328,16 @@ public class PhoneWindowManager implements WindowManagerPolicy {
int modifierState = event.getModifierState();
boolean keyguardOn = keyguardOn();
boolean canLaunchApp = isUserSetupComplete() && !keyguardOn;
+ if (!event.isCancelled() && Arrays.stream(event.getKeycodes()).anyMatch(
+ (keycode) -> keycode == KeyEvent.KEYCODE_POWER)) {
+ mPowerKeyHandled = true;
+ }
switch (gestureType) {
case KeyGestureEvent.KEY_GESTURE_TYPE_RECENT_APPS:
if (complete) {
showRecentApps(false);
}
- return true;
+ break;
case KeyGestureEvent.KEY_GESTURE_TYPE_APP_SWITCH:
if (!keyguardOn) {
if (start) {
@@ -4276,7 +4346,7 @@ public class PhoneWindowManager implements WindowManagerPolicy {
toggleRecentApps();
}
}
- return true;
+ break;
case KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_ASSISTANT:
case KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_VOICE_ASSISTANT:
if (complete && canLaunchApp) {
@@ -4284,33 +4354,33 @@ public class PhoneWindowManager implements WindowManagerPolicy {
deviceId, SystemClock.uptimeMillis(),
AssistUtils.INVOCATION_TYPE_UNKNOWN);
}
- return true;
+ break;
case KeyGestureEvent.KEY_GESTURE_TYPE_HOME:
if (complete) {
// Post to main thread to avoid blocking input pipeline.
mHandler.post(() -> handleShortPressOnHome(displayId));
}
- return true;
+ break;
case KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_SYSTEM_SETTINGS:
if (complete && canLaunchApp) {
showSystemSettings();
}
- return true;
+ break;
case KeyGestureEvent.KEY_GESTURE_TYPE_LOCK_SCREEN:
if (complete) {
lockNow(null /* options */);
}
- return true;
+ break;
case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_NOTIFICATION_PANEL:
if (complete) {
toggleNotificationPanel();
}
- return true;
+ break;
case KeyGestureEvent.KEY_GESTURE_TYPE_TAKE_SCREENSHOT:
if (complete) {
interceptScreenshotChord(SCREENSHOT_KEY_OTHER, 0 /*pressDelay*/);
}
- return true;
+ break;
case KeyGestureEvent.KEY_GESTURE_TYPE_TRIGGER_BUG_REPORT:
if (complete && mEnableBugReportKeyboardShortcut) {
try {
@@ -4321,12 +4391,12 @@ public class PhoneWindowManager implements WindowManagerPolicy {
Slog.d(TAG, "Error taking bugreport", e);
}
}
- return true;
+ break;
case KeyGestureEvent.KEY_GESTURE_TYPE_BACK:
if (complete) {
injectBackGesture(SystemClock.uptimeMillis());
}
- return true;
+ break;
case KeyGestureEvent.KEY_GESTURE_TYPE_MULTI_WINDOW_NAVIGATION:
if (complete) {
StatusBarManagerInternal statusbar = getStatusBarManagerInternal();
@@ -4335,7 +4405,7 @@ public class PhoneWindowManager implements WindowManagerPolicy {
getTargetDisplayIdForKeyGestureEvent(event));
}
}
- return true;
+ break;
case KeyGestureEvent.KEY_GESTURE_TYPE_DESKTOP_MODE:
if (complete) {
StatusBarManagerInternal statusbar = getStatusBarManagerInternal();
@@ -4344,24 +4414,24 @@ public class PhoneWindowManager implements WindowManagerPolicy {
getTargetDisplayIdForKeyGestureEvent(event));
}
}
- return true;
+ break;
case KeyGestureEvent.KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_LEFT:
if (complete) {
moveFocusedTaskToStageSplit(getTargetDisplayIdForKeyGestureEvent(event),
true /* leftOrTop */);
}
- return true;
+ break;
case KeyGestureEvent.KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_RIGHT:
if (complete) {
moveFocusedTaskToStageSplit(getTargetDisplayIdForKeyGestureEvent(event),
false /* leftOrTop */);
}
- return true;
+ break;
case KeyGestureEvent.KEY_GESTURE_TYPE_OPEN_SHORTCUT_HELPER:
if (complete) {
toggleKeyboardShortcutsMenu(deviceId);
}
- return true;
+ break;
case KeyGestureEvent.KEY_GESTURE_TYPE_BRIGHTNESS_UP:
case KeyGestureEvent.KEY_GESTURE_TYPE_BRIGHTNESS_DOWN:
if (complete) {
@@ -4369,32 +4439,32 @@ public class PhoneWindowManager implements WindowManagerPolicy {
gestureType == KeyGestureEvent.KEY_GESTURE_TYPE_BRIGHTNESS_UP ? 1 : -1;
changeDisplayBrightnessValue(displayId, direction);
}
- return true;
+ break;
case KeyGestureEvent.KEY_GESTURE_TYPE_RECENT_APPS_SWITCHER:
if (start) {
showRecentApps(true);
} else {
hideRecentApps(true, false);
}
- return true;
+ break;
case KeyGestureEvent.KEY_GESTURE_TYPE_ALL_APPS:
case KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_ALL_APPS:
if (complete && isKeyEventForCurrentUser(event.getDisplayId(),
event.getKeycodes()[0], "launchAllAppsViaA11y")) {
launchAllAppsAction();
}
- return true;
+ break;
case KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_SEARCH:
if (complete && canLaunchApp) {
launchTargetSearchActivity();
}
- return true;
+ break;
case KeyGestureEvent.KEY_GESTURE_TYPE_LANGUAGE_SWITCH:
if (complete) {
int direction = (modifierState & KeyEvent.META_SHIFT_MASK) != 0 ? -1 : 1;
sendSwitchKeyboardLayout(displayId, focusedToken, direction);
}
- return true;
+ break;
case KeyGestureEvent.KEY_GESTURE_TYPE_SCREENSHOT_CHORD:
if (start) {
// Screenshot chord is pressed: Wait for long press delay before taking
@@ -4404,14 +4474,14 @@ public class PhoneWindowManager implements WindowManagerPolicy {
} else {
cancelPendingScreenshotChordAction();
}
- return true;
+ break;
case KeyGestureEvent.KEY_GESTURE_TYPE_RINGER_TOGGLE_CHORD:
if (start) {
interceptRingerToggleChord();
} else {
cancelPendingRingerToggleChordAction();
}
- return true;
+ break;
case KeyGestureEvent.KEY_GESTURE_TYPE_GLOBAL_ACTIONS:
if (start) {
performHapticFeedback(
@@ -4421,40 +4491,34 @@ public class PhoneWindowManager implements WindowManagerPolicy {
} else {
cancelGlobalActionsAction();
}
- return true;
+ break;
case KeyGestureEvent.KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT:
if (start) {
interceptBugreportGestureTv();
} else {
cancelBugreportGestureTv();
}
- return true;
+ break;
case KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT:
if (complete && mAccessibilityShortcutController.isAccessibilityShortcutAvailable(
isKeyguardLocked())) {
mHandler.sendMessage(mHandler.obtainMessage(MSG_ACCESSIBILITY_SHORTCUT));
}
- return true;
+ break;
case KeyGestureEvent.KEY_GESTURE_TYPE_CLOSE_ALL_DIALOGS:
if (complete) {
mContext.closeSystemDialogs();
}
- return true;
+ break;
case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_TALKBACK:
- if (enableTalkbackAndMagnifierKeyGestures()) {
- if (complete) {
- mTalkbackShortcutController.toggleTalkback(mCurrentUserId,
- TalkbackShortcutController.ShortcutSource.KEYBOARD);
- }
- return true;
+ if (complete) {
+ mTalkbackShortcutController.toggleTalkback(mCurrentUserId,
+ TalkbackShortcutController.ShortcutSource.KEYBOARD);
}
break;
case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_VOICE_ACCESS:
- if (enableVoiceAccessKeyGestures()) {
- if (complete) {
- mVoiceAccessShortcutController.toggleVoiceAccess(mCurrentUserId);
- }
- return true;
+ if (complete) {
+ mVoiceAccessShortcutController.toggleVoiceAccess(mCurrentUserId);
}
break;
case KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_APPLICATION:
@@ -4463,7 +4527,7 @@ public class PhoneWindowManager implements WindowManagerPolicy {
&& mModifierShortcutManager.launchApplication(data)) {
dismissKeyboardShortcutsMenu();
}
- return true;
+ break;
case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_DO_NOT_DISTURB:
NotificationManager nm = getNotificationService();
if (nm != null) {
@@ -4472,9 +4536,12 @@ public class PhoneWindowManager implements WindowManagerPolicy {
: Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS, null,
"Key gesture DND", true);
}
- return true;
+ break;
+ default:
+ Log.w(TAG, "Received a key gesture " + event
+ + " that was not registered by this handler");
+ break;
}
- return false;
}
private void changeDisplayBrightnessValue(int displayId, int direction) {
diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
index 798c794edaf5..0f6cc24f1fc9 100644
--- a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
+++ b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
@@ -87,6 +87,7 @@ import android.service.quicksettings.TileService;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.IndentingPrintWriter;
+import android.util.IntArray;
import android.util.Pair;
import android.util.Slog;
import android.util.SparseArray;
@@ -102,6 +103,7 @@ import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.inputmethod.SoftInputShowHideReason;
import com.android.internal.logging.InstanceId;
import com.android.internal.os.TransferPipe;
+import com.android.internal.statusbar.DisableStates;
import com.android.internal.statusbar.IAddTileResultCallback;
import com.android.internal.statusbar.ISessionListener;
import com.android.internal.statusbar.IStatusBar;
@@ -124,6 +126,7 @@ import com.android.server.policy.GlobalActionsProvider;
import com.android.server.power.ShutdownCheckPoints;
import com.android.server.power.ShutdownThread;
import com.android.server.wm.ActivityTaskManagerInternal;
+import com.android.systemui.shared.Flags;
import java.io.FileDescriptor;
import java.io.PrintWriter;
@@ -1344,48 +1347,76 @@ public class StatusBarManagerService extends IStatusBarService.Stub implements D
return mTracingEnabled;
}
- // TODO(b/117478341): make it aware of multi-display if needed.
+ /**
+ * Disable status bar features. Pass the bitwise-or of the {@code #DISABLE_*} flags.
+ * To re-enable everything, pass {@code #DISABLE_NONE}.
+ *
+ * Warning: Only pass {@code #DISABLE_*} flags into this function, do not use
+ * {@code #DISABLE2_*} flags.
+ */
@Override
public void disable(int what, IBinder token, String pkg) {
disableForUser(what, token, pkg, mCurrentUserId);
}
- // TODO(b/117478341): make it aware of multi-display if needed.
+ /**
+ * Disable status bar features for a given user. Pass the bitwise-or of the
+ * {@code #DISABLE_*} flags. To re-enable everything, pass {@code #DISABLE_NONE}.
+ *
+ * Warning: Only pass {@code #DISABLE_*} flags into this function, do not use
+ * {@code #DISABLE2_*} flags.
+ */
@Override
public void disableForUser(int what, IBinder token, String pkg, int userId) {
enforceStatusBar();
enforceValidCallingUser();
synchronized (mLock) {
- disableLocked(DEFAULT_DISPLAY, userId, what, token, pkg, 1);
+ if (Flags.statusBarConnectedDisplays()) {
+ IntArray displayIds = new IntArray();
+ for (int i = 0; i < mDisplayUiState.size(); i++) {
+ displayIds.add(mDisplayUiState.keyAt(i));
+ }
+ disableAllDisplaysLocked(displayIds, userId, what, token, pkg, /* whichFlag= */ 1);
+ } else {
+ disableLocked(DEFAULT_DISPLAY, userId, what, token, pkg, /* whichFlag= */ 1);
+ }
}
}
- // TODO(b/117478341): make it aware of multi-display if needed.
/**
- * Disable additional status bar features. Pass the bitwise-or of the DISABLE2_* flags.
- * To re-enable everything, pass {@link #DISABLE2_NONE}.
+ * Disable additional status bar features. Pass the bitwise-or of the {@code #DISABLE2_*} flags.
+ * To re-enable everything, pass {@code #DISABLE2_NONE}.
*
- * Warning: Only pass DISABLE2_* flags into this function, do not use DISABLE_* flags.
+ * Warning: Only pass {@code #DISABLE2_*} flags into this function, do not use
+ * {@code #DISABLE_*} flags.
*/
@Override
public void disable2(int what, IBinder token, String pkg) {
disable2ForUser(what, token, pkg, mCurrentUserId);
}
- // TODO(b/117478341): make it aware of multi-display if needed.
/**
- * Disable additional status bar features for a given user. Pass the bitwise-or of the
- * DISABLE2_* flags. To re-enable everything, pass {@link #DISABLE_NONE}.
+ * Disable additional status bar features for a given user. Pass the bitwise-or
+ * of the {@code #DISABLE2_*} flags. To re-enable everything, pass {@code #DISABLE2_NONE}.
*
- * Warning: Only pass DISABLE2_* flags into this function, do not use DISABLE_* flags.
+ * Warning: Only pass {@code #DISABLE2_*} flags into this function, do not use
+ * {@code #DISABLE_*} flags.
*/
@Override
public void disable2ForUser(int what, IBinder token, String pkg, int userId) {
enforceStatusBar();
synchronized (mLock) {
- disableLocked(DEFAULT_DISPLAY, userId, what, token, pkg, 2);
+ if (Flags.statusBarConnectedDisplays()) {
+ IntArray displayIds = new IntArray();
+ for (int i = 0; i < mDisplayUiState.size(); i++) {
+ displayIds.add(mDisplayUiState.keyAt(i));
+ }
+ disableAllDisplaysLocked(displayIds, userId, what, token, pkg, /* whichFlag= */ 2);
+ } else {
+ disableLocked(DEFAULT_DISPLAY, userId, what, token, pkg, /* whichFlag= */ 2);
+ }
}
}
@@ -1414,6 +1445,42 @@ public class StatusBarManagerService extends IStatusBarService.Stub implements D
}
}
+ // This method batches disable state across all displays into a single remote call
+ // (IStatusBar#disableForAllDisplays) for efficiency and calls
+ // NotificationDelegate#onSetDisabled only if any display's disable state changes.
+ private void disableAllDisplaysLocked(IntArray displayIds, int userId, int what, IBinder token,
+ String pkg, int whichFlag) {
+ // It's important that the the callback and the call to mBar get done
+ // in the same order when multiple threads are calling this function
+ // so they are paired correctly. The messages on the handler will be
+ // handled in the order they were enqueued, but will be outside the lock.
+ manageDisableListLocked(userId, what, token, pkg, whichFlag);
+
+ // Ensure state for the current user is applied, even if passed a non-current user.
+ final int net1 = gatherDisableActionsLocked(mCurrentUserId, 1);
+ final int net2 = gatherDisableActionsLocked(mCurrentUserId, 2);
+
+ IStatusBar bar = mBar;
+ Map<Integer, Pair<Integer, Integer>> displaysWithNewDisableStates = new HashMap<>();
+ for (int displayId : displayIds.toArray()) {
+ final UiState state = getUiState(displayId);
+ if (!state.disableEquals(net1, net2)) {
+ state.setDisabled(net1, net2);
+ displaysWithNewDisableStates.put(displayId, new Pair(net1, net2));
+ }
+ }
+ if (bar != null) {
+ try {
+ bar.disableForAllDisplays(new DisableStates(displaysWithNewDisableStates));
+ } catch (RemoteException ex) {
+ Slog.e(TAG, "Unable to disable Status bar.", ex);
+ }
+ }
+ if (!displaysWithNewDisableStates.isEmpty()) {
+ mHandler.post(() -> mNotificationDelegate.onSetDisabled(net1));
+ }
+ }
+
/**
* Get the currently applied disable flags, in the form of one Pair<Integer, Integer>.
*
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index b76b23161e78..b9ab863a2805 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -158,7 +158,6 @@ import static com.android.server.wm.ActivityRecordProto.FRONT_OF_TASK;
import static com.android.server.wm.ActivityRecordProto.IN_SIZE_COMPAT_MODE;
import static com.android.server.wm.ActivityRecordProto.IS_ANIMATING;
import static com.android.server.wm.ActivityRecordProto.IS_USER_FULLSCREEN_OVERRIDE_ENABLED;
-import static com.android.server.wm.ActivityRecordProto.LAST_ALL_DRAWN;
import static com.android.server.wm.ActivityRecordProto.LAST_DROP_INPUT_MODE;
import static com.android.server.wm.ActivityRecordProto.LAST_SURFACE_SHOWING;
import static com.android.server.wm.ActivityRecordProto.MIN_ASPECT_RATIO;
@@ -723,7 +722,6 @@ final class ActivityRecord extends WindowToken {
private int mNumInterestingWindows;
private int mNumDrawnWindows;
boolean allDrawn;
- private boolean mLastAllDrawn;
/**
* Solely for reporting to ActivityMetricsLogger. Just tracks whether, the last time this
@@ -1148,13 +1146,11 @@ final class ActivityRecord extends WindowToken {
if (mAppStopped) {
pw.print(prefix); pw.print("mAppStopped="); pw.println(mAppStopped);
}
- if (mNumInterestingWindows != 0 || mNumDrawnWindows != 0
- || allDrawn || mLastAllDrawn) {
+ if (mNumInterestingWindows != 0 || mNumDrawnWindows != 0 || allDrawn) {
pw.print(prefix); pw.print("mNumInterestingWindows=");
pw.print(mNumInterestingWindows);
pw.print(" mNumDrawnWindows="); pw.print(mNumDrawnWindows);
pw.print(" allDrawn="); pw.print(allDrawn);
- pw.print(" lastAllDrawn="); pw.print(mLastAllDrawn);
pw.println(")");
}
if (mStartingData != null || firstWindowDrawn) {
@@ -3665,13 +3661,6 @@ final class ActivityRecord extends WindowToken {
if (endTask) {
mAtmService.getLockTaskController().clearLockedTask(task);
- // This activity was in the top focused root task and this is the last
- // activity in that task, give this activity a higher layer so it can stay on
- // top before the closing task transition be executed.
- if (mayAdjustTop) {
- mNeedsZBoost = true;
- mDisplayContent.assignWindowLayers(false /* setLayoutNeeded */);
- }
}
} else if (!isState(PAUSING)) {
if (mVisibleRequested) {
@@ -5155,7 +5144,6 @@ final class ActivityRecord extends WindowToken {
void clearAllDrawn() {
allDrawn = false;
- mLastAllDrawn = false;
}
/**
@@ -6599,35 +6587,6 @@ final class ActivityRecord extends WindowToken {
nowVisible = false;
}
- @Override
- void checkAppWindowsReadyToShow() {
- if (allDrawn == mLastAllDrawn) {
- return;
- }
-
- mLastAllDrawn = allDrawn;
- if (!allDrawn) {
- return;
- }
-
- setAppLayoutChanges(FINISH_LAYOUT_REDO_ANIM, "checkAppWindowsReadyToShow");
-
- // We can now show all of the drawn windows!
- if (canShowWindows()) {
- showAllWindowsLocked();
- }
- }
-
- /**
- * This must be called while inside a transaction.
- */
- void showAllWindowsLocked() {
- forAllWindows(windowState -> {
- if (DEBUG_VISIBILITY) Slog.v(TAG, "performing show on: " + windowState);
- windowState.performShowLocked();
- }, false /* traverseTopToBottom */);
- }
-
void updateReportedVisibilityLocked() {
if (DEBUG_VISIBILITY) Slog.v(TAG, "Update reported visibility: " + this);
final int count = mChildren.size();
@@ -7241,11 +7200,6 @@ final class ActivityRecord extends WindowToken {
}
@Override
- boolean needsZBoost() {
- return mNeedsZBoost || super.needsZBoost();
- }
-
- @Override
public SurfaceControl getAnimationLeashParent() {
// For transitions in the root pinned task (menu activity) we just let them occur as a child
// of the root pinned task.
@@ -9393,7 +9347,6 @@ final class ActivityRecord extends WindowToken {
proto.write(NUM_INTERESTING_WINDOWS, mNumInterestingWindows);
proto.write(NUM_DRAWN_WINDOWS, mNumDrawnWindows);
proto.write(ALL_DRAWN, allDrawn);
- proto.write(LAST_ALL_DRAWN, mLastAllDrawn);
if (mStartingWindow != null) {
mStartingWindow.writeIdentifierToProto(proto, STARTING_WINDOW);
}
diff --git a/services/core/java/com/android/server/wm/AsyncRotationController.java b/services/core/java/com/android/server/wm/AsyncRotationController.java
index d3fd0e3199a3..f75b17fa1569 100644
--- a/services/core/java/com/android/server/wm/AsyncRotationController.java
+++ b/services/core/java/com/android/server/wm/AsyncRotationController.java
@@ -234,7 +234,7 @@ class AsyncRotationController extends FadeAnimationController implements Consume
}
for (int i = mTargetWindowTokens.size() - 1; i >= 0; i--) {
final Operation op = mTargetWindowTokens.valueAt(i);
- if (op.mIsCompletionPending || op.mAction == Operation.ACTION_SEAMLESS) {
+ if (op.mIsCompletionPending || op.mActions == Operation.ACTION_SEAMLESS) {
// Skip completed target. And seamless windows use the signal from blast sync.
continue;
}
@@ -264,17 +264,18 @@ class AsyncRotationController extends FadeAnimationController implements Consume
op.mDrawTransaction = null;
if (DEBUG) Slog.d(TAG, "finishOp merge transaction " + windowToken.getTopChild());
}
- if (op.mAction == Operation.ACTION_TOGGLE_IME) {
+ if (op.mActions == Operation.ACTION_TOGGLE_IME) {
if (DEBUG) Slog.d(TAG, "finishOp fade-in IME " + windowToken.getTopChild());
fadeWindowToken(true /* show */, windowToken, ANIMATION_TYPE_TOKEN_TRANSFORM,
(type, anim) -> mDisplayContent.getInsetsStateController()
.getImeSourceProvider().reportImeDrawnForOrganizer());
- } else if (op.mAction == Operation.ACTION_FADE) {
+ } else if ((op.mActions & Operation.ACTION_FADE) != 0) {
if (DEBUG) Slog.d(TAG, "finishOp fade-in " + windowToken.getTopChild());
// The previous animation leash will be dropped when preparing fade-in animation, so
// simply apply new animation without restoring the transformation.
fadeWindowToken(true /* show */, windowToken, ANIMATION_TYPE_TOKEN_TRANSFORM);
- } else if (op.isValidSeamless()) {
+ }
+ if (op.isValidSeamless()) {
if (DEBUG) Slog.d(TAG, "finishOp undo seamless " + windowToken.getTopChild());
final SurfaceControl.Transaction t = windowToken.getSyncTransaction();
clearTransform(t, op.mLeash);
@@ -339,7 +340,7 @@ class AsyncRotationController extends FadeAnimationController implements Consume
}
if (mTransitionOp == OP_APP_SWITCH && token.mTransitionController.inTransition()) {
final Operation op = mTargetWindowTokens.get(token);
- if (op != null && op.mAction == Operation.ACTION_FADE) {
+ if (op != null && op.mActions == Operation.ACTION_FADE) {
// Defer showing to onTransitionFinished().
if (DEBUG) Slog.d(TAG, "Defer completion " + token.getTopChild());
return false;
@@ -367,11 +368,12 @@ class AsyncRotationController extends FadeAnimationController implements Consume
for (int i = mTargetWindowTokens.size() - 1; i >= 0; i--) {
final WindowToken windowToken = mTargetWindowTokens.keyAt(i);
final Operation op = mTargetWindowTokens.valueAt(i);
- if (op.mAction == Operation.ACTION_FADE || op.mAction == Operation.ACTION_TOGGLE_IME) {
+ if ((op.mActions & Operation.ACTION_FADE) != 0
+ || op.mActions == Operation.ACTION_TOGGLE_IME) {
fadeWindowToken(false /* show */, windowToken, ANIMATION_TYPE_TOKEN_TRANSFORM);
op.mLeash = windowToken.getAnimationLeash();
if (DEBUG) Slog.d(TAG, "Start fade-out " + windowToken.getTopChild());
- } else if (op.mAction == Operation.ACTION_SEAMLESS) {
+ } else if (op.mActions == Operation.ACTION_SEAMLESS) {
op.mLeash = windowToken.mSurfaceControl;
if (DEBUG) Slog.d(TAG, "Start seamless " + windowToken.getTopChild());
}
@@ -481,13 +483,13 @@ class AsyncRotationController extends FadeAnimationController implements Consume
/** Returns {@code true} if the controller will run fade animations on the window. */
boolean hasFadeOperation(WindowToken token) {
final Operation op = mTargetWindowTokens.get(token);
- return op != null && op.mAction == Operation.ACTION_FADE;
+ return op != null && (op.mActions & Operation.ACTION_FADE) != 0;
}
/** Returns {@code true} if the window is un-rotated to original rotation. */
boolean hasSeamlessOperation(WindowToken token) {
final Operation op = mTargetWindowTokens.get(token);
- return op != null && op.mAction == Operation.ACTION_SEAMLESS;
+ return op != null && (op.mActions & Operation.ACTION_SEAMLESS) != 0;
}
/**
@@ -541,7 +543,7 @@ class AsyncRotationController extends FadeAnimationController implements Consume
final Operation op = mTargetWindowTokens.valueAt(i);
final SurfaceControl leash = op.mLeash;
if (leash == null || !leash.isValid()) continue;
- if (mHasScreenRotationAnimation && op.mAction == Operation.ACTION_FADE) {
+ if (mHasScreenRotationAnimation && op.mActions == Operation.ACTION_FADE) {
// Hide the windows immediately because a screenshot layer should cover the screen.
t.setAlpha(leash, 0f);
if (DEBUG) {
@@ -707,7 +709,7 @@ class AsyncRotationController extends FadeAnimationController implements Consume
* start transaction of rotation transition is applied.
*/
private boolean canDrawBeforeStartTransaction(Operation op) {
- return op.mAction != Operation.ACTION_SEAMLESS;
+ return (op.mActions & Operation.ACTION_SEAMLESS) == 0;
}
void dump(PrintWriter pw, String prefix) {
@@ -723,14 +725,14 @@ class AsyncRotationController extends FadeAnimationController implements Consume
/** The operation to control the rotation appearance associated with window token. */
private static class Operation {
@Retention(RetentionPolicy.SOURCE)
- @IntDef(value = { ACTION_SEAMLESS, ACTION_FADE, ACTION_TOGGLE_IME })
+ @IntDef(flag = true, value = { ACTION_SEAMLESS, ACTION_FADE, ACTION_TOGGLE_IME })
@interface Action {}
static final int ACTION_SEAMLESS = 1;
- static final int ACTION_FADE = 2;
- /** The action to toggle the IME window appearance */
- static final int ACTION_TOGGLE_IME = 3;
- final @Action int mAction;
+ static final int ACTION_FADE = 1 << 1;
+ /** The action to toggle the IME window appearance. It can only be used exclusively. */
+ static final int ACTION_TOGGLE_IME = 1 << 2;
+ final @Action int mActions;
/** The leash of window token. It can be animation leash or the token itself. */
SurfaceControl mLeash;
/** Whether the window is drawn before the transition starts. */
@@ -744,17 +746,17 @@ class AsyncRotationController extends FadeAnimationController implements Consume
*/
SurfaceControl.Transaction mDrawTransaction;
- Operation(@Action int action) {
- mAction = action;
+ Operation(@Action int actions) {
+ mActions = actions;
}
boolean isValidSeamless() {
- return mAction == ACTION_SEAMLESS && mLeash != null && mLeash.isValid();
+ return (mActions & ACTION_SEAMLESS) != 0 && mLeash != null && mLeash.isValid();
}
@Override
public String toString() {
- return "Operation{a=" + mAction + " pending=" + mIsCompletionPending + '}';
+ return "Operation{a=" + mActions + " pending=" + mIsCompletionPending + '}';
}
}
}
diff --git a/services/core/java/com/android/server/wm/BackgroundActivityStartCallback.java b/services/core/java/com/android/server/wm/BackgroundActivityStartCallback.java
index eec648dae24a..826d5fb1c333 100644
--- a/services/core/java/com/android/server/wm/BackgroundActivityStartCallback.java
+++ b/services/core/java/com/android/server/wm/BackgroundActivityStartCallback.java
@@ -24,6 +24,16 @@ import java.util.Collection;
* Callback to decide activity starts and related operations based on originating tokens.
*/
public interface BackgroundActivityStartCallback {
+ BackgroundActivityStartCallbackResult RESULT_FALSE =
+ new BackgroundActivityStartCallbackResult(false, null);
+ BackgroundActivityStartCallbackResult RESULT_TRUE =
+ new BackgroundActivityStartCallbackResult(true, null);
+
+ record BackgroundActivityStartCallbackResult(
+ boolean allowed,
+ IBinder token
+ ) {}
+
/**
* Returns true if the background activity start originating from {@code tokens} should be
* allowed or not.
@@ -34,7 +44,8 @@ public interface BackgroundActivityStartCallback {
* This will be called holding the WM and local lock, don't do anything costly or invoke AM/WM
* methods here directly.
*/
- boolean isActivityStartAllowed(Collection<IBinder> tokens, int uid, String packageName);
+ BackgroundActivityStartCallbackResult isActivityStartAllowed(Collection<IBinder> tokens,
+ int uid, String packageName);
/**
* Returns whether {@code uid} can send {@link android.content.Intent
diff --git a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
index f50a68cc5389..f8a50b3fda04 100644
--- a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
+++ b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
@@ -51,6 +51,7 @@ import static com.android.window.flags.Flags.balRequireOptInByPendingIntentCreat
import static com.android.window.flags.Flags.balShowToastsBlocked;
import static com.android.window.flags.Flags.balStrictModeGracePeriod;
import static com.android.window.flags.Flags.balStrictModeRo;
+import static com.android.window.flags.Flags.balAdditionalLogging;
import static java.lang.annotation.RetentionPolicy.SOURCE;
import static java.util.Objects.requireNonNull;
@@ -1939,6 +1940,7 @@ public class BackgroundActivityStartController {
}
}
logIfOnlyAllowedBy(finalVerdict, state, BAL_ALLOW_NON_APP_VISIBLE_WINDOW);
+ logIfOnlyAllowedBy(finalVerdict, state, BAL_ALLOW_TOKEN);
if (balImprovedMetrics()) {
if (shouldLogStats(finalVerdict, state)) {
@@ -1998,8 +2000,10 @@ public class BackgroundActivityStartController {
return false;
} else {
// log to determine grace period length distribution
- Slog.wtf(TAG, "Activity start ONLY allowed by " + balCodeToString(balCode) + " "
- + finalVerdict.mMessage + ": " + state);
+ if (balAdditionalLogging()) {
+ Slog.wtf(TAG, "Activity start ONLY allowed by " + balCodeToString(balCode) + " "
+ + finalVerdict.mMessage + ": " + state);
+ }
return true;
}
}
diff --git a/services/core/java/com/android/server/wm/BackgroundLaunchProcessController.java b/services/core/java/com/android/server/wm/BackgroundLaunchProcessController.java
index 31b239421baf..2605310db6f4 100644
--- a/services/core/java/com/android/server/wm/BackgroundLaunchProcessController.java
+++ b/services/core/java/com/android/server/wm/BackgroundLaunchProcessController.java
@@ -128,11 +128,16 @@ class BackgroundLaunchProcessController {
return new BalVerdict(BAL_ALLOW_PERMISSION, /*background*/
"process instrumenting with background activity starts privileges");
}
- // Allow if the flag was explicitly set.
- if (checkConfiguration.checkOtherExemptions && isBackgroundStartAllowedByToken(uid,
- packageName, checkConfiguration.isCheckingForFgsStart)) {
- return new BalVerdict(balImprovedMetrics() ? BAL_ALLOW_TOKEN : BAL_ALLOW_PERMISSION,
- /*background*/ "process allowed by token");
+ // Allow if the token is explicitly allowed.
+ if (checkConfiguration.checkOtherExemptions) {
+ BalVerdict tokenVerdict = isBackgroundStartAllowedByToken(uid,
+ packageName, checkConfiguration.isCheckingForFgsStart);
+ if (tokenVerdict.allows()) {
+ if (!balImprovedMetrics()) {
+ return new BalVerdict(BAL_ALLOW_PERMISSION, tokenVerdict.toString());
+ }
+ return tokenVerdict;
+ }
}
// Allow if the caller is bound by a UID that's currently foreground.
// But still respect the appSwitchState.
@@ -174,42 +179,53 @@ class BackgroundLaunchProcessController {
* isCheckingForFgsStart is false, we ask the callback if the start is allowed for these tokens,
* otherwise if there is no callback we allow.
*/
- private boolean isBackgroundStartAllowedByToken(int uid, String packageName,
+ private BalVerdict isBackgroundStartAllowedByToken(int uid, String packageName,
boolean isCheckingForFgsStart) {
synchronized (this) {
if (mBackgroundStartPrivileges == null
|| mBackgroundStartPrivileges.isEmpty()) {
// no tokens to allow anything
- return false;
+ return BalVerdict.BLOCK;
}
if (isCheckingForFgsStart) {
// check if any token allows foreground service starts
for (int i = mBackgroundStartPrivileges.size(); i-- > 0; ) {
if (mBackgroundStartPrivileges.valueAt(i).allowsBackgroundFgsStarts()) {
- return true;
+ return new BalVerdict(BAL_ALLOW_TOKEN, "process allowed by token");
}
}
- return false;
+ return BalVerdict.BLOCK;
}
if (mBackgroundActivityStartCallback == null) {
// without a callback just check if any token allows background activity starts
for (int i = mBackgroundStartPrivileges.size(); i-- > 0; ) {
if (mBackgroundStartPrivileges.valueAt(i)
.allowsBackgroundActivityStarts()) {
- return true;
+ return new BalVerdict(BAL_ALLOW_TOKEN, "process allowed by token");
}
}
- return false;
+ return BalVerdict.BLOCK;
}
List<IBinder> binderTokens = getOriginatingTokensThatAllowBal();
if (binderTokens.isEmpty()) {
// no tokens to allow anything
- return false;
+ return BalVerdict.BLOCK;
}
// The callback will decide.
- return mBackgroundActivityStartCallback.isActivityStartAllowed(
+ BackgroundActivityStartCallback.BackgroundActivityStartCallbackResult
+ activityStartAllowed = mBackgroundActivityStartCallback.isActivityStartAllowed(
binderTokens, uid, packageName);
+ if (!activityStartAllowed.allowed()) {
+ return BalVerdict.BLOCK;
+ }
+ if (activityStartAllowed.token() == null) {
+ return new BalVerdict(BAL_ALLOW_TOKEN,
+ "process allowed by callback (token ignored) tokens: " + binderTokens);
+ }
+ return new BalVerdict(BAL_ALLOW_TOKEN,
+ "process allowed by callback (token: " + activityStartAllowed.token()
+ + ") tokens: " + binderTokens);
}
}
diff --git a/services/core/java/com/android/server/wm/DisplayArea.java b/services/core/java/com/android/server/wm/DisplayArea.java
index 6718ae435cd9..d7d5b44ed210 100644
--- a/services/core/java/com/android/server/wm/DisplayArea.java
+++ b/services/core/java/com/android/server/wm/DisplayArea.java
@@ -332,12 +332,6 @@ public class DisplayArea<T extends WindowContainer> extends WindowContainer<T> {
}
@Override
- boolean needsZBoost() {
- // Z Boost should only happen at or below the ActivityStack level.
- return false;
- }
-
- @Override
boolean fillsParent() {
return true;
}
diff --git a/services/core/java/com/android/server/wm/OWNERS b/services/core/java/com/android/server/wm/OWNERS
index 243a5326b545..0989fc05e0bb 100644
--- a/services/core/java/com/android/server/wm/OWNERS
+++ b/services/core/java/com/android/server/wm/OWNERS
@@ -26,7 +26,7 @@ mcarli@google.com
per-file Background*Start* = set noparent
per-file Background*Start* = file:/BAL_OWNERS
per-file Background*Start* = ogunwale@google.com, louischang@google.com
-per-file BackgroundLaunchProcessController.java = file:/BAL_OWNERS
+per-file BackgroundLaunchProcessController*.java = file:/BAL_OWNERS
# File related to activity callers
per-file ActivityCallerState.java = file:/core/java/android/app/COMPONENT_CALLER_OWNERS
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index 0531828be6d4..ec17d131958b 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -35,8 +35,6 @@ import static android.content.Intent.FLAG_ACTIVITY_RETAIN_IN_RECENTS;
import static android.content.Intent.FLAG_ACTIVITY_TASK_ON_HOME;
import static android.content.pm.ActivityInfo.FLAG_RELINQUISH_TASK_IDENTITY;
import static android.content.pm.ActivityInfo.FLAG_SHOW_FOR_ALL_USERS;
-import static android.content.pm.ActivityInfo.FORCE_NON_RESIZE_APP;
-import static android.content.pm.ActivityInfo.FORCE_RESIZE_APP;
import static android.content.pm.ActivityInfo.RESIZE_MODE_FORCE_RESIZABLE_LANDSCAPE_ONLY;
import static android.content.pm.ActivityInfo.RESIZE_MODE_FORCE_RESIZABLE_PORTRAIT_ONLY;
import static android.content.pm.ActivityInfo.RESIZE_MODE_FORCE_RESIZABLE_PRESERVE_ORIENTATION;
@@ -51,7 +49,6 @@ import static android.view.Display.INVALID_DISPLAY;
import static android.view.SurfaceControl.METADATA_TASK_ID;
import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_STARTING;
import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
-import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_RESIZEABLE_ACTIVITY_OVERRIDES;
import static android.view.WindowManager.TRANSIT_CLOSE;
import static android.view.WindowManager.TRANSIT_FLAG_APP_CRASHED;
import static android.view.WindowManager.TRANSIT_OPEN;
@@ -132,7 +129,6 @@ import android.app.IActivityController;
import android.app.PictureInPictureParams;
import android.app.TaskInfo;
import android.app.WindowConfiguration;
-import android.app.compat.CompatChanges;
import android.content.ComponentName;
import android.content.Intent;
import android.content.pm.ActivityInfo;
@@ -514,10 +510,16 @@ class Task extends TaskFragment {
boolean mIsPerceptible = false;
/**
- * Whether the compatibility overrides that change the resizability of the app should be allowed
- * for the specific app.
+ * Whether the task has been forced resizable, which is determined by the
+ * activity that started this task.
*/
- boolean mAllowForceResizeOverride = true;
+ private boolean mForceResizeOverride;
+
+ /**
+ * Whether the task has been forced non-resizable, which is determined by
+ * the activity that started this task.
+ */
+ private boolean mForceNonResizeOverride;
private static final int TRANSLUCENT_TIMEOUT_MSG = FIRST_ACTIVITY_TASK_MSG + 1;
@@ -675,7 +677,6 @@ class Task extends TaskFragment {
intent = _intent;
mMinWidth = minWidth;
mMinHeight = minHeight;
- updateAllowForceResizeOverride();
}
mAtmService.getTaskChangeNotificationController().notifyTaskCreated(_taskId, realActivity);
mHandler = new ActivityTaskHandler(mTaskSupervisor.mLooper);
@@ -946,6 +947,7 @@ class Task extends TaskFragment {
mCallingPackage = r.launchedFromPackage;
mCallingFeatureId = r.launchedFromFeatureId;
setIntent(intent != null ? intent : r.intent, info != null ? info : r.info);
+ updateForceResizeOverrides(r);
}
setLockTaskAuth(r);
}
@@ -1038,7 +1040,6 @@ class Task extends TaskFragment {
mTaskSupervisor.mRecentTasks.remove(this);
mTaskSupervisor.mRecentTasks.add(this);
}
- updateAllowForceResizeOverride();
}
/** Sets the original minimal width and height. */
@@ -1855,15 +1856,14 @@ class Task extends TaskFragment {
-1 /* don't check PID */, -1 /* don't check UID */, this);
}
- private void updateAllowForceResizeOverride() {
- try {
- mAllowForceResizeOverride = mAtmService.mContext.getPackageManager().getPropertyAsUser(
- PROPERTY_COMPAT_ALLOW_RESIZEABLE_ACTIVITY_OVERRIDES,
- getBasePackageName(), null /* className */, mUserId).getBoolean();
- } catch (PackageManager.NameNotFoundException e) {
- // Package not found or property not defined, reset to default value.
- mAllowForceResizeOverride = true;
- }
+ private void updateForceResizeOverrides(@NonNull ActivityRecord r) {
+ final AppCompatResizeOverrides resizeOverrides = r.mAppCompatController
+ .getResizeOverrides();
+ mForceResizeOverride = resizeOverrides.shouldOverrideForceResizeApp()
+ || r.isUniversalResizeable()
+ || r.mAppCompatController.getAspectRatioOverrides()
+ .hasFullscreenOverride();
+ mForceNonResizeOverride = resizeOverrides.shouldOverrideForceNonResizeApp();
}
/**
@@ -2882,17 +2882,8 @@ class Task extends TaskFragment {
final boolean forceResizable = mAtmService.mForceResizableActivities
&& getActivityType() == ACTIVITY_TYPE_STANDARD;
if (forceResizable) return true;
-
- final UserHandle userHandle = UserHandle.getUserHandleForUid(mUserId);
- final boolean forceResizableOverride = mAllowForceResizeOverride
- && CompatChanges.isChangeEnabled(
- FORCE_RESIZE_APP, getBasePackageName(), userHandle);
- final boolean forceNonResizableOverride = mAllowForceResizeOverride
- && CompatChanges.isChangeEnabled(
- FORCE_NON_RESIZE_APP, getBasePackageName(), userHandle);
-
- if (forceNonResizableOverride) return false;
- return forceResizableOverride || ActivityInfo.isResizeableMode(mResizeMode)
+ if (mForceNonResizeOverride) return false;
+ return mForceResizeOverride || ActivityInfo.isResizeableMode(mResizeMode)
|| (mSupportsPictureInPicture && checkPictureInPictureSupport);
}
@@ -3633,43 +3624,39 @@ class Task extends TaskFragment {
int layer = 0;
boolean decorSurfacePlaced = false;
- // We use two passes as a way to promote children which
- // need Z-boosting to the end of the list.
for (int j = 0; j < mChildren.size(); ++j) {
final WindowContainer wc = mChildren.get(j);
wc.assignChildLayers(t);
- if (!wc.needsZBoost()) {
- // Place the decor surface under any untrusted content.
- if (mDecorSurfaceContainer != null
- && !mDecorSurfaceContainer.mIsBoosted
- && !decorSurfacePlaced
- && shouldPlaceDecorSurfaceBelowContainer(wc)) {
- mDecorSurfaceContainer.assignLayer(t, layer++);
- decorSurfacePlaced = true;
- }
- wc.assignLayer(t, layer++);
-
- // Boost the adjacent TaskFragment for dimmer if needed.
- final TaskFragment taskFragment = wc.asTaskFragment();
- if (taskFragment != null && taskFragment.isEmbedded()
- && taskFragment.hasAdjacentTaskFragment()) {
- final int[] nextLayer = { layer };
- taskFragment.forOtherAdjacentTaskFragments(adjacentTf -> {
- if (adjacentTf.shouldBoostDimmer()) {
- adjacentTf.assignLayer(t, nextLayer[0]++);
- }
- });
- layer = nextLayer[0];
- }
+ // Place the decor surface under any untrusted content.
+ if (mDecorSurfaceContainer != null
+ && !mDecorSurfaceContainer.mIsBoosted
+ && !decorSurfacePlaced
+ && shouldPlaceDecorSurfaceBelowContainer(wc)) {
+ mDecorSurfaceContainer.assignLayer(t, layer++);
+ decorSurfacePlaced = true;
+ }
+ wc.assignLayer(t, layer++);
+
+ // Boost the adjacent TaskFragment for dimmer if needed.
+ final TaskFragment taskFragment = wc.asTaskFragment();
+ if (taskFragment != null && taskFragment.isEmbedded()
+ && taskFragment.hasAdjacentTaskFragment()) {
+ final int[] nextLayer = { layer };
+ taskFragment.forOtherAdjacentTaskFragments(adjacentTf -> {
+ if (adjacentTf.shouldBoostDimmer()) {
+ adjacentTf.assignLayer(t, nextLayer[0]++);
+ }
+ });
+ layer = nextLayer[0];
+ }
- // Place the decor surface just above the owner TaskFragment.
- if (mDecorSurfaceContainer != null
- && !mDecorSurfaceContainer.mIsBoosted
- && !decorSurfacePlaced
- && wc == mDecorSurfaceContainer.mOwnerTaskFragment) {
- mDecorSurfaceContainer.assignLayer(t, layer++);
- decorSurfacePlaced = true;
- }
+ // Place the decor surface just above the owner TaskFragment.
+ if (mDecorSurfaceContainer != null
+ && !mDecorSurfaceContainer.mIsBoosted
+ && !decorSurfacePlaced
+ && wc == mDecorSurfaceContainer.mOwnerTaskFragment) {
+ mDecorSurfaceContainer.assignLayer(t, layer++);
+ decorSurfacePlaced = true;
}
}
@@ -3679,12 +3666,6 @@ class Task extends TaskFragment {
mDecorSurfaceContainer.assignLayer(t, layer++);
}
- for (int j = 0; j < mChildren.size(); ++j) {
- final WindowContainer wc = mChildren.get(j);
- if (wc.needsZBoost()) {
- wc.assignLayer(t, layer++);
- }
- }
if (mOverlayHost != null) {
mOverlayHost.setLayer(t, layer++);
}
diff --git a/services/core/java/com/android/server/wm/TaskDisplayArea.java b/services/core/java/com/android/server/wm/TaskDisplayArea.java
index fb7bab4b3e26..1de139696c07 100644
--- a/services/core/java/com/android/server/wm/TaskDisplayArea.java
+++ b/services/core/java/com/android/server/wm/TaskDisplayArea.java
@@ -48,7 +48,6 @@ import android.content.pm.ActivityInfo.ScreenOrientation;
import android.content.res.Configuration;
import android.graphics.Color;
import android.os.UserHandle;
-import android.util.IntArray;
import android.util.Slog;
import android.view.SurfaceControl;
import android.view.WindowManager;
@@ -102,9 +101,6 @@ final class TaskDisplayArea extends DisplayArea<WindowContainer> {
private final ArrayList<WindowContainer> mTmpAlwaysOnTopChildren = new ArrayList<>();
private final ArrayList<WindowContainer> mTmpNormalChildren = new ArrayList<>();
private final ArrayList<WindowContainer> mTmpHomeChildren = new ArrayList<>();
- private final IntArray mTmpNeedsZBoostIndexes = new IntArray();
-
- private ArrayList<Task> mTmpTasks = new ArrayList<>();
private ActivityTaskManagerService mAtmService;
@@ -740,40 +736,14 @@ final class TaskDisplayArea extends DisplayArea<WindowContainer> {
*/
private int adjustRootTaskLayer(SurfaceControl.Transaction t,
ArrayList<WindowContainer> children, int startLayer) {
- mTmpNeedsZBoostIndexes.clear();
final int childCount = children.size();
- boolean hasAdjacentTask = false;
for (int i = 0; i < childCount; i++) {
final WindowContainer child = children.get(i);
- final TaskDisplayArea childTda = child.asTaskDisplayArea();
- final boolean childNeedsZBoost = childTda != null
- ? childTda.childrenNeedZBoost()
- : child.needsZBoost();
-
- if (childNeedsZBoost) {
- mTmpNeedsZBoostIndexes.add(i);
- continue;
- }
-
- child.assignLayer(t, startLayer++);
- }
-
- final int zBoostSize = mTmpNeedsZBoostIndexes.size();
- for (int i = 0; i < zBoostSize; i++) {
- final WindowContainer child = children.get(mTmpNeedsZBoostIndexes.get(i));
child.assignLayer(t, startLayer++);
}
return startLayer;
}
- private boolean childrenNeedZBoost() {
- final boolean[] needsZBoost = new boolean[1];
- forAllRootTasks(task -> {
- needsZBoost[0] |= task.needsZBoost();
- });
- return needsZBoost[0];
- }
-
void setBackgroundColor(@ColorInt int colorInt) {
setBackgroundColor(colorInt, false /* restore */);
}
diff --git a/services/core/java/com/android/server/wm/TaskFpsCallbackController.java b/services/core/java/com/android/server/wm/TaskFpsCallbackController.java
index 8c798759c890..665c5cffd9ff 100644
--- a/services/core/java/com/android/server/wm/TaskFpsCallbackController.java
+++ b/services/core/java/com/android/server/wm/TaskFpsCallbackController.java
@@ -16,7 +16,6 @@
package com.android.server.wm;
-import android.content.Context;
import android.os.IBinder;
import android.os.RemoteException;
import android.window.ITaskFpsCallback;
@@ -25,12 +24,10 @@ import java.util.HashMap;
final class TaskFpsCallbackController {
- private final Context mContext;
private final HashMap<IBinder, Long> mTaskFpsCallbacks;
private final HashMap<IBinder, IBinder.DeathRecipient> mDeathRecipients;
- TaskFpsCallbackController(Context context) {
- mContext = context;
+ TaskFpsCallbackController() {
mTaskFpsCallbacks = new HashMap<>();
mDeathRecipients = new HashMap<>();
}
diff --git a/services/core/java/com/android/server/wm/WindowAnimator.java b/services/core/java/com/android/server/wm/WindowAnimator.java
index 3f2b40c1d7c9..e50545d41655 100644
--- a/services/core/java/com/android/server/wm/WindowAnimator.java
+++ b/services/core/java/com/android/server/wm/WindowAnimator.java
@@ -18,10 +18,7 @@ package com.android.server.wm;
import static com.android.internal.protolog.WmProtoLogGroups.WM_SHOW_TRANSACTIONS;
import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_ALL;
-import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_APP_TRANSITION;
-import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_SCREEN_ROTATION;
import static com.android.server.wm.WindowContainer.AnimationFlags.CHILDREN;
-import static com.android.server.wm.WindowContainer.AnimationFlags.TRANSITION;
import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_WINDOW_TRACE;
import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME;
import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
@@ -57,9 +54,6 @@ public class WindowAnimator {
/** Is any window animating? */
private boolean mLastRootAnimating;
- /** True if we are running any animations that require expensive composition. */
- private boolean mRunningExpensiveAnimations;
-
final Choreographer.FrameCallback mAnimationFrameCallback;
/** Time of current animation step. Reset on each iteration */
@@ -138,8 +132,6 @@ public class WindowAnimator {
scheduleAnimation();
final RootWindowContainer root = mService.mRoot;
- final boolean useShellTransition = root.mTransitionController.isShellTransitionsEnabled();
- final int animationFlags = useShellTransition ? CHILDREN : (TRANSITION | CHILDREN);
boolean rootAnimating = false;
mCurrentTime = frameTimeNs / TimeUtils.NANOS_PER_MS;
if (DEBUG_WINDOW_TRACE) {
@@ -164,17 +156,13 @@ public class WindowAnimator {
for (int i = 0; i < numDisplays; i++) {
final DisplayContent dc = root.getChildAt(i);
-
- if (!useShellTransition) {
- dc.checkAppWindowsReadyToShow();
- }
if (accessibilityController.hasCallbacks()) {
accessibilityController
.recomputeMagnifiedRegionAndDrawMagnifiedRegionBorderIfNeeded(
dc.mDisplayId);
}
- if (dc.isAnimating(animationFlags, ANIMATION_TYPE_ALL)) {
+ if (dc.isAnimating(CHILDREN, ANIMATION_TYPE_ALL)) {
rootAnimating = true;
if (!dc.mLastContainsRunningSurfaceAnimator) {
dc.mLastContainsRunningSurfaceAnimator = true;
@@ -211,11 +199,6 @@ public class WindowAnimator {
}
mLastRootAnimating = rootAnimating;
- // APP_TRANSITION, SCREEN_ROTATION, TYPE_RECENTS are handled by shell transition.
- if (!useShellTransition) {
- updateRunningExpensiveAnimationsLegacy();
- }
-
final ArrayList<Runnable> afterPrepareSurfacesRunnables = mAfterPrepareSurfacesRunnables;
if (!afterPrepareSurfacesRunnables.isEmpty()) {
mAfterPrepareSurfacesRunnables = new ArrayList<>();
@@ -244,21 +227,6 @@ public class WindowAnimator {
}
}
- private void updateRunningExpensiveAnimationsLegacy() {
- final boolean runningExpensiveAnimations =
- mService.mRoot.isAnimating(TRANSITION | CHILDREN /* flags */,
- ANIMATION_TYPE_APP_TRANSITION
- | ANIMATION_TYPE_SCREEN_ROTATION /* typesToCheck */);
- if (runningExpensiveAnimations && !mRunningExpensiveAnimations) {
- mService.mSnapshotController.setPause(true);
- mTransaction.setEarlyWakeupStart();
- } else if (!runningExpensiveAnimations && mRunningExpensiveAnimations) {
- mService.mSnapshotController.setPause(false);
- mTransaction.setEarlyWakeupEnd();
- }
- mRunningExpensiveAnimations = runningExpensiveAnimations;
- }
-
public void dumpLocked(PrintWriter pw, String prefix, boolean dumpAll) {
final String subPrefix = " " + prefix;
diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java
index 5cbba355a06f..5b4870b0c0c7 100644
--- a/services/core/java/com/android/server/wm/WindowContainer.java
+++ b/services/core/java/com/android/server/wm/WindowContainer.java
@@ -279,9 +279,6 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer<
*/
int mTransitFlags;
- /** Whether this container should be boosted at the top of all its siblings. */
- @VisibleForTesting boolean mNeedsZBoost;
-
/** Layer used to constrain the animation to a container's stack bounds. */
SurfaceControl mAnimationBoundsLayer;
@@ -1476,14 +1473,6 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer<
return stillDeferringRemoval;
}
- /** Checks if all windows in an app are all drawn and shows them if needed. */
- void checkAppWindowsReadyToShow() {
- for (int i = mChildren.size() - 1; i >= 0; --i) {
- final WindowContainer wc = mChildren.get(i);
- wc.checkAppWindowsReadyToShow();
- }
- }
-
/**
* Called when this container or one of its descendants changed its requested orientation, and
* wants this container to handle it or pass it to its parent.
@@ -2744,15 +2733,7 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer<
for (int j = 0; j < mChildren.size(); ++j) {
final WindowContainer wc = mChildren.get(j);
wc.assignChildLayers(t);
- if (!wc.needsZBoost()) {
- wc.assignLayer(t, layer++);
- }
- }
- for (int j = 0; j < mChildren.size(); ++j) {
- final WindowContainer wc = mChildren.get(j);
- if (wc.needsZBoost()) {
- wc.assignLayer(t, layer++);
- }
+ wc.assignLayer(t, layer++);
}
if (mOverlayHost != null) {
mOverlayHost.setLayer(t, layer++);
@@ -2764,16 +2745,6 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer<
scheduleAnimation();
}
- boolean needsZBoost() {
- if (mNeedsZBoost) return true;
- for (int i = 0; i < mChildren.size(); i++) {
- if (mChildren.get(i).needsZBoost()) {
- return true;
- }
- }
- return false;
- }
-
/**
* Write to a protocol buffer output stream. Protocol buffer message definition is at
* {@link com.android.server.wm.WindowContainerProto}.
@@ -3114,7 +3085,6 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer<
public void onAnimationLeashLost(Transaction t) {
mLastLayer = -1;
mAnimationLeash = null;
- mNeedsZBoost = false;
reassignLayer(t);
updateSurfacePosition(t);
}
@@ -3140,7 +3110,6 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer<
protected void onAnimationFinished(@AnimationType int type, AnimationAdapter anim) {
doAnimationFinished(type, anim);
mWmService.onAnimationFinished();
- mNeedsZBoost = false;
}
/**
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index fb38c581d222..a9bb690d4e53 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -1449,7 +1449,7 @@ public class WindowManagerService extends IWindowManager.Stub
mPresentationController = new PresentationController();
mBlurController = new BlurController(mContext, mPowerManager);
- mTaskFpsCallbackController = new TaskFpsCallbackController(mContext);
+ mTaskFpsCallbackController = new TaskFpsCallbackController();
mAccessibilityController = new AccessibilityController(this);
mScreenRecordingCallbackController = new ScreenRecordingCallbackController(this);
mSystemPerformanceHinter = new SystemPerformanceHinter(mContext, displayId -> {
@@ -2602,6 +2602,14 @@ public class WindowManagerService extends IWindowManager.Stub
// in the new out values right now we need to force a layout.
mWindowPlacerLocked.performSurfacePlacement(true /* force */);
+ if (!win.mHaveFrame && displayContent.mWaitingForConfig) {
+ // We just forcibly triggered the layout, but this could still be intercepted by
+ // mWaitingForConfig. Here, we are forcefully marking a value for mLayoutSeq to
+ // ensure that the resize can occur properly later. Otherwise, the window's frame
+ // will remain empty forever.
+ win.mLayoutSeq = displayContent.mLayoutSeq;
+ }
+
if (shouldRelayout) {
Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "relayoutWindow: viewVisibility_1");
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index af5200102fc0..22ddd5f39b24 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -4995,18 +4995,6 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
return true;
}
- @Override
- boolean needsZBoost() {
- final InsetsControlTarget target = getDisplayContent().getImeTarget(IME_TARGET_LAYERING);
- if (mIsImWindow && target != null) {
- final ActivityRecord activity = target.getWindow().mActivityRecord;
- if (activity != null) {
- return activity.needsZBoost();
- }
- }
- return false;
- }
-
private boolean isStartingWindowAssociatedToTask() {
return mStartingData != null && mStartingData.mAssociatedTask != null;
}
diff --git a/services/credentials/java/com/android/server/credentials/MetricUtilities.java b/services/credentials/java/com/android/server/credentials/MetricUtilities.java
index ac4aac694c3a..11edb93dffea 100644
--- a/services/credentials/java/com/android/server/credentials/MetricUtilities.java
+++ b/services/credentials/java/com/android/server/credentials/MetricUtilities.java
@@ -383,7 +383,9 @@ public class MetricUtilities {
/* api_name */
initialPhaseMetric.getApiName(),
/* primary_candidates_indicated */
- candidatePrimaryProviderList
+ candidatePrimaryProviderList,
+ /* api_prepared */
+ initialPhaseMetric.hasApiUsedPrepareFlow()
);
} catch (Exception e) {
Slog.w(TAG, "Unexpected error during candidate provider uid metric emit: " + e);
@@ -442,7 +444,9 @@ public class MetricUtilities {
/* autofill_session_id */
initialPhaseMetric.getAutofillSessionId(),
/* autofill_request_id */
- initialPhaseMetric.getAutofillRequestId()
+ initialPhaseMetric.getAutofillRequestId(),
+ /* api_prepared */
+ initialPhaseMetric.hasApiUsedPrepareFlow()
);
} catch (Exception e) {
Slog.w(TAG, "Unexpected error during initial metric emit: " + e);
diff --git a/services/credentials/java/com/android/server/credentials/PrepareGetRequestSession.java b/services/credentials/java/com/android/server/credentials/PrepareGetRequestSession.java
index d60807c7b001..2d4360edf3a8 100644
--- a/services/credentials/java/com/android/server/credentials/PrepareGetRequestSession.java
+++ b/services/credentials/java/com/android/server/credentials/PrepareGetRequestSession.java
@@ -27,6 +27,7 @@ import android.credentials.GetCredentialRequest;
import android.credentials.IGetCredentialCallback;
import android.credentials.IPrepareGetCredentialCallback;
import android.credentials.PrepareGetCredentialResponseInternal;
+import android.credentials.flags.Flags;
import android.credentials.selection.GetCredentialProviderData;
import android.credentials.selection.ProviderData;
import android.credentials.selection.RequestInfo;
@@ -60,7 +61,12 @@ public class PrepareGetRequestSession extends GetRequestSession {
int numTypes = (request.getCredentialOptions().stream()
.map(CredentialOption::getType).collect(
Collectors.toSet())).size(); // Dedupe type strings
- mRequestSessionMetric.collectGetFlowInitialMetricInfo(request);
+ if (!Flags.fixMetricDuplicationEmits()) {
+ mRequestSessionMetric.collectGetFlowInitialMetricInfo(request);
+ } else {
+ mRequestSessionMetric.collectGetFlowInitialMetricInfo(request,
+ /*isApiPrepared=*/ true);
+ }
mPrepareGetCredentialCallback = prepareGetCredentialCallback;
Slog.i(TAG, "PrepareGetRequestSession constructed.");
diff --git a/services/credentials/java/com/android/server/credentials/metrics/InitialPhaseMetric.java b/services/credentials/java/com/android/server/credentials/metrics/InitialPhaseMetric.java
index 8a4e86c440b3..811b97a5bf03 100644
--- a/services/credentials/java/com/android/server/credentials/metrics/InitialPhaseMetric.java
+++ b/services/credentials/java/com/android/server/credentials/metrics/InitialPhaseMetric.java
@@ -55,6 +55,9 @@ public class InitialPhaseMetric {
// The request id of autofill if the request is from autofill, defaults to -1
private int mAutofillRequestId = -1;
+ // Indicates if this API call used the prepare flow, defaults to false
+ private boolean mApiUsedPrepareFlow = false;
+
public InitialPhaseMetric(int sessionIdTrackOne) {
mSessionIdCaller = sessionIdTrackOne;
@@ -173,4 +176,17 @@ public class InitialPhaseMetric {
public int[] getUniqueRequestCounts() {
return mRequestCounts.values().stream().mapToInt(Integer::intValue).toArray();
}
+
+ /* ------ API Prepared ------ */
+
+ public void setApiUsedPrepareFlow(boolean apiUsedPrepareFlow) {
+ mApiUsedPrepareFlow = apiUsedPrepareFlow;
+ }
+
+ /**
+ * @return a boolean indicating if this API call utilized a prepare flow
+ */
+ public boolean hasApiUsedPrepareFlow() {
+ return mApiUsedPrepareFlow;
+ }
}
diff --git a/services/credentials/java/com/android/server/credentials/metrics/RequestSessionMetric.java b/services/credentials/java/com/android/server/credentials/metrics/RequestSessionMetric.java
index 619a56846e95..dc1747f803ea 100644
--- a/services/credentials/java/com/android/server/credentials/metrics/RequestSessionMetric.java
+++ b/services/credentials/java/com/android/server/credentials/metrics/RequestSessionMetric.java
@@ -225,6 +225,22 @@ public class RequestSessionMetric {
}
/**
+ * Collects initializations for Get flow metrics.
+ *
+ * @param request the get credential request containing information to parse for metrics
+ * @param isApiPrepared indicates this API flow utilized the 'prepare' flow
+ */
+ public void collectGetFlowInitialMetricInfo(GetCredentialRequest request,
+ boolean isApiPrepared) {
+ try {
+ collectGetFlowInitialMetricInfo(request);
+ mInitialPhaseMetric.setApiUsedPrepareFlow(isApiPrepared);
+ } catch (Exception e) {
+ Slog.i(TAG, "Unexpected error collecting get flow initial metric: " + e);
+ }
+ }
+
+ /**
* During browsing, where multiple entries can be selected, this collects the browsing phase
* metric information. This is emitted together with the final phase, and the recursive path
* with authentication entries, which may occur in rare circumstances, are captured.
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index 788b3b883160..56ec27a0ba87 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -121,6 +121,7 @@ import com.android.internal.util.EmergencyAffordanceManager;
import com.android.internal.util.FrameworkStatsLog;
import com.android.internal.widget.ILockSettings;
import com.android.internal.widget.LockSettingsInternal;
+import com.android.modules.utils.build.SdkLevel;
import com.android.server.accessibility.AccessibilityManagerService;
import com.android.server.accounts.AccountManagerService;
import com.android.server.adb.AdbService;
@@ -2265,19 +2266,27 @@ public final class SystemServer implements Dumpable {
Slog.i(TAG, "Not starting VpnManagerService");
}
- t.traceBegin("StartVcnManagementService");
- try {
- if (VcnLocation.IS_VCN_IN_MAINLINE) {
- mSystemServiceManager.startServiceFromJar(
- CONNECTIVITY_SERVICE_INITIALIZER_B_CLASS,
- CONNECTIVITY_SERVICE_APEX_PATH);
- } else {
- mSystemServiceManager.startService(CONNECTIVITY_SERVICE_INITIALIZER_B_CLASS);
+ // TODO: b/374174952 In the end state, VCN registration will be moved to Tethering
+ // module. Thus the following code block should be removed after Baklava is released
+ if (!VcnLocation.IS_VCN_IN_MAINLINE || !SdkLevel.isAtLeastB()) {
+ t.traceBegin("StartVcnManagementService");
+
+ try {
+ if (!VcnLocation.IS_VCN_IN_MAINLINE) {
+ mSystemServiceManager.startService(
+ CONNECTIVITY_SERVICE_INITIALIZER_B_CLASS);
+ } else {
+ // When VCN is in mainline but the SDK level is B-, start the service with
+ // the apex path. This path can only be hit on an unfinalized B platform
+ mSystemServiceManager.startServiceFromJar(
+ CONNECTIVITY_SERVICE_INITIALIZER_B_CLASS,
+ CONNECTIVITY_SERVICE_APEX_PATH);
+ }
+ } catch (Throwable e) {
+ reportWtf("starting VCN Management Service", e);
}
- } catch (Throwable e) {
- reportWtf("starting VCN Management Service", e);
+ t.traceEnd();
}
- t.traceEnd();
t.traceBegin("StartSystemUpdateManagerService");
try {
diff --git a/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionPolicy.kt b/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionPolicy.kt
index 232bb83fdf9f..5a140d53a4d8 100644
--- a/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionPolicy.kt
+++ b/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionPolicy.kt
@@ -1753,6 +1753,13 @@ class AppIdPermissionPolicy : SchemePolicy() {
}
val appIdPermissionFlags = newState.mutateUserState(userId)!!.mutateAppIdPermissionFlags()
val permissionFlags = appIdPermissionFlags.mutateOrPut(appId) { MutableIndexedMap() }
+ // for debugging possible races TODO(b/401768134)
+ oldState.userStates[userId]?.appIdPermissionFlags[appId]?.map?.let {
+ if (permissionFlags.map === it) {
+ throw IllegalStateException("Unexpected sharing between old/new state")
+ }
+ }
+
permissionFlags.putWithDefault(permissionName, newFlags, 0)
if (permissionFlags.isEmpty()) {
appIdPermissionFlags -= appId
diff --git a/services/tests/displayservicetests/Android.bp b/services/tests/displayservicetests/Android.bp
index 36ea24195789..c85053d13e68 100644
--- a/services/tests/displayservicetests/Android.bp
+++ b/services/tests/displayservicetests/Android.bp
@@ -51,6 +51,7 @@ android_test {
data: [
":DisplayManagerTestApp",
+ ":TopologyTestApp",
],
certificate: "platform",
diff --git a/services/tests/displayservicetests/AndroidManifest.xml b/services/tests/displayservicetests/AndroidManifest.xml
index 205ff058275a..76f219b7433b 100644
--- a/services/tests/displayservicetests/AndroidManifest.xml
+++ b/services/tests/displayservicetests/AndroidManifest.xml
@@ -29,6 +29,7 @@
<uses-permission android:name="android.permission.READ_DEVICE_CONFIG" />
<uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
<uses-permission android:name="android.permission.MANAGE_USB" />
+ <uses-permission android:name="android.permission.MANAGE_DISPLAYS" />
<!-- Permissions needed for DisplayTransformManagerTest -->
<uses-permission android:name="android.permission.CHANGE_CONFIGURATION" />
diff --git a/services/tests/displayservicetests/AndroidTest.xml b/services/tests/displayservicetests/AndroidTest.xml
index f3697bbffd5c..2fe37233870f 100644
--- a/services/tests/displayservicetests/AndroidTest.xml
+++ b/services/tests/displayservicetests/AndroidTest.xml
@@ -28,6 +28,7 @@
<option name="cleanup-apks" value="true" />
<option name="install-arg" value="-t" />
<option name="test-file-name" value="DisplayManagerTestApp.apk" />
+ <option name="test-file-name" value="TopologyTestApp.apk" />
</target_preparer>
<option name="test-tag" value="DisplayServiceTests" />
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayEventDeliveryTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayEventDeliveryTest.java
index 1f45792e5097..bf4b61347bab 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayEventDeliveryTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayEventDeliveryTest.java
@@ -16,7 +16,6 @@
package com.android.server.display;
-import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_CACHED;
import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY;
import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC;
import static android.util.DisplayMetrics.DENSITY_HIGH;
@@ -27,19 +26,11 @@ import static org.junit.Assert.assertNull;
import static org.junit.Assert.fail;
import static org.junit.Assume.assumeTrue;
-import android.app.ActivityManager;
-import android.app.Instrumentation;
-import android.content.Context;
import android.content.Intent;
-import android.hardware.display.DisplayManager;
import android.hardware.display.VirtualDisplay;
-import android.os.BinderProxy;
import android.os.Handler;
-import android.os.HandlerThread;
import android.os.Looper;
import android.os.Message;
-import android.os.Messenger;
-import android.platform.test.annotations.AppModeSdkSandbox;
import android.platform.test.annotations.RequiresFlagsEnabled;
import android.platform.test.flag.junit.CheckFlagsRule;
import android.platform.test.flag.junit.DeviceFlagsValueProvider;
@@ -48,10 +39,7 @@ import android.util.SparseArray;
import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
-import androidx.test.platform.app.InstrumentationRegistry;
-import com.android.compatibility.common.util.SystemUtil;
-import com.android.compatibility.common.util.TestUtils;
import com.android.server.am.Flags;
import org.junit.After;
@@ -63,9 +51,7 @@ import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameter;
import org.junit.runners.Parameterized.Parameters;
-import java.io.IOException;
import java.util.Arrays;
-import java.util.concurrent.CountDownLatch;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
@@ -73,8 +59,7 @@ import java.util.concurrent.TimeUnit;
* Tests that applications can receive display events correctly.
*/
@RunWith(Parameterized.class)
-@AppModeSdkSandbox(reason = "Allow test in the SDK sandbox (does not prevent other modes).")
-public class DisplayEventDeliveryTest {
+public class DisplayEventDeliveryTest extends EventDeliveryTestBase {
private static final String TAG = "DisplayEventDeliveryTest";
@Rule
@@ -85,37 +70,17 @@ public class DisplayEventDeliveryTest {
private static final int WIDTH = 720;
private static final int HEIGHT = 480;
- private static final int MESSAGE_LAUNCHED = 1;
- private static final int MESSAGE_CALLBACK = 2;
-
private static final int DISPLAY_ADDED = 1;
private static final int DISPLAY_CHANGED = 2;
private static final int DISPLAY_REMOVED = 3;
- private static final long DISPLAY_EVENT_TIMEOUT_MSEC = 100;
- private static final long TEST_FAILURE_TIMEOUT_MSEC = 10000;
-
private static final String TEST_PACKAGE =
"com.android.servicestests.apps.displaymanagertestapp";
private static final String TEST_ACTIVITY = TEST_PACKAGE + ".DisplayEventActivity";
private static final String TEST_DISPLAYS = "DISPLAYS";
- private static final String TEST_MESSENGER = "MESSENGER";
private final Object mLock = new Object();
- private Instrumentation mInstrumentation;
- private Context mContext;
- private DisplayManager mDisplayManager;
- private ActivityManager mActivityManager;
- private ActivityManager.OnUidImportanceListener mUidImportanceListener;
- private CountDownLatch mLatchActivityLaunch;
- private CountDownLatch mLatchActivityCached;
- private HandlerThread mHandlerThread;
- private Handler mHandler;
- private Messenger mMessenger;
- private int mPid;
- private int mUid;
-
/**
* Array of DisplayBundle. The test handler uses it to check if certain display events have
* been sent to DisplayEventActivity.
@@ -167,7 +132,7 @@ public class DisplayEventDeliveryTest {
*/
public void assertNoDisplayEvents() {
try {
- assertNull(mExpectations.poll(DISPLAY_EVENT_TIMEOUT_MSEC, TimeUnit.MILLISECONDS));
+ assertNull(mExpectations.poll(EVENT_TIMEOUT_MSEC, TimeUnit.MILLISECONDS));
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
@@ -239,37 +204,17 @@ public class DisplayEventDeliveryTest {
}
@Before
- public void setUp() throws Exception {
- mInstrumentation = InstrumentationRegistry.getInstrumentation();
- mContext = mInstrumentation.getContext();
- mDisplayManager = mContext.getSystemService(DisplayManager.class);
- mLatchActivityLaunch = new CountDownLatch(1);
- mLatchActivityCached = new CountDownLatch(1);
- mActivityManager = mContext.getSystemService(ActivityManager.class);
- mUidImportanceListener = (uid, importance) -> {
- if (uid == mUid && importance == IMPORTANCE_CACHED) {
- Log.d(TAG, "Listener " + uid + " becomes " + importance);
- mLatchActivityCached.countDown();
- }
- };
- SystemUtil.runWithShellPermissionIdentity(() ->
- mActivityManager.addOnUidImportanceListener(mUidImportanceListener,
- IMPORTANCE_CACHED));
+ public void setUp() {
+ super.setUp();
// The lock is not functionally necessary but eliminates lint error messages.
synchronized (mLock) {
mDisplayBundles = new SparseArray<>();
}
- mHandlerThread = new HandlerThread("handler");
- mHandlerThread.start();
- mHandler = new TestHandler(mHandlerThread.getLooper());
- mMessenger = new Messenger(mHandler);
- mPid = 0;
}
@After
public void tearDown() throws Exception {
- mActivityManager.removeOnUidImportanceListener(mUidImportanceListener);
- mHandlerThread.quitSafely();
+ super.tearDown();
synchronized (mLock) {
for (int i = 0; i < mDisplayBundles.size(); i++) {
DisplayBundle bundle = mDisplayBundles.valueAt(i);
@@ -278,7 +223,31 @@ public class DisplayEventDeliveryTest {
}
mDisplayBundles.clear();
}
- SystemUtil.runShellCommand(mInstrumentation, "am force-stop " + TEST_PACKAGE);
+ }
+
+ @Override
+ protected String getTag() {
+ return TAG;
+ }
+
+ @Override
+ protected Handler getHandler(Looper looper) {
+ return new TestHandler(looper);
+ }
+
+ @Override
+ protected String getTestPackage() {
+ return TEST_PACKAGE;
+ }
+
+ @Override
+ protected String getTestActivity() {
+ return TEST_ACTIVITY;
+ }
+
+ @Override
+ protected void putExtra(Intent intent) {
+ intent.putExtra(TEST_DISPLAYS, mDisplayCount);
}
/**
@@ -291,42 +260,8 @@ public class DisplayEventDeliveryTest {
}
/**
- * Return true if the freezer is enabled on this platform and if freezer notifications are
- * supported. It is not enough to test that the freezer notification feature is enabled
- * because some devices do not have the necessary kernel support.
- */
- private boolean isAppFreezerEnabled() {
- try {
- return mActivityManager.getService().isAppFreezerEnabled()
- && android.os.Flags.binderFrozenStateChangeCallback()
- && BinderProxy.isFrozenStateChangeCallbackSupported();
- } catch (Exception e) {
- Log.e(TAG, "isAppFreezerEnabled() failed: " + e);
- return false;
- }
- }
-
- private void waitForProcessFreeze(int pid, long timeoutMs) {
- // TODO: Add a listener to monitor freezer state changes.
- SystemUtil.runWithShellPermissionIdentity(() -> {
- TestUtils.waitUntil("Timed out waiting for test process to be frozen; pid=" + pid,
- (int) TimeUnit.MILLISECONDS.toSeconds(timeoutMs),
- () -> mActivityManager.isProcessFrozen(pid));
- });
- }
-
- private void waitForProcessUnfreeze(int pid, long timeoutMs) {
- // TODO: Add a listener to monitor freezer state changes.
- SystemUtil.runWithShellPermissionIdentity(() -> {
- TestUtils.waitUntil("Timed out waiting for test process to be frozen; pid=" + pid,
- (int) TimeUnit.MILLISECONDS.toSeconds(timeoutMs),
- () -> !mActivityManager.isProcessFrozen(pid));
- });
- }
-
- /**
- * Create virtual displays, change their configurations and release them. The number of
- * displays is set by the {@link #mDisplays} variable.
+ * Create virtual displays, change their configurations and release them. The number of
+ * displays is set by the {@link #data()} parameter.
*/
private void testDisplayEventsInternal(boolean cached, boolean frozen) {
Log.d(TAG, "Start test testDisplayEvents " + mDisplayCount + " " + cached + " " + frozen);
@@ -445,110 +380,6 @@ public class DisplayEventDeliveryTest {
}
/**
- * Launch the test activity that would listen to display events. Return its process ID.
- */
- private int launchTestActivity() {
- Intent intent = new Intent(Intent.ACTION_MAIN);
- intent.setClassName(TEST_PACKAGE, TEST_ACTIVITY);
- intent.putExtra(TEST_MESSENGER, mMessenger);
- intent.putExtra(TEST_DISPLAYS, mDisplayCount);
- intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- SystemUtil.runWithShellPermissionIdentity(
- () -> {
- mContext.startActivity(intent);
- },
- android.Manifest.permission.START_ACTIVITIES_FROM_SDK_SANDBOX);
- waitLatch(mLatchActivityLaunch);
-
- try {
- String cmd = "pidof " + TEST_PACKAGE;
- String result = SystemUtil.runShellCommand(mInstrumentation, cmd);
- return Integer.parseInt(result.trim());
- } catch (IOException e) {
- fail("failed to get pid of test package");
- return 0;
- } catch (NumberFormatException e) {
- fail("failed to parse pid " + e);
- return 0;
- }
- }
-
- /**
- * Bring the test activity back to top
- */
- private void bringTestActivityTop() {
- Intent intent = new Intent(Intent.ACTION_MAIN);
- intent.setClassName(TEST_PACKAGE, TEST_ACTIVITY);
- intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
- SystemUtil.runWithShellPermissionIdentity(
- () -> {
- mContext.startActivity(intent);
- },
- android.Manifest.permission.START_ACTIVITIES_FROM_SDK_SANDBOX);
- }
-
- /**
- * Bring the test activity into cached mode by launching another 2 apps
- */
- private void makeTestActivityCached() {
- // Launch another activity to bring the test activity into background
- Intent intent = new Intent(Intent.ACTION_MAIN);
- intent.setClass(mContext, SimpleActivity.class);
- intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
-
- // Launch another activity to bring the test activity into cached mode
- Intent intent2 = new Intent(Intent.ACTION_MAIN);
- intent2.setClass(mContext, SimpleActivity2.class);
- intent2.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- SystemUtil.runWithShellPermissionIdentity(
- () -> {
- mInstrumentation.startActivitySync(intent);
- mInstrumentation.startActivitySync(intent2);
- },
- android.Manifest.permission.START_ACTIVITIES_FROM_SDK_SANDBOX);
- waitLatch(mLatchActivityCached);
- }
-
- // Sleep, ignoring interrupts.
- private void pause(int s) {
- try { Thread.sleep(s * 1000); } catch (Exception e) { }
- }
-
- /**
- * Freeze the test activity.
- */
- private void makeTestActivityFrozen(int pid) {
- // The delay here is meant to allow pending binder transactions to drain. A process
- // cannot be frozen if it has pending binder transactions, and attempting to freeze such a
- // process more than a few times will result in the system killing the process.
- pause(5);
- try {
- String cmd = "am freeze --sticky ";
- SystemUtil.runShellCommand(mInstrumentation, cmd + TEST_PACKAGE);
- } catch (IOException e) {
- fail(e.toString());
- }
- // Wait for the freeze to complete in the kernel and for the frozen process
- // notification to settle out.
- waitForProcessFreeze(pid, 5 * 1000);
- }
-
- /**
- * Freeze the test activity.
- */
- private void makeTestActivityUnfrozen(int pid) {
- try {
- String cmd = "am unfreeze --sticky ";
- SystemUtil.runShellCommand(mInstrumentation, cmd + TEST_PACKAGE);
- } catch (IOException e) {
- fail(e.toString());
- }
- // Wait for the freeze to complete in the kernel and for the frozen process
- // notification to settle out.
- waitForProcessUnfreeze(pid, 5 * 1000);
- }
-
- /**
* Create a virtual display
*
* @param name The name of the new virtual display
@@ -560,15 +391,4 @@ public class DisplayEventDeliveryTest {
VIRTUAL_DISPLAY_FLAG_PUBLIC | VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY
/* flags: a public virtual display that another app can access */);
}
-
- /**
- * Wait for CountDownLatch with timeout
- */
- private void waitLatch(CountDownLatch latch) {
- try {
- latch.await(TEST_FAILURE_TIMEOUT_MSEC, TimeUnit.MILLISECONDS);
- } catch (InterruptedException e) {
- throw new RuntimeException(e);
- }
- }
}
diff --git a/services/tests/displayservicetests/src/com/android/server/display/EventDeliveryTestBase.java b/services/tests/displayservicetests/src/com/android/server/display/EventDeliveryTestBase.java
new file mode 100644
index 000000000000..2911b9bb35c7
--- /dev/null
+++ b/services/tests/displayservicetests/src/com/android/server/display/EventDeliveryTestBase.java
@@ -0,0 +1,259 @@
+/*
+ * 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.display;
+
+import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_CACHED;
+
+import static org.junit.Assert.fail;
+
+import android.app.ActivityManager;
+import android.app.Instrumentation;
+import android.content.Context;
+import android.content.Intent;
+import android.hardware.display.DisplayManager;
+import android.os.BinderProxy;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Messenger;
+import android.platform.test.annotations.AppModeSdkSandbox;
+import android.util.Log;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.compatibility.common.util.SystemUtil;
+import com.android.compatibility.common.util.TestUtils;
+
+import java.io.IOException;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+@AppModeSdkSandbox(reason = "Allow test in the SDK sandbox (does not prevent other modes).")
+public abstract class EventDeliveryTestBase {
+ protected static final int MESSAGE_LAUNCHED = 1;
+ protected static final int MESSAGE_CALLBACK = 2;
+
+ protected static final long EVENT_TIMEOUT_MSEC = 100;
+ protected static final long TEST_FAILURE_TIMEOUT_MSEC = 10000;
+
+ private static final String TEST_MESSENGER = "MESSENGER";
+
+ private Instrumentation mInstrumentation;
+ private Context mContext;
+ protected DisplayManager mDisplayManager;
+ private ActivityManager mActivityManager;
+ private ActivityManager.OnUidImportanceListener mUidImportanceListener;
+ protected CountDownLatch mLatchActivityLaunch;
+ private CountDownLatch mLatchActivityCached;
+ private HandlerThread mHandlerThread;
+ private Handler mHandler;
+ private Messenger mMessenger;
+ protected int mPid;
+ protected int mUid;
+
+ protected abstract String getTag();
+
+ protected abstract Handler getHandler(Looper looper);
+
+ protected abstract String getTestPackage();
+
+ protected abstract String getTestActivity();
+
+ protected abstract void putExtra(Intent intent);
+
+ protected void setUp() {
+ mInstrumentation = InstrumentationRegistry.getInstrumentation();
+ mContext = mInstrumentation.getContext();
+ mDisplayManager = mContext.getSystemService(DisplayManager.class);
+ mLatchActivityLaunch = new CountDownLatch(1);
+ mLatchActivityCached = new CountDownLatch(1);
+ mActivityManager = mContext.getSystemService(ActivityManager.class);
+ mUidImportanceListener = (uid, importance) -> {
+ if (uid == mUid && importance == IMPORTANCE_CACHED) {
+ Log.d(getTag(), "Listener " + uid + " becomes " + importance);
+ mLatchActivityCached.countDown();
+ }
+ };
+ SystemUtil.runWithShellPermissionIdentity(() ->
+ mActivityManager.addOnUidImportanceListener(mUidImportanceListener,
+ IMPORTANCE_CACHED));
+ mHandlerThread = new HandlerThread("handler");
+ mHandlerThread.start();
+ mHandler = getHandler(mHandlerThread.getLooper());
+ mMessenger = new Messenger(mHandler);
+ mPid = 0;
+ }
+
+ protected void tearDown() throws Exception {
+ mActivityManager.removeOnUidImportanceListener(mUidImportanceListener);
+ mHandlerThread.quitSafely();
+ SystemUtil.runShellCommand(mInstrumentation, "am force-stop " + getTestPackage());
+ }
+
+ /**
+ * Return true if the freezer is enabled on this platform and if freezer notifications are
+ * supported. It is not enough to test that the freezer notification feature is enabled
+ * because some devices do not have the necessary kernel support.
+ */
+ protected boolean isAppFreezerEnabled() {
+ try {
+ return ActivityManager.getService().isAppFreezerEnabled()
+ && android.os.Flags.binderFrozenStateChangeCallback()
+ && BinderProxy.isFrozenStateChangeCallbackSupported();
+ } catch (Exception e) {
+ Log.e(getTag(), "isAppFreezerEnabled() failed: " + e);
+ return false;
+ }
+ }
+
+ private void waitForProcessFreeze(int pid, long timeoutMs) {
+ // TODO: Add a listener to monitor freezer state changes.
+ SystemUtil.runWithShellPermissionIdentity(() -> {
+ TestUtils.waitUntil(
+ "Timed out waiting for test process to be frozen; pid=" + pid,
+ (int) TimeUnit.MILLISECONDS.toSeconds(timeoutMs),
+ () -> mActivityManager.isProcessFrozen(pid));
+ });
+ }
+
+ private void waitForProcessUnfreeze(int pid, long timeoutMs) {
+ // TODO: Add a listener to monitor freezer state changes.
+ SystemUtil.runWithShellPermissionIdentity(() -> {
+ TestUtils.waitUntil("Timed out waiting for test process to be frozen; pid=" + pid,
+ (int) TimeUnit.MILLISECONDS.toSeconds(timeoutMs),
+ () -> !mActivityManager.isProcessFrozen(pid));
+ });
+ }
+
+ /**
+ * Launch the test activity that would listen to events. Return its process ID.
+ */
+ protected int launchTestActivity() {
+ Intent intent = new Intent(Intent.ACTION_MAIN);
+ intent.setClassName(getTestPackage(), getTestActivity());
+ intent.putExtra(TEST_MESSENGER, mMessenger);
+ putExtra(intent);
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ SystemUtil.runWithShellPermissionIdentity(
+ () -> {
+ mContext.startActivity(intent);
+ },
+ android.Manifest.permission.START_ACTIVITIES_FROM_SDK_SANDBOX);
+ waitLatch(mLatchActivityLaunch);
+
+ try {
+ String cmd = "pidof " + getTestPackage();
+ String result = SystemUtil.runShellCommand(mInstrumentation, cmd);
+ return Integer.parseInt(result.trim());
+ } catch (IOException e) {
+ fail("failed to get pid of test package");
+ return 0;
+ } catch (NumberFormatException e) {
+ fail("failed to parse pid " + e);
+ return 0;
+ }
+ }
+
+ /**
+ * Bring the test activity back to top
+ */
+ protected void bringTestActivityTop() {
+ Intent intent = new Intent(Intent.ACTION_MAIN);
+ intent.setClassName(getTestPackage(), getTestActivity());
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
+ SystemUtil.runWithShellPermissionIdentity(
+ () -> {
+ mContext.startActivity(intent);
+ },
+ android.Manifest.permission.START_ACTIVITIES_FROM_SDK_SANDBOX);
+ }
+
+
+ /**
+ * Bring the test activity into cached mode by launching another 2 apps
+ */
+ protected void makeTestActivityCached() {
+ // Launch another activity to bring the test activity into background
+ Intent intent = new Intent(Intent.ACTION_MAIN);
+ intent.setClass(mContext, SimpleActivity.class);
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
+
+ // Launch another activity to bring the test activity into cached mode
+ Intent intent2 = new Intent(Intent.ACTION_MAIN);
+ intent2.setClass(mContext, SimpleActivity2.class);
+ intent2.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ SystemUtil.runWithShellPermissionIdentity(
+ () -> {
+ mInstrumentation.startActivitySync(intent);
+ mInstrumentation.startActivitySync(intent2);
+ },
+ android.Manifest.permission.START_ACTIVITIES_FROM_SDK_SANDBOX);
+ waitLatch(mLatchActivityCached);
+ }
+
+ // Sleep, ignoring interrupts.
+ private void pause(int s) {
+ try {
+ Thread.sleep(s * 1000L);
+ } catch (Exception ignored) { }
+ }
+
+ /**
+ * Freeze the test activity.
+ */
+ protected void makeTestActivityFrozen(int pid) {
+ // The delay here is meant to allow pending binder transactions to drain. A process
+ // cannot be frozen if it has pending binder transactions, and attempting to freeze such a
+ // process more than a few times will result in the system killing the process.
+ pause(5);
+ try {
+ String cmd = "am freeze --sticky ";
+ SystemUtil.runShellCommand(mInstrumentation, cmd + getTestPackage());
+ } catch (IOException e) {
+ fail(e.toString());
+ }
+ // Wait for the freeze to complete in the kernel and for the frozen process
+ // notification to settle out.
+ waitForProcessFreeze(pid, 5 * 1000);
+ }
+
+ /**
+ * Freeze the test activity.
+ */
+ protected void makeTestActivityUnfrozen(int pid) {
+ try {
+ String cmd = "am unfreeze --sticky ";
+ SystemUtil.runShellCommand(mInstrumentation, cmd + getTestPackage());
+ } catch (IOException e) {
+ fail(e.toString());
+ }
+ // Wait for the freeze to complete in the kernel and for the frozen process
+ // notification to settle out.
+ waitForProcessUnfreeze(pid, 5 * 1000);
+ }
+
+ /**
+ * Wait for CountDownLatch with timeout
+ */
+ private void waitLatch(CountDownLatch latch) {
+ try {
+ latch.await(TEST_FAILURE_TIMEOUT_MSEC, TimeUnit.MILLISECONDS);
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+}
diff --git a/services/tests/displayservicetests/src/com/android/server/display/TopologyUpdateDeliveryTest.java b/services/tests/displayservicetests/src/com/android/server/display/TopologyUpdateDeliveryTest.java
new file mode 100644
index 000000000000..5fd248dba53f
--- /dev/null
+++ b/services/tests/displayservicetests/src/com/android/server/display/TopologyUpdateDeliveryTest.java
@@ -0,0 +1,243 @@
+/*
+ * 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.display;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeTrue;
+
+import android.content.Intent;
+import android.hardware.display.DisplayTopology;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Tests that applications can receive topology updates correctly.
+ */
+public class TopologyUpdateDeliveryTest extends EventDeliveryTestBase {
+ private static final String TAG = TopologyUpdateDeliveryTest.class.getSimpleName();
+
+ @Rule
+ public final CheckFlagsRule mCheckFlagsRule =
+ DeviceFlagsValueProvider.createCheckFlagsRule();
+
+ private static final String TEST_PACKAGE = "com.android.servicestests.apps.topologytestapp";
+ private static final String TEST_ACTIVITY = TEST_PACKAGE + ".TopologyUpdateActivity";
+
+ // Topology updates we expect to receive before timeout
+ private final LinkedBlockingQueue<DisplayTopology> mExpectations = new LinkedBlockingQueue<>();
+
+ /**
+ * Add the received topology update from the test activity to the queue
+ *
+ * @param topology The corresponding topology update
+ */
+ private void addTopologyUpdate(DisplayTopology topology) {
+ Log.d(TAG, "Received " + topology);
+ mExpectations.offer(topology);
+ }
+
+ /**
+ * Assert that there isn't any unexpected display event from the test activity
+ */
+ private void assertNoTopologyUpdates() {
+ try {
+ assertNull(mExpectations.poll(EVENT_TIMEOUT_MSEC, TimeUnit.MILLISECONDS));
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * Wait for the expected topology update from the test activity
+ *
+ * @param expect The expected topology update
+ */
+ private void waitTopologyUpdate(DisplayTopology expect) {
+ while (true) {
+ try {
+ DisplayTopology update = mExpectations.poll(TEST_FAILURE_TIMEOUT_MSEC,
+ TimeUnit.MILLISECONDS);
+ assertNotNull(update);
+ if (expect.equals(update)) {
+ Log.d(TAG, "Found " + update);
+ return;
+ }
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+
+ private class TestHandler extends Handler {
+ TestHandler(Looper looper) {
+ super(looper);
+ }
+
+ @Override
+ public void handleMessage(@NonNull Message msg) {
+ switch (msg.what) {
+ case MESSAGE_LAUNCHED:
+ mPid = msg.arg1;
+ mUid = msg.arg2;
+ Log.d(TAG, "Launched " + mPid + " " + mUid);
+ mLatchActivityLaunch.countDown();
+ break;
+ case MESSAGE_CALLBACK:
+ DisplayTopology topology = (DisplayTopology) msg.obj;
+ Log.d(TAG, "Callback " + topology);
+ addTopologyUpdate(topology);
+ break;
+ default:
+ fail("Unexpected value: " + msg.what);
+ break;
+ }
+ }
+ }
+
+ @Before
+ public void setUp() {
+ super.setUp();
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ super.tearDown();
+ }
+
+ @Override
+ protected String getTag() {
+ return TAG;
+ }
+
+ @Override
+ protected Handler getHandler(Looper looper) {
+ return new TestHandler(looper);
+ }
+
+ @Override
+ protected String getTestPackage() {
+ return TEST_PACKAGE;
+ }
+
+ @Override
+ protected String getTestActivity() {
+ return TEST_ACTIVITY;
+ }
+
+ @Override
+ protected void putExtra(Intent intent) { }
+
+ private void testTopologyUpdateInternal(boolean cached, boolean frozen) {
+ Log.d(TAG, "Start test testTopologyUpdate " + cached + " " + frozen);
+ // Launch activity and start listening to topology updates
+ int pid = launchTestActivity();
+
+ // The test activity in cached or frozen mode won't receive the pending topology updates.
+ if (cached) {
+ makeTestActivityCached();
+ }
+ if (frozen) {
+ makeTestActivityFrozen(pid);
+ }
+
+ // Change the topology
+ int primaryDisplayId = 3;
+ DisplayTopology.TreeNode root = new DisplayTopology.TreeNode(primaryDisplayId,
+ /* width= */ 600, /* height= */ 400, DisplayTopology.TreeNode.POSITION_LEFT,
+ /* offset= */ 0);
+ DisplayTopology.TreeNode child = new DisplayTopology.TreeNode(/* displayId= */ 1,
+ /* width= */ 800, /* height= */ 600, DisplayTopology.TreeNode.POSITION_LEFT,
+ /* offset= */ 0);
+ root.addChild(child);
+ DisplayTopology topology = new DisplayTopology(root, primaryDisplayId);
+ mDisplayManager.setDisplayTopology(topology);
+
+ if (cached || frozen) {
+ assertNoTopologyUpdates();
+ } else {
+ waitTopologyUpdate(topology);
+ }
+
+ // Unfreeze the test activity, if it was frozen.
+ if (frozen) {
+ makeTestActivityUnfrozen(pid);
+ }
+
+ if (cached || frozen) {
+ // Always ensure the test activity is not cached.
+ bringTestActivityTop();
+
+ // The test activity becomes non-cached and should receive the pending topology updates
+ waitTopologyUpdate(topology);
+ }
+ }
+
+ @Test
+ @RequiresFlagsEnabled(com.android.server.display.feature.flags.Flags.FLAG_DISPLAY_TOPOLOGY)
+ public void testTopologyUpdate() {
+ testTopologyUpdateInternal(false, false);
+ }
+
+ /**
+ * The app is moved to cached and the test verifies that no updates are delivered to the cached
+ * app.
+ */
+ @Test
+ @RequiresFlagsEnabled(com.android.server.display.feature.flags.Flags.FLAG_DISPLAY_TOPOLOGY)
+ public void testTopologyUpdateCached() {
+ testTopologyUpdateInternal(true, false);
+ }
+
+ /**
+ * The app is frozen and the test verifies that no updates are delivered to the frozen app.
+ */
+ @RequiresFlagsEnabled({com.android.server.am.Flags.FLAG_DEFER_DISPLAY_EVENTS_WHEN_FROZEN,
+ com.android.server.display.feature.flags.Flags.FLAG_DISPLAY_TOPOLOGY})
+ @Test
+ public void testTopologyUpdateFrozen() {
+ assumeTrue(isAppFreezerEnabled());
+ testTopologyUpdateInternal(false, true);
+ }
+
+ /**
+ * The app is cached and frozen and the test verifies that no updates are delivered to the app.
+ */
+ @RequiresFlagsEnabled({com.android.server.am.Flags.FLAG_DEFER_DISPLAY_EVENTS_WHEN_FROZEN,
+ com.android.server.display.feature.flags.Flags.FLAG_DISPLAY_TOPOLOGY})
+ @Test
+ public void testTopologyUpdateCachedFrozen() {
+ assumeTrue(isAppFreezerEnabled());
+ testTopologyUpdateInternal(true, true);
+ }
+}
diff --git a/services/tests/displayservicetests/src/com/android/server/display/mode/AppRequestObserverTest.kt b/services/tests/displayservicetests/src/com/android/server/display/mode/AppRequestObserverTest.kt
index 1f3f19fa3ea8..218728541774 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/mode/AppRequestObserverTest.kt
+++ b/services/tests/displayservicetests/src/com/android/server/display/mode/AppRequestObserverTest.kt
@@ -89,6 +89,39 @@ class AppRequestObserverTest {
assertThat(renderRateVote).isEqualTo(testCase.expectedRenderRateVote)
}
+ @Test
+ fun testAppRequestVote_externalDisplay() {
+ val displayModeDirector = DisplayModeDirector(
+ context, testHandler, mockInjector, mockFlags, mockDisplayDeviceConfigProvider)
+ val modes = arrayOf(
+ Display.Mode(1, 1000, 1000, 60f),
+ Display.Mode(2, 1000, 1000, 90f),
+ )
+
+ displayModeDirector.injectAppSupportedModesByDisplay(
+ SparseArray<Array<Display.Mode>>().apply {
+ append(Display.DEFAULT_DISPLAY, modes)
+ })
+ displayModeDirector.injectDefaultModeByDisplay(SparseArray<Display.Mode>().apply {
+ append(Display.DEFAULT_DISPLAY, modes[0])
+ })
+ displayModeDirector.addExternalDisplayId(Display.DEFAULT_DISPLAY)
+
+ displayModeDirector.appRequestObserver.setAppRequest(Display.DEFAULT_DISPLAY, 1, 0f, 0f, 0f)
+
+ val baseModeVote = displayModeDirector.getVote(Display.DEFAULT_DISPLAY,
+ Vote.PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE)
+ assertThat(baseModeVote).isEqualTo(BaseModeRefreshRateVote(60f))
+
+ val sizeVote = displayModeDirector.getVote(Display.DEFAULT_DISPLAY,
+ Vote.PRIORITY_APP_REQUEST_SIZE)
+ assertThat(sizeVote).isNull()
+
+ val renderRateVote = displayModeDirector.getVote(Display.DEFAULT_DISPLAY,
+ Vote.PRIORITY_APP_REQUEST_RENDER_FRAME_RATE_RANGE)
+ assertThat(renderRateVote).isNull()
+ }
+
enum class AppRequestTestCase(
val ignoreRefreshRateRequest: Boolean,
val modeId: Int,
diff --git a/services/tests/displayservicetests/src/com/android/server/display/mode/ModeChangeObserverTest.kt b/services/tests/displayservicetests/src/com/android/server/display/mode/ModeChangeObserverTest.kt
new file mode 100644
index 000000000000..a5fb6cdc8d4f
--- /dev/null
+++ b/services/tests/displayservicetests/src/com/android/server/display/mode/ModeChangeObserverTest.kt
@@ -0,0 +1,187 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.display.mode
+
+import android.os.Looper
+import android.view.Display
+import android.view.DisplayAddress
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.junit.MockitoJUnit
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
+
+private const val PHYSICAL_DISPLAY_ID_1 = 1L
+private const val PHYSICAL_DISPLAY_ID_2 = 2L
+private const val MODE_ID_1 = 3
+private const val MODE_ID_2 = 4
+private const val LOGICAL_DISPLAY_ID = 5
+private val physicalAddress1 = DisplayAddress.fromPhysicalDisplayId(PHYSICAL_DISPLAY_ID_1)
+private val physicalAddress2 = DisplayAddress.fromPhysicalDisplayId(PHYSICAL_DISPLAY_ID_2)
+
+/**
+ * Tests for ModeChangeObserver, comply with changes in b/31925610
+ */
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+public class ModeChangeObserverTest {
+ @get:Rule
+ val mockitoRule = MockitoJUnit.rule()
+
+ // System Under Test
+ private lateinit var modeChangeObserver: ModeChangeObserver
+
+ // Non Mocks
+ private val looper = Looper.getMainLooper()
+ private val votesStorage = VotesStorage({}, null)
+
+ // Mocks
+ private val mockInjector = mock<DisplayModeDirector.Injector>()
+ private val mockDisplay1 = mock<Display>()
+ private val mockDisplay2 = mock<Display>()
+
+ @Before
+ fun setUp() {
+ whenever(mockInjector.getDisplay(LOGICAL_DISPLAY_ID)).thenReturn(mockDisplay1)
+ whenever(mockDisplay1.getAddress()).thenReturn(physicalAddress1)
+ whenever(mockInjector.getDisplays()).thenReturn(arrayOf<Display>())
+ modeChangeObserver = ModeChangeObserver(votesStorage, mockInjector, looper)
+ modeChangeObserver.observe()
+ }
+
+ @Test
+ fun testOnModeRejectedBeforeDisplayAdded() {
+ val rejectedModes = HashSet<Int>()
+ rejectedModes.add(MODE_ID_1)
+ rejectedModes.add(MODE_ID_2)
+
+ // ModeRejected is called before display is mapped, hence votes are null
+ modeChangeObserver.mModeChangeListener.onModeRejected(PHYSICAL_DISPLAY_ID_1, MODE_ID_1)
+ modeChangeObserver.mModeChangeListener.onModeRejected(PHYSICAL_DISPLAY_ID_1, MODE_ID_2)
+ val votes = votesStorage.getVotes(LOGICAL_DISPLAY_ID)
+ assertThat(votes.size()).isEqualTo(0)
+
+ // Display is mapped to a Logical Display Id, now the Rejected Mode Votes get updated
+ modeChangeObserver.mDisplayListener.onDisplayAdded(LOGICAL_DISPLAY_ID)
+ val newVotes = votesStorage.getVotes(LOGICAL_DISPLAY_ID)
+ assertThat(newVotes.size()).isEqualTo(1)
+ val vote = newVotes.get(Vote.PRIORITY_REJECTED_MODES)
+ assertThat(vote).isInstanceOf(RejectedModesVote::class.java)
+ val rejectedModesVote = vote as RejectedModesVote
+ assertThat(rejectedModesVote.mModeIds.size).isEqualTo(rejectedModes.size)
+ assertThat(rejectedModesVote.mModeIds).contains(MODE_ID_1)
+ assertThat(rejectedModesVote.mModeIds).contains(MODE_ID_2)
+ }
+
+ @Test
+ fun testOnDisplayAddedBeforeOnModeRejected() {
+ // Display is mapped to the corresponding Logical Id, but Mode Rejected no received yet
+ // Verify that the Vote is still Null
+ modeChangeObserver.mDisplayListener.onDisplayAdded(LOGICAL_DISPLAY_ID)
+ val votes = votesStorage.getVotes(LOGICAL_DISPLAY_ID)
+ assertThat(votes.size()).isEqualTo(0)
+
+ // ModeRejected Event received for the mapped display
+ modeChangeObserver.mModeChangeListener.onModeRejected(PHYSICAL_DISPLAY_ID_1, MODE_ID_1)
+ val newVotes = votesStorage.getVotes(LOGICAL_DISPLAY_ID)
+ assertThat(newVotes.size()).isEqualTo(1)
+ val vote = newVotes.get(Vote.PRIORITY_REJECTED_MODES)
+ assertThat(vote).isInstanceOf(RejectedModesVote::class.java)
+ val rejectedModesVote = vote as RejectedModesVote
+ assertThat(rejectedModesVote.mModeIds.size).isEqualTo(1)
+ assertThat(rejectedModesVote.mModeIds).contains(MODE_ID_1)
+ }
+
+ @Test
+ fun testOnDisplayAddedThenRejectedThenRemoved() {
+ // Display is mapped to its Logical Display Id
+ modeChangeObserver.mDisplayListener.onDisplayAdded(LOGICAL_DISPLAY_ID)
+ val votes = votesStorage.getVotes(LOGICAL_DISPLAY_ID)
+ assertThat(votes.size()).isEqualTo(0)
+
+ // ModeRejected Event is received for mapped display
+ modeChangeObserver.mModeChangeListener.onModeRejected(PHYSICAL_DISPLAY_ID_1, MODE_ID_1)
+ val newVotes = votesStorage.getVotes(LOGICAL_DISPLAY_ID)
+ assertThat(newVotes.size()).isEqualTo(1)
+ val vote = newVotes.get(Vote.PRIORITY_REJECTED_MODES)
+ assertThat(vote).isInstanceOf(RejectedModesVote::class.java)
+ val rejectedModesVote = vote as RejectedModesVote
+ assertThat(rejectedModesVote.mModeIds.size).isEqualTo(1)
+ assertThat(rejectedModesVote.mModeIds).contains(MODE_ID_1)
+
+ // Display mapping is removed, hence remove the votes
+ modeChangeObserver.mDisplayListener.onDisplayRemoved(LOGICAL_DISPLAY_ID)
+ val finalVotes = votesStorage.getVotes(LOGICAL_DISPLAY_ID)
+ assertThat(finalVotes.size()).isEqualTo(0)
+ }
+
+ @Test
+ fun testForModesRejectedAfterDisplayChanged() {
+ // Mock Display 1 is mapped to logicalId
+ modeChangeObserver.mDisplayListener.onDisplayAdded(LOGICAL_DISPLAY_ID)
+ val votes = votesStorage.getVotes(LOGICAL_DISPLAY_ID)
+ assertThat(votes.size()).isEqualTo(0)
+
+ // Mode Rejected received for PhysicalId2 not mapped yet, so votes are null
+ whenever(mockInjector.getDisplay(LOGICAL_DISPLAY_ID)).thenReturn(mockDisplay2)
+ whenever(mockDisplay2.getAddress()).thenReturn(physicalAddress2)
+ modeChangeObserver.mModeChangeListener.onModeRejected(PHYSICAL_DISPLAY_ID_2, MODE_ID_2)
+ val changedVotes = votesStorage.getVotes(LOGICAL_DISPLAY_ID)
+ assertThat(changedVotes.size()).isEqualTo(0)
+
+ // Display mapping changed, now PhysicalId2 is mapped to the LogicalId, votes get updated
+ modeChangeObserver.mDisplayListener.onDisplayChanged(LOGICAL_DISPLAY_ID)
+ val finalVotes = votesStorage.getVotes(LOGICAL_DISPLAY_ID)
+ assertThat(finalVotes.size()).isEqualTo(1)
+ val finalVote = finalVotes.get(Vote.PRIORITY_REJECTED_MODES)
+ assertThat(finalVote).isInstanceOf(RejectedModesVote::class.java)
+ val newRejectedModesVote = finalVote as RejectedModesVote
+ assertThat(newRejectedModesVote.mModeIds.size).isEqualTo(1)
+ assertThat(newRejectedModesVote.mModeIds).contains(MODE_ID_2)
+ }
+
+ @Test
+ fun testForModesNotRejectedAfterDisplayChanged() {
+ // Mock Display 1 is added
+ modeChangeObserver.mDisplayListener.onDisplayAdded(LOGICAL_DISPLAY_ID)
+ val votes = votesStorage.getVotes(LOGICAL_DISPLAY_ID)
+ assertThat(votes.size()).isEqualTo(0)
+
+ // Mode Rejected received for Display 1, votes added for rejected mode
+ modeChangeObserver.mModeChangeListener.onModeRejected(PHYSICAL_DISPLAY_ID_1, MODE_ID_1)
+ val newVotes = votesStorage.getVotes(LOGICAL_DISPLAY_ID)
+ assertThat(newVotes.size()).isEqualTo(1)
+ val vote = newVotes.get(Vote.PRIORITY_REJECTED_MODES)
+ assertThat(vote).isInstanceOf(RejectedModesVote::class.java)
+ val rejectedModesVote = vote as RejectedModesVote
+ assertThat(rejectedModesVote.mModeIds.size).isEqualTo(1)
+ assertThat(rejectedModesVote.mModeIds).contains(MODE_ID_1)
+
+ // Display Changed such that logical Id corresponds to PhysicalDisplayId2
+ // Rejected Modes Vote is removed
+ whenever(mockInjector.getDisplay(LOGICAL_DISPLAY_ID)).thenReturn(mockDisplay2)
+ whenever(mockDisplay2.getAddress()).thenReturn(physicalAddress2)
+ modeChangeObserver.mDisplayListener.onDisplayChanged(LOGICAL_DISPLAY_ID)
+ val finalVotes = votesStorage.getVotes(LOGICAL_DISPLAY_ID)
+ assertThat(finalVotes.size()).isEqualTo(0)
+ }
+} \ No newline at end of file
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/ProcessObserverTest.java b/services/tests/mockingservicestests/src/com/android/server/am/ProcessObserverTest.java
index 43becc59c3cb..558ea29d85fc 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/ProcessObserverTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/ProcessObserverTest.java
@@ -42,6 +42,8 @@ import android.os.Binder;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.IBinder;
+import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.SetFlagsRule;
import android.util.Log;
import androidx.test.filters.MediumTest;
@@ -79,6 +81,8 @@ public class ProcessObserverTest {
@Rule
public final ApplicationExitInfoTest.ServiceThreadRule
mServiceThreadRule = new ApplicationExitInfoTest.ServiceThreadRule();
+ @Rule
+ public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
private Context mContext;
private HandlerThread mHandlerThread;
@@ -239,8 +243,8 @@ public class ProcessObserverTest {
/**
* Verify that a process start event is dispatched to process observers.
*/
- @Ignore("b/323959187")
@Test
+ @EnableFlags(android.app.Flags.FLAG_ENABLE_PROCESS_OBSERVER_BROADCAST_ON_PROCESS_STARTED)
public void testNormal() throws Exception {
ProcessRecord app = startProcess();
verify(mProcessObserver).onProcessStarted(
diff --git a/services/tests/servicestests/Android.bp b/services/tests/servicestests/Android.bp
index d702cae248a9..067fba9893e5 100644
--- a/services/tests/servicestests/Android.bp
+++ b/services/tests/servicestests/Android.bp
@@ -31,6 +31,7 @@ android_test {
"test-apps/SuspendTestApp/src/**/*.java",
"test-apps/DisplayManagerTestApp/src/**/*.java",
+ "test-apps/TopologyTestApp/src/**/*.java",
],
static_libs: [
@@ -141,6 +142,7 @@ android_test {
data: [
":DisplayManagerTestApp",
+ ":TopologyTestApp",
":SimpleServiceTestApp1",
":SimpleServiceTestApp2",
":SimpleServiceTestApp3",
diff --git a/services/tests/servicestests/AndroidTest.xml b/services/tests/servicestests/AndroidTest.xml
index 5298251b79f7..9a4983482522 100644
--- a/services/tests/servicestests/AndroidTest.xml
+++ b/services/tests/servicestests/AndroidTest.xml
@@ -36,6 +36,7 @@
<option name="cleanup-apks" value="true" />
<option name="install-arg" value="-t" />
<option name="test-file-name" value="DisplayManagerTestApp.apk" />
+ <option name="test-file-name" value="TopologyTestApp.apk" />
<option name="test-file-name" value="FrameworksServicesTests.apk" />
<option name="test-file-name" value="SuspendTestApp.apk" />
<option name="test-file-name" value="SimpleServiceTestApp1.apk" />
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java
index 2ccd33648c3e..e0bc3e76f31d 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java
@@ -82,6 +82,7 @@ import android.content.pm.PackageManager;
import android.content.pm.PackageManagerInternal;
import android.content.pm.ResolveInfo;
import android.content.pm.ServiceInfo;
+import android.content.pm.UserInfo;
import android.content.res.XmlResourceParser;
import android.graphics.drawable.Icon;
import android.hardware.display.DisplayManager;
@@ -131,6 +132,7 @@ import com.android.internal.accessibility.util.ShortcutUtils;
import com.android.internal.compat.IPlatformCompat;
import com.android.internal.content.PackageMonitor;
import com.android.server.LocalServices;
+import com.android.server.SystemService;
import com.android.server.accessibility.AccessibilityManagerService.AccessibilityDisplayListener;
import com.android.server.accessibility.magnification.FullScreenMagnificationController;
import com.android.server.accessibility.magnification.MagnificationConnectionManager;
@@ -2122,6 +2124,36 @@ public class AccessibilityManagerServiceTest {
}
@Test
+ @EnableFlags(Flags.FLAG_MANAGER_LIFECYCLE_USER_CHANGE)
+ public void lifecycle_onUserSwitching_switchesUser() throws RemoteException {
+ mA11yms.mUserInitializationCompleteCallbacks.add(mUserInitializationCompleteCallback);
+ AccessibilityManagerService.Lifecycle lifecycle =
+ new AccessibilityManagerService.Lifecycle(mTestableContext, mA11yms);
+ int newUserId = mA11yms.getCurrentUserIdLocked() + 1;
+
+ lifecycle.onUserSwitching(
+ new SystemService.TargetUser(new UserInfo(0, "USER", 0)),
+ new SystemService.TargetUser(new UserInfo(newUserId, "USER", 0)));
+ mTestableLooper.processAllMessages();
+
+ verify(mUserInitializationCompleteCallback).onUserInitializationComplete(newUserId);
+ }
+
+ @Test
+ @DisableFlags(Flags.FLAG_MANAGER_LIFECYCLE_USER_CHANGE)
+ public void intent_user_switched_switchesUser() throws RemoteException {
+ mA11yms.mUserInitializationCompleteCallbacks.add(mUserInitializationCompleteCallback);
+ int newUserId = mA11yms.getCurrentUserIdLocked() + 1;
+ final Intent intent = new Intent(Intent.ACTION_USER_SWITCHED);
+ intent.putExtra(Intent.EXTRA_USER_HANDLE, newUserId);
+
+ sendBroadcastToAccessibilityManagerService(intent, mA11yms.getCurrentUserIdLocked());
+ mTestableLooper.processAllMessages();
+
+ verify(mUserInitializationCompleteCallback).onUserInitializationComplete(newUserId);
+ }
+
+ @Test
@DisableFlags(android.provider.Flags.FLAG_A11Y_STANDALONE_GESTURE_ENABLED)
public void getShortcutTypeForGenericShortcutCalls_softwareType() {
final AccessibilityUserState userState = new AccessibilityUserState(
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickControllerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickControllerTest.java
index 99c922ca30c4..df77866b5e7f 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickControllerTest.java
@@ -25,6 +25,7 @@ import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -866,6 +867,23 @@ public class AutoclickControllerTest {
@Test
@EnableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_AUTOCLICK_INDICATOR)
+ public void scrollPanelController_directionalButtonsHideIndicator() {
+ injectFakeMouseActionHoverMoveEvent();
+
+ // Create a spy on the real object to verify method calls.
+ AutoclickIndicatorView spyIndicatorView = spy(mController.mAutoclickIndicatorView);
+ mController.mAutoclickIndicatorView = spyIndicatorView;
+
+ // Simulate hover on direction button.
+ mController.mScrollPanelController.onHoverButtonChange(
+ AutoclickScrollPanel.DIRECTION_UP, true);
+
+ // Verify clearIndicator was called.
+ verify(spyIndicatorView).clearIndicator();
+ }
+
+ @Test
+ @EnableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_AUTOCLICK_INDICATOR)
public void hoverOnAutoclickPanel_rightClickType_forceTriggerLeftClick() {
MotionEventCaptor motionEventCaptor = new MotionEventCaptor();
mController.setNext(motionEventCaptor);
diff --git a/services/tests/servicestests/src/com/android/server/audio/AudioVolumeChangeHandlerTest.java b/services/tests/servicestests/src/com/android/server/audio/AudioVolumeChangeHandlerTest.java
new file mode 100644
index 000000000000..f252a9848bbf
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/audio/AudioVolumeChangeHandlerTest.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.audio;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.junit.Assert.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.media.INativeAudioVolumeGroupCallback;
+import android.media.audio.common.AudioVolumeGroupChangeEvent;
+import android.media.audiopolicy.IAudioVolumeChangeDispatcher;
+import android.os.IBinder;
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.filters.MediumTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@MediumTest
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class AudioVolumeChangeHandlerTest {
+ private static final long DEFAULT_TIMEOUT_MS = 1000;
+
+ private AudioSystemAdapter mSpyAudioSystem;
+
+ AudioVolumeChangeHandler mAudioVolumeChangedHandler;
+
+ private final IAudioVolumeChangeDispatcher.Stub mMockDispatcher =
+ mock(IAudioVolumeChangeDispatcher.Stub.class);
+
+ @Before
+ public void setUp() {
+ mSpyAudioSystem = spy(new NoOpAudioSystemAdapter());
+ when(mMockDispatcher.asBinder()).thenReturn(mock(IBinder.class));
+ mAudioVolumeChangedHandler = new AudioVolumeChangeHandler(mSpyAudioSystem);
+ }
+
+ @Test
+ public void registerListener_withInvalidCallback() {
+ IAudioVolumeChangeDispatcher.Stub nullCb = null;
+ NullPointerException thrown = assertThrows(NullPointerException.class, () -> {
+ mAudioVolumeChangedHandler.registerListener(nullCb);
+ });
+
+ assertWithMessage("Exception for invalid registration").that(thrown).hasMessageThat()
+ .contains("Volume group callback");
+ }
+
+ @Test
+ public void unregisterListener_withInvalidCallback() {
+ IAudioVolumeChangeDispatcher.Stub nullCb = null;
+ mAudioVolumeChangedHandler.registerListener(mMockDispatcher);
+
+ NullPointerException thrown = assertThrows(NullPointerException.class, () -> {
+ mAudioVolumeChangedHandler.unregisterListener(nullCb);
+ });
+
+ assertWithMessage("Exception for invalid un-registration").that(thrown).hasMessageThat()
+ .contains("Volume group callback");
+ }
+
+ @Test
+ public void registerListener() {
+ mAudioVolumeChangedHandler.registerListener(mMockDispatcher);
+
+ verify(mSpyAudioSystem).registerAudioVolumeGroupCallback(any());
+ }
+
+ @Test
+ public void onAudioVolumeGroupChanged() throws Exception {
+ mAudioVolumeChangedHandler.registerListener(mMockDispatcher);
+ AudioVolumeGroupChangeEvent volEvent = new AudioVolumeGroupChangeEvent();
+ volEvent.groupId = 666;
+ volEvent.flags = AudioVolumeGroupChangeEvent.VOLUME_FLAG_FROM_KEY;
+
+ captureRegisteredNativeCallback().onAudioVolumeGroupChanged(volEvent);
+
+ verify(mMockDispatcher, timeout(DEFAULT_TIMEOUT_MS)).onAudioVolumeGroupChanged(
+ eq(volEvent.groupId), eq(volEvent.flags));
+ }
+
+ @Test
+ public void onAudioVolumeGroupChanged_withMultipleCallback() throws Exception {
+ int callbackCount = 10;
+ List<IAudioVolumeChangeDispatcher.Stub> validCbs =
+ new ArrayList<IAudioVolumeChangeDispatcher.Stub>();
+ for (int i = 0; i < callbackCount; i++) {
+ IAudioVolumeChangeDispatcher.Stub cb = mock(IAudioVolumeChangeDispatcher.Stub.class);
+ when(cb.asBinder()).thenReturn(mock(IBinder.class));
+ validCbs.add(cb);
+ }
+ for (IAudioVolumeChangeDispatcher.Stub cb : validCbs) {
+ mAudioVolumeChangedHandler.registerListener(cb);
+ }
+ AudioVolumeGroupChangeEvent volEvent = new AudioVolumeGroupChangeEvent();
+ volEvent.groupId = 666;
+ volEvent.flags = AudioVolumeGroupChangeEvent.VOLUME_FLAG_FROM_KEY;
+ captureRegisteredNativeCallback().onAudioVolumeGroupChanged(volEvent);
+
+ for (IAudioVolumeChangeDispatcher.Stub cb : validCbs) {
+ verify(cb, timeout(DEFAULT_TIMEOUT_MS)).onAudioVolumeGroupChanged(
+ eq(volEvent.groupId), eq(volEvent.flags));
+ }
+ }
+
+ private INativeAudioVolumeGroupCallback captureRegisteredNativeCallback() {
+ ArgumentCaptor<INativeAudioVolumeGroupCallback> nativeAudioVolumeGroupCallbackCaptor =
+ ArgumentCaptor.forClass(INativeAudioVolumeGroupCallback.class);
+ verify(mSpyAudioSystem, timeout(DEFAULT_TIMEOUT_MS))
+ .registerAudioVolumeGroupCallback(nativeAudioVolumeGroupCallbackCaptor.capture());
+ return nativeAudioVolumeGroupCallbackCaptor.getValue();
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/location/contexthub/ContextHubEndpointTest.java b/services/tests/servicestests/src/com/android/server/location/contexthub/ContextHubEndpointTest.java
index 43b1ec393bf6..87cd1560509c 100644
--- a/services/tests/servicestests/src/com/android/server/location/contexthub/ContextHubEndpointTest.java
+++ b/services/tests/servicestests/src/com/android/server/location/contexthub/ContextHubEndpointTest.java
@@ -19,7 +19,9 @@ package com.android.server.location.contexthub;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.times;
@@ -29,6 +31,7 @@ import static org.mockito.Mockito.when;
import android.content.Context;
import android.hardware.contexthub.EndpointInfo;
import android.hardware.contexthub.ErrorCode;
+import android.hardware.contexthub.HubEndpoint;
import android.hardware.contexthub.HubEndpointInfo;
import android.hardware.contexthub.HubEndpointInfo.HubEndpointIdentifier;
import android.hardware.contexthub.HubMessage;
@@ -385,6 +388,49 @@ public class ContextHubEndpointTest {
unregisterExampleEndpoint(endpoint);
}
+ @Test
+ public void testUnreliableMessageFailureClosesSession() throws RemoteException {
+ IContextHubEndpoint endpoint = registerExampleEndpoint();
+ int sessionId = openTestSession(endpoint);
+
+ doThrow(new RemoteException("Intended exception in test"))
+ .when(mMockCallback)
+ .onMessageReceived(anyInt(), any(HubMessage.class));
+ mEndpointManager.onMessageReceived(sessionId, SAMPLE_UNRELIABLE_MESSAGE);
+ ArgumentCaptor<HubMessage> messageCaptor = ArgumentCaptor.forClass(HubMessage.class);
+ verify(mMockCallback).onMessageReceived(eq(sessionId), messageCaptor.capture());
+ assertThat(messageCaptor.getValue()).isEqualTo(SAMPLE_UNRELIABLE_MESSAGE);
+
+ verify(mMockEndpointCommunications).closeEndpointSession(sessionId, Reason.UNSPECIFIED);
+ verify(mMockCallback).onSessionClosed(sessionId, HubEndpoint.REASON_FAILURE);
+ assertThat(mEndpointManager.getNumAvailableSessions()).isEqualTo(SESSION_ID_RANGE);
+
+ unregisterExampleEndpoint(endpoint);
+ }
+
+ @Test
+ public void testSendUnreliableMessageFailureClosesSession() throws RemoteException {
+ IContextHubEndpoint endpoint = registerExampleEndpoint();
+ int sessionId = openTestSession(endpoint);
+
+ doThrow(new RemoteException("Intended exception in test"))
+ .when(mMockEndpointCommunications)
+ .sendMessageToEndpoint(anyInt(), any(Message.class));
+ endpoint.sendMessage(sessionId, SAMPLE_UNRELIABLE_MESSAGE, /* callback= */ null);
+ ArgumentCaptor<Message> messageCaptor = ArgumentCaptor.forClass(Message.class);
+ verify(mMockEndpointCommunications)
+ .sendMessageToEndpoint(eq(sessionId), messageCaptor.capture());
+ Message message = messageCaptor.getValue();
+ assertThat(message.type).isEqualTo(SAMPLE_UNRELIABLE_MESSAGE.getMessageType());
+ assertThat(message.content).isEqualTo(SAMPLE_UNRELIABLE_MESSAGE.getMessageBody());
+
+ verify(mMockEndpointCommunications).closeEndpointSession(sessionId, Reason.UNSPECIFIED);
+ verify(mMockCallback).onSessionClosed(sessionId, HubEndpoint.REASON_FAILURE);
+ assertThat(mEndpointManager.getNumAvailableSessions()).isEqualTo(SESSION_ID_RANGE);
+
+ unregisterExampleEndpoint(endpoint);
+ }
+
/** A helper method to create a session and validates reliable message sending. */
private void testMessageTransactionInternal(
IContextHubEndpoint endpoint, boolean deliverMessageStatus) throws RemoteException {
diff --git a/services/tests/servicestests/src/com/android/server/statusbar/StatusBarManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/statusbar/StatusBarManagerServiceTest.java
index 148c96850d34..6d682ccef98d 100644
--- a/services/tests/servicestests/src/com/android/server/statusbar/StatusBarManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/statusbar/StatusBarManagerServiceTest.java
@@ -36,6 +36,7 @@ import static android.app.StatusBarManager.DISABLE_SYSTEM_INFO;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.nullable;
@@ -69,16 +70,20 @@ import android.os.Binder;
import android.os.Looper;
import android.os.RemoteException;
import android.os.UserHandle;
+import android.platform.test.annotations.EnableFlags;
import android.service.quicksettings.TileService;
import android.testing.TestableContext;
+import android.util.Pair;
import androidx.test.InstrumentationRegistry;
+import com.android.internal.statusbar.DisableStates;
import com.android.internal.statusbar.IAddTileResultCallback;
import com.android.internal.statusbar.IStatusBar;
import com.android.server.LocalServices;
import com.android.server.policy.GlobalActionsProvider;
import com.android.server.wm.ActivityTaskManagerInternal;
+import com.android.systemui.shared.Flags;
import libcore.junit.util.compat.CoreCompatChangeRule;
@@ -105,6 +110,7 @@ public class StatusBarManagerServiceTest {
TEST_SERVICE);
private static final CharSequence APP_NAME = "AppName";
private static final CharSequence TILE_LABEL = "Tile label";
+ private static final int SECONDARY_DISPLAY_ID = 2;
@Rule
public final TestableContext mContext =
@@ -749,6 +755,40 @@ public class StatusBarManagerServiceTest {
}
@Test
+ @EnableFlags(Flags.FLAG_STATUS_BAR_CONNECTED_DISPLAYS)
+ public void testDisableForAllDisplays() throws Exception {
+ int user1Id = 0;
+ mockUidCheck();
+ mockCurrentUserCheck(user1Id);
+
+ mStatusBarManagerService.onDisplayAdded(SECONDARY_DISPLAY_ID);
+
+ int expectedFlags = DISABLE_MASK & DISABLE_BACK;
+ String pkg = mContext.getPackageName();
+
+ // before disabling
+ assertEquals(DISABLE_NONE,
+ mStatusBarManagerService.getDisableFlags(mMockStatusBar, user1Id)[0]);
+
+ // disable
+ mStatusBarManagerService.disable(expectedFlags, mMockStatusBar, pkg);
+
+ ArgumentCaptor<DisableStates> disableStatesCaptor = ArgumentCaptor.forClass(
+ DisableStates.class);
+ verify(mMockStatusBar).disableForAllDisplays(disableStatesCaptor.capture());
+ DisableStates capturedDisableStates = disableStatesCaptor.getValue();
+ assertTrue(capturedDisableStates.animate);
+ assertEquals(capturedDisableStates.displaysWithStates.size(), 2);
+ Pair<Integer, Integer> display0States = capturedDisableStates.displaysWithStates.get(0);
+ assertEquals((int) display0States.first, expectedFlags);
+ assertEquals((int) display0States.second, 0);
+ Pair<Integer, Integer> display2States = capturedDisableStates.displaysWithStates.get(
+ SECONDARY_DISPLAY_ID);
+ assertEquals((int) display2States.first, expectedFlags);
+ assertEquals((int) display2States.second, 0);
+ }
+
+ @Test
public void testSetHomeDisabled() throws Exception {
int expectedFlags = DISABLE_MASK & DISABLE_HOME;
String pkg = mContext.getPackageName();
@@ -851,6 +891,40 @@ public class StatusBarManagerServiceTest {
}
@Test
+ @EnableFlags(Flags.FLAG_STATUS_BAR_CONNECTED_DISPLAYS)
+ public void testDisable2ForAllDisplays() throws Exception {
+ int user1Id = 0;
+ mockUidCheck();
+ mockCurrentUserCheck(user1Id);
+
+ mStatusBarManagerService.onDisplayAdded(SECONDARY_DISPLAY_ID);
+
+ int expectedFlags = DISABLE2_MASK & DISABLE2_NOTIFICATION_SHADE;
+ String pkg = mContext.getPackageName();
+
+ // before disabling
+ assertEquals(DISABLE_NONE,
+ mStatusBarManagerService.getDisableFlags(mMockStatusBar, user1Id)[0]);
+
+ // disable
+ mStatusBarManagerService.disable2(expectedFlags, mMockStatusBar, pkg);
+
+ ArgumentCaptor<DisableStates> disableStatesCaptor = ArgumentCaptor.forClass(
+ DisableStates.class);
+ verify(mMockStatusBar).disableForAllDisplays(disableStatesCaptor.capture());
+ DisableStates capturedDisableStates = disableStatesCaptor.getValue();
+ assertTrue(capturedDisableStates.animate);
+ assertEquals(capturedDisableStates.displaysWithStates.size(), 2);
+ Pair<Integer, Integer> display0States = capturedDisableStates.displaysWithStates.get(0);
+ assertEquals((int) display0States.first, 0);
+ assertEquals((int) display0States.second, expectedFlags);
+ Pair<Integer, Integer> display2States = capturedDisableStates.displaysWithStates.get(
+ SECONDARY_DISPLAY_ID);
+ assertEquals((int) display2States.first, 0);
+ assertEquals((int) display2States.second, expectedFlags);
+ }
+
+ @Test
public void testSetQuickSettingsDisabled2() throws Exception {
int expectedFlags = DISABLE2_MASK & DISABLE2_QUICK_SETTINGS;
String pkg = mContext.getPackageName();
diff --git a/services/tests/servicestests/src/com/android/server/systemconfig/SystemConfigTest.java b/services/tests/servicestests/src/com/android/server/systemconfig/SystemConfigTest.java
index 857a9767d9ee..842c441e09f2 100644
--- a/services/tests/servicestests/src/com/android/server/systemconfig/SystemConfigTest.java
+++ b/services/tests/servicestests/src/com/android/server/systemconfig/SystemConfigTest.java
@@ -452,14 +452,14 @@ public class SystemConfigTest {
+ " <library \n"
+ " name=\"foo\"\n"
+ " file=\"" + mFooJar + "\"\n"
- + " on-bootclasspath-before=\"A\"\n"
+ + " on-bootclasspath-before=\"Q\"\n"
+ " on-bootclasspath-since=\"W\"\n"
+ " />\n\n"
+ " </permissions>";
parseSharedLibraries(contents);
assertFooIsOnlySharedLibrary();
SystemConfig.SharedLibraryEntry entry = mSysConfig.getSharedLibraries().get("foo");
- assertThat(entry.onBootclasspathBefore).isEqualTo("A");
+ assertThat(entry.onBootclasspathBefore).isEqualTo("Q");
assertThat(entry.onBootclasspathSince).isEqualTo("W");
}
diff --git a/services/tests/servicestests/test-apps/TopologyTestApp/Android.bp b/services/tests/servicestests/test-apps/TopologyTestApp/Android.bp
new file mode 100644
index 000000000000..dcf9cc216687
--- /dev/null
+++ b/services/tests/servicestests/test-apps/TopologyTestApp/Android.bp
@@ -0,0 +1,38 @@
+// 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 {
+ // See: http://go/android-license-faq
+ // A large-scale-change added 'default_applicable_licenses' to import
+ // all of the 'license_kinds' from "frameworks_base_license"
+ // to get the below license kinds:
+ // SPDX-license-identifier-Apache-2.0
+ default_applicable_licenses: ["frameworks_base_license"],
+}
+
+android_test_helper_app {
+ name: "TopologyTestApp",
+
+ srcs: ["**/*.java"],
+
+ dex_preopt: {
+ enabled: false,
+ },
+ optimize: {
+ enabled: false,
+ },
+
+ platform_apis: true,
+ certificate: "platform",
+}
diff --git a/services/tests/servicestests/test-apps/TopologyTestApp/AndroidManifest.xml b/services/tests/servicestests/test-apps/TopologyTestApp/AndroidManifest.xml
new file mode 100644
index 000000000000..dad2315148df
--- /dev/null
+++ b/services/tests/servicestests/test-apps/TopologyTestApp/AndroidManifest.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2025 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.servicestests.apps.topologytestapp">
+
+ <uses-permission android:name="android.permission.MANAGE_DISPLAYS" />
+
+ <application android:label="TopologyUpdateTestApp">
+ <activity android:name="com.android.servicestests.apps.topologytestapp.TopologyUpdateActivity"
+ android:exported="true" />
+ </application>
+
+</manifest>
diff --git a/services/tests/servicestests/test-apps/TopologyTestApp/OWNERS b/services/tests/servicestests/test-apps/TopologyTestApp/OWNERS
new file mode 100644
index 000000000000..e9557f84f8fb
--- /dev/null
+++ b/services/tests/servicestests/test-apps/TopologyTestApp/OWNERS
@@ -0,0 +1,3 @@
+# Bug component: 345010
+
+include /services/core/java/com/android/server/display/OWNERS
diff --git a/services/tests/servicestests/test-apps/TopologyTestApp/src/com/android/servicestests/apps/topologytestapp/TopologyUpdateActivity.java b/services/tests/servicestests/test-apps/TopologyTestApp/src/com/android/servicestests/apps/topologytestapp/TopologyUpdateActivity.java
new file mode 100644
index 000000000000..b35ba3c2c60c
--- /dev/null
+++ b/services/tests/servicestests/test-apps/TopologyTestApp/src/com/android/servicestests/apps/topologytestapp/TopologyUpdateActivity.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.servicestests.apps.topologytestapp;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.hardware.display.DisplayManager;
+import android.hardware.display.DisplayTopology;
+import android.os.Bundle;
+import android.os.Message;
+import android.os.Messenger;
+import android.os.Process;
+import android.os.RemoteException;
+import android.util.Log;
+
+import java.util.function.Consumer;
+
+/**
+ * A simple activity listening to topology updates
+ */
+public class TopologyUpdateActivity extends Activity {
+ public static final int MESSAGE_LAUNCHED = 1;
+ public static final int MESSAGE_CALLBACK = 2;
+
+ private static final String TAG = TopologyUpdateActivity.class.getSimpleName();
+
+ private static final String TEST_MESSENGER = "MESSENGER";
+
+ private Messenger mMessenger;
+ private DisplayManager mDisplayManager;
+ private final Consumer<DisplayTopology> mTopologyListener = this::callback;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ Intent intent = getIntent();
+ mMessenger = intent.getParcelableExtra(TEST_MESSENGER, Messenger.class);
+ mDisplayManager = getApplicationContext().getSystemService(DisplayManager.class);
+ mDisplayManager.registerTopologyListener(getMainExecutor(), mTopologyListener);
+ launched();
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ mDisplayManager.unregisterTopologyListener(mTopologyListener);
+ }
+
+ private void launched() {
+ try {
+ Message msg = Message.obtain();
+ msg.what = MESSAGE_LAUNCHED;
+ msg.arg1 = android.os.Process.myPid();
+ msg.arg2 = Process.myUid();
+ Log.d(TAG, "Launched");
+ mMessenger.send(msg);
+ } catch (RemoteException e) {
+ e.rethrowAsRuntimeException();
+ }
+ }
+
+ private void callback(DisplayTopology topology) {
+ try {
+ Message msg = Message.obtain();
+ msg.what = MESSAGE_CALLBACK;
+ msg.obj = topology;
+ Log.d(TAG, "Msg " + topology);
+ mMessenger.send(msg);
+ } catch (RemoteException e) {
+ e.rethrowAsRuntimeException();
+ }
+ }
+}
diff --git a/services/tests/wmtests/src/com/android/server/policy/KeyGestureEventTests.java b/services/tests/wmtests/src/com/android/server/policy/KeyGestureEventTests.java
index fcdf88f16550..0495e967c0e3 100644
--- a/services/tests/wmtests/src/com/android/server/policy/KeyGestureEventTests.java
+++ b/services/tests/wmtests/src/com/android/server/policy/KeyGestureEventTests.java
@@ -39,8 +39,6 @@ import androidx.test.filters.MediumTest;
import com.android.hardware.input.Flags;
import com.android.internal.annotations.Keep;
-import junit.framework.Assert;
-
import junitparams.JUnitParamsRunner;
import junitparams.Parameters;
@@ -433,112 +431,94 @@ public class KeyGestureEventTests extends ShortcutKeyTestBase {
@Test
public void testKeyGestureRecentApps() {
- Assert.assertTrue(
- sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_RECENT_APPS));
+ sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_RECENT_APPS);
mPhoneWindowManager.assertShowRecentApps();
}
@Test
public void testKeyGestureAppSwitch() {
- Assert.assertTrue(
- sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_APP_SWITCH));
+ sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_APP_SWITCH);
mPhoneWindowManager.assertToggleRecentApps();
}
@Test
public void testKeyGestureLaunchAssistant() {
- Assert.assertTrue(
- sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_ASSISTANT));
+ sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_ASSISTANT);
mPhoneWindowManager.assertSearchManagerLaunchAssist();
}
@Test
public void testKeyGestureLaunchVoiceAssistant() {
- Assert.assertTrue(
- sendKeyGestureEventComplete(
- KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_VOICE_ASSISTANT));
+ sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_VOICE_ASSISTANT);
mPhoneWindowManager.assertSearchManagerLaunchAssist();
}
@Test
public void testKeyGestureGoHome() {
- Assert.assertTrue(
- sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_HOME));
+ sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_HOME);
mPhoneWindowManager.assertGoToHomescreen();
}
@Test
public void testKeyGestureLaunchSystemSettings() {
- Assert.assertTrue(
- sendKeyGestureEventComplete(
- KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_SYSTEM_SETTINGS));
+ sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_SYSTEM_SETTINGS);
mPhoneWindowManager.assertLaunchSystemSettings();
}
@Test
public void testKeyGestureLock() {
- Assert.assertTrue(
- sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_LOCK_SCREEN));
+ sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_LOCK_SCREEN);
mPhoneWindowManager.assertLockedAfterAppTransitionFinished();
}
@Test
public void testKeyGestureToggleNotificationPanel() throws RemoteException {
- Assert.assertTrue(
- sendKeyGestureEventComplete(
- KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_NOTIFICATION_PANEL));
+ sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_NOTIFICATION_PANEL);
mPhoneWindowManager.assertTogglePanel();
}
@Test
public void testKeyGestureScreenshot() {
- Assert.assertTrue(
- sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_TAKE_SCREENSHOT));
+ sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_TAKE_SCREENSHOT);
mPhoneWindowManager.assertTakeScreenshotCalled();
}
@Test
public void testKeyGestureTriggerBugReport() throws RemoteException {
- Assert.assertTrue(
- sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_TRIGGER_BUG_REPORT));
+ sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_TRIGGER_BUG_REPORT);
mPhoneWindowManager.assertTakeBugreport(true);
}
@Test
public void testKeyGestureBack() {
- Assert.assertTrue(sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_BACK));
+ sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_BACK);
mPhoneWindowManager.assertBackEventInjected();
}
@Test
public void testKeyGestureMultiWindowNavigation() {
- Assert.assertTrue(sendKeyGestureEventComplete(
- KeyGestureEvent.KEY_GESTURE_TYPE_MULTI_WINDOW_NAVIGATION));
+ sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_MULTI_WINDOW_NAVIGATION);
mPhoneWindowManager.assertMoveFocusedTaskToFullscreen();
}
@Test
public void testKeyGestureDesktopMode() {
- Assert.assertTrue(
- sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_DESKTOP_MODE));
+ sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_DESKTOP_MODE);
mPhoneWindowManager.assertMoveFocusedTaskToDesktop();
}
@Test
public void testKeyGestureSplitscreenNavigation() {
- Assert.assertTrue(sendKeyGestureEventComplete(
- KeyGestureEvent.KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_LEFT));
+ sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_LEFT);
mPhoneWindowManager.assertMoveFocusedTaskToStageSplit(true);
- Assert.assertTrue(sendKeyGestureEventComplete(
- KeyGestureEvent.KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_RIGHT));
+ sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_RIGHT);
mPhoneWindowManager.assertMoveFocusedTaskToStageSplit(false);
}
@Test
public void testKeyGestureShortcutHelper() {
- Assert.assertTrue(sendKeyGestureEventComplete(
- KeyGestureEvent.KEY_GESTURE_TYPE_OPEN_SHORTCUT_HELPER));
+ sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_OPEN_SHORTCUT_HELPER);
mPhoneWindowManager.assertToggleShortcutsMenu();
}
@@ -549,173 +529,139 @@ public class KeyGestureEventTests extends ShortcutKeyTestBase {
for (int i = 0; i < currentBrightness.length; i++) {
mPhoneWindowManager.prepareBrightnessDecrease(currentBrightness[i]);
- Assert.assertTrue(
- sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_BRIGHTNESS_DOWN));
+ sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_BRIGHTNESS_DOWN);
mPhoneWindowManager.verifyNewBrightness(newBrightness[i]);
}
}
@Test
public void testKeyGestureRecentAppSwitcher() {
- Assert.assertTrue(sendKeyGestureEventStart(
- KeyGestureEvent.KEY_GESTURE_TYPE_RECENT_APPS_SWITCHER));
+ sendKeyGestureEventStart(KeyGestureEvent.KEY_GESTURE_TYPE_RECENT_APPS_SWITCHER);
mPhoneWindowManager.assertShowRecentApps();
-
- Assert.assertTrue(sendKeyGestureEventComplete(
- KeyGestureEvent.KEY_GESTURE_TYPE_RECENT_APPS_SWITCHER));
+ sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_RECENT_APPS_SWITCHER);
mPhoneWindowManager.assertHideRecentApps();
}
@Test
public void testKeyGestureLanguageSwitch() {
- Assert.assertTrue(
- sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_LANGUAGE_SWITCH));
+ sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_LANGUAGE_SWITCH);
mPhoneWindowManager.assertSwitchKeyboardLayout(1, DEFAULT_DISPLAY);
- Assert.assertTrue(
- sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_LANGUAGE_SWITCH,
- KeyEvent.META_SHIFT_ON));
+ sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_LANGUAGE_SWITCH,
+ KeyEvent.META_SHIFT_ON);
mPhoneWindowManager.assertSwitchKeyboardLayout(-1, DEFAULT_DISPLAY);
}
@Test
public void testKeyGestureLaunchSearch() {
- Assert.assertTrue(
- sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_SEARCH));
+ sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_SEARCH);
mPhoneWindowManager.assertLaunchSearch();
}
@Test
public void testKeyGestureScreenshotChord() {
- Assert.assertTrue(
- sendKeyGestureEventStart(KeyGestureEvent.KEY_GESTURE_TYPE_SCREENSHOT_CHORD));
+ sendKeyGestureEventStart(KeyGestureEvent.KEY_GESTURE_TYPE_SCREENSHOT_CHORD);
mPhoneWindowManager.moveTimeForward(500);
- Assert.assertTrue(
- sendKeyGestureEventCancel(KeyGestureEvent.KEY_GESTURE_TYPE_SCREENSHOT_CHORD));
+ sendKeyGestureEventCancel(KeyGestureEvent.KEY_GESTURE_TYPE_SCREENSHOT_CHORD);
mPhoneWindowManager.assertTakeScreenshotCalled();
}
@Test
public void testKeyGestureScreenshotChordCancelled() {
- Assert.assertTrue(
- sendKeyGestureEventStart(KeyGestureEvent.KEY_GESTURE_TYPE_SCREENSHOT_CHORD));
- Assert.assertTrue(
- sendKeyGestureEventCancel(KeyGestureEvent.KEY_GESTURE_TYPE_SCREENSHOT_CHORD));
+ sendKeyGestureEventStart(KeyGestureEvent.KEY_GESTURE_TYPE_SCREENSHOT_CHORD);
+ sendKeyGestureEventCancel(KeyGestureEvent.KEY_GESTURE_TYPE_SCREENSHOT_CHORD);
mPhoneWindowManager.assertTakeScreenshotNotCalled();
}
@Test
public void testKeyGestureRingerToggleChord() {
mPhoneWindowManager.overridePowerVolumeUp(POWER_VOLUME_UP_BEHAVIOR_MUTE);
- Assert.assertTrue(
- sendKeyGestureEventStart(KeyGestureEvent.KEY_GESTURE_TYPE_RINGER_TOGGLE_CHORD));
+ sendKeyGestureEventStart(KeyGestureEvent.KEY_GESTURE_TYPE_RINGER_TOGGLE_CHORD);
mPhoneWindowManager.moveTimeForward(500);
- Assert.assertTrue(
- sendKeyGestureEventCancel(KeyGestureEvent.KEY_GESTURE_TYPE_RINGER_TOGGLE_CHORD));
+ sendKeyGestureEventCancel(KeyGestureEvent.KEY_GESTURE_TYPE_RINGER_TOGGLE_CHORD);
mPhoneWindowManager.assertVolumeMute();
}
@Test
public void testKeyGestureRingerToggleChordCancelled() {
mPhoneWindowManager.overridePowerVolumeUp(POWER_VOLUME_UP_BEHAVIOR_MUTE);
- Assert.assertTrue(
- sendKeyGestureEventStart(KeyGestureEvent.KEY_GESTURE_TYPE_RINGER_TOGGLE_CHORD));
- Assert.assertTrue(
- sendKeyGestureEventCancel(KeyGestureEvent.KEY_GESTURE_TYPE_RINGER_TOGGLE_CHORD));
+ sendKeyGestureEventStart(KeyGestureEvent.KEY_GESTURE_TYPE_RINGER_TOGGLE_CHORD);
+ sendKeyGestureEventCancel(KeyGestureEvent.KEY_GESTURE_TYPE_RINGER_TOGGLE_CHORD);
mPhoneWindowManager.assertVolumeNotMuted();
}
@Test
public void testKeyGestureGlobalAction() {
mPhoneWindowManager.overridePowerVolumeUp(POWER_VOLUME_UP_BEHAVIOR_GLOBAL_ACTIONS);
- Assert.assertTrue(
- sendKeyGestureEventStart(KeyGestureEvent.KEY_GESTURE_TYPE_GLOBAL_ACTIONS));
+ sendKeyGestureEventStart(KeyGestureEvent.KEY_GESTURE_TYPE_GLOBAL_ACTIONS);
mPhoneWindowManager.moveTimeForward(500);
- Assert.assertTrue(
- sendKeyGestureEventCancel(KeyGestureEvent.KEY_GESTURE_TYPE_GLOBAL_ACTIONS));
+ sendKeyGestureEventCancel(KeyGestureEvent.KEY_GESTURE_TYPE_GLOBAL_ACTIONS);
mPhoneWindowManager.assertShowGlobalActionsCalled();
}
@Test
public void testKeyGestureGlobalActionCancelled() {
mPhoneWindowManager.overridePowerVolumeUp(POWER_VOLUME_UP_BEHAVIOR_GLOBAL_ACTIONS);
- Assert.assertTrue(
- sendKeyGestureEventStart(KeyGestureEvent.KEY_GESTURE_TYPE_GLOBAL_ACTIONS));
- Assert.assertTrue(
- sendKeyGestureEventCancel(KeyGestureEvent.KEY_GESTURE_TYPE_GLOBAL_ACTIONS));
+ sendKeyGestureEventStart(KeyGestureEvent.KEY_GESTURE_TYPE_GLOBAL_ACTIONS);
+ sendKeyGestureEventCancel(KeyGestureEvent.KEY_GESTURE_TYPE_GLOBAL_ACTIONS);
mPhoneWindowManager.assertShowGlobalActionsNotCalled();
}
@Test
public void testKeyGestureTvTriggerBugReport() {
- Assert.assertTrue(
- sendKeyGestureEventStart(KeyGestureEvent.KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT));
+ sendKeyGestureEventStart(KeyGestureEvent.KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT);
mPhoneWindowManager.moveTimeForward(1000);
- Assert.assertTrue(
- sendKeyGestureEventCancel(KeyGestureEvent.KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT));
+ sendKeyGestureEventCancel(KeyGestureEvent.KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT);
mPhoneWindowManager.assertBugReportTakenForTv();
}
@Test
public void testKeyGestureTvTriggerBugReportCancelled() {
- Assert.assertTrue(
- sendKeyGestureEventStart(KeyGestureEvent.KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT));
- Assert.assertTrue(
- sendKeyGestureEventCancel(KeyGestureEvent.KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT));
+ sendKeyGestureEventStart(KeyGestureEvent.KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT);
+ sendKeyGestureEventCancel(KeyGestureEvent.KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT);
mPhoneWindowManager.assertBugReportNotTakenForTv();
}
@Test
public void testKeyGestureAccessibilityShortcut() {
- Assert.assertTrue(
- sendKeyGestureEventComplete(
- KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT));
+ sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT);
mPhoneWindowManager.assertAccessibilityKeychordCalled();
}
@Test
public void testKeyGestureCloseAllDialogs() {
- Assert.assertTrue(
- sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_CLOSE_ALL_DIALOGS));
+ sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_CLOSE_ALL_DIALOGS);
mPhoneWindowManager.assertCloseAllDialogs();
}
@Test
@EnableFlags(com.android.hardware.input.Flags.FLAG_ENABLE_TALKBACK_AND_MAGNIFIER_KEY_GESTURES)
public void testKeyGestureToggleTalkback() {
- Assert.assertTrue(
- sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_TALKBACK));
+ sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_TALKBACK);
mPhoneWindowManager.assertTalkBack(true);
- Assert.assertTrue(
- sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_TALKBACK));
+ sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_TALKBACK);
mPhoneWindowManager.assertTalkBack(false);
}
@Test
@EnableFlags(com.android.hardware.input.Flags.FLAG_ENABLE_VOICE_ACCESS_KEY_GESTURES)
public void testKeyGestureToggleVoiceAccess() {
- Assert.assertTrue(
- sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_VOICE_ACCESS));
+ sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_VOICE_ACCESS);
mPhoneWindowManager.assertVoiceAccess(true);
- Assert.assertTrue(
- sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_VOICE_ACCESS));
+ sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_VOICE_ACCESS);
mPhoneWindowManager.assertVoiceAccess(false);
}
@Test
public void testKeyGestureToggleDoNotDisturb() {
mPhoneWindowManager.overrideZenMode(Settings.Global.ZEN_MODE_OFF);
- Assert.assertTrue(
- sendKeyGestureEventComplete(
- KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_DO_NOT_DISTURB));
+ sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_DO_NOT_DISTURB);
mPhoneWindowManager.assertZenMode(Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS);
mPhoneWindowManager.overrideZenMode(Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS);
- Assert.assertTrue(
- sendKeyGestureEventComplete(
- KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_DO_NOT_DISTURB));
+ sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_DO_NOT_DISTURB);
mPhoneWindowManager.assertZenMode(Settings.Global.ZEN_MODE_OFF);
}
diff --git a/services/tests/wmtests/src/com/android/server/policy/PhoneWindowManagerTests.java b/services/tests/wmtests/src/com/android/server/policy/PhoneWindowManagerTests.java
index 32a3b7f2c9cc..f3d5e39ec127 100644
--- a/services/tests/wmtests/src/com/android/server/policy/PhoneWindowManagerTests.java
+++ b/services/tests/wmtests/src/com/android/server/policy/PhoneWindowManagerTests.java
@@ -35,6 +35,7 @@ import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.when;
import static com.android.server.policy.PhoneWindowManager.EXTRA_TRIGGER_HUB;
+import static com.android.server.policy.PhoneWindowManager.SHORT_PRESS_POWER_DREAM_OR_AWAKE_OR_SLEEP;
import static com.android.server.policy.PhoneWindowManager.SHORT_PRESS_POWER_HUB_OR_DREAM_OR_SLEEP;
import static com.google.common.truth.Truth.assertThat;
@@ -43,6 +44,7 @@ import static org.junit.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyList;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
@@ -63,6 +65,7 @@ import android.view.contentprotection.flags.Flags;
import androidx.test.filters.SmallTest;
import com.android.internal.util.test.LocalServiceKeeperRule;
+import com.android.internal.widget.LockPatternUtils;
import com.android.server.input.InputManagerInternal;
import com.android.server.pm.UserManagerInternal;
import com.android.server.policy.keyguard.KeyguardServiceDelegate;
@@ -119,6 +122,8 @@ public class PhoneWindowManagerTests {
private DisplayPolicy mDisplayPolicy;
@Mock
private KeyguardServiceDelegate mKeyguardServiceDelegate;
+ @Mock
+ private LockPatternUtils mLockPatternUtils;
@Before
public void setUp() {
@@ -146,7 +151,7 @@ public class PhoneWindowManagerTests {
mPhoneWindowManager.mKeyguardDelegate = mKeyguardServiceDelegate;
final InputManager im = mock(InputManager.class);
- doNothing().when(im).registerKeyGestureEventHandler(any());
+ doNothing().when(im).registerKeyGestureEventHandler(anyList(), any());
doReturn(im).when(mContext).getSystemService(eq(Context.INPUT_SERVICE));
}
@@ -253,6 +258,7 @@ public class PhoneWindowManagerTests {
@Test
public void powerPress_hubOrDreamOrSleep_goesToSleepFromDream() {
when(mDisplayPolicy.isAwake()).thenReturn(true);
+ when(mLockPatternUtils.isLockScreenDisabled(anyInt())).thenReturn(false);
initPhoneWindowManager();
// Set power button behavior.
@@ -274,6 +280,7 @@ public class PhoneWindowManagerTests {
@Test
public void powerPress_hubOrDreamOrSleep_hubAvailableLocks() {
when(mDisplayPolicy.isAwake()).thenReturn(true);
+ when(mLockPatternUtils.isLockScreenDisabled(anyInt())).thenReturn(false);
mContext.getTestablePermissions().setPermission(android.Manifest.permission.DEVICE_POWER,
PERMISSION_GRANTED);
initPhoneWindowManager();
@@ -302,6 +309,7 @@ public class PhoneWindowManagerTests {
@Test
public void powerPress_hubOrDreamOrSleep_hubNotAvailableDreams() {
when(mDisplayPolicy.isAwake()).thenReturn(true);
+ when(mLockPatternUtils.isLockScreenDisabled(anyInt())).thenReturn(false);
initPhoneWindowManager();
// Set power button behavior.
@@ -322,6 +330,77 @@ public class PhoneWindowManagerTests {
verify(mDreamManagerInternal).requestDream();
}
+ @Test
+ public void powerPress_dreamOrAwakeOrSleep_awakeFromDream() {
+ when(mDisplayPolicy.isAwake()).thenReturn(true);
+ initPhoneWindowManager();
+
+ // Set power button behavior.
+ Settings.Global.putInt(mContext.getContentResolver(),
+ Settings.Global.POWER_BUTTON_SHORT_PRESS,
+ SHORT_PRESS_POWER_DREAM_OR_AWAKE_OR_SLEEP);
+ mPhoneWindowManager.updateSettings(null);
+
+ // Can not dream when device is dreaming.
+ when(mDreamManagerInternal.canStartDreaming(any(Boolean.class))).thenReturn(false);
+ // Device is dreaming.
+ when(mDreamManagerInternal.isDreaming()).thenReturn(true);
+
+ // Power button pressed.
+ int eventTime = 0;
+ mPhoneWindowManager.powerPress(eventTime, 1, 0);
+
+ // Dream is stopped.
+ verify(mDreamManagerInternal)
+ .stopDream(false /*immediate*/, "short press power" /*reason*/);
+ }
+
+ @Test
+ public void powerPress_dreamOrAwakeOrSleep_canNotDreamGoToSleep() {
+ when(mDisplayPolicy.isAwake()).thenReturn(true);
+ initPhoneWindowManager();
+
+ // Set power button behavior.
+ Settings.Global.putInt(mContext.getContentResolver(),
+ Settings.Global.POWER_BUTTON_SHORT_PRESS,
+ SHORT_PRESS_POWER_DREAM_OR_AWAKE_OR_SLEEP);
+ mPhoneWindowManager.updateSettings(null);
+
+ // Can not dream for other reasons.
+ when(mDreamManagerInternal.canStartDreaming(any(Boolean.class))).thenReturn(false);
+ // Device is not dreaming.
+ when(mDreamManagerInternal.isDreaming()).thenReturn(false);
+
+ // Power button pressed.
+ int eventTime = 0;
+ mPhoneWindowManager.powerPress(eventTime, 1, 0);
+
+ // Device goes to sleep.
+ verify(mPowerManager).goToSleep(eventTime, PowerManager.GO_TO_SLEEP_REASON_POWER_BUTTON, 0);
+ }
+
+ @Test
+ public void powerPress_dreamOrAwakeOrSleep_dreamFromActive() {
+ when(mDisplayPolicy.isAwake()).thenReturn(true);
+ initPhoneWindowManager();
+
+ // Set power button behavior.
+ Settings.Global.putInt(mContext.getContentResolver(),
+ Settings.Global.POWER_BUTTON_SHORT_PRESS,
+ SHORT_PRESS_POWER_DREAM_OR_AWAKE_OR_SLEEP);
+ mPhoneWindowManager.updateSettings(null);
+
+ // Can dream when active.
+ when(mDreamManagerInternal.canStartDreaming(any(Boolean.class))).thenReturn(true);
+
+ // Power button pressed.
+ int eventTime = 0;
+ mPhoneWindowManager.powerPress(eventTime, 1, 0);
+
+ // Dream is requested.
+ verify(mDreamManagerInternal).requestDream();
+ }
+
private void initPhoneWindowManager() {
mPhoneWindowManager.mDefaultDisplayPolicy = mDisplayPolicy;
mPhoneWindowManager.mDefaultDisplayRotation = mock(DisplayRotation.class);
@@ -345,6 +424,11 @@ public class PhoneWindowManagerTests {
return mKeyguardServiceDelegate;
}
+ @Override
+ LockPatternUtils getLockPatternUtils() {
+ return mLockPatternUtils;
+ }
+
/**
* {@code WindowWakeUpPolicy} registers a local service in its constructor, easier to just
* mock it out so we don't have to unregister it after every test.
diff --git a/services/tests/wmtests/src/com/android/server/policy/ShortcutKeyTestBase.java b/services/tests/wmtests/src/com/android/server/policy/ShortcutKeyTestBase.java
index c57adfd69b06..f89c6f638384 100644
--- a/services/tests/wmtests/src/com/android/server/policy/ShortcutKeyTestBase.java
+++ b/services/tests/wmtests/src/com/android/server/policy/ShortcutKeyTestBase.java
@@ -238,33 +238,33 @@ class ShortcutKeyTestBase {
sendKeyCombination(new int[]{keyCode}, durationMillis, false, DEFAULT_DISPLAY);
}
- boolean sendKeyGestureEventStart(int gestureType) {
- return mPhoneWindowManager.sendKeyGestureEvent(
+ void sendKeyGestureEventStart(int gestureType) {
+ mPhoneWindowManager.sendKeyGestureEvent(
new KeyGestureEvent.Builder().setKeyGestureType(gestureType).setAction(
KeyGestureEvent.ACTION_GESTURE_START).build());
}
- boolean sendKeyGestureEventComplete(int gestureType) {
- return mPhoneWindowManager.sendKeyGestureEvent(
+ void sendKeyGestureEventComplete(int gestureType) {
+ mPhoneWindowManager.sendKeyGestureEvent(
new KeyGestureEvent.Builder().setKeyGestureType(gestureType).setAction(
KeyGestureEvent.ACTION_GESTURE_COMPLETE).build());
}
- boolean sendKeyGestureEventCancel(int gestureType) {
- return mPhoneWindowManager.sendKeyGestureEvent(
+ void sendKeyGestureEventCancel(int gestureType) {
+ mPhoneWindowManager.sendKeyGestureEvent(
new KeyGestureEvent.Builder().setKeyGestureType(gestureType).setAction(
KeyGestureEvent.ACTION_GESTURE_COMPLETE).setFlags(
KeyGestureEvent.FLAG_CANCELLED).build());
}
- boolean sendKeyGestureEventComplete(int gestureType, int modifierState) {
- return mPhoneWindowManager.sendKeyGestureEvent(
+ void sendKeyGestureEventComplete(int gestureType, int modifierState) {
+ mPhoneWindowManager.sendKeyGestureEvent(
new KeyGestureEvent.Builder().setModifierState(modifierState).setKeyGestureType(
gestureType).setAction(KeyGestureEvent.ACTION_GESTURE_COMPLETE).build());
}
- boolean sendKeyGestureEventComplete(int keycode, int modifierState, int gestureType) {
- return mPhoneWindowManager.sendKeyGestureEvent(
+ void sendKeyGestureEventComplete(int keycode, int modifierState, int gestureType) {
+ mPhoneWindowManager.sendKeyGestureEvent(
new KeyGestureEvent.Builder().setKeycodes(new int[]{keycode}).setModifierState(
modifierState).setKeyGestureType(gestureType).setAction(
KeyGestureEvent.ACTION_GESTURE_COMPLETE).build());
diff --git a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java
index e56fd3c6272d..7b6d361c55d4 100644
--- a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java
+++ b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java
@@ -49,6 +49,7 @@ import static com.android.server.policy.PhoneWindowManager.POWER_VOLUME_UP_BEHAV
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyList;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.CALLS_REAL_METHODS;
import static org.mockito.Mockito.after;
@@ -353,7 +354,7 @@ class TestPhoneWindowManager {
doReturn(mAppOpsManager).when(mContext).getSystemService(eq(AppOpsManager.class));
doReturn(mDisplayManager).when(mContext).getSystemService(eq(DisplayManager.class));
doReturn(mInputManager).when(mContext).getSystemService(eq(InputManager.class));
- doNothing().when(mInputManager).registerKeyGestureEventHandler(any());
+ doNothing().when(mInputManager).registerKeyGestureEventHandler(anyList(), any());
doNothing().when(mInputManager).unregisterKeyGestureEventHandler(any());
doReturn(mPackageManager).when(mContext).getPackageManager();
doReturn(mSensorPrivacyManager).when(mContext).getSystemService(
@@ -476,8 +477,8 @@ class TestPhoneWindowManager {
mPhoneWindowManager.interceptUnhandledKey(event, mInputToken);
}
- boolean sendKeyGestureEvent(KeyGestureEvent event) {
- return mPhoneWindowManager.handleKeyGestureEvent(event, mInputToken);
+ void sendKeyGestureEvent(KeyGestureEvent event) {
+ mPhoneWindowManager.handleKeyGestureEvent(event, mInputToken);
}
/**
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
index 7f242dea9f45..773a566f6315 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
@@ -1459,21 +1459,6 @@ public class ActivityRecordTests extends WindowTestsBase {
}
/**
- * Verify that finish bottom activity from a task won't boost it to top.
- */
- @Test
- public void testFinishBottomActivityIfPossible_noZBoost() {
- final ActivityRecord bottomActivity = createActivityWithTask();
- final ActivityRecord topActivity = new ActivityBuilder(mAtm)
- .setTask(bottomActivity.getTask()).build();
- topActivity.setVisibleRequested(true);
- // simulating bottomActivity as a trampoline activity.
- bottomActivity.setState(RESUMED, "test");
- bottomActivity.finishIfPossible("test", false);
- assertFalse(bottomActivity.mNeedsZBoost);
- }
-
- /**
* Verify that complete finish request for visible activity must be delayed before the next one
* becomes visible.
*/
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppWindowTokenAnimationTests.java b/services/tests/wmtests/src/com/android/server/wm/AppWindowTokenAnimationTests.java
deleted file mode 100644
index e4628c45a15b..000000000000
--- a/services/tests/wmtests/src/com/android/server/wm/AppWindowTokenAnimationTests.java
+++ /dev/null
@@ -1,142 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open 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.dx.mockito.inline.extended.ExtendedMockito.verify;
-import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_APP_TRANSITION;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.ArgumentMatchers.intThat;
-
-import android.platform.test.annotations.Presubmit;
-import android.view.SurfaceControl;
-
-import androidx.test.filters.SmallTest;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-
-/**
- * Animation related tests for the {@link ActivityRecord} class.
- *
- * Build/Install/Run:
- * atest AppWindowTokenAnimationTests
- */
-@SmallTest
-@Presubmit
-@RunWith(WindowTestRunner.class)
-public class AppWindowTokenAnimationTests extends WindowTestsBase {
-
- private ActivityRecord mActivity;
-
- @Mock
- private AnimationAdapter mSpec;
-
- @Before
- public void setUp() throws Exception {
- MockitoAnnotations.initMocks(this);
-
- mActivity = createActivityRecord(mDisplayContent);
- }
-
- @Test
- public void clipAfterAnim_boundsLayerIsCreated() {
- mActivity.mNeedsAnimationBoundsLayer = true;
-
- mActivity.mSurfaceAnimator.startAnimation(mTransaction, mSpec, true /* hidden */,
- ANIMATION_TYPE_APP_TRANSITION);
- verify(mTransaction).reparent(eq(mActivity.getSurfaceControl()),
- eq(mActivity.mSurfaceAnimator.mLeash));
- verify(mTransaction).reparent(eq(mActivity.mSurfaceAnimator.mLeash),
- eq(mActivity.mAnimationBoundsLayer));
- }
-
- @Test
- public void clipAfterAnim_boundsLayerZBoosted() {
- final Task task = mActivity.getTask();
- final ActivityRecord topActivity = createActivityRecord(task);
- task.assignChildLayers(mTransaction);
-
- assertThat(topActivity.getLastLayer()).isGreaterThan(mActivity.getLastLayer());
-
- mActivity.mNeedsAnimationBoundsLayer = true;
- mActivity.mNeedsZBoost = true;
- mActivity.mSurfaceAnimator.startAnimation(mTransaction, mSpec, true /* hidden */,
- ANIMATION_TYPE_APP_TRANSITION);
-
- verify(mTransaction).setLayer(eq(mActivity.mAnimationBoundsLayer),
- intThat(layer -> layer > topActivity.getLastLayer()));
-
- // The layer should be restored after the animation leash is removed.
- mActivity.onAnimationLeashLost(mTransaction);
- assertThat(mActivity.mNeedsZBoost).isFalse();
- assertThat(topActivity.getLastLayer()).isGreaterThan(mActivity.getLastLayer());
- }
-
- @Test
- public void clipAfterAnim_boundsLayerIsDestroyed() {
- mActivity.mNeedsAnimationBoundsLayer = true;
- mActivity.mSurfaceAnimator.startAnimation(mTransaction, mSpec, true /* hidden */,
- ANIMATION_TYPE_APP_TRANSITION);
- final SurfaceControl leash = mActivity.mSurfaceAnimator.mLeash;
- final SurfaceControl animationBoundsLayer = mActivity.mAnimationBoundsLayer;
- final ArgumentCaptor<SurfaceAnimator.OnAnimationFinishedCallback> callbackCaptor =
- ArgumentCaptor.forClass(
- SurfaceAnimator.OnAnimationFinishedCallback.class);
- verify(mSpec).startAnimation(any(), any(), eq(ANIMATION_TYPE_APP_TRANSITION),
- callbackCaptor.capture());
-
- callbackCaptor.getValue().onAnimationFinished(
- ANIMATION_TYPE_APP_TRANSITION, mSpec);
- verify(mTransaction).remove(eq(leash));
- verify(mTransaction).remove(eq(animationBoundsLayer));
- assertThat(mActivity.mNeedsAnimationBoundsLayer).isFalse();
- }
-
- @Test
- public void clipAfterAnimCancelled_boundsLayerIsDestroyed() {
- mActivity.mNeedsAnimationBoundsLayer = true;
- mActivity.mSurfaceAnimator.startAnimation(mTransaction, mSpec, true /* hidden */,
- ANIMATION_TYPE_APP_TRANSITION);
- final SurfaceControl leash = mActivity.mSurfaceAnimator.mLeash;
- final SurfaceControl animationBoundsLayer = mActivity.mAnimationBoundsLayer;
-
- mActivity.mSurfaceAnimator.cancelAnimation();
- verify(mTransaction).remove(eq(leash));
- verify(mTransaction).remove(eq(animationBoundsLayer));
- assertThat(mActivity.mNeedsAnimationBoundsLayer).isFalse();
- }
-
- @Test
- public void clipNoneAnim_boundsLayerIsNotCreated() {
- mActivity.mNeedsAnimationBoundsLayer = false;
-
- mActivity.mSurfaceAnimator.startAnimation(mTransaction, mSpec, true /* hidden */,
- ANIMATION_TYPE_APP_TRANSITION);
- verify(mTransaction).reparent(eq(mActivity.getSurfaceControl()),
- eq(mActivity.mSurfaceAnimator.mLeash));
- assertThat(mActivity.mAnimationBoundsLayer).isNull();
- }
-}
diff --git a/services/tests/wmtests/src/com/android/server/wm/BackgroundLaunchProcessControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/BackgroundLaunchProcessControllerTests.java
index 27e147d98b1f..747b09cb5660 100644
--- a/services/tests/wmtests/src/com/android/server/wm/BackgroundLaunchProcessControllerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/BackgroundLaunchProcessControllerTests.java
@@ -74,14 +74,14 @@ public class BackgroundLaunchProcessControllerTests {
BackgroundActivityStartCallback mCallback = new BackgroundActivityStartCallback() {
@Override
- public boolean isActivityStartAllowed(Collection<IBinder> tokens, int uid,
- String packageName) {
+ public BackgroundActivityStartCallbackResult isActivityStartAllowed(
+ Collection<IBinder> tokens, int uid, String packageName) {
for (IBinder token : tokens) {
if (token == null || mActivityStartAllowed.contains(token)) {
- return true;
+ return new BackgroundActivityStartCallbackResult(true, token);
}
}
- return false;
+ return RESULT_FALSE;
}
@Override
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskDisplayAreaTests.java b/services/tests/wmtests/src/com/android/server/wm/TaskDisplayAreaTests.java
index ec83c50e95aa..1aa8681c9bfd 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskDisplayAreaTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskDisplayAreaTests.java
@@ -49,10 +49,8 @@ import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
-import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.clearInvocations;
-import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import android.app.ActivityOptions;
@@ -192,24 +190,6 @@ public class TaskDisplayAreaTests extends WindowTestsBase {
}
@Test
- public void testActivityWithZBoost_taskDisplayAreaDoesNotMoveUp() {
- final Task rootTask = createTask(mDisplayContent);
- final Task task = createTaskInRootTask(rootTask, 0 /* userId */);
- final ActivityRecord activity = createNonAttachedActivityRecord(mDisplayContent);
- task.addChild(activity, 0 /* addPos */);
- final TaskDisplayArea taskDisplayArea = activity.getDisplayArea();
- activity.mNeedsAnimationBoundsLayer = true;
- activity.mNeedsZBoost = true;
- spyOn(taskDisplayArea.mSurfaceAnimator);
-
- mDisplayContent.assignChildLayers(mTransaction);
-
- assertThat(activity.needsZBoost()).isTrue();
- assertThat(taskDisplayArea.needsZBoost()).isFalse();
- verify(taskDisplayArea.mSurfaceAnimator, never()).setLayer(eq(mTransaction), anyInt());
- }
-
- @Test
public void testRootTaskPositionChildAt() {
Task pinnedTask = createTask(
mDisplayContent, WINDOWING_MODE_PINNED, ACTIVITY_TYPE_STANDARD);
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskTests.java b/services/tests/wmtests/src/com/android/server/wm/TaskTests.java
index 044aacc4b988..b617f0285606 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskTests.java
@@ -100,8 +100,6 @@ import androidx.test.filters.MediumTest;
import com.android.modules.utils.TypedXmlPullParser;
import com.android.modules.utils.TypedXmlSerializer;
-import libcore.junit.util.compat.CoreCompatChangeRule;
-
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
@@ -414,79 +412,96 @@ public class TaskTests extends WindowTestsBase {
}
@Test
- @CoreCompatChangeRule.EnableCompatChanges({ActivityInfo.FORCE_RESIZE_APP})
- public void testIsResizeable_nonResizeable_forceResize_overridesEnabled_Resizeable() {
+ public void testIsResizeable_nonResizeable_forceResize_overridesEnabled_resizeable() {
final Task task = new TaskBuilder(mSupervisor)
.setCreateActivity(true)
- .setComponent(
- ComponentName.createRelative(mContext, SizeCompatTests.class.getName()))
.build();
task.setResizeMode(RESIZE_MODE_UNRESIZEABLE);
+ final ActivityRecord activity = task.getRootActivity();
+ final AppCompatResizeOverrides resizeOverrides =
+ activity.mAppCompatController.getResizeOverrides();
+ spyOn(activity);
+ spyOn(resizeOverrides);
+ doReturn(true).when(resizeOverrides).shouldOverrideForceResizeApp();
+ task.intent = null;
+ task.setIntent(activity);
// Override should take effect and task should be resizeable.
assertTrue(task.getTaskInfo().isResizeable);
}
@Test
- @CoreCompatChangeRule.EnableCompatChanges({ActivityInfo.FORCE_RESIZE_APP})
- public void testIsResizeable_nonResizeable_forceResize_overridesDisabled_nonResizeable() {
+ public void testIsResizeable_resizeable_forceNonResize_overridesEnabled_nonResizeable() {
final Task task = new TaskBuilder(mSupervisor)
.setCreateActivity(true)
- .setComponent(
- ComponentName.createRelative(mContext, SizeCompatTests.class.getName()))
.build();
- task.setResizeMode(RESIZE_MODE_UNRESIZEABLE);
-
- // Disallow resize overrides.
- task.mAllowForceResizeOverride = false;
+ task.setResizeMode(RESIZE_MODE_RESIZEABLE);
+ final ActivityRecord activity = task.getRootActivity();
+ final AppCompatResizeOverrides resizeOverrides =
+ activity.mAppCompatController.getResizeOverrides();
+ spyOn(activity);
+ spyOn(resizeOverrides);
+ doReturn(true).when(resizeOverrides).shouldOverrideForceNonResizeApp();
+ task.intent = null;
+ task.setIntent(activity);
- // Override should not take effect and task should be un-resizeable.
+ // Override should take effect and task should be un-resizeable.
assertFalse(task.getTaskInfo().isResizeable);
}
@Test
- @CoreCompatChangeRule.EnableCompatChanges({ActivityInfo.FORCE_NON_RESIZE_APP})
- public void testIsResizeable_resizeable_forceNonResize_overridesEnabled_nonResizeable() {
+ public void testIsResizeable_resizeableTask_fullscreenOverride_resizeable() {
final Task task = new TaskBuilder(mSupervisor)
.setCreateActivity(true)
- .setComponent(
- ComponentName.createRelative(mContext, SizeCompatTests.class.getName()))
.build();
- task.setResizeMode(RESIZE_MODE_RESIZEABLE);
+ task.setResizeMode(RESIZE_MODE_UNRESIZEABLE);
+ final ActivityRecord activity = task.getRootActivity();
+ final AppCompatAspectRatioOverrides aspectRatioOverrides =
+ activity.mAppCompatController.getAspectRatioOverrides();
+ spyOn(aspectRatioOverrides);
+ doReturn(true).when(aspectRatioOverrides).hasFullscreenOverride();
+ task.intent = null;
+ task.setIntent(activity);
- // Override should take effect and task should be un-resizeable.
- assertFalse(task.getTaskInfo().isResizeable);
+ // Override should take effect and task should be resizeable.
+ assertTrue(task.getTaskInfo().isResizeable);
}
@Test
- @CoreCompatChangeRule.EnableCompatChanges({ActivityInfo.FORCE_NON_RESIZE_APP})
- public void testIsResizeable_resizeable_forceNonResize_overridesDisabled_Resizeable() {
+ public void testIsResizeable_resizeableTask_universalResizeable_resizeable() {
final Task task = new TaskBuilder(mSupervisor)
.setCreateActivity(true)
- .setComponent(
- ComponentName.createRelative(mContext, SizeCompatTests.class.getName()))
.build();
- task.setResizeMode(RESIZE_MODE_RESIZEABLE);
-
- // Disallow resize overrides.
- task.mAllowForceResizeOverride = false;
+ task.setResizeMode(RESIZE_MODE_UNRESIZEABLE);
+ final ActivityRecord activity = task.getRootActivity();
+ spyOn(activity);
+ doReturn(true).when(activity).isUniversalResizeable();
+ task.intent = null;
+ task.setIntent(activity);
- // Override should not take effect and task should be resizeable.
+ // Override should take effect and task should be resizeable.
assertTrue(task.getTaskInfo().isResizeable);
}
@Test
- @CoreCompatChangeRule.EnableCompatChanges({ActivityInfo.FORCE_NON_RESIZE_APP})
- public void testIsResizeable_systemWideForceResize_compatForceNonResize__Resizeable() {
+ public void testIsResizeable_systemWideForceResize_compatForceNonResize_resizeable() {
final Task task = new TaskBuilder(mSupervisor)
.setCreateActivity(true)
- .setComponent(
- ComponentName.createRelative(mContext, SizeCompatTests.class.getName()))
+ .setComponent(ComponentName.createRelative(mContext, TaskTests.class.getName()))
.build();
task.setResizeMode(RESIZE_MODE_RESIZEABLE);
// Set system-wide force resizeable override.
task.mAtmService.mForceResizableActivities = true;
+ final ActivityRecord activity = task.getRootActivity();
+ final AppCompatResizeOverrides resizeOverrides =
+ activity.mAppCompatController.getResizeOverrides();
+ spyOn(activity);
+ spyOn(resizeOverrides);
+ doReturn(true).when(resizeOverrides).shouldOverrideForceNonResizeApp();
+ task.intent = null;
+ task.setIntent(activity);
+
// System wide override should tak priority over app compat override so the task should
// remain resizeable.
assertTrue(task.getTaskInfo().isResizeable);
diff --git a/telephony/java/android/telephony/CarrierConfigManager.java b/telephony/java/android/telephony/CarrierConfigManager.java
index 14915109999c..19574fd95ac7 100644
--- a/telephony/java/android/telephony/CarrierConfigManager.java
+++ b/telephony/java/android/telephony/CarrierConfigManager.java
@@ -11493,17 +11493,17 @@ public class CarrierConfigManager {
+ "target=GERAN|UTRAN|EUTRAN|NGRAN|IWLAN, type=allowed"});
PersistableBundle auto_data_switch_rat_signal_score_string_bundle = new PersistableBundle();
auto_data_switch_rat_signal_score_string_bundle.putIntArray(
- "NR_SA_MMWAVE", new int[]{10000, 13227, 16000, 18488, 20017});
+ "NR_SA_MMWAVE", new int[]{6300, 10227, 16000, 18488, 19017});
auto_data_switch_rat_signal_score_string_bundle.putIntArray(
- "NR_NSA_MMWAVE", new int[]{8000, 10227, 12488, 15017, 15278});
+ "NR_NSA_MMWAVE", new int[]{5700, 9227, 12488, 13517, 15978});
auto_data_switch_rat_signal_score_string_bundle.putIntArray(
"LTE", new int[]{3731, 5965, 8618, 11179, 13384});
auto_data_switch_rat_signal_score_string_bundle.putIntArray(
- "LTE_CA", new int[]{3831, 6065, 8718, 11379, 13484});
+ "LTE_CA", new int[]{3831, 6065, 8718, 11379, 14484});
auto_data_switch_rat_signal_score_string_bundle.putIntArray(
- "NR_SA", new int[]{5288, 6795, 6955, 7562, 9713});
+ "NR_SA", new int[]{2288, 6795, 6955, 7562, 15484});
auto_data_switch_rat_signal_score_string_bundle.putIntArray(
- "NR_NSA", new int[]{5463, 6827, 8029, 9007, 9428});
+ "NR_NSA", new int[]{2463, 6827, 8029, 9007, 15884});
auto_data_switch_rat_signal_score_string_bundle.putIntArray(
"UMTS", new int[]{100, 169, 183, 192, 300});
auto_data_switch_rat_signal_score_string_bundle.putIntArray(
diff --git a/telephony/java/com/android/internal/telephony/ITelephony.aidl b/telephony/java/com/android/internal/telephony/ITelephony.aidl
index 0dd0a42d44b4..b8aa9e8646bd 100644
--- a/telephony/java/com/android/internal/telephony/ITelephony.aidl
+++ b/telephony/java/com/android/internal/telephony/ITelephony.aidl
@@ -3238,6 +3238,15 @@ interface ITelephony {
boolean setOemEnabledSatelliteProvisionStatus(in boolean reset, in boolean isProvisioned);
/**
+ * This API is used by CTS to override the version of the config data
+ *
+ * @param reset Whether to restore the original version
+ * @param version The overriding version
+ * @return {@code true} if successful, {@code false} otherwise
+ */
+ boolean overrideConfigDataVersion(in boolean reset, in int version);
+
+ /**
* Test method to confirm the file contents are not altered.
*/
@JavaPassthrough(annotation="@android.annotation.RequiresPermission("
diff --git a/tests/Input/src/android/hardware/input/KeyGestureEventHandlerTest.kt b/tests/Input/src/android/hardware/input/KeyGestureEventHandlerTest.kt
index 794fd0255726..c62bd0b72584 100644
--- a/tests/Input/src/android/hardware/input/KeyGestureEventHandlerTest.kt
+++ b/tests/Input/src/android/hardware/input/KeyGestureEventHandlerTest.kt
@@ -18,12 +18,10 @@ package android.hardware.input
import android.content.Context
import android.content.ContextWrapper
-import android.os.IBinder
import android.platform.test.annotations.Presubmit
import android.platform.test.flag.junit.SetFlagsRule
import android.view.KeyEvent
import androidx.test.core.app.ApplicationProvider
-import com.android.server.testutils.any
import com.android.test.input.MockInputManagerRule
import org.junit.Before
import org.junit.Rule
@@ -37,6 +35,7 @@ import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertNull
import kotlin.test.fail
+import org.junit.Assert.assertThrows
/**
* Tests for [InputManager.KeyGestureEventHandler].
@@ -82,7 +81,7 @@ class KeyGestureEventHandlerTest {
// Handle key gesture handler registration.
doAnswer {
- val listener = it.getArgument(0) as IKeyGestureHandler
+ val listener = it.getArgument(1) as IKeyGestureHandler
if (registeredListener != null &&
registeredListener!!.asBinder() != listener.asBinder()) {
// There can only be one registered key gesture handler per process.
@@ -90,7 +89,7 @@ class KeyGestureEventHandlerTest {
}
registeredListener = listener
null
- }.`when`(inputManagerRule.mock).registerKeyGestureHandler(any())
+ }.`when`(inputManagerRule.mock).registerKeyGestureHandler(Mockito.any(), Mockito.any())
// Handle key gesture handler being unregistered.
doAnswer {
@@ -101,7 +100,7 @@ class KeyGestureEventHandlerTest {
}
registeredListener = null
null
- }.`when`(inputManagerRule.mock).unregisterKeyGestureHandler(any())
+ }.`when`(inputManagerRule.mock).unregisterKeyGestureHandler(Mockito.any())
}
private fun handleKeyGestureEvent(event: KeyGestureEvent) {
@@ -121,11 +120,12 @@ class KeyGestureEventHandlerTest {
var callbackCount = 0
// Add a key gesture event listener
- inputManager.registerKeyGestureEventHandler(KeyGestureHandler { event, _ ->
+ inputManager.registerKeyGestureEventHandler(
+ listOf(KeyGestureEvent.KEY_GESTURE_TYPE_HOME)
+ ) { event, _ ->
assertEquals(HOME_GESTURE_EVENT, event)
callbackCount++
- true
- })
+ }
// Request handling for key gesture event will notify the handler.
handleKeyGestureEvent(HOME_GESTURE_EVENT)
@@ -135,29 +135,41 @@ class KeyGestureEventHandlerTest {
@Test
fun testAddingHandlersRegistersInternalCallbackHandler() {
// Set up two callbacks.
- val callback1 = KeyGestureHandler { _, _ -> false }
- val callback2 = KeyGestureHandler { _, _ -> false }
+ val callback1 = InputManager.KeyGestureEventHandler { _, _ -> }
+ val callback2 = InputManager.KeyGestureEventHandler { _, _ -> }
assertNull(registeredListener)
// Adding the handler should register the callback with InputManagerService.
- inputManager.registerKeyGestureEventHandler(callback1)
+ inputManager.registerKeyGestureEventHandler(
+ listOf(KeyGestureEvent.KEY_GESTURE_TYPE_HOME),
+ callback1
+ )
assertNotNull(registeredListener)
// Adding another handler should not register new internal listener.
val currListener = registeredListener
- inputManager.registerKeyGestureEventHandler(callback2)
+ inputManager.registerKeyGestureEventHandler(
+ listOf(KeyGestureEvent.KEY_GESTURE_TYPE_BACK),
+ callback2
+ )
assertEquals(currListener, registeredListener)
}
@Test
fun testRemovingHandlersUnregistersInternalCallbackHandler() {
// Set up two callbacks.
- val callback1 = KeyGestureHandler { _, _ -> false }
- val callback2 = KeyGestureHandler { _, _ -> false }
+ val callback1 = InputManager.KeyGestureEventHandler { _, _ -> }
+ val callback2 = InputManager.KeyGestureEventHandler { _, _ -> }
- inputManager.registerKeyGestureEventHandler(callback1)
- inputManager.registerKeyGestureEventHandler(callback2)
+ inputManager.registerKeyGestureEventHandler(
+ listOf(KeyGestureEvent.KEY_GESTURE_TYPE_HOME),
+ callback1
+ )
+ inputManager.registerKeyGestureEventHandler(
+ listOf(KeyGestureEvent.KEY_GESTURE_TYPE_BACK),
+ callback2
+ )
// Only removing all handlers should remove the internal callback
inputManager.unregisterKeyGestureEventHandler(callback1)
@@ -172,47 +184,74 @@ class KeyGestureEventHandlerTest {
var callbackCount1 = 0
var callbackCount2 = 0
// Handler 1 captures all home gestures
- val callback1 = KeyGestureHandler { event, _ ->
+ val callback1 = InputManager.KeyGestureEventHandler { event, _ ->
callbackCount1++
- event.keyGestureType == KeyGestureEvent.KEY_GESTURE_TYPE_HOME
+ assertEquals(KeyGestureEvent.KEY_GESTURE_TYPE_HOME, event.keyGestureType)
}
- // Handler 2 captures all gestures
- val callback2 = KeyGestureHandler { _, _ ->
+ // Handler 2 captures all back gestures
+ val callback2 = InputManager.KeyGestureEventHandler { event, _ ->
callbackCount2++
- true
+ assertEquals(KeyGestureEvent.KEY_GESTURE_TYPE_BACK, event.keyGestureType)
}
// Add both key gesture event handlers
- inputManager.registerKeyGestureEventHandler(callback1)
- inputManager.registerKeyGestureEventHandler(callback2)
+ inputManager.registerKeyGestureEventHandler(
+ listOf(KeyGestureEvent.KEY_GESTURE_TYPE_HOME),
+ callback1
+ )
+ inputManager.registerKeyGestureEventHandler(
+ listOf(KeyGestureEvent.KEY_GESTURE_TYPE_BACK),
+ callback2
+ )
- // Request handling for key gesture event, should notify callbacks in order. So, only the
- // first handler should receive a callback since it captures the event.
+ // Request handling for home key gesture event, should notify only callback1
handleKeyGestureEvent(HOME_GESTURE_EVENT)
assertEquals(1, callbackCount1)
assertEquals(0, callbackCount2)
- // Second handler should receive the event since the first handler doesn't capture the event
+ // Request handling for back key gesture event, should notify only callback2
handleKeyGestureEvent(BACK_GESTURE_EVENT)
- assertEquals(2, callbackCount1)
+ assertEquals(1, callbackCount1)
assertEquals(1, callbackCount2)
inputManager.unregisterKeyGestureEventHandler(callback1)
- // Request handling for key gesture event, should still trigger callback2 but not callback1.
+
+ // Request handling for home key gesture event, should not trigger callback2
handleKeyGestureEvent(HOME_GESTURE_EVENT)
- assertEquals(2, callbackCount1)
- assertEquals(2, callbackCount2)
+ assertEquals(1, callbackCount1)
+ assertEquals(1, callbackCount2)
+ }
+
+ @Test
+ fun testUnableToRegisterSameHandlerTwice() {
+ val handler = InputManager.KeyGestureEventHandler { _, _ -> }
+
+ inputManager.registerKeyGestureEventHandler(
+ listOf(KeyGestureEvent.KEY_GESTURE_TYPE_HOME),
+ handler
+ )
+
+ assertThrows(IllegalArgumentException::class.java) {
+ inputManager.registerKeyGestureEventHandler(
+ listOf(KeyGestureEvent.KEY_GESTURE_TYPE_BACK), handler
+ )
+ }
}
- inner class KeyGestureHandler(
- private var handler: (event: KeyGestureEvent, token: IBinder?) -> Boolean
- ) : InputManager.KeyGestureEventHandler {
+ @Test
+ fun testUnableToRegisterSameGestureTwice() {
+ val handler1 = InputManager.KeyGestureEventHandler { _, _ -> }
+ val handler2 = InputManager.KeyGestureEventHandler { _, _ -> }
+
+ inputManager.registerKeyGestureEventHandler(
+ listOf(KeyGestureEvent.KEY_GESTURE_TYPE_HOME),
+ handler1
+ )
- override fun handleKeyGestureEvent(
- event: KeyGestureEvent,
- focusedToken: IBinder?
- ): Boolean {
- return handler(event, focusedToken)
+ assertThrows(IllegalArgumentException::class.java) {
+ inputManager.registerKeyGestureEventHandler(
+ listOf(KeyGestureEvent.KEY_GESTURE_TYPE_HOME), handler2
+ )
}
}
}
diff --git a/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt b/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt
index 4f1fb6487b19..163dda84a71c 100644
--- a/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt
+++ b/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt
@@ -63,6 +63,7 @@ import org.junit.After
import org.junit.Assert.assertArrayEquals
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
+import org.junit.Assert.assertThrows
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Rule
@@ -107,7 +108,10 @@ class KeyGestureControllerTests {
const val SETTINGS_KEY_BEHAVIOR_SETTINGS_ACTIVITY = 0
const val SETTINGS_KEY_BEHAVIOR_NOTIFICATION_PANEL = 1
const val SETTINGS_KEY_BEHAVIOR_NOTHING = 2
+ const val SYSTEM_PID = 0
const val TEST_PID = 10
+ const val RANDOM_PID1 = 11
+ const val RANDOM_PID2 = 12
}
@JvmField
@@ -170,6 +174,7 @@ class KeyGestureControllerTests {
return atomicFile
}
})
+ startNewInputGlobalTestSession()
}
@After
@@ -199,17 +204,22 @@ class KeyGestureControllerTests {
val correctIm = context.getSystemService(InputManager::class.java)!!
val virtualDevice = correctIm.getInputDevice(KeyCharacterMap.VIRTUAL_KEYBOARD)!!
val kcm = virtualDevice.keyCharacterMap!!
- inputManagerGlobalSession = InputManagerGlobal.createTestSession(iInputManager)
- val inputManager = InputManager(context)
- Mockito.`when`(context.getSystemService(Mockito.eq(Context.INPUT_SERVICE)))
- .thenReturn(inputManager)
-
val keyboardDevice = InputDevice.Builder().setId(DEVICE_ID).build()
Mockito.`when`(iInputManager.inputDeviceIds).thenReturn(intArrayOf(DEVICE_ID))
Mockito.`when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardDevice)
ExtendedMockito.`when`(KeyCharacterMap.load(Mockito.anyInt())).thenReturn(kcm)
}
+ private fun startNewInputGlobalTestSession() {
+ if (this::inputManagerGlobalSession.isInitialized) {
+ inputManagerGlobalSession.close()
+ }
+ inputManagerGlobalSession = InputManagerGlobal.createTestSession(iInputManager)
+ val inputManager = InputManager(context)
+ Mockito.`when`(context.getSystemService(Mockito.eq(Context.INPUT_SERVICE)))
+ .thenReturn(inputManager)
+ }
+
private fun setupKeyGestureController() {
keyGestureController =
KeyGestureController(
@@ -225,13 +235,14 @@ class KeyGestureControllerTests {
return accessibilityShortcutController
}
})
- Mockito.`when`(iInputManager.registerKeyGestureHandler(Mockito.any()))
+ Mockito.`when`(iInputManager.registerKeyGestureHandler(Mockito.any(), Mockito.any()))
.thenAnswer {
val args = it.arguments
if (args[0] != null) {
keyGestureController.registerKeyGestureHandler(
- args[0] as IKeyGestureHandler,
- TEST_PID
+ args[0] as IntArray,
+ args[1] as IKeyGestureHandler,
+ SYSTEM_PID
)
}
}
@@ -285,59 +296,6 @@ class KeyGestureControllerTests {
)
}
- @Test
- fun testKeyGestureEvent_multipleGestureHandlers() {
- setupKeyGestureController()
-
- // Set up two callbacks.
- var callbackCount1 = 0
- var callbackCount2 = 0
- var selfCallback = 0
- val externalHandler1 = KeyGestureHandler { _, _ ->
- callbackCount1++
- true
- }
- val externalHandler2 = KeyGestureHandler { _, _ ->
- callbackCount2++
- true
- }
- val selfHandler = KeyGestureHandler { _, _ ->
- selfCallback++
- false
- }
-
- // Register key gesture handler: External process (last in priority)
- keyGestureController.registerKeyGestureHandler(externalHandler1, currentPid + 1)
-
- // Register key gesture handler: External process (second in priority)
- keyGestureController.registerKeyGestureHandler(externalHandler2, currentPid - 1)
-
- // Register key gesture handler: Self process (first in priority)
- keyGestureController.registerKeyGestureHandler(selfHandler, currentPid)
-
- keyGestureController.handleKeyGesture(/* deviceId = */ 0, intArrayOf(KeyEvent.KEYCODE_HOME),
- /* modifierState = */ 0, KeyGestureEvent.KEY_GESTURE_TYPE_HOME,
- KeyGestureEvent.ACTION_GESTURE_COMPLETE, /* displayId */ 0,
- /* focusedToken = */ null, /* flags = */ 0, /* appLaunchData = */null
- )
-
- assertEquals(
- "Self handler should get callbacks first",
- 1,
- selfCallback
- )
- assertEquals(
- "Higher priority handler should get callbacks first",
- 1,
- callbackCount2
- )
- assertEquals(
- "Lower priority handler should not get callbacks if already handled",
- 0,
- callbackCount1
- )
- }
-
class TestData(
val name: String,
val keys: IntArray,
@@ -789,10 +747,6 @@ class KeyGestureControllerTests {
)
fun testCustomKeyGesturesNotAllowedForSystemGestures(test: TestData) {
setupKeyGestureController()
- // Need to re-init so that bookmarks are correctly blocklisted
- Mockito.`when`(iInputManager.getAppLaunchBookmarks())
- .thenReturn(keyGestureController.appLaunchBookmarks)
- keyGestureController.systemRunning()
val builder = InputGestureData.Builder()
.setKeyGestureType(test.expectedKeyGestureType)
@@ -1163,9 +1117,6 @@ class KeyGestureControllerTests {
KeyEvent.KEYCODE_FULLSCREEN
)
- val handler = KeyGestureHandler { _, _ -> false }
- keyGestureController.registerKeyGestureHandler(handler, 0)
-
for (key in testKeys) {
sendKeys(intArrayOf(key), assertNotSentToApps = true)
}
@@ -1179,6 +1130,7 @@ class KeyGestureControllerTests {
testKeyGestureNotProduced(
"SEARCH -> Default Search",
intArrayOf(KeyEvent.KEYCODE_SEARCH),
+ intArrayOf(KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_SEARCH)
)
}
@@ -1207,6 +1159,10 @@ class KeyGestureControllerTests {
testKeyGestureNotProduced(
"SETTINGS -> Do Nothing",
intArrayOf(KeyEvent.KEYCODE_SETTINGS),
+ intArrayOf(
+ KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_SEARCH,
+ KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_NOTIFICATION_PANEL
+ )
)
}
@@ -1290,28 +1246,6 @@ class KeyGestureControllerTests {
)
),
TestData(
- "VOLUME_DOWN + VOLUME_UP -> Accessibility Chord",
- intArrayOf(KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.KEYCODE_VOLUME_UP),
- KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD,
- intArrayOf(KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.KEYCODE_VOLUME_UP),
- 0,
- intArrayOf(
- KeyGestureEvent.ACTION_GESTURE_START,
- KeyGestureEvent.ACTION_GESTURE_COMPLETE
- )
- ),
- TestData(
- "BACK + DPAD_DOWN -> Accessibility Chord(for TV)",
- intArrayOf(KeyEvent.KEYCODE_BACK, KeyEvent.KEYCODE_DPAD_DOWN),
- KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD,
- intArrayOf(KeyEvent.KEYCODE_BACK, KeyEvent.KEYCODE_DPAD_DOWN),
- 0,
- intArrayOf(
- KeyGestureEvent.ACTION_GESTURE_START,
- KeyGestureEvent.ACTION_GESTURE_COMPLETE
- )
- ),
- TestData(
"BACK + DPAD_CENTER -> TV Trigger Bug Report",
intArrayOf(KeyEvent.KEYCODE_BACK, KeyEvent.KEYCODE_DPAD_CENTER),
KeyGestureEvent.KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT,
@@ -1428,9 +1362,11 @@ class KeyGestureControllerTests {
testLooper.dispatchAll()
// Reinitialize the gesture controller simulating a login/logout for the user.
+ startNewInputGlobalTestSession()
setupKeyGestureController()
keyGestureController.setCurrentUserId(userId)
testLooper.dispatchAll()
+
val savedInputGestures = keyGestureController.getCustomInputGestures(userId, null)
assertEquals(
"Test: $test doesn't produce correct number of saved input gestures",
@@ -1469,6 +1405,7 @@ class KeyGestureControllerTests {
// Delete the old data and reinitialize the controller simulating a "fresh" install.
tempFile.delete()
+ startNewInputGlobalTestSession()
setupKeyGestureController()
keyGestureController.setCurrentUserId(userId)
testLooper.dispatchAll()
@@ -1541,9 +1478,12 @@ class KeyGestureControllerTests {
val handledEvents = mutableListOf<KeyGestureEvent>()
val handler = KeyGestureHandler { event, _ ->
handledEvents.add(KeyGestureEvent(event))
- true
}
- keyGestureController.registerKeyGestureHandler(handler, 0)
+ keyGestureController.registerKeyGestureHandler(
+ intArrayOf(test.expectedKeyGestureType),
+ handler,
+ TEST_PID
+ )
handledEvents.clear()
keyGestureController.handleTouchpadGesture(test.touchpadGestureType)
@@ -1570,7 +1510,7 @@ class KeyGestureControllerTests {
event.appLaunchData
)
- keyGestureController.unregisterKeyGestureHandler(handler, 0)
+ keyGestureController.unregisterKeyGestureHandler(handler, TEST_PID)
}
@Test
@@ -1591,9 +1531,11 @@ class KeyGestureControllerTests {
testLooper.dispatchAll()
// Reinitialize the gesture controller simulating a login/logout for the user.
+ startNewInputGlobalTestSession()
setupKeyGestureController()
keyGestureController.setCurrentUserId(userId)
testLooper.dispatchAll()
+
val savedInputGestures = keyGestureController.getCustomInputGestures(userId, null)
assertEquals(
"Test: $test doesn't produce correct number of saved input gestures",
@@ -1627,6 +1569,7 @@ class KeyGestureControllerTests {
// Delete the old data and reinitialize the controller simulating a "fresh" install.
tempFile.delete()
+ startNewInputGlobalTestSession()
setupKeyGestureController()
keyGestureController.setCurrentUserId(userId)
testLooper.dispatchAll()
@@ -1699,13 +1642,97 @@ class KeyGestureControllerTests {
Mockito.verify(accessibilityShortcutController, never()).performAccessibilityShortcut()
}
+ @Test
+ fun testUnableToRegisterFromSamePidTwice() {
+ setupKeyGestureController()
+
+ val handler1 = KeyGestureHandler { _, _ -> }
+ val handler2 = KeyGestureHandler { _, _ -> }
+ keyGestureController.registerKeyGestureHandler(
+ intArrayOf(KeyGestureEvent.KEY_GESTURE_TYPE_HOME),
+ handler1,
+ RANDOM_PID1
+ )
+
+ assertThrows(IllegalStateException::class.java) {
+ keyGestureController.registerKeyGestureHandler(
+ intArrayOf(KeyGestureEvent.KEY_GESTURE_TYPE_BACK),
+ handler2,
+ RANDOM_PID1
+ )
+ }
+ }
+
+ @Test
+ fun testUnableToRegisterSameGestureTwice() {
+ setupKeyGestureController()
+
+ val handler1 = KeyGestureHandler { _, _ -> }
+ val handler2 = KeyGestureHandler { _, _ -> }
+ keyGestureController.registerKeyGestureHandler(
+ intArrayOf(KeyGestureEvent.KEY_GESTURE_TYPE_HOME),
+ handler1,
+ RANDOM_PID1
+ )
+
+ assertThrows(IllegalArgumentException::class.java) {
+ keyGestureController.registerKeyGestureHandler(
+ intArrayOf(KeyGestureEvent.KEY_GESTURE_TYPE_HOME),
+ handler2,
+ RANDOM_PID2
+ )
+ }
+ }
+
+ @Test
+ fun testUnableToRegisterEmptyListOfGestures() {
+ setupKeyGestureController()
+
+ val handler = KeyGestureHandler { _, _ -> }
+
+ assertThrows(IllegalArgumentException::class.java) {
+ keyGestureController.registerKeyGestureHandler(
+ intArrayOf(),
+ handler,
+ RANDOM_PID1
+ )
+ }
+ }
+
+ @Test
+ fun testGestureHandlerNotCalledOnceUnregistered() {
+ setupKeyGestureController()
+
+ var callbackCount = 0
+ val handler1 = KeyGestureHandler { _, _ -> callbackCount++ }
+ keyGestureController.registerKeyGestureHandler(
+ intArrayOf(KeyGestureEvent.KEY_GESTURE_TYPE_RECENT_APPS),
+ handler1,
+ TEST_PID
+ )
+ sendKeys(intArrayOf(KeyEvent.KEYCODE_RECENT_APPS))
+ assertEquals(1, callbackCount)
+
+ keyGestureController.unregisterKeyGestureHandler(
+ handler1,
+ TEST_PID
+ )
+
+ // Callback should not be sent after unregister
+ sendKeys(intArrayOf(KeyEvent.KEYCODE_RECENT_APPS))
+ assertEquals(1, callbackCount)
+ }
+
private fun testKeyGestureInternal(test: TestData) {
val handledEvents = mutableListOf<KeyGestureEvent>()
val handler = KeyGestureHandler { event, _ ->
handledEvents.add(KeyGestureEvent(event))
- true
}
- keyGestureController.registerKeyGestureHandler(handler, 0)
+ keyGestureController.registerKeyGestureHandler(
+ intArrayOf(test.expectedKeyGestureType),
+ handler,
+ TEST_PID
+ )
handledEvents.clear()
sendKeys(test.keys)
@@ -1744,16 +1771,19 @@ class KeyGestureControllerTests {
)
}
- keyGestureController.unregisterKeyGestureHandler(handler, 0)
+ keyGestureController.unregisterKeyGestureHandler(handler, TEST_PID)
}
- private fun testKeyGestureNotProduced(testName: String, testKeys: IntArray) {
+ private fun testKeyGestureNotProduced(
+ testName: String,
+ testKeys: IntArray,
+ possibleGestures: IntArray
+ ) {
var handledEvents = mutableListOf<KeyGestureEvent>()
val handler = KeyGestureHandler { event, _ ->
handledEvents.add(KeyGestureEvent(event))
- true
}
- keyGestureController.registerKeyGestureHandler(handler, 0)
+ keyGestureController.registerKeyGestureHandler(possibleGestures, handler, TEST_PID)
handledEvents.clear()
sendKeys(testKeys)
@@ -1823,10 +1853,10 @@ class KeyGestureControllerTests {
}
inner class KeyGestureHandler(
- private var handler: (event: AidlKeyGestureEvent, token: IBinder?) -> Boolean
+ private var handler: (event: AidlKeyGestureEvent, token: IBinder?) -> Unit
) : IKeyGestureHandler.Stub() {
- override fun handleKeyGesture(event: AidlKeyGestureEvent, token: IBinder?): Boolean {
- return handler(event, token)
+ override fun handleKeyGesture(event: AidlKeyGestureEvent, token: IBinder?) {
+ handler(event, token)
}
}
}
diff --git a/tests/Input/src/com/android/test/input/AnrTest.kt b/tests/Input/src/com/android/test/input/AnrTest.kt
index f8cb86b7b1fe..3ad3763a5d20 100644
--- a/tests/Input/src/com/android/test/input/AnrTest.kt
+++ b/tests/Input/src/com/android/test/input/AnrTest.kt
@@ -16,6 +16,7 @@
package com.android.test.input
import android.app.ActivityManager
+import android.app.ActivityTaskManager
import android.app.ApplicationExitInfo
import android.app.Instrumentation
import android.content.Intent
@@ -28,6 +29,7 @@ import android.os.SystemClock
import android.server.wm.CtsWindowInfoUtils.getWindowCenter
import android.server.wm.CtsWindowInfoUtils.waitForWindowOnTop
import android.testing.PollingCheck
+import android.util.Log
import android.view.InputEvent
import android.view.MotionEvent
import android.view.MotionEvent.ACTION_DOWN
@@ -46,21 +48,19 @@ import com.android.cts.input.inputeventmatchers.withMotionAction
import java.time.Duration
import java.util.concurrent.LinkedBlockingQueue
import java.util.function.Supplier
-import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Assert.fail
-import org.junit.Before
+import org.junit.Assume.assumeTrue
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
/**
- * Click on the center of the window identified by the provided window token.
- * The click is performed using "UinputTouchScreen" device.
- * If the touchscreen device is closed too soon, it may cause the click to be dropped. Therefore,
- * the provided runnable can ensure that the click is delivered before the device is closed, thus
- * avoiding this race.
+ * Click on the center of the window identified by the provided window token. The click is performed
+ * using "UinputTouchScreen" device. If the touchscreen device is closed too soon, it may cause the
+ * click to be dropped. Therefore, the provided runnable can ensure that the click is delivered
+ * before the device is closed, thus avoiding this race.
*/
private fun clickOnWindow(
token: IBinder,
@@ -104,6 +104,10 @@ class AnrTest {
private val remoteInputEvents = LinkedBlockingQueue<InputEvent>()
private val verifier = BlockingQueueEventVerifier(remoteInputEvents)
+ // Some devices don't support ANR error dialogs, such as cars, TVs, etc.
+ private val anrDialogsAreSupported =
+ ActivityTaskManager.currentUiModeSupportsErrorDialogs(instrumentation.targetContext)
+
val binder =
object : IAnrTestService.Stub() {
override fun provideActivityInfo(token: IBinder, displayId: Int, pid: Int) {
@@ -121,34 +125,37 @@ class AnrTest {
@get:Rule val debugInputRule = DebugInputRule()
- @Before
- fun setUp() {
- startUnresponsiveActivity()
- PACKAGE_NAME = UnresponsiveGestureMonitorActivity::class.java.getPackage()!!.getName()
- }
-
- @After fun tearDown() {}
-
@Test
@DebugInputRule.DebugInput(bug = 339924248)
fun testGestureMonitorAnr_Close() {
+ startUnresponsiveActivity()
+ val timestamp = System.currentTimeMillis()
triggerAnr()
- clickCloseAppOnAnrDialog()
+ if (anrDialogsAreSupported) {
+ clickCloseAppOnAnrDialog()
+ } else {
+ Log.i(TAG, "The device does not support ANR dialogs, skipping check for ANR window")
+ // We still want to wait for the app to get killed by the ActivityManager
+ }
+ waitForNewExitReasonAfter(timestamp)
}
@Test
@DebugInputRule.DebugInput(bug = 339924248)
fun testGestureMonitorAnr_Wait() {
+ assumeTrue(anrDialogsAreSupported)
+ startUnresponsiveActivity()
triggerAnr()
clickWaitOnAnrDialog()
SystemClock.sleep(500) // Wait at least 500ms after tapping on wait
// ANR dialog should reappear after a delay - find the close button on it to verify
+ val timestamp = System.currentTimeMillis()
clickCloseAppOnAnrDialog()
+ waitForNewExitReasonAfter(timestamp)
}
private fun clickCloseAppOnAnrDialog() {
// Find anr dialog and kill app
- val timestamp = System.currentTimeMillis()
val uiDevice: UiDevice = UiDevice.getInstance(instrumentation)
val closeAppButton: UiObject2? =
uiDevice.wait(Until.findObject(By.res("android:id/aerr_close")), 20000)
@@ -157,14 +164,6 @@ class AnrTest {
return
}
closeAppButton.click()
- /**
- * We must wait for the app to be fully closed before exiting this test. This is because
- * another test may again invoke 'am start' for the same activity. If the 1st process that
- * got ANRd isn't killed by the time second 'am start' runs, the killing logic will apply to
- * the newly launched 'am start' instance, and the second test will fail because the
- * unresponsive activity will never be launched.
- */
- waitForNewExitReasonAfter(timestamp)
}
private fun clickWaitOnAnrDialog() {
@@ -180,16 +179,27 @@ class AnrTest {
}
private fun getExitReasons(): List<ApplicationExitInfo> {
+ val packageName = UnresponsiveGestureMonitorActivity::class.java.getPackage()!!.name
lateinit var infos: List<ApplicationExitInfo>
instrumentation.runOnMainSync {
val am = instrumentation.getContext().getSystemService(ActivityManager::class.java)!!
- infos = am.getHistoricalProcessExitReasons(PACKAGE_NAME, remotePid!!, NO_MAX)
+ infos = am.getHistoricalProcessExitReasons(packageName, remotePid!!, NO_MAX)
}
return infos
}
+ /**
+ * We must wait for the app to be fully closed before exiting this test. This is because another
+ * test may again invoke 'am start' for the same activity. If the 1st process that got ANRd
+ * isn't killed by the time second 'am start' runs, the killing logic will apply to the newly
+ * launched 'am start' instance, and the second test will fail because the unresponsive activity
+ * will never be launched.
+ *
+ * Also, we must ensure that we wait until it's killed, so that the next test can launch this
+ * activity again.
+ */
private fun waitForNewExitReasonAfter(timestamp: Long) {
- PollingCheck.waitFor {
+ PollingCheck.waitFor(Duration.ofSeconds(20).toMillis() * Build.HW_TIMEOUT_MULTIPLIER) {
val reasons = getExitReasons()
!reasons.isEmpty() && reasons[0].timestamp >= timestamp
}
@@ -199,16 +209,15 @@ class AnrTest {
}
private fun triggerAnr() {
- clickOnWindow(
- remoteWindowToken!!,
- remoteDisplayId!!,
- instrumentation,
- ) { verifier.assertReceivedMotion(withMotionAction(ACTION_DOWN)) }
+ clickOnWindow(remoteWindowToken!!, remoteDisplayId!!, instrumentation) {
+ verifier.assertReceivedMotion(withMotionAction(ACTION_DOWN))
+ }
SystemClock.sleep(DISPATCHING_TIMEOUT.toLong()) // default ANR timeout for gesture monitors
}
private fun startUnresponsiveActivity() {
+ remoteWindowToken = null
val intent =
Intent(instrumentation.targetContext, UnresponsiveGestureMonitorActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_NEW_DOCUMENT
@@ -218,12 +227,17 @@ class AnrTest {
instrumentation.targetContext.startActivity(intent)
// first, wait for the token to become valid
PollingCheck.check(
- "UnresponsiveGestureMonitorActivity failed to call 'provideActivityInfo'",
- Duration.ofSeconds(5).toMillis()) { remoteWindowToken != null }
+ "UnresponsiveGestureMonitorActivity failed to call 'provideActivityInfo'",
+ Duration.ofSeconds(10).toMillis() * Build.HW_TIMEOUT_MULTIPLIER,
+ ) {
+ remoteWindowToken != null
+ }
// next, wait for the window of the activity to get on top
// we could combine the two checks above, but the current setup makes it easier to detect
// errors
- assertTrue("Remote activity window did not become visible",
- waitForWindowOnTop(Duration.ofSeconds(5), Supplier { remoteWindowToken }))
+ assertTrue(
+ "Remote activity window did not become visible",
+ waitForWindowOnTop(Duration.ofSeconds(5), Supplier { remoteWindowToken }),
+ )
}
}
diff --git a/tools/aapt2/ResourcesInternal.proto b/tools/aapt2/ResourcesInternal.proto
index f4735a2f6ce7..380c5f21103c 100644
--- a/tools/aapt2/ResourcesInternal.proto
+++ b/tools/aapt2/ResourcesInternal.proto
@@ -50,8 +50,11 @@ message CompiledFile {
// Any symbols this file auto-generates/exports (eg. @+id/foo in an XML file).
repeated Symbol exported_symbol = 5;
- // The status of the flag the file is behind if any
+ // The status of the read only flag the file is behind if any
uint32 flag_status = 6;
bool flag_negated = 7;
string flag_name = 8;
+
+ // Whether the file uses read/write feature flags
+ bool uses_readwrite_feature_flags = 9;
}
diff --git a/tools/aapt2/cmd/Compile.cpp b/tools/aapt2/cmd/Compile.cpp
index a5e18d35a256..3b4f5429f254 100644
--- a/tools/aapt2/cmd/Compile.cpp
+++ b/tools/aapt2/cmd/Compile.cpp
@@ -407,6 +407,45 @@ static bool IsValidFile(IAaptContext* context, const std::string& input_path) {
return true;
}
+class FindReadWriteFlagsVisitor : public xml::Visitor {
+ public:
+ FindReadWriteFlagsVisitor(const FeatureFlagValues& feature_flag_values)
+ : feature_flag_values_(feature_flag_values) {
+ }
+
+ void Visit(xml::Element* node) override {
+ if (had_flags_) {
+ return;
+ }
+ auto* attr = node->FindAttribute(xml::kSchemaAndroid, xml::kAttrFeatureFlag);
+ if (attr != nullptr) {
+ std::string_view flag_name = util::TrimWhitespace(attr->value);
+ if (flag_name.starts_with('!')) {
+ flag_name = flag_name.substr(1);
+ }
+ if (auto it = feature_flag_values_.find(flag_name); it != feature_flag_values_.end()) {
+ if (!it->second.read_only) {
+ had_flags_ = true;
+ return;
+ }
+ } else {
+ // Flag not passed to aapt2, must evaluate at runtime
+ had_flags_ = true;
+ return;
+ }
+ }
+ VisitChildren(node);
+ }
+
+ bool HadFlags() const {
+ return had_flags_;
+ }
+
+ private:
+ bool had_flags_ = false;
+ const FeatureFlagValues& feature_flag_values_;
+};
+
static bool CompileXml(IAaptContext* context, const CompileOptions& options,
const ResourcePathData& path_data, io::IFile* file, IArchiveWriter* writer,
const std::string& output_path) {
@@ -436,6 +475,10 @@ static bool CompileXml(IAaptContext* context, const CompileOptions& options,
xmlres->file.type = ResourceFile::Type::kProtoXml;
xmlres->file.flag = ParseFlag(path_data.flag_name);
+ FindReadWriteFlagsVisitor visitor(options.feature_flag_values);
+ xmlres->root->Accept(&visitor);
+ xmlres->file.uses_readwrite_feature_flags = visitor.HadFlags();
+
if (xmlres->file.flag) {
std::string error;
auto flag_status = GetFlagStatus(xmlres->file.flag, options.feature_flag_values, &error);
diff --git a/tools/aapt2/cmd/Convert.h b/tools/aapt2/cmd/Convert.h
index 9452e588953e..5576ec0b882f 100644
--- a/tools/aapt2/cmd/Convert.h
+++ b/tools/aapt2/cmd/Convert.h
@@ -38,14 +38,14 @@ class ConvertCommand : public Command {
"--enable-sparse-encoding",
"Enables encoding sparse entries using a binary search tree.\n"
"This decreases APK size at the cost of resource retrieval performance.\n"
- "Only applies sparse encoding to Android O+ resources or all resources if minSdk of "
- "the APK is O+",
+ "Only applies sparse encoding if minSdk of the APK is >= 32",
&enable_sparse_encoding_);
- AddOptionalSwitch("--force-sparse-encoding",
- "Enables encoding sparse entries using a binary search tree.\n"
- "This decreases APK size at the cost of resource retrieval performance.\n"
- "Applies sparse encoding to all resources regardless of minSdk.",
- &force_sparse_encoding_);
+ AddOptionalSwitch(
+ "--force-sparse-encoding",
+ "Enables encoding sparse entries using a binary search tree.\n"
+ "This decreases APK size at the cost of resource retrieval performance.\n"
+ "Only applies sparse encoding if minSdk of the APK is >= 32 or is not set",
+ &force_sparse_encoding_);
AddOptionalSwitch(
"--enable-compact-entries",
"This decreases APK size by using compact resource entries for simple data types.",
diff --git a/tools/aapt2/cmd/Link.cpp b/tools/aapt2/cmd/Link.cpp
index 755dbb6f8e42..0e18ee250993 100644
--- a/tools/aapt2/cmd/Link.cpp
+++ b/tools/aapt2/cmd/Link.cpp
@@ -615,6 +615,8 @@ bool ResourceFileFlattener::Flatten(ResourceTable* table, IArchiveWriter* archiv
file_op.xml_to_flatten->file.source = file_ref->GetSource();
file_op.xml_to_flatten->file.name =
ResourceName(pkg->name, type->named_type, entry->name);
+ file_op.xml_to_flatten->file.uses_readwrite_feature_flags =
+ config_value->uses_readwrite_feature_flags;
}
// NOTE(adamlesinski): Explicitly construct a StringPiece here, or
@@ -647,6 +649,17 @@ bool ResourceFileFlattener::Flatten(ResourceTable* table, IArchiveWriter* archiv
}
}
+ FeatureFlagsFilterOptions flags_filter_options;
+ // Don't fail on unrecognized flags or flags without values as these flags might be
+ // defined and have a value by the time they are evaluated at runtime.
+ flags_filter_options.fail_on_unrecognized_flags = false;
+ flags_filter_options.flags_must_have_value = false;
+ flags_filter_options.remove_disabled_elements = true;
+ FeatureFlagsFilter flags_filter(options_.feature_flag_values, flags_filter_options);
+ if (!flags_filter.Consume(context_, file_op.xml_to_flatten.get())) {
+ return 1;
+ }
+
std::vector<std::unique_ptr<xml::XmlResource>> versioned_docs =
LinkAndVersionXmlFile(table, &file_op);
if (versioned_docs.empty()) {
@@ -673,6 +686,7 @@ bool ResourceFileFlattener::Flatten(ResourceTable* table, IArchiveWriter* archiv
// Update the output format of this XML file.
file_ref->type = XmlFileTypeForOutputFormat(options_.output_format);
+
bool result = table->AddResource(
NewResourceBuilder(file.name)
.SetValue(std::move(file_ref), file.config)
@@ -685,14 +699,6 @@ bool ResourceFileFlattener::Flatten(ResourceTable* table, IArchiveWriter* archiv
}
}
- FeatureFlagsFilterOptions flags_filter_options;
- flags_filter_options.fail_on_unrecognized_flags = false;
- flags_filter_options.flags_must_have_value = false;
- FeatureFlagsFilter flags_filter(options_.feature_flag_values, flags_filter_options);
- if (!flags_filter.Consume(context_, doc.get())) {
- return 1;
- }
-
error |= !FlattenXml(context_, *doc, dst_path, options_.keep_raw_values,
false /*utf16*/, options_.output_format, archive_writer);
}
diff --git a/tools/aapt2/cmd/Link.h b/tools/aapt2/cmd/Link.h
index 977978834fcd..54a8c8625eb5 100644
--- a/tools/aapt2/cmd/Link.h
+++ b/tools/aapt2/cmd/Link.h
@@ -164,9 +164,12 @@ class LinkCommand : public Command {
AddOptionalSwitch("--no-resource-removal", "Disables automatic removal of resources without\n"
"defaults. Use this only when building runtime resource overlay packages.",
&options_.no_resource_removal);
- AddOptionalSwitch("--enable-sparse-encoding",
- "This decreases APK size at the cost of resource retrieval performance.",
- &options_.use_sparse_encoding);
+ AddOptionalSwitch(
+ "--enable-sparse-encoding",
+ "Enables encoding sparse entries using a binary search tree.\n"
+ "This decreases APK size at the cost of resource retrieval performance.\n"
+ "Only applies sparse encoding if minSdk of the APK is >= 32",
+ &options_.use_sparse_encoding);
AddOptionalSwitch("--enable-compact-entries",
"This decreases APK size by using compact resource entries for simple data types.",
&options_.table_flattener_options.use_compact_entries);
diff --git a/tools/aapt2/cmd/Optimize.h b/tools/aapt2/cmd/Optimize.h
index 012b0f230ca2..a8f547e3d96c 100644
--- a/tools/aapt2/cmd/Optimize.h
+++ b/tools/aapt2/cmd/Optimize.h
@@ -108,14 +108,14 @@ class OptimizeCommand : public Command {
"--enable-sparse-encoding",
"Enables encoding sparse entries using a binary search tree.\n"
"This decreases APK size at the cost of resource retrieval performance.\n"
- "Only applies sparse encoding to Android O+ resources or all resources if minSdk of "
- "the APK is O+",
+ "Only applies sparse encoding if minSdk of the APK is >= 32",
&options_.enable_sparse_encoding);
- AddOptionalSwitch("--force-sparse-encoding",
- "Enables encoding sparse entries using a binary search tree.\n"
- "This decreases APK size at the cost of resource retrieval performance.\n"
- "Applies sparse encoding to all resources regardless of minSdk.",
- &options_.force_sparse_encoding);
+ AddOptionalSwitch(
+ "--force-sparse-encoding",
+ "Enables encoding sparse entries using a binary search tree.\n"
+ "This decreases APK size at the cost of resource retrieval performance.\n"
+ "Only applies sparse encoding if minSdk of the APK is >= 32 or is not set",
+ &options_.force_sparse_encoding);
AddOptionalSwitch(
"--enable-compact-entries",
"This decreases APK size by using compact resource entries for simple data types.",
diff --git a/tools/aapt2/format/binary/TableFlattener.cpp b/tools/aapt2/format/binary/TableFlattener.cpp
index 50144ae816b6..d19c9f2d5d75 100644
--- a/tools/aapt2/format/binary/TableFlattener.cpp
+++ b/tools/aapt2/format/binary/TableFlattener.cpp
@@ -197,13 +197,16 @@ class PackageFlattener {
bool sparse_encode = sparse_entries_ == SparseEntriesMode::Enabled ||
sparse_entries_ == SparseEntriesMode::Forced;
- if (sparse_entries_ == SparseEntriesMode::Forced ||
- (context_->GetMinSdkVersion() == 0 && config.sdkVersion == 0)) {
- // Sparse encode if forced or sdk version is not set in context and config.
- } else {
- // Otherwise, only sparse encode if the entries will be read on platforms S_V2+.
- sparse_encode = sparse_encode && (context_->GetMinSdkVersion() >= SDK_S_V2);
- }
+ // Only sparse encode if the entries will be read on platforms S_V2+. Sparse encoding
+ // is not supported on older platforms (b/197642721, b/197976367).
+ //
+ // We also allow sparse encoding for minSdk is 0 (not set) if sparse encoding is forced,
+ // in order to support Bundletool's usage of aapt2 where minSdk is not set in splits.
+ bool meets_min_sdk_requirement_for_sparse_encoding =
+ (context_->GetMinSdkVersion() >= SDK_S_V2) ||
+ (context_->GetMinSdkVersion() == 0 && sparse_entries_ == SparseEntriesMode::Forced);
+
+ sparse_encode = sparse_encode && meets_min_sdk_requirement_for_sparse_encoding;
// Only sparse encode if the offsets are representable in 2 bytes.
sparse_encode = sparse_encode && short_offsets;
diff --git a/tools/aapt2/format/binary/TableFlattener_test.cpp b/tools/aapt2/format/binary/TableFlattener_test.cpp
index 9156b96b67ec..0e8aae14a350 100644
--- a/tools/aapt2/format/binary/TableFlattener_test.cpp
+++ b/tools/aapt2/format/binary/TableFlattener_test.cpp
@@ -15,6 +15,7 @@
*/
#include "format/binary/TableFlattener.h"
+#include <string>
#include "android-base/stringprintf.h"
#include "androidfw/TypeWrappers.h"
@@ -326,6 +327,28 @@ static std::unique_ptr<ResourceTable> BuildTableWithSparseEntries(
return table;
}
+static void CheckSparseEntries(IAaptContext* context, const ConfigDescription& sparse_config,
+ const std::string& sparse_contents) {
+ ResourceTable sparse_table;
+ BinaryResourceParser parser(context->GetDiagnostics(), &sparse_table, Source("test.arsc"),
+ sparse_contents.data(), sparse_contents.size());
+ ASSERT_TRUE(parser.Parse());
+
+ auto value = test::GetValueForConfig<BinaryPrimitive>(&sparse_table, "android:string/foo_0",
+ sparse_config);
+ ASSERT_THAT(value, NotNull());
+ EXPECT_EQ(0u, value->value.data);
+
+ ASSERT_THAT(test::GetValueForConfig<BinaryPrimitive>(&sparse_table, "android:string/foo_1",
+ sparse_config),
+ IsNull());
+
+ value = test::GetValueForConfig<BinaryPrimitive>(&sparse_table, "android:string/foo_4",
+ sparse_config);
+ ASSERT_THAT(value, NotNull());
+ EXPECT_EQ(4u, value->value.data);
+}
+
TEST_F(TableFlattenerTest, FlattenSparseEntryWithMinSdkSV2) {
std::unique_ptr<IAaptContext> context = test::ContextBuilder()
.SetCompilationPackage("android")
@@ -347,29 +370,56 @@ TEST_F(TableFlattenerTest, FlattenSparseEntryWithMinSdkSV2) {
EXPECT_GT(no_sparse_contents.size(), sparse_contents.size());
- // Attempt to parse the sparse contents.
+ CheckSparseEntries(context.get(), sparse_config, sparse_contents);
+}
- ResourceTable sparse_table;
- BinaryResourceParser parser(context->GetDiagnostics(), &sparse_table, Source("test.arsc"),
- sparse_contents.data(), sparse_contents.size());
- ASSERT_TRUE(parser.Parse());
+TEST_F(TableFlattenerTest, FlattenSparseEntryWithMinSdkSV2AndForced) {
+ std::unique_ptr<IAaptContext> context = test::ContextBuilder()
+ .SetCompilationPackage("android")
+ .SetPackageId(0x01)
+ .SetMinSdkVersion(SDK_S_V2)
+ .Build();
- auto value = test::GetValueForConfig<BinaryPrimitive>(&sparse_table, "android:string/foo_0",
- sparse_config);
- ASSERT_THAT(value, NotNull());
- EXPECT_EQ(0u, value->value.data);
+ const ConfigDescription sparse_config = test::ParseConfigOrDie("en-rGB");
+ auto table_in = BuildTableWithSparseEntries(context.get(), sparse_config, 0.25f);
- ASSERT_THAT(test::GetValueForConfig<BinaryPrimitive>(&sparse_table, "android:string/foo_1",
- sparse_config),
- IsNull());
+ TableFlattenerOptions options;
+ options.sparse_entries = SparseEntriesMode::Forced;
- value = test::GetValueForConfig<BinaryPrimitive>(&sparse_table, "android:string/foo_4",
- sparse_config);
- ASSERT_THAT(value, NotNull());
- EXPECT_EQ(4u, value->value.data);
+ std::string no_sparse_contents;
+ ASSERT_TRUE(Flatten(context.get(), {}, table_in.get(), &no_sparse_contents));
+
+ std::string sparse_contents;
+ ASSERT_TRUE(Flatten(context.get(), options, table_in.get(), &sparse_contents));
+
+ EXPECT_GT(no_sparse_contents.size(), sparse_contents.size());
+
+ CheckSparseEntries(context.get(), sparse_config, sparse_contents);
}
-TEST_F(TableFlattenerTest, FlattenSparseEntryWithConfigSdkVersionSV2) {
+TEST_F(TableFlattenerTest, FlattenSparseEntryWithMinSdkBeforeSV2) {
+ std::unique_ptr<IAaptContext> context = test::ContextBuilder()
+ .SetCompilationPackage("android")
+ .SetPackageId(0x01)
+ .SetMinSdkVersion(SDK_LOLLIPOP)
+ .Build();
+
+ const ConfigDescription sparse_config = test::ParseConfigOrDie("en-rGB");
+ auto table_in = BuildTableWithSparseEntries(context.get(), sparse_config, 0.25f);
+
+ TableFlattenerOptions options;
+ options.sparse_entries = SparseEntriesMode::Enabled;
+
+ std::string no_sparse_contents;
+ ASSERT_TRUE(Flatten(context.get(), {}, table_in.get(), &no_sparse_contents));
+
+ std::string sparse_contents;
+ ASSERT_TRUE(Flatten(context.get(), options, table_in.get(), &sparse_contents));
+
+ EXPECT_EQ(no_sparse_contents.size(), sparse_contents.size());
+}
+
+TEST_F(TableFlattenerTest, FlattenSparseEntryWithMinSdkBeforeSV2AndConfigSdkVersionSV2) {
std::unique_ptr<IAaptContext> context = test::ContextBuilder()
.SetCompilationPackage("android")
.SetPackageId(0x01)
@@ -391,7 +441,7 @@ TEST_F(TableFlattenerTest, FlattenSparseEntryWithConfigSdkVersionSV2) {
EXPECT_EQ(no_sparse_contents.size(), sparse_contents.size());
}
-TEST_F(TableFlattenerTest, FlattenSparseEntryRegardlessOfMinSdkWhenForced) {
+TEST_F(TableFlattenerTest, FlattenSparseEntryWithMinSdkBeforeSV2AndForced) {
std::unique_ptr<IAaptContext> context = test::ContextBuilder()
.SetCompilationPackage("android")
.SetPackageId(0x01)
@@ -410,7 +460,7 @@ TEST_F(TableFlattenerTest, FlattenSparseEntryRegardlessOfMinSdkWhenForced) {
std::string sparse_contents;
ASSERT_TRUE(Flatten(context.get(), options, table_in.get(), &sparse_contents));
- EXPECT_GT(no_sparse_contents.size(), sparse_contents.size());
+ EXPECT_EQ(no_sparse_contents.size(), sparse_contents.size());
}
TEST_F(TableFlattenerTest, FlattenSparseEntryWithSdkVersionNotSet) {
@@ -429,28 +479,28 @@ TEST_F(TableFlattenerTest, FlattenSparseEntryWithSdkVersionNotSet) {
std::string sparse_contents;
ASSERT_TRUE(Flatten(context.get(), options, table_in.get(), &sparse_contents));
- EXPECT_GT(no_sparse_contents.size(), sparse_contents.size());
+ EXPECT_EQ(no_sparse_contents.size(), sparse_contents.size());
+}
- // Attempt to parse the sparse contents.
+TEST_F(TableFlattenerTest, FlattenSparseEntryWithSdkVersionNotSetAndForced) {
+ std::unique_ptr<IAaptContext> context =
+ test::ContextBuilder().SetCompilationPackage("android").SetPackageId(0x01).Build();
- ResourceTable sparse_table;
- BinaryResourceParser parser(context->GetDiagnostics(), &sparse_table, Source("test.arsc"),
- sparse_contents.data(), sparse_contents.size());
- ASSERT_TRUE(parser.Parse());
+ const ConfigDescription sparse_config = test::ParseConfigOrDie("en-rGB");
+ auto table_in = BuildTableWithSparseEntries(context.get(), sparse_config, 0.25f);
- auto value = test::GetValueForConfig<BinaryPrimitive>(&sparse_table, "android:string/foo_0",
- sparse_config);
- ASSERT_THAT(value, NotNull());
- EXPECT_EQ(0u, value->value.data);
+ TableFlattenerOptions options;
+ options.sparse_entries = SparseEntriesMode::Forced;
- ASSERT_THAT(test::GetValueForConfig<BinaryPrimitive>(&sparse_table, "android:string/foo_1",
- sparse_config),
- IsNull());
+ std::string no_sparse_contents;
+ ASSERT_TRUE(Flatten(context.get(), {}, table_in.get(), &no_sparse_contents));
- value = test::GetValueForConfig<BinaryPrimitive>(&sparse_table, "android:string/foo_4",
- sparse_config);
- ASSERT_THAT(value, NotNull());
- EXPECT_EQ(4u, value->value.data);
+ std::string sparse_contents;
+ ASSERT_TRUE(Flatten(context.get(), options, table_in.get(), &sparse_contents));
+
+ EXPECT_GT(no_sparse_contents.size(), sparse_contents.size());
+
+ CheckSparseEntries(context.get(), sparse_config, sparse_contents);
}
TEST_F(TableFlattenerTest, DoNotUseSparseEntryForDenseConfig) {
diff --git a/tools/aapt2/format/proto/ProtoDeserialize.cpp b/tools/aapt2/format/proto/ProtoDeserialize.cpp
index 91ec3485ac3b..b8936553a193 100644
--- a/tools/aapt2/format/proto/ProtoDeserialize.cpp
+++ b/tools/aapt2/format/proto/ProtoDeserialize.cpp
@@ -640,6 +640,7 @@ bool DeserializeCompiledFileFromPb(const pb::internal::CompiledFile& pb_file,
out_file->name = name_ref.ToResourceName();
out_file->source.path = pb_file.source_path();
out_file->type = DeserializeFileReferenceTypeFromPb(pb_file.type());
+ out_file->uses_readwrite_feature_flags = pb_file.uses_readwrite_feature_flags();
out_file->flag_status = (FlagStatus)pb_file.flag_status();
if (!pb_file.flag_name().empty()) {
diff --git a/tools/aapt2/format/proto/ProtoSerialize.cpp b/tools/aapt2/format/proto/ProtoSerialize.cpp
index fcc77d5a9d6d..da99c4f5917c 100644
--- a/tools/aapt2/format/proto/ProtoSerialize.cpp
+++ b/tools/aapt2/format/proto/ProtoSerialize.cpp
@@ -767,6 +767,7 @@ void SerializeCompiledFileToPb(const ResourceFile& file, pb::internal::CompiledF
out_file->set_flag_negated(file.flag->negated);
out_file->set_flag_name(file.flag->name);
}
+ out_file->set_uses_readwrite_feature_flags(file.uses_readwrite_feature_flags);
for (const SourcedResourceName& exported : file.exported_symbols) {
pb::internal::CompiledFile_Symbol* pb_symbol = out_file->add_exported_symbol();
diff --git a/tools/aapt2/link/FlaggedResources_test.cpp b/tools/aapt2/link/FlaggedResources_test.cpp
index 47a71fe36e9f..4dcb8507fa45 100644
--- a/tools/aapt2/link/FlaggedResources_test.cpp
+++ b/tools/aapt2/link/FlaggedResources_test.cpp
@@ -226,9 +226,11 @@ TEST_F(FlaggedResourcesTest, ReadWriteFlagInXmlGetsFlagged) {
}
}
}
+
ASSERT_TRUE(found) << "No entry for layout1 at v36 with FLAG_USES_FEATURE_FLAGS bit set";
- // There should only be 1 entry that has the FLAG_USES_FEATURE_FLAGS bit of flags set to 1
- ASSERT_EQ(fields_flagged, 1);
+ // There should only be 2 entry that has the FLAG_USES_FEATURE_FLAGS bit of flags set to 1, the
+ // three versions of the layout file that has flags
+ ASSERT_EQ(fields_flagged, 3);
}
} // namespace aapt
diff --git a/tools/aapt2/link/FlaggedXmlVersioner.cpp b/tools/aapt2/link/FlaggedXmlVersioner.cpp
index 8a3337c446cb..626cae73bfa2 100644
--- a/tools/aapt2/link/FlaggedXmlVersioner.cpp
+++ b/tools/aapt2/link/FlaggedXmlVersioner.cpp
@@ -35,10 +35,6 @@ class AllDisabledFlagsVisitor : public xml::Visitor {
VisitChildren(node);
}
- bool HadFlags() const {
- return had_flags_;
- }
-
private:
bool FixupOrShouldRemove(const std::unique_ptr<xml::Node>& node) {
if (auto* el = NodeCast<Element>(node.get())) {
@@ -47,7 +43,6 @@ class AllDisabledFlagsVisitor : public xml::Visitor {
return false;
}
- had_flags_ = true;
// This class assumes all flags are disabled so we want to remove any elements behind flags
// unless the flag specification is negated. In the negated case we remove the featureFlag
// attribute because we have already determined whether we are keeping the element or not.
@@ -62,56 +57,27 @@ class AllDisabledFlagsVisitor : public xml::Visitor {
return false;
}
-
- bool had_flags_ = false;
-};
-
-// An xml visitor that goes through the a doc and determines if any elements are behind a flag.
-class FindFlagsVisitor : public xml::Visitor {
- public:
- void Visit(xml::Element* node) override {
- if (had_flags_) {
- return;
- }
- auto* attr = node->FindAttribute(xml::kSchemaAndroid, xml::kAttrFeatureFlag);
- if (attr != nullptr) {
- had_flags_ = true;
- return;
- }
- VisitChildren(node);
- }
-
- bool HadFlags() const {
- return had_flags_;
- }
-
- bool had_flags_ = false;
};
std::vector<std::unique_ptr<xml::XmlResource>> FlaggedXmlVersioner::Process(IAaptContext* context,
xml::XmlResource* doc) {
std::vector<std::unique_ptr<xml::XmlResource>> docs;
- if ((static_cast<ApiVersion>(doc->file.config.sdkVersion) >= SDK_BAKLAVA) ||
- (static_cast<ApiVersion>(context->GetMinSdkVersion()) >= SDK_BAKLAVA)) {
+ if (!doc->file.uses_readwrite_feature_flags) {
+ docs.push_back(doc->Clone());
+ } else if ((static_cast<ApiVersion>(doc->file.config.sdkVersion) >= SDK_BAKLAVA) ||
+ (static_cast<ApiVersion>(context->GetMinSdkVersion()) >= SDK_BAKLAVA)) {
// Support for read/write flags was added in baklava so if the doc will only get used on
// baklava or later we can just return the original doc.
docs.push_back(doc->Clone());
- FindFlagsVisitor visitor;
- doc->root->Accept(&visitor);
- docs.back()->file.uses_readwrite_feature_flags = visitor.HadFlags();
} else {
auto preBaklavaVersion = doc->Clone();
AllDisabledFlagsVisitor visitor;
preBaklavaVersion->root->Accept(&visitor);
- preBaklavaVersion->file.uses_readwrite_feature_flags = false;
docs.push_back(std::move(preBaklavaVersion));
- if (visitor.HadFlags()) {
- auto baklavaVersion = doc->Clone();
- baklavaVersion->file.config.sdkVersion = SDK_BAKLAVA;
- baklavaVersion->file.uses_readwrite_feature_flags = true;
- docs.push_back(std::move(baklavaVersion));
- }
+ auto baklavaVersion = doc->Clone();
+ baklavaVersion->file.config.sdkVersion = SDK_BAKLAVA;
+ docs.push_back(std::move(baklavaVersion));
}
return docs;
}
diff --git a/tools/aapt2/link/FlaggedXmlVersioner_test.cpp b/tools/aapt2/link/FlaggedXmlVersioner_test.cpp
index 0c1314f165cc..0dc464253385 100644
--- a/tools/aapt2/link/FlaggedXmlVersioner_test.cpp
+++ b/tools/aapt2/link/FlaggedXmlVersioner_test.cpp
@@ -101,6 +101,7 @@ TEST_F(FlaggedXmlVersionerTest, PreBaklavaGetsSplit) {
<TextView android:featureFlag="package.flag" /><TextView /><TextView />
</LinearLayout>)");
doc->file.config.sdkVersion = SDK_GINGERBREAD;
+ doc->file.uses_readwrite_feature_flags = true;
FlaggedXmlVersioner versioner;
auto results = versioner.Process(context_.get(), doc.get());
@@ -131,6 +132,7 @@ TEST_F(FlaggedXmlVersionerTest, NoVersionGetsSplit) {
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android">
<TextView android:featureFlag="package.flag" /><TextView /><TextView />
</LinearLayout>)");
+ doc->file.uses_readwrite_feature_flags = true;
FlaggedXmlVersioner versioner;
auto results = versioner.Process(context_.get(), doc.get());
@@ -162,6 +164,7 @@ TEST_F(FlaggedXmlVersionerTest, NegatedFlagAttributeRemoved) {
<TextView android:featureFlag="!package.flag" /><TextView /><TextView />
</LinearLayout>)");
doc->file.config.sdkVersion = SDK_GINGERBREAD;
+ doc->file.uses_readwrite_feature_flags = true;
FlaggedXmlVersioner versioner;
auto results = versioner.Process(context_.get(), doc.get());
@@ -192,6 +195,7 @@ TEST_F(FlaggedXmlVersionerTest, NegatedFlagAttributeRemovedNoSpecifiedVersion) {
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android">
<TextView android:featureFlag="!package.flag" /><TextView /><TextView />
</LinearLayout>)");
+ doc->file.uses_readwrite_feature_flags = true;
FlaggedXmlVersioner versioner;
auto results = versioner.Process(context_.get(), doc.get());
diff --git a/tools/aapt2/link/TableMerger.cpp b/tools/aapt2/link/TableMerger.cpp
index 1d4adc4a57d8..17f332397317 100644
--- a/tools/aapt2/link/TableMerger.cpp
+++ b/tools/aapt2/link/TableMerger.cpp
@@ -295,6 +295,8 @@ bool TableMerger::DoMerge(const android::Source& src, ResourceTablePackage* src_
dst_config_value =
dst_entry->FindOrCreateValue(src_config_value->config, src_config_value->product);
}
+ dst_config_value->uses_readwrite_feature_flags |=
+ src_config_value->uses_readwrite_feature_flags;
// Continue if we're taking the new resource.
CloningValueTransformer cloner(&main_table_->string_pool);
@@ -378,12 +380,13 @@ bool TableMerger::MergeFile(const ResourceFile& file_desc, bool overlay, io::IFi
file_ref->file = file;
file_ref->SetFlagStatus(file_desc.flag_status);
file_ref->SetFlag(file_desc.flag);
-
ResourceTablePackage* pkg = table.FindOrCreatePackage(file_desc.name.package);
- pkg->FindOrCreateType(file_desc.name.type)
- ->FindOrCreateEntry(file_desc.name.entry)
- ->FindOrCreateValue(file_desc.config, {})
- ->value = std::move(file_ref);
+ ResourceConfigValue* config_value = pkg->FindOrCreateType(file_desc.name.type)
+ ->FindOrCreateEntry(file_desc.name.entry)
+ ->FindOrCreateValue(file_desc.config, {});
+
+ config_value->value = std::move(file_ref);
+ config_value->uses_readwrite_feature_flags = file_desc.uses_readwrite_feature_flags;
return DoMerge(file->GetSource(), pkg, false /*mangle*/, overlay /*overlay*/, true /*allow_new*/);
}
diff --git a/tools/aapt2/readme.md b/tools/aapt2/readme.md
index 6bdbaaed9858..413f817ea8fd 100644
--- a/tools/aapt2/readme.md
+++ b/tools/aapt2/readme.md
@@ -5,6 +5,10 @@
2017. This README will be updated more frequently in the future.
- Added a new flag `--no-compress-fonts`. This can significantly speed up loading fonts from APK
assets, at the cost of increasing the storage size of the APK.
+- Changed the behavior of `--enable-sparse-encoding`. Sparse encoding is only applied if the
+ minSdkVersion is >= 32.
+- Changed the behavior of `--force-sparse-encoding`. Sparse encoding is only applied if the
+ minSdkVersion is >= 32 or is not set.
## Version 2.19
- Added navigation resource type.