summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java4
-rw-r--r--cmds/app_process/Android.bp1
-rw-r--r--cmds/uinput/src/com/android/commands/uinput/EvemuParser.java14
-rw-r--r--cmds/uinput/tests/src/com/android/commands/uinput/tests/EvemuParserTest.java5
-rw-r--r--core/api/current.txt28
-rw-r--r--core/api/lint-baseline.txt11
-rw-r--r--core/api/system-current.txt3
-rw-r--r--core/api/test-current.txt2
-rw-r--r--core/java/android/app/Activity.java15
-rw-r--r--core/java/android/app/FullscreenRequestHandler.java15
-rw-r--r--core/java/android/app/Notification.java73
-rw-r--r--core/java/android/app/NotificationChannel.java1
-rw-r--r--core/java/android/app/NotificationChannelGroup.java9
-rw-r--r--core/java/android/app/NotificationManager.java35
-rw-r--r--core/java/android/app/jank/JankTracker.java7
-rw-r--r--core/java/android/app/supervision/ISupervisionManager.aidl2
-rw-r--r--core/java/android/app/supervision/SupervisionManager.java22
-rw-r--r--core/java/android/appwidget/AppWidgetHostView.java139
-rw-r--r--core/java/android/appwidget/AppWidgetManager.java18
-rw-r--r--core/java/android/companion/virtual/flags/flags.aconfig2
-rw-r--r--core/java/android/content/om/FabricatedOverlay.java31
-rw-r--r--core/java/android/content/pm/flags.aconfig7
-rw-r--r--core/java/android/content/res/ApkAssets.java2
-rw-r--r--core/java/android/content/res/AssetManager.java11
-rw-r--r--core/java/android/content/res/OWNERS5
-rw-r--r--core/java/android/content/res/Resources.java23
-rw-r--r--core/java/android/content/res/ResourcesImpl.java17
-rw-r--r--core/java/android/content/res/XmlBlock.java10
-rw-r--r--core/java/android/content/theming/FieldColor.java80
-rw-r--r--core/java/android/content/theming/FieldColorBoth.java70
-rw-r--r--core/java/android/content/theming/FieldColorIndex.java64
-rw-r--r--core/java/android/content/theming/FieldColorSource.java76
-rw-r--r--core/java/android/content/theming/FieldThemeStyle.java76
-rw-r--r--core/java/android/content/theming/ThemeSettings.java200
-rw-r--r--core/java/android/content/theming/ThemeSettingsField.java287
-rw-r--r--core/java/android/content/theming/ThemeSettingsUpdater.java244
-rw-r--r--core/java/android/content/theming/ThemeStyle.java180
-rw-r--r--core/java/android/hardware/display/DisplayManagerGlobal.java4
-rw-r--r--core/java/android/hardware/input/KeyGestureEvent.java8
-rw-r--r--core/java/android/inputmethodservice/InputMethodService.java59
-rw-r--r--core/java/android/inputmethodservice/NavigationBarController.java76
-rw-r--r--core/java/android/os/BatteryUsageStats.java33
-rw-r--r--core/java/android/os/Parcel.java8
-rw-r--r--core/java/android/permission/flags.aconfig9
-rw-r--r--core/java/android/security/flags.aconfig14
-rw-r--r--core/java/android/service/dreams/flags.aconfig2
-rw-r--r--core/java/android/text/Layout.java6
-rw-r--r--core/java/android/util/TypedValue.java6
-rw-r--r--core/java/android/view/IWindowManager.aidl21
-rw-r--r--core/java/android/view/InsetsAnimationControlImpl.java5
-rw-r--r--core/java/android/view/InsetsAnimationControlRunner.java6
-rw-r--r--core/java/android/view/InsetsAnimationThreadControlRunner.java13
-rw-r--r--core/java/android/view/InsetsController.java60
-rw-r--r--core/java/android/view/InsetsResizeAnimationRunner.java5
-rw-r--r--core/java/android/view/InsetsSourceConsumer.java6
-rw-r--r--core/java/android/view/RoundScrollbarRenderer.java4
-rw-r--r--core/java/android/view/ScrollCaptureConnection.java44
-rw-r--r--core/java/android/view/SurfaceControl.java2
-rw-r--r--core/java/android/view/SyncRtSurfaceTransactionApplier.java52
-rw-r--r--core/java/android/view/View.java67
-rw-r--r--core/java/android/view/ViewRootImpl.java70
-rw-r--r--core/java/android/view/WindowManager.java77
-rw-r--r--core/java/android/view/XrWindowProperties.java30
-rw-r--r--core/java/android/view/autofill/AutofillFeatureFlags.java2
-rw-r--r--core/java/android/view/inputmethod/ImeTracker.java6
-rw-r--r--core/java/android/view/inputmethod/InputMethodManager.java40
-rw-r--r--core/java/android/view/inputmethod/RemoteInputConnectionImpl.java23
-rw-r--r--core/java/android/widget/RemoteViews.java58
-rw-r--r--core/java/android/window/DesktopModeFlags.java10
-rw-r--r--core/java/android/window/flags/lse_desktop_experience.aconfig20
-rw-r--r--core/java/android/window/flags/windowing_frontend.aconfig11
-rw-r--r--core/java/android/window/flags/windowing_sdk.aconfig22
-rw-r--r--core/java/com/android/internal/content/FileSystemProvider.java11
-rw-r--r--core/java/com/android/internal/inputmethod/InputMethodDebug.java2
-rw-r--r--core/java/com/android/internal/inputmethod/SoftInputShowHideReason.java7
-rw-r--r--core/java/com/android/internal/policy/DecorView.java130
-rw-r--r--core/java/com/android/internal/policy/KeyInterceptionInfo.java4
-rw-r--r--core/java/com/android/internal/policy/SystemBarUtils.java4
-rw-r--r--core/java/com/android/internal/protolog/ProtoLogCommandHandler.java12
-rw-r--r--core/java/com/android/internal/util/ContrastColorUtil.java9
-rw-r--r--core/java/com/android/internal/widget/ActionBarContextView.java12
-rw-r--r--core/java/com/android/internal/widget/ActionBarOverlayLayout.java85
-rw-r--r--core/java/com/android/internal/widget/LockPatternUtils.java35
-rw-r--r--core/java/com/android/internal/widget/NotificationProgressBar.java36
-rw-r--r--core/jni/Android.bp1
-rw-r--r--core/jni/android_tracing_PerfettoDataSource.cpp3
-rw-r--r--core/jni/android_util_AssetManager.cpp8
-rw-r--r--core/jni/fd_utils.cpp3
-rw-r--r--core/proto/android/server/windowmanagerservice.proto2
-rw-r--r--core/res/Android.bp1
-rw-r--r--core/res/AndroidManifest.xml11
-rw-r--r--core/res/res/drawable/ic_standby.xml10
-rw-r--r--core/res/res/drawable/notification_progress_indeterminate_horizontal_material.xml167
-rw-r--r--core/res/res/drawable/vector_notification_progress_indeterminate_horizontal.xml59
-rw-r--r--core/res/res/values-gl/strings.xml4
-rw-r--r--core/res/res/values-ko/strings.xml2
-rw-r--r--core/res/res/values-ne/strings.xml2
-rw-r--r--core/res/res/values-tl/strings.xml2
-rw-r--r--core/res/res/values-watch/config.xml4
-rw-r--r--core/res/res/values/config.xml10
-rw-r--r--core/res/res/values/public-staging.xml2
-rw-r--r--core/res/res/values/strings.xml9
-rw-r--r--core/res/res/values/styles_material.xml1
-rw-r--r--core/res/res/values/symbols.xml12
-rw-r--r--core/res/res/xml/sms_short_codes.xml2
-rw-r--r--core/tests/coretests/res/xml/flags.xml4
-rw-r--r--core/tests/coretests/src/android/app/NotificationManagerTest.java69
-rw-r--r--core/tests/coretests/src/android/app/NotificationTest.java61
-rw-r--r--core/tests/coretests/src/android/appwidget/AppWidgetEventsTest.kt138
-rw-r--r--core/tests/coretests/src/android/content/pm/UserInfoTest.java164
-rw-r--r--core/tests/coretests/src/android/content/res/XmlResourcesFlaggedTest.kt116
-rw-r--r--core/tests/coretests/src/android/text/LayoutTest.java102
-rw-r--r--core/tests/coretests/src/android/view/ScrollCaptureConnectionTest.java70
-rw-r--r--core/tests/coretests/src/android/view/ViewRootImplTest.java141
-rw-r--r--core/tests/overlaytests/device/Android.bp2
-rw-r--r--core/tests/overlaytests/device/res/values/config.xml4
-rw-r--r--core/tests/overlaytests/device/src/com/android/overlaytest/FabricatedOverlaysTest.java86
-rw-r--r--data/etc/platform.xml6
-rw-r--r--data/etc/privapp-permissions-platform.xml2
-rw-r--r--data/keyboards/Android.bp11
-rw-r--r--data/keyboards/keyboards.mk10
-rw-r--r--libs/WindowManager/Shell/res/values/styles.xml1
-rw-r--r--libs/WindowManager/Shell/shared/res/values/dimen.xml2
-rw-r--r--libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/GroupedTaskInfo.java2
-rw-r--r--libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DragZoneFactory.kt2
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java3
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitState.java12
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java3
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandler.kt34
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayModeController.kt78
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java76
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt84
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksOrganizer.kt16
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizer.kt102
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java45
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java19
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultSurfaceAnimator.java2
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java11
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/transition/FocusTransitionObserver.java19
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java8
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt27
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/AppHandleAndHeaderVisibilityHelper.kt15
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/ButtonBackgroundDrawableUtils.kt12
-rw-r--r--libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/Android.bp1
-rw-r--r--libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/UnminimizeAppFromTaskbar.kt19
-rw-r--r--libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/AndroidTestTemplate.xml4
-rw-r--r--libs/WindowManager/Shell/tests/e2e/utils/src/com/android/wm/shell/SimulatedConnectedDisplayTestRule.kt172
-rw-r--r--libs/WindowManager/Shell/tests/flicker/Android.bp16
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandlerTest.kt20
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayModeControllerTest.kt463
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicatorTest.kt60
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt80
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizerTest.kt167
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java3
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/DefaultTransitionHandlerTest.java3
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/HomeTransitionObserverTest.java3
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java10
-rw-r--r--libs/androidfw/AssetManager2.cpp25
-rw-r--r--libs/androidfw/LoadedArsc.cpp12
-rw-r--r--libs/androidfw/include/androidfw/AssetManager2.h15
-rw-r--r--libs/androidfw/include/androidfw/LoadedArsc.h3
-rw-r--r--libs/androidfw/tests/AssetManager2_bench.cpp2
-rw-r--r--libs/androidfw/tests/AssetManager2_test.cpp17
-rw-r--r--libs/androidfw/tests/data/flagged/AndroidManifest.xml20
-rw-r--r--libs/androidfw/tests/data/flagged/R.h35
-rwxr-xr-xlibs/androidfw/tests/data/flagged/build28
-rw-r--r--libs/androidfw/tests/data/flagged/flagged.apkbin0 -> 1837 bytes
-rw-r--r--libs/androidfw/tests/data/flagged/res/xml/flagged.xml18
-rw-r--r--libs/hwui/Android.bp8
-rw-r--r--libs/hwui/jni/Bitmap.cpp14
-rw-r--r--location/api/system-current.txt22
-rw-r--r--location/java/android/location/BeidouAssistance.java27
-rw-r--r--location/java/android/location/GalileoAssistance.java27
-rw-r--r--location/java/android/location/GlonassAssistance.java55
-rw-r--r--location/java/android/location/GpsAssistance.java27
-rw-r--r--location/java/android/location/QzssAssistance.java27
-rw-r--r--location/java/android/location/flags/location.aconfig10
-rw-r--r--media/java/android/media/MediaCodec.java72
-rw-r--r--media/java/android/media/flags/media_better_together.aconfig10
-rw-r--r--media/java/android/media/projection/MediaProjectionAppContent.aidl19
-rw-r--r--media/java/android/media/projection/MediaProjectionAppContent.java123
-rw-r--r--media/java/android/media/projection/MediaProjectionConfig.java354
-rw-r--r--media/java/android/media/projection/MediaProjectionManager.java10
-rw-r--r--media/java/android/media/projection/TEST_MAPPING2
-rw-r--r--media/java/android/media/quality/ActiveProcessingPicture.java19
-rw-r--r--media/java/android/media/quality/MediaQualityManager.java41
-rw-r--r--media/java/android/media/quality/PictureProfile.java40
-rw-r--r--media/java/android/media/quality/aidl/android/media/quality/IActiveProcessingPictureListener.aidl (renamed from packages/SystemUI/src/com/android/systemui/media/controls/shared/model/SmartspaceMediaLoadingModel.kt)23
-rw-r--r--media/java/android/media/quality/aidl/android/media/quality/IMediaQualityManager.aidl10
-rw-r--r--media/java/android/media/tv/extension/scan/IScanInterface.aidl2
-rw-r--r--media/tests/projection/Android.bp1
-rw-r--r--media/tests/projection/src/android/media/projection/MediaProjectionAppContentTest.java86
-rw-r--r--media/tests/projection/src/android/media/projection/MediaProjectionConfigTest.java32
-rw-r--r--packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java5
-rw-r--r--packages/SettingsLib/BannerMessagePreference/res/color/settingslib_banner_button_background_normal.xml4
-rw-r--r--packages/SettingsLib/BannerMessagePreference/res/color/settingslib_banner_filled_button_content_high.xml22
-rw-r--r--packages/SettingsLib/BannerMessagePreference/res/color/settingslib_banner_filled_button_content_low.xml22
-rw-r--r--packages/SettingsLib/BannerMessagePreference/res/color/settingslib_banner_filled_button_content_medium.xml22
-rw-r--r--packages/SettingsLib/BannerMessagePreference/res/color/settingslib_banner_filled_button_content_normal.xml22
-rw-r--r--packages/SettingsLib/BannerMessagePreference/res/color/settingslib_banner_outline_button_content.xml22
-rw-r--r--packages/SettingsLib/BannerMessagePreference/res/color/settingslib_banner_outline_button_stroke_normal.xml22
-rw-r--r--packages/SettingsLib/BannerMessagePreference/res/values-v36/styles_expressive.xml1
-rw-r--r--packages/SettingsLib/BannerMessagePreference/src/com/android/settingslib/widget/BannerMessagePreference.java74
-rw-r--r--packages/SettingsLib/Metadata/Android.bp3
-rw-r--r--packages/SettingsLib/Metadata/proguard.pgcfg8
-rw-r--r--packages/SettingsLib/SelectorWithWidgetPreference/res/layout-v36/settingslib_preference_selector_with_widget.xml (renamed from packages/SettingsLib/SelectorWithWidgetPreference/res/layout-v36/settingslib_expressive_preference_selector_with_widget.xml)26
-rw-r--r--packages/SettingsLib/SelectorWithWidgetPreference/src/com/android/settingslib/widget/SelectorWithWidgetPreference.java5
-rw-r--r--packages/SettingsLib/SettingsSpinner/res/drawable-v36/settings_expressive_spinner_dropdown_background.xml21
-rw-r--r--packages/SettingsLib/SettingsSpinner/res/drawable-v36/settings_expressive_spinner_dropdown_item_selected.xml28
-rw-r--r--packages/SettingsLib/SettingsSpinner/res/layout-v33/settings_spinner_view.xml2
-rw-r--r--packages/SettingsLib/SettingsSpinner/res/layout-v36/settings_expressive_spinner_preference_full.xml27
-rw-r--r--packages/SettingsLib/SettingsSpinner/res/layout-v36/settings_expressive_spinner_preference_full_outlined.xml33
-rw-r--r--packages/SettingsLib/SettingsSpinner/res/layout-v36/settings_expressive_spinner_preference_outlined.xml33
-rw-r--r--packages/SettingsLib/SettingsSpinner/res/layout-v36/settings_expressive_spinner_view_full.xml22
-rw-r--r--packages/SettingsLib/SettingsSpinner/res/layout-v36/settings_expressive_spinner_view_full_outlined.xml26
-rw-r--r--packages/SettingsLib/SettingsSpinner/res/layout-v36/settings_expressive_spinner_view_large.xml22
-rw-r--r--packages/SettingsLib/SettingsSpinner/res/layout-v36/settings_expressive_spinner_view_large_outlined.xml26
-rw-r--r--packages/SettingsLib/SettingsSpinner/res/layout-v36/settings_expressvie_spinner_dropdown_view.xml36
-rw-r--r--packages/SettingsLib/SettingsSpinner/res/layout-v36/settings_expressvie_spinner_view.xml22
-rw-r--r--packages/SettingsLib/SettingsSpinner/res/layout-v36/settings_expressvie_spinner_view_outlined.xml26
-rw-r--r--packages/SettingsLib/SettingsSpinner/res/values-v33/styles.xml2
-rw-r--r--packages/SettingsLib/SettingsSpinner/res/values-v36/attr.xml25
-rw-r--r--packages/SettingsLib/SettingsSpinner/res/values-v36/styles.xml59
-rw-r--r--packages/SettingsLib/SettingsSpinner/src/com/android/settingslib/widget/SettingsSpinnerAdapter.java92
-rw-r--r--packages/SettingsLib/SettingsSpinner/src/com/android/settingslib/widget/SettingsSpinnerPreference.java92
-rw-r--r--packages/SettingsLib/SettingsTheme/res/drawable-v36/settingslib_expressive_spinner_background.xml53
-rw-r--r--packages/SettingsLib/SettingsTheme/res/drawable-v36/settingslib_expressive_spinner_background_outlined.xml55
-rw-r--r--packages/SettingsLib/SettingsTheme/res/drawable-v36/settingslib_expressive_spinner_dropdown_background.xml38
-rw-r--r--packages/SettingsLib/SettingsTheme/res/values-v36/styles_expressive.xml13
-rw-r--r--packages/SettingsLib/SettingsTheme/res/values-v36/themes_expressive.xml8
-rw-r--r--packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/dialog/SettingsAlterDialogContent.kt8
-rw-r--r--packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppInfoPage.kt17
-rw-r--r--packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppList.kt3
-rw-r--r--packages/SettingsLib/StatusBannerPreference/src/com/android/settingslib/widget/StatusBannerPreference.kt13
-rw-r--r--packages/SettingsLib/res/xml/timezones.xml1
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/RestrictedLockUtilsInternal.java42
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java9
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/display/DisplayDensityUtils.java42
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/display/OWNERS5
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/wifi/WifiUtils.kt7
-rw-r--r--packages/SettingsLib/tests/robotests/src/com/android/settingslib/RestrictedLockUtilsTest.java10
-rw-r--r--packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/BannerMessagePreferenceTest.java240
-rw-r--r--packages/SettingsLib/tests/robotests/src/com/android/settingslib/wifi/WifiUtilsTest.java3
-rw-r--r--packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java39
-rw-r--r--packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java48
-rw-r--r--packages/SettingsProvider/test/src/com/android/providers/settings/SettingsHelperTest.java15
-rw-r--r--packages/Shell/AndroidManifest.xml1
-rw-r--r--packages/SystemUI/OWNERS1
-rw-r--r--packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/view/A11yMenuOverlayLayout.java2
-rw-r--r--packages/SystemUI/aconfig/systemui.aconfig52
-rw-r--r--packages/SystemUI/animation/src/com/android/systemui/animation/DialogTransitionAnimator.kt36
-rw-r--r--packages/SystemUI/animation/src/com/android/systemui/animation/RemoteAnimationRunnerCompat.java4
-rw-r--r--packages/SystemUI/animation/src/com/android/systemui/animation/TransitionAnimator.kt9
-rw-r--r--packages/SystemUI/compose/core/src/com/android/compose/gesture/effect/ContentOverscrollEffect.kt7
-rw-r--r--packages/SystemUI/compose/core/tests/src/com/android/compose/gesture/effect/OffsetOverscrollEffectTest.kt181
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt43
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt4
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationStackNestedScrollConnection.kt151
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt27
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeOverlay.kt6
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt5
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt10
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt31
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt7
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/UserActionDistanceScopeImpl.kt6
-rw-r--r--packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt72
-rw-r--r--packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt32
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/MagnificationSettingsControllerTest.java6
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinderTest.kt1
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/clipboardoverlay/ActionIntentCreatorTest.kt81
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalSceneRepositoryImplTest.kt21
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalAutoOpenInteractorTest.kt4
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt13
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalSceneInteractorTest.kt62
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalSettingsInteractorTest.kt4
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/communal/posturing/domain/interactor/PosturingInteractorTest.kt2
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/communal/util/UserTouchActivityNotifierTest.kt74
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartableTest.kt34
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorTest.kt29
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/ui/viewmodel/UdfpsAccessibilityOverlayViewModelTest.kt81
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayContainerViewControllerTest.java24
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt3
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/WakeGestureMonitorTest.kt101
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/source/TestShortcuts.kt2
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractorTest.kt76
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/binder/WindowManagerLockscreenVisibilityManagerTest.kt70
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToPrimaryBouncerTransitionViewModelTest.kt63
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModelTest.kt229
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt93
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardSmartspaceViewModelTest.kt64
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToDreamingTransitionViewModelTest.kt74
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/lowlightclock/LowLightMonitorTest.java198
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/lowlightclock/LowLightMonitorTest.kt271
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/MediaTestHelper.kt33
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/data/repository/MediaDataRepositoryTest.kt40
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/data/repository/MediaFilterRepositoryTest.kt132
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/interactor/MediaControlInteractorTest.kt34
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoaderTest.kt3
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListenerTest.kt6
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/media/dialog/MediaOutputAdapterLegacyTest.java1
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.kt763
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/mediaprojection/permission/ShareToAppPermissionDialogDelegateTest.kt39
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/mediaprojection/permission/SystemCastPermissionDialogDelegateTest.kt37
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModelTest.kt35
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/screenshot/scroll/ScrollCaptureControllerTest.java15
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ShadeControllerSceneImplTest.kt16
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/BlurUtilsTest.kt48
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationShadeDepthControllerTest.kt3
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/OperatorNameViewControllerTest.kt10
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryTest.java18
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImplTest.kt14
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractorImplTest.kt708
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/AODPromotedNotificationsInteractorTest.kt142
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java29
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImplTest.kt26
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerTest.java14
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculatorTest.kt8
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorKairosAdapterTest.kt2
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModelTest.kt43
-rw-r--r--packages/SystemUI/res-keyguard/values/styles.xml8
-rw-r--r--packages/SystemUI/res/drawable/global_actions_lite_button_background.xml29
-rw-r--r--packages/SystemUI/res/drawable/ic_add_circle_rounded.xml27
-rw-r--r--packages/SystemUI/res/drawable/ic_check_circle_filled.xml27
-rw-r--r--packages/SystemUI/res/drawable/ic_expand_less_rounded.xml25
-rw-r--r--packages/SystemUI/res/drawable/ic_expand_more_rounded.xml25
-rw-r--r--packages/SystemUI/res/drawable/ic_qs_category_accessibility.xml26
-rw-r--r--packages/SystemUI/res/drawable/ic_qs_category_connectivty.xml26
-rw-r--r--packages/SystemUI/res/drawable/ic_qs_category_display.xml26
-rw-r--r--packages/SystemUI/res/drawable/ic_qs_category_privacy.xml26
-rw-r--r--packages/SystemUI/res/drawable/ic_qs_category_provided_by_apps.xml26
-rw-r--r--packages/SystemUI/res/drawable/ic_qs_category_unknown.xml26
-rw-r--r--packages/SystemUI/res/drawable/ic_qs_category_utilities.xml26
-rw-r--r--packages/SystemUI/res/drawable/media_output_dialog_background_reduced_radius.xml20
-rw-r--r--packages/SystemUI/res/drawable/media_output_dialog_footer_background.xml22
-rw-r--r--packages/SystemUI/res/drawable/media_output_dialog_item_fixed_volume_background.xml20
-rw-r--r--packages/SystemUI/res/drawable/media_output_dialog_round_button_ripple.xml24
-rw-r--r--packages/SystemUI/res/drawable/media_output_item_expandable_button_background.xml24
-rw-r--r--packages/SystemUI/res/layout/biometric_prompt_one_pane_layout.xml2
-rw-r--r--packages/SystemUI/res/layout/biometric_prompt_two_pane_layout.xml2
-rw-r--r--packages/SystemUI/res/layout/combined_qs_header.xml2
-rw-r--r--packages/SystemUI/res/layout/media_output_dialog.xml30
-rw-r--r--packages/SystemUI/res/layout/media_output_list_item_device.xml141
-rw-r--r--packages/SystemUI/res/layout/media_output_list_item_group_divider.xml70
-rw-r--r--packages/SystemUI/res/layout/notification_conversation_info.xml10
-rw-r--r--packages/SystemUI/res/values-night/colors.xml10
-rw-r--r--packages/SystemUI/res/values/colors.xml12
-rw-r--r--packages/SystemUI/res/values/dimens.xml12
-rw-r--r--packages/SystemUI/res/values/strings.xml15
-rw-r--r--packages/SystemUI/res/values/styles.xml37
-rw-r--r--packages/SystemUI/shared/src/com/android/systemui/shared/recents/model/Task.java7
-rw-r--r--packages/SystemUI/src/com/android/systemui/accessibility/MagnificationImpl.java40
-rw-r--r--packages/SystemUI/src/com/android/systemui/accessibility/MagnificationSettingsController.java12
-rw-r--r--packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/UdfpsOverlayInteractor.kt24
-rw-r--r--packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt60
-rw-r--r--packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinder.kt98
-rw-r--r--packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt17
-rw-r--r--packages/SystemUI/src/com/android/systemui/brightness/ui/compose/BrightnessSlider.kt16
-rw-r--r--packages/SystemUI/src/com/android/systemui/clipboardoverlay/ActionIntentCreator.kt51
-rw-r--r--packages/SystemUI/src/com/android/systemui/clipboardoverlay/dagger/ClipboardOverlayModule.java6
-rw-r--r--packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt9
-rw-r--r--packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSceneRepository.kt73
-rw-r--r--packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSettingsRepository.kt9
-rw-r--r--packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt19
-rw-r--r--packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneInteractor.kt61
-rw-r--r--packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractor.kt14
-rw-r--r--packages/SystemUI/src/com/android/systemui/communal/posturing/domain/interactor/PosturingInteractor.kt51
-rw-r--r--packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalTransitionKeys.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt5
-rw-r--r--packages/SystemUI/src/com/android/systemui/communal/util/UserTouchActivityNotifier.kt58
-rw-r--r--packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartable.kt14
-rw-r--r--packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/SystemUIDeviceEntryFaceAuthInteractor.kt19
-rw-r--r--packages/SystemUI/src/com/android/systemui/deviceentry/ui/binder/UdfpsAccessibilityOverlayBinder.kt43
-rw-r--r--packages/SystemUI/src/com/android/systemui/deviceentry/ui/view/UdfpsAccessibilityOverlay.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/deviceentry/ui/viewmodel/AlternateBouncerUdfpsAccessibilityOverlayViewModel.kt13
-rw-r--r--packages/SystemUI/src/com/android/systemui/deviceentry/ui/viewmodel/DeviceEntryUdfpsAccessibilityOverlayViewModel.kt3
-rw-r--r--packages/SystemUI/src/com/android/systemui/deviceentry/ui/viewmodel/UdfpsAccessibilityOverlayViewModel.kt21
-rw-r--r--packages/SystemUI/src/com/android/systemui/display/data/repository/DisplayWindowPropertiesRepository.kt3
-rw-r--r--packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayContainerViewController.java9
-rw-r--r--packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java23
-rw-r--r--packages/SystemUI/src/com/android/systemui/dreams/WakeGestureMonitor.kt74
-rw-r--r--packages/SystemUI/src/com/android/systemui/education/data/repository/UserContextualEducationRepository.kt12
-rw-r--r--packages/SystemUI/src/com/android/systemui/flags/Flags.kt3
-rw-r--r--packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java91
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java6
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/WindowManagerLockscreenVisibilityManager.kt54
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/dagger/PrimaryBouncerTransitionModule.kt10
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt5
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt1
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt61
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractor.kt1
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractor.kt6
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt5
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardWakeDirectlyToGoneInteractor.kt19
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/AccessibilityActionsViewBinder.kt27
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerViewBinder.kt22
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBlueprintViewBinder.kt8
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt24
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSmartspaceViewBinder.kt70
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodNotificationIconsSection.kt6
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodPromotedNotificationSection.kt10
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt22
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSection.kt244
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/transitions/ClockSizeTransition.kt6
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/transitions/DefaultClockSteppingTransition.kt1
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AccessibilityActionsViewModel.kt14
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToPrimaryBouncerTransitionViewModel.kt49
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModel.kt66
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToGlanceableHubTransitionViewModel.kt6
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt3
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardSmartspaceViewModel.kt39
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToDreamingTransitionViewModel.kt59
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModel.kt22
-rw-r--r--packages/SystemUI/src/com/android/systemui/lowlightclock/AmbientLightModeMonitor.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/lowlightclock/LowLightMonitor.java148
-rw-r--r--packages/SystemUI/src/com/android/systemui/lowlightclock/LowLightMonitor.kt116
-rw-r--r--packages/SystemUI/src/com/android/systemui/lowlightclock/dagger/LowLightModule.java152
-rw-r--r--packages/SystemUI/src/com/android/systemui/lowlightclock/dagger/LowLightModule.kt146
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/controls/data/model/MediaSortKeyModel.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/controls/data/repository/MediaDataRepository.kt33
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/controls/data/repository/MediaFilterRepository.kt131
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataFilterImpl.kt156
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImpl.kt176
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterImpl.kt194
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoader.kt6
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt154
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaProcessingHelper.kt3
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaSessionBasedFilter.kt31
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListener.kt64
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaCarouselInteractor.kt35
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/controls/shared/model/MediaCommonModel.kt16
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/controls/shared/model/MediaDataLoadingModel.kt6
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/controls/shared/model/MediaRecommendationsModel.kt44
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/controls/shared/model/SmartspaceMediaDataProvider.kt46
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt175
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaControlPanel.java2
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaCarouselViewModel.kt13
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/SeekBarViewModel.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFlags.kt3
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/dialog/MediaItem.java72
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.kt688
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapterBase.java12
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java126
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputColorScheme.kt103
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java7
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java67
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/dialog/OutputMediaItemListProxy.java16
-rw-r--r--packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionUtils.kt25
-rw-r--r--packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModel.kt29
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CommonTile.kt15
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt72
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/shared/model/TileCategory.kt37
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetAdapter.java7
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContentController.java11
-rw-r--r--packages/SystemUI/src/com/android/systemui/scene/shared/logger/SceneLogger.kt15
-rw-r--r--packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt27
-rw-r--r--packages/SystemUI/src/com/android/systemui/screenshot/scroll/ScrollCaptureController.java23
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt19
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/OWNERS13
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt1
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayAwareModule.kt20
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/ShadeStateTraceLogger.kt15
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractor.kt29
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/BlurUtils.kt153
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/ImmersiveModeConfirmation.java8
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt32
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/OperatorNameViewController.java8
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/shared/StatusBarNotifChips.kt9
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/ChipContent.kt6
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/OngoingActivityChipModel.kt9
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/TimeRemainingState.kt5
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/featurepods/media/domain/interactor/MediaControlChipInteractor.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java1
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/AODPromotedNotification.kt123
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractor.kt125
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationUiForceExpanded.kt63
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/AODPromotedNotificationInteractor.kt28
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/shared/model/PromotedNotificationContentModel.kt10
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java24
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java5
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfo.java7
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java4
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowImageInflater.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationTemplateViewWrapper.java3
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/AvalancheReplaceHunWhenCritical.kt (renamed from packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationUiAod.kt)21
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java11
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java6
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculator.kt5
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt36
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt25
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/StackedMobileIconBinder.kt7
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModel.kt14
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/view/SingleBindableStatusBarComposeIconView.kt5
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyStateInflater.kt76
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/SensitiveNotificationProtectionInteractor.kt48
-rw-r--r--packages/SystemUI/src/com/android/systemui/util/display/DisplayHelper.java12
-rw-r--r--packages/SystemUI/src/com/android/systemui/util/kotlin/AsyncSensorManagerExt.kt57
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/accessibility/IMagnificationConnectionTest.java5
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/accessibility/MagnificationTest.java5
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt8
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsDialogLiteTest.java171
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTestKt.kt18
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataFilterImplTest.kt352
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImplTest.kt81
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterImplTest.kt756
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt83
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaPlayerDataTest.kt99
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerTest.kt83
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaControlPanelTest.kt2
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaSwitchingControllerTest.java56
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/media/dialog/OutputMediaItemListProxyTest.java67
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt18
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java13
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfoTest.java31
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/RowImageInflaterTest.kt4
-rw-r--r--packages/SystemUI/tests/utils/src/android/hardware/display/FakeAmbientDisplayConfiguration.kt3
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalSceneRepository.kt15
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalSceneInteractorKosmos.kt4
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractorKosmos.kt2
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/communal/util/UserTouchActivityNotifierKosmos.kt28
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/data/ui/viewmodel/UdfpsAccessibilityOverlayViewModelKosmos.kt12
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/dreams/WakeGestureMonitorKosmos.kt35
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractorKosmos.kt2
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AccessibilityActionsViewModelKosmos.kt2
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToPrimaryBouncerViewModelKosmos.kt29
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModelKosmos.kt6
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/GoneToGlanceableHubTransitionViewModelKosmos.kt25
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt1
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToDreamingTransitionViewModelKosmos.kt29
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/data/repository/MediaDataRepositoryKosmos.kt5
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterKosmos.kt12
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorKosmos.kt4
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListenerKosmos.kt2
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractorKosmos.kt2
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeOverlayContentViewModelKosmos.kt4
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractorKosmos.kt4
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/AODPromotedNotificationInteractorKosmos.kt4
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt3
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/domain/interactor/SensitiveNotificationProtectionInteractorKosmos.kt25
-rw-r--r--ravenwood/README.md23
-rw-r--r--ravenwood/api-maintainers.md94
-rwxr-xr-xravenwood/scripts/ravenwood-stats-collector.sh4
-rw-r--r--ravenwood/test-authors.md193
-rw-r--r--ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/HostStubGenStats.kt10
-rw-r--r--ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/asm/AsmUtils.kt4
-rw-r--r--ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/dumper/ApiDumper.kt129
-rw-r--r--ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/FilterPolicyWithReason.kt10
-rw-r--r--services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java82
-rw-r--r--services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickScrollPanel.java2
-rw-r--r--services/art-profile-extra8
-rw-r--r--services/core/Android.bp1
-rw-r--r--services/core/java/com/android/server/BinaryTransparencyService.java29
-rw-r--r--services/core/java/com/android/server/GestureLauncherService.java43
-rw-r--r--services/core/java/com/android/server/am/BroadcastHistory.java43
-rw-r--r--services/core/java/com/android/server/audio/HardeningEnforcer.java37
-rw-r--r--services/core/java/com/android/server/biometrics/AuthenticationStats.java32
-rw-r--r--services/core/java/com/android/server/biometrics/AuthenticationStatsBroadcastReceiver.java3
-rw-r--r--services/core/java/com/android/server/biometrics/AuthenticationStatsCollector.java113
-rw-r--r--services/core/java/com/android/server/biometrics/AuthenticationStatsPersister.java72
-rw-r--r--services/core/java/com/android/server/biometrics/sensors/BiometricNotification.java5
-rw-r--r--services/core/java/com/android/server/biometrics/sensors/BiometricNotificationImpl.java5
-rw-r--r--services/core/java/com/android/server/biometrics/sensors/BiometricNotificationUtils.java66
-rw-r--r--services/core/java/com/android/server/biometrics/sensors/EnrollClient.java11
-rw-r--r--services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceEnrollClient.java10
-rw-r--r--services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClient.java3
-rw-r--r--services/core/java/com/android/server/clipboard/Android.bp18
-rw-r--r--services/core/java/com/android/server/clipboard/ClipboardService.java87
-rw-r--r--services/core/java/com/android/server/clipboard/flags.aconfig9
-rw-r--r--services/core/java/com/android/server/connectivity/PacProxyService.java5
-rw-r--r--services/core/java/com/android/server/input/InputManagerService.java9
-rw-r--r--services/core/java/com/android/server/input/KeyGestureController.java2
-rw-r--r--services/core/java/com/android/server/input/KeyboardBacklightController.java33
-rw-r--r--services/core/java/com/android/server/input/NativeInputManagerService.java6
-rw-r--r--services/core/java/com/android/server/input/SysfsNodeMonitor.java203
-rw-r--r--services/core/java/com/android/server/input/debug/TouchpadDebugViewController.java12
-rw-r--r--services/core/java/com/android/server/inputmethod/IInputMethodClientInvoker.java2
-rw-r--r--services/core/java/com/android/server/inputmethod/IInputMethodInvoker.java4
-rw-r--r--services/core/java/com/android/server/inputmethod/InputMethodManagerService.java43
-rw-r--r--services/core/java/com/android/server/location/contexthub/ContextHubEndpointManager.java10
-rw-r--r--services/core/java/com/android/server/location/contexthub/ContextHubService.java10
-rw-r--r--services/core/java/com/android/server/location/injector/SystemEmergencyHelper.java81
-rw-r--r--services/core/java/com/android/server/locksettings/LockSettingsService.java76
-rw-r--r--services/core/java/com/android/server/media/LegacyDeviceRouteController.java5
-rw-r--r--services/core/java/com/android/server/media/MediaSessionService.java10
-rw-r--r--services/core/java/com/android/server/media/quality/MediaQualityService.java793
-rw-r--r--services/core/java/com/android/server/media/quality/MediaQualityUtils.java89
-rw-r--r--services/core/java/com/android/server/pm/ComputerEngine.java3
-rw-r--r--services/core/java/com/android/server/pm/UserManagerService.java16
-rw-r--r--services/core/java/com/android/server/pm/permission/PermissionManagerService.java15
-rw-r--r--services/core/java/com/android/server/policy/PhoneWindowManager.java219
-rw-r--r--services/core/java/com/android/server/security/AttestationVerificationManagerService.java5
-rw-r--r--services/core/java/com/android/server/theming/ThemeSettingsManager.java166
-rw-r--r--services/core/java/com/android/server/tv/TvInputHal.java21
-rw-r--r--services/core/java/com/android/server/tv/TvInputHardwareManager.java21
-rw-r--r--services/core/java/com/android/server/tv/TvInputManagerService.java19
-rw-r--r--services/core/java/com/android/server/updates/CertPinInstallReceiver.java20
-rw-r--r--services/core/java/com/android/server/wm/ActivityClientController.java23
-rw-r--r--services/core/java/com/android/server/wm/DisplayAreaPolicy.java3
-rw-r--r--services/core/java/com/android/server/wm/DisplayContent.java49
-rw-r--r--services/core/java/com/android/server/wm/DisplayWindowSettings.java2
-rw-r--r--services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java17
-rw-r--r--services/core/java/com/android/server/wm/RecentTasks.java10
-rw-r--r--services/core/java/com/android/server/wm/Task.java42
-rw-r--r--services/core/java/com/android/server/wm/TaskChangeNotificationController.java25
-rw-r--r--services/core/java/com/android/server/wm/TaskSystemBarsListenerController.java6
-rw-r--r--services/core/java/com/android/server/wm/WindowContainer.java4
-rw-r--r--services/core/java/com/android/server/wm/WindowManagerInternal.java2
-rw-r--r--services/core/java/com/android/server/wm/WindowManagerService.java69
-rw-r--r--services/core/java/com/android/server/wm/WindowState.java9
-rw-r--r--services/core/java/com/android/server/wm/WindowToken.java62
-rw-r--r--services/core/jni/Android.bp2
-rw-r--r--services/core/jni/com_android_server_input_InputManagerService.cpp7
-rw-r--r--services/core/jni/com_android_server_tv_TvInputHal.cpp7
-rw-r--r--services/core/jni/gnss/GnssAssistance.cpp22
-rw-r--r--services/core/jni/gnss/GnssAssistance.h4
-rw-r--r--services/core/jni/tvinput/JTvInputHal.cpp19
-rw-r--r--services/core/jni/tvinput/JTvInputHal.h2
-rw-r--r--services/credentials/java/com/android/server/credentials/metrics/RequestSessionMetric.java2
-rw-r--r--services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java7
-rw-r--r--services/java/com/android/server/flags.aconfig7
-rw-r--r--services/permission/java/com/android/server/permission/access/AccessPolicy.kt2
-rw-r--r--services/permission/java/com/android/server/permission/access/permission/AppIdPermissionPolicy.kt166
-rw-r--r--services/permission/java/com/android/server/permission/access/permission/AppIdPermissionUpgrade.kt222
-rw-r--r--services/supervision/java/com/android/server/supervision/SupervisionService.java39
-rw-r--r--services/tests/PermissionServiceMockingTests/src/com/android/server/permission/test/AppIdPermissionPolicyPermissionStatesTest.kt437
-rw-r--r--services/tests/PermissionServiceMockingTests/src/com/android/server/permission/test/BasePermissionPolicyTest.kt9
-rw-r--r--services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsRule.java2
-rw-r--r--services/tests/servicestests/Android.bp12
-rw-r--r--services/tests/servicestests/AndroidManifest.xml15
-rw-r--r--services/tests/servicestests/res/xml/test_magnification_a11y_service.xml24
-rw-r--r--services/tests/servicestests/src/com/android/server/GestureLauncherServiceTest.java40
-rw-r--r--services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickControllerTest.java387
-rw-r--r--services/tests/servicestests/src/com/android/server/accessibility/integration/FullScreenMagnificationMouseFollowingTest.kt378
-rw-r--r--services/tests/servicestests/src/com/android/server/biometrics/AuthenticationStatsCollectorTest.java268
-rw-r--r--services/tests/servicestests/src/com/android/server/biometrics/AuthenticationStatsPersisterTest.java72
-rw-r--r--services/tests/servicestests/src/com/android/server/biometrics/AuthenticationStatsTest.java4
-rw-r--r--services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceEnrollClientTest.java43
-rw-r--r--services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClientTest.java45
-rw-r--r--services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTestable.java2
-rw-r--r--services/tests/servicestests/src/com/android/server/media/LegacyDeviceRouteControllerTest.java28
-rw-r--r--services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserInfoTest.java239
-rw-r--r--services/tests/servicestests/src/com/android/server/supervision/SupervisionServiceTest.kt58
-rw-r--r--services/tests/servicestests/src/com/android/server/theming/FieldColorBothTests.java99
-rw-r--r--services/tests/servicestests/src/com/android/server/theming/FieldColorIndexTests.java103
-rw-r--r--services/tests/servicestests/src/com/android/server/theming/FieldColorSourceTests.java94
-rw-r--r--services/tests/servicestests/src/com/android/server/theming/FieldColorTests.java107
-rw-r--r--services/tests/servicestests/src/com/android/server/theming/FieldThemeStyleTests.java86
-rw-r--r--services/tests/servicestests/src/com/android/server/theming/TEST_MAPPING7
-rw-r--r--services/tests/servicestests/src/com/android/server/theming/ThemeSettingsFieldTests.java169
-rw-r--r--services/tests/servicestests/src/com/android/server/theming/ThemeSettingsManagerTests.java114
-rw-r--r--services/tests/servicestests/src/com/android/server/theming/ThemeSettingsTests.java108
-rw-r--r--services/tests/servicestests/src/com/android/server/theming/ThemeSettingsUpdaterTests.java154
-rw-r--r--services/tests/wmtests/src/com/android/server/policy/PowerKeyGestureTests.java143
-rw-r--r--services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java30
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java61
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/DisplayWindowSettingsTests.java16
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java15
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/TaskTests.java32
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java105
-rw-r--r--telecomm/java/android/telecom/Connection.java10
-rw-r--r--tests/AppJankTest/src/android/app/jank/tests/IntegrationTests.java47
-rw-r--r--tests/AppJankTest/src/android/app/jank/tests/JankUtils.java35
-rw-r--r--tests/Input/src/com/android/server/input/InputManagerServiceTests.kt22
-rw-r--r--tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt2
-rw-r--r--tests/Input/src/com/android/server/input/KeyboardBacklightControllerTests.kt83
-rw-r--r--tests/Tracing/src/com/android/internal/protolog/ProcessedPerfettoProtoLogImplTest.java2
669 files changed, 20609 insertions, 9209 deletions
diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java b/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java
index 5dfb3754e8fb..7e421676b3c9 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java
@@ -2039,8 +2039,8 @@ class JobConcurrencyManager {
DeviceConfig.Properties properties =
DeviceConfig.getProperties(DeviceConfig.NAMESPACE_JOB_SCHEDULER);
- // Concurrency limit should be in the range [8, MAX_CONCURRENCY_LIMIT].
- mSteadyStateConcurrencyLimit = Math.max(8, Math.min(MAX_CONCURRENCY_LIMIT,
+ // Concurrency limit should be in the range [1, MAX_CONCURRENCY_LIMIT].
+ mSteadyStateConcurrencyLimit = Math.max(1, Math.min(MAX_CONCURRENCY_LIMIT,
properties.getInt(KEY_CONCURRENCY_LIMIT, DEFAULT_CONCURRENCY_LIMIT)));
mScreenOffAdjustmentDelayMs = properties.getLong(
diff --git a/cmds/app_process/Android.bp b/cmds/app_process/Android.bp
index 3c7609e1d8ed..a1575173ded6 100644
--- a/cmds/app_process/Android.bp
+++ b/cmds/app_process/Android.bp
@@ -56,7 +56,6 @@ cc_binary {
"libsigchain",
"libutils",
- "libutilscallstack",
// This is a list of libraries that need to be included in order to avoid
// bad apps. This prevents a library from having a mismatch when resolving
diff --git a/cmds/uinput/src/com/android/commands/uinput/EvemuParser.java b/cmds/uinput/src/com/android/commands/uinput/EvemuParser.java
index d3e62d5351f0..017d9563b9a8 100644
--- a/cmds/uinput/src/com/android/commands/uinput/EvemuParser.java
+++ b/cmds/uinput/src/com/android/commands/uinput/EvemuParser.java
@@ -61,17 +61,18 @@ public class EvemuParser implements EventParser {
mReader = in;
}
- private @Nullable String findNextLine() throws IOException {
+ private void findNextLine() throws IOException {
String line = "";
while (line != null && line.length() == 0) {
String unstrippedLine = mReader.readLine();
if (unstrippedLine == null) {
mAtEndOfFile = true;
- return null;
+ mNextLine = null;
+ return;
}
line = stripComments(unstrippedLine);
}
- return line;
+ mNextLine = line;
}
private static String stripComments(String line) {
@@ -92,7 +93,7 @@ public class EvemuParser implements EventParser {
*/
public @Nullable String peekLine() throws IOException {
if (mNextLine == null && !mAtEndOfFile) {
- mNextLine = findNextLine();
+ findNextLine();
}
return mNextLine;
}
@@ -103,7 +104,10 @@ public class EvemuParser implements EventParser {
mNextLine = null;
}
- public boolean isAtEndOfFile() {
+ public boolean isAtEndOfFile() throws IOException {
+ if (mNextLine == null && !mAtEndOfFile) {
+ findNextLine();
+ }
return mAtEndOfFile;
}
diff --git a/cmds/uinput/tests/src/com/android/commands/uinput/tests/EvemuParserTest.java b/cmds/uinput/tests/src/com/android/commands/uinput/tests/EvemuParserTest.java
index 5239fbc7e0a8..f18cab51fb4d 100644
--- a/cmds/uinput/tests/src/com/android/commands/uinput/tests/EvemuParserTest.java
+++ b/cmds/uinput/tests/src/com/android/commands/uinput/tests/EvemuParserTest.java
@@ -216,6 +216,9 @@ public class EvemuParserTest {
assertInjectEvent(parser.getNextEvent(), 0x2, 0x0, 1, -1);
assertInjectEvent(parser.getNextEvent(), 0x2, 0x1, -2);
assertInjectEvent(parser.getNextEvent(), 0x0, 0x0, 0);
+
+ // Now we should be at the end of the file.
+ assertThat(parser.getNextEvent()).isNull();
}
@Test
@@ -246,6 +249,8 @@ public class EvemuParserTest {
assertInjectEvent(parser.getNextEvent(), 0x1, 0x15, 1, 1_000_000);
assertInjectEvent(parser.getNextEvent(), 0x0, 0x0, 0);
+
+ assertThat(parser.getNextEvent()).isNull();
}
@Test
diff --git a/core/api/current.txt b/core/api/current.txt
index 1a13a39eade8..07224db7dcd3 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -273,6 +273,7 @@ package android {
field public static final String READ_SYNC_SETTINGS = "android.permission.READ_SYNC_SETTINGS";
field public static final String READ_SYNC_STATS = "android.permission.READ_SYNC_STATS";
field @FlaggedApi("com.android.settingslib.flags.settings_catalyst") public static final String READ_SYSTEM_PREFERENCES = "android.permission.READ_SYSTEM_PREFERENCES";
+ field @FlaggedApi("com.android.update_engine.minor_changes_2025q4") public static final String READ_UPDATE_ENGINE_LOGS = "android.permission.READ_UPDATE_ENGINE_LOGS";
field public static final String READ_VOICEMAIL = "com.android.voicemail.permission.READ_VOICEMAIL";
field public static final String REBOOT = "android.permission.REBOOT";
field public static final String RECEIVE_BOOT_COMPLETED = "android.permission.RECEIVE_BOOT_COMPLETED";
@@ -2483,7 +2484,6 @@ package android {
field public static final int primary = 16908300; // 0x102000c
field public static final int progress = 16908301; // 0x102000d
field public static final int redo = 16908339; // 0x1020033
- field @FlaggedApi("android.appwidget.flags.engagement_metrics") public static final int remoteViewsMetricsId;
field public static final int replaceText = 16908340; // 0x1020034
field public static final int secondaryProgress = 16908303; // 0x102000f
field public static final int selectAll = 16908319; // 0x102001f
@@ -12367,6 +12367,7 @@ package android.content.om {
method @NonNull public void setResourceValue(@NonNull String, @NonNull android.os.ParcelFileDescriptor, @Nullable String);
method @FlaggedApi("android.content.res.asset_file_descriptor_frro") @NonNull public void setResourceValue(@NonNull String, @NonNull android.content.res.AssetFileDescriptor, @Nullable String);
method @FlaggedApi("android.content.res.dimension_frro") public void setResourceValue(@NonNull String, float, int, @Nullable String);
+ method @FlaggedApi("android.content.res.dimension_frro") public void setResourceValue(@NonNull String, float, @Nullable String);
method public void setTargetOverlayable(@Nullable String);
}
@@ -27223,12 +27224,34 @@ package android.media.projection {
method public void onStop();
}
+ @FlaggedApi("com.android.media.projection.flags.app_content_sharing") public final class MediaProjectionAppContent implements android.os.Parcelable {
+ ctor public MediaProjectionAppContent(@NonNull android.graphics.Bitmap, @NonNull CharSequence, int);
+ method public int describeContents();
+ method public void writeToParcel(@NonNull android.os.Parcel, int);
+ field @NonNull public static final android.os.Parcelable.Creator<android.media.projection.MediaProjectionAppContent> CREATOR;
+ }
+
public final class MediaProjectionConfig implements android.os.Parcelable {
method @NonNull public static android.media.projection.MediaProjectionConfig createConfigForDefaultDisplay();
method @NonNull public static android.media.projection.MediaProjectionConfig createConfigForUserChoice();
method public int describeContents();
+ method @FlaggedApi("com.android.media.projection.flags.app_content_sharing") public int getInitiallySelectedSource();
+ method @FlaggedApi("com.android.media.projection.flags.app_content_sharing") public int getProjectionSources();
+ method @FlaggedApi("com.android.media.projection.flags.app_content_sharing") @Nullable public CharSequence getRequesterHint();
+ method @FlaggedApi("com.android.media.projection.flags.app_content_sharing") public boolean isSourceEnabled(int);
method public void writeToParcel(@NonNull android.os.Parcel, int);
field @NonNull public static final android.os.Parcelable.Creator<android.media.projection.MediaProjectionConfig> CREATOR;
+ field @FlaggedApi("com.android.media.projection.flags.app_content_sharing") public static final int PROJECTION_SOURCE_APP = 8; // 0x8
+ field @FlaggedApi("com.android.media.projection.flags.app_content_sharing") public static final int PROJECTION_SOURCE_APP_CONTENT = 16; // 0x10
+ field @FlaggedApi("com.android.media.projection.flags.app_content_sharing") public static final int PROJECTION_SOURCE_DISPLAY = 2; // 0x2
+ }
+
+ @FlaggedApi("com.android.media.projection.flags.app_content_sharing") public static final class MediaProjectionConfig.Builder {
+ ctor public MediaProjectionConfig.Builder();
+ method @NonNull public android.media.projection.MediaProjectionConfig build();
+ method @NonNull public android.media.projection.MediaProjectionConfig.Builder setInitiallySelectedSource(int);
+ method @NonNull public android.media.projection.MediaProjectionConfig.Builder setRequesterHint(@Nullable String);
+ method @NonNull public android.media.projection.MediaProjectionConfig.Builder setSourceEnabled(int, boolean);
}
public final class MediaProjectionManager {
@@ -44051,6 +44074,7 @@ package android.telecom {
field public static final String EVENT_CALL_PULL_FAILED = "android.telecom.event.CALL_PULL_FAILED";
field public static final String EVENT_CALL_REMOTELY_HELD = "android.telecom.event.CALL_REMOTELY_HELD";
field public static final String EVENT_CALL_REMOTELY_UNHELD = "android.telecom.event.CALL_REMOTELY_UNHELD";
+ field @FlaggedApi("com.android.server.telecom.flags.call_sequencing_call_resume_failed") public static final String EVENT_CALL_RESUME_FAILED = "android.telecom.event.CALL_RESUME_FAILED";
field public static final String EVENT_CALL_SWITCH_FAILED = "android.telecom.event.CALL_SWITCH_FAILED";
field public static final String EVENT_MERGE_COMPLETE = "android.telecom.event.MERGE_COMPLETE";
field public static final String EVENT_MERGE_START = "android.telecom.event.MERGE_START";
@@ -56114,6 +56138,7 @@ package android.view {
@FlaggedApi("android.xr.xr_manifest_entries") public final class XrWindowProperties {
field @FlaggedApi("android.xr.xr_manifest_entries") public static final String PROPERTY_XR_ACTIVITY_START_MODE = "android.window.PROPERTY_XR_ACTIVITY_START_MODE";
field @FlaggedApi("android.xr.xr_manifest_entries") public static final String PROPERTY_XR_BOUNDARY_TYPE_RECOMMENDED = "android.window.PROPERTY_XR_BOUNDARY_TYPE_RECOMMENDED";
+ field @FlaggedApi("android.xr.xr_manifest_entries") public static final String PROPERTY_XR_USES_CUSTOM_FULL_SPACE_MANAGED_ANIMATION = "android.window.PROPERTY_XR_USES_CUSTOM_FULL_SPACE_MANAGED_ANIMATION";
field @FlaggedApi("android.xr.xr_manifest_entries") public static final String XR_ACTIVITY_START_MODE_FULL_SPACE_MANAGED = "XR_ACTIVITY_START_MODE_FULL_SPACE_MANAGED";
field @FlaggedApi("android.xr.xr_manifest_entries") public static final String XR_ACTIVITY_START_MODE_FULL_SPACE_UNMANAGED = "XR_ACTIVITY_START_MODE_FULL_SPACE_UNMANAGED";
field @FlaggedApi("android.xr.xr_manifest_entries") public static final String XR_ACTIVITY_START_MODE_HOME_SPACE = "XR_ACTIVITY_START_MODE_HOME_SPACE";
@@ -61755,6 +61780,7 @@ package android.widget {
method public void setTextViewText(@IdRes int, CharSequence);
method public void setTextViewTextSize(@IdRes int, int, float);
method public void setUri(@IdRes int, String, android.net.Uri);
+ method @FlaggedApi("android.appwidget.flags.engagement_metrics") public void setUsageEventTag(@IdRes int, int);
method public void setViewLayoutHeight(@IdRes int, float, int);
method public void setViewLayoutHeightAttr(@IdRes int, @AttrRes int);
method public void setViewLayoutHeightDimen(@IdRes int, @DimenRes int);
diff --git a/core/api/lint-baseline.txt b/core/api/lint-baseline.txt
index 577113b80d84..3895a512abc7 100644
--- a/core/api/lint-baseline.txt
+++ b/core/api/lint-baseline.txt
@@ -1,4 +1,3 @@
-
// Baseline format: 1.0
BroadcastBehavior: android.app.AlarmManager#ACTION_NEXT_ALARM_CLOCK_CHANGED:
Field 'ACTION_NEXT_ALARM_CLOCK_CHANGED' is missing @BroadcastBehavior
@@ -244,6 +243,8 @@ BroadcastBehavior: android.telephony.TelephonyManager#ACTION_SUBSCRIPTION_SPECIF
Field 'ACTION_SUBSCRIPTION_SPECIFIC_CARRIER_IDENTITY_CHANGED' is missing @BroadcastBehavior
BroadcastBehavior: android.telephony.euicc.EuiccManager#ACTION_NOTIFY_CARRIER_SETUP_INCOMPLETE:
Field 'ACTION_NOTIFY_CARRIER_SETUP_INCOMPLETE' is missing @BroadcastBehavior
+
+
DeprecationMismatch: android.accounts.AccountManager#newChooseAccountIntent(android.accounts.Account, java.util.ArrayList<android.accounts.Account>, String[], boolean, String, String, String[], android.os.Bundle):
Method android.accounts.AccountManager.newChooseAccountIntent(android.accounts.Account, java.util.ArrayList<android.accounts.Account>, String[], boolean, String, String, String[], android.os.Bundle): @Deprecated annotation (present) and @deprecated doc tag (not present) do not match
DeprecationMismatch: android.app.Activity#enterPictureInPictureMode():
@@ -380,6 +381,8 @@ DeprecationMismatch: android.webkit.WebViewDatabase#hasFormData():
Method android.webkit.WebViewDatabase.hasFormData(): @Deprecated annotation (present) and @deprecated doc tag (not present) do not match
DeprecationMismatch: javax.microedition.khronos.egl.EGL10#eglCreatePixmapSurface(javax.microedition.khronos.egl.EGLDisplay, javax.microedition.khronos.egl.EGLConfig, Object, int[]):
Method javax.microedition.khronos.egl.EGL10.eglCreatePixmapSurface(javax.microedition.khronos.egl.EGLDisplay, javax.microedition.khronos.egl.EGLConfig, Object, int[]): @Deprecated annotation (present) and @deprecated doc tag (not present) do not match
+
+
FlaggedApiLiteral: android.Manifest.permission#BIND_APP_FUNCTION_SERVICE:
@FlaggedApi contains a string literal, but should reference the field generated by aconfig (android.app.appfunctions.flags.Flags.FLAG_ENABLE_APP_FUNCTION_MANAGER).
FlaggedApiLiteral: android.Manifest.permission#BIND_TV_AD_SERVICE:
@@ -390,6 +393,8 @@ FlaggedApiLiteral: android.Manifest.permission#QUERY_ADVANCED_PROTECTION_MODE:
@FlaggedApi contains a string literal, but should reference the field generated by aconfig (android.security.Flags.FLAG_AAPM_API).
FlaggedApiLiteral: android.Manifest.permission#RANGING:
@FlaggedApi contains a string literal, but should reference the field generated by aconfig (android.permission.flags.Flags.FLAG_RANGING_PERMISSION_ENABLED).
+FlaggedApiLiteral: android.Manifest.permission#READ_UPDATE_ENGINE_LOGS:
+ @FlaggedApi contains a string literal, but should reference the field generated by aconfig (com.android.update_engine.Flags.FLAG_MINOR_CHANGES_2025Q4, however this flag doesn't seem to exist).
FlaggedApiLiteral: android.Manifest.permission#REQUEST_OBSERVE_DEVICE_UUID_PRESENCE:
@FlaggedApi contains a string literal, but should reference the field generated by aconfig (android.companion.Flags.FLAG_DEVICE_PRESENCE).
FlaggedApiLiteral: android.R.attr#adServiceTypes:
@@ -1110,10 +1115,14 @@ RequiresPermission: android.webkit.WebSettings#setBlockNetworkLoads(boolean):
Method 'setBlockNetworkLoads' documentation mentions permissions without declaring @RequiresPermission
RequiresPermission: android.webkit.WebSettings#setGeolocationEnabled(boolean):
Method 'setGeolocationEnabled' documentation mentions permissions without declaring @RequiresPermission
+
+
Todo: android.hardware.camera2.params.StreamConfigurationMap:
Documentation mentions 'TODO'
Todo: android.provider.ContactsContract.RawContacts#newEntityIterator(android.database.Cursor):
Documentation mentions 'TODO'
+
+
UnflaggedApi: android.R.color#on_surface_disabled_material:
New API must be flagged with @FlaggedApi: field android.R.color.on_surface_disabled_material
UnflaggedApi: android.R.color#outline_disabled_material:
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index 42c60b0ba0da..a0547411cd9e 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -2950,6 +2950,7 @@ package android.app.supervision {
@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();
+ method @FlaggedApi("android.permission.flags.enable_system_supervision_role_behavior") @RequiresPermission(android.Manifest.permission.MANAGE_ROLE_HOLDERS) public boolean shouldAllowBypassingSupervisionRoleQualification();
}
}
@@ -18977,10 +18978,8 @@ package android.view {
public static class WindowManager.LayoutParams extends android.view.ViewGroup.LayoutParams implements android.os.Parcelable {
method public final long getUserActivityTimeout();
- method @FlaggedApi("com.android.hardware.input.override_power_key_behavior_in_focused_window") @RequiresPermission(android.Manifest.permission.OVERRIDE_SYSTEM_KEY_BEHAVIOR_IN_FOCUSED_WINDOW) public boolean isReceivePowerKeyDoublePressEnabled();
method public boolean isSystemApplicationOverlay();
method @FlaggedApi("android.companion.virtualdevice.flags.status_bar_and_insets") public void setInsetsParams(@NonNull java.util.List<android.view.WindowManager.InsetsParams>);
- method @FlaggedApi("com.android.hardware.input.override_power_key_behavior_in_focused_window") @RequiresPermission(android.Manifest.permission.OVERRIDE_SYSTEM_KEY_BEHAVIOR_IN_FOCUSED_WINDOW) public void setReceivePowerKeyDoublePressEnabled(boolean);
method @RequiresPermission(android.Manifest.permission.SYSTEM_APPLICATION_OVERLAY) public void setSystemApplicationOverlay(boolean);
method public final void setUserActivityTimeout(long);
field @RequiresPermission(android.Manifest.permission.HIDE_NON_SYSTEM_OVERLAY_WINDOWS) public static final int SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS = 524288; // 0x80000
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index daa1902edf02..1e21991cd380 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -369,7 +369,7 @@ package android.app {
}
public final class NotificationChannel implements android.os.Parcelable {
- method @FlaggedApi("android.service.notification.notification_conversation_channel_management") @NonNull public android.app.NotificationChannel copy();
+ method @NonNull public android.app.NotificationChannel copy();
method public int getOriginalImportance();
method public boolean isImportanceLockedByCriticalDeviceFunction();
method public void lockFields(int);
diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java
index d5df48a2ea22..c129fde3f819 100644
--- a/core/java/android/app/Activity.java
+++ b/core/java/android/app/Activity.java
@@ -3174,6 +3174,15 @@ public class Activity extends ContextThemeWrapper
throw new IllegalArgumentException("Expected non-null picture-in-picture params");
}
if (!mCanEnterPictureInPicture) {
+ if (isTvImplicitEnterPipProhibited()) {
+ // Don't throw exception on TV so that apps don't crash when not adapted to new
+ // restrictions.
+ Log.e(TAG,
+ "Activity must be resumed to enter picture-in-picture and not about to be"
+ + " paused. Implicit app entry is only permitted on TV if android"
+ + ".permission.TV_IMPLICIT_ENTER_PIP is held by the app.");
+ return false;
+ }
throw new IllegalStateException("Activity must be resumed to enter"
+ " picture-in-picture");
}
@@ -3212,7 +3221,7 @@ public class Activity extends ContextThemeWrapper
return ActivityTaskManager.getMaxNumPictureInPictureActions(this);
}
- private boolean isImplicitEnterPipProhibited() {
+ private boolean isTvImplicitEnterPipProhibited() {
PackageManager pm = getPackageManager();
if (android.app.Flags.enableTvImplicitEnterPipRestriction()) {
return pm.hasSystemFeature(PackageManager.FEATURE_LEANBACK)
@@ -9346,7 +9355,7 @@ public class Activity extends ContextThemeWrapper
+ mComponent.getClassName());
}
- if (isImplicitEnterPipProhibited()) {
+ if (isTvImplicitEnterPipProhibited()) {
mCanEnterPictureInPicture = false;
}
@@ -9376,7 +9385,7 @@ public class Activity extends ContextThemeWrapper
final void performUserLeaving() {
onUserInteraction();
- if (isImplicitEnterPipProhibited()) {
+ if (isTvImplicitEnterPipProhibited()) {
mCanEnterPictureInPicture = false;
}
onUserLeaveHint();
diff --git a/core/java/android/app/FullscreenRequestHandler.java b/core/java/android/app/FullscreenRequestHandler.java
index c78c66aa62c0..5529349dea70 100644
--- a/core/java/android/app/FullscreenRequestHandler.java
+++ b/core/java/android/app/FullscreenRequestHandler.java
@@ -18,6 +18,7 @@ package android.app;
import static android.app.Activity.FULLSCREEN_MODE_REQUEST_EXIT;
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
import android.annotation.IntDef;
import android.annotation.NonNull;
@@ -27,6 +28,7 @@ import android.os.Bundle;
import android.os.IBinder;
import android.os.IRemoteCallback;
import android.os.OutcomeReceiver;
+import android.window.DesktopModeFlags;
/**
* @hide
@@ -35,13 +37,15 @@ public class FullscreenRequestHandler {
@IntDef(prefix = { "RESULT_" }, value = {
RESULT_APPROVED,
RESULT_FAILED_NOT_IN_FULLSCREEN_WITH_HISTORY,
- RESULT_FAILED_NOT_TOP_FOCUSED
+ RESULT_FAILED_NOT_TOP_FOCUSED,
+ RESULT_FAILED_ALREADY_FULLY_EXPANDED
})
public @interface RequestResult {}
public static final int RESULT_APPROVED = 0;
public static final int RESULT_FAILED_NOT_IN_FULLSCREEN_WITH_HISTORY = 1;
public static final int RESULT_FAILED_NOT_TOP_FOCUSED = 2;
+ public static final int RESULT_FAILED_ALREADY_FULLY_EXPANDED = 3;
public static final String REMOTE_CALLBACK_RESULT_KEY = "result";
@@ -87,6 +91,9 @@ public class FullscreenRequestHandler {
case RESULT_FAILED_NOT_TOP_FOCUSED:
e = new IllegalStateException("The window is not the top focused window.");
break;
+ case RESULT_FAILED_ALREADY_FULLY_EXPANDED:
+ e = new IllegalStateException("The window is already fully expanded.");
+ break;
default:
callback.onResult(null);
break;
@@ -101,6 +108,12 @@ public class FullscreenRequestHandler {
if (windowingMode != WINDOWING_MODE_FULLSCREEN) {
return RESULT_FAILED_NOT_IN_FULLSCREEN_WITH_HISTORY;
}
+ return RESULT_APPROVED;
+ }
+ if (DesktopModeFlags.ENABLE_REQUEST_FULLSCREEN_BUGFIX.isTrue()
+ && (windowingMode == WINDOWING_MODE_FULLSCREEN
+ || windowingMode == WINDOWING_MODE_MULTI_WINDOW)) {
+ return RESULT_FAILED_ALREADY_FULLY_EXPANDED;
}
return RESULT_APPROVED;
}
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java
index f5277fd86a57..521b70b599f6 100644
--- a/core/java/android/app/Notification.java
+++ b/core/java/android/app/Notification.java
@@ -3253,9 +3253,24 @@ public class Notification implements Parcelable
* @hide
*/
public boolean hasTitle() {
- return extras != null
- && (!TextUtils.isEmpty(extras.getCharSequence(EXTRA_TITLE))
- || !TextUtils.isEmpty(extras.getCharSequence(EXTRA_TITLE_BIG)));
+ if (extras == null) {
+ return false;
+ }
+ // CallStyle notifications only use the other person's name as the title.
+ if (isStyle(CallStyle.class)) {
+ Person person = extras.getParcelable(EXTRA_CALL_PERSON, Person.class);
+ return person != null && !TextUtils.isEmpty(person.getName());
+ }
+ // non-CallStyle notifications can use EXTRA_TITLE
+ if (!TextUtils.isEmpty(extras.getCharSequence(EXTRA_TITLE))) {
+ return true;
+ }
+ // BigTextStyle notifications first use EXTRA_TITLE_BIG
+ if (isStyle(BigTextStyle.class)) {
+ return !TextUtils.isEmpty(extras.getCharSequence(EXTRA_TITLE_BIG));
+ } else {
+ return false;
+ }
}
/**
@@ -3280,12 +3295,23 @@ public class Notification implements Parcelable
*/
@FlaggedApi(Flags.FLAG_API_RICH_ONGOING)
public boolean hasPromotableCharacteristics() {
- return isColorizedRequested()
- && isOngoingEvent()
- && hasTitle()
- && !isGroupSummary()
- && !containsCustomViews()
- && hasPromotableStyle();
+ if (!isOngoingEvent() || isGroupSummary() || containsCustomViews() || !hasTitle()) {
+ return false;
+ }
+ // Only "Ongoing CallStyle" notifications are promotable without EXTRA_COLORIZED
+ if (isOngoingCallStyle()) {
+ return true;
+ }
+ return isColorizedRequested() && hasPromotableStyle();
+ }
+
+ /** Returns whether the notification is CallStyle.forOngoingCall(). */
+ private boolean isOngoingCallStyle() {
+ if (!isStyle(CallStyle.class)) {
+ return false;
+ }
+ int callType = extras.getInt(EXTRA_CALL_TYPE, CallStyle.CALL_TYPE_UNKNOWN);
+ return callType == CallStyle.CALL_TYPE_ONGOING;
}
/**
@@ -6096,6 +6122,21 @@ public class Notification implements Parcelable
return mColors;
}
+ private void updateHeaderBackgroundColor(RemoteViews contentView,
+ StandardTemplateParams p) {
+ if (!Flags.uiRichOngoing()) {
+ return;
+ }
+ if (isBackgroundColorized(p)) {
+ contentView.setInt(R.id.notification_header, "setBackgroundColor",
+ getBackgroundColor(p));
+ } else {
+ // Clear it!
+ contentView.setInt(R.id.notification_header, "setBackgroundResource",
+ 0);
+ }
+ }
+
private void updateBackgroundColor(RemoteViews contentView,
StandardTemplateParams p) {
if (isBackgroundColorized(p)) {
@@ -6900,7 +6941,7 @@ public class Notification implements Parcelable
* @hide
*/
public RemoteViews makeNotificationGroupHeader() {
- return makeNotificationHeader(mParams.reset()
+ return makeNotificationHeader(mParams.reset().disallowColorization()
.viewType(StandardTemplateParams.VIEW_TYPE_GROUP_HEADER)
.fillTextsFrom(this));
}
@@ -6912,12 +6953,11 @@ public class Notification implements Parcelable
* @param p the template params to inflate this with
*/
private RemoteViews makeNotificationHeader(StandardTemplateParams p) {
- // Headers on their own are never colorized
- p.disallowColorization();
RemoteViews header = new BuilderRemoteViews(mContext.getApplicationInfo(),
getHeaderLayoutResource());
resetNotificationHeader(header);
bindNotificationHeader(header, p);
+ updateHeaderBackgroundColor(header, p);
if (Flags.notificationsRedesignTemplates()
&& (p.mViewType == StandardTemplateParams.VIEW_TYPE_MINIMIZED
|| p.mViewType == StandardTemplateParams.VIEW_TYPE_PUBLIC)) {
@@ -7041,6 +7081,10 @@ public class Notification implements Parcelable
savedBundle.getBoolean(EXTRA_SHOW_CHRONOMETER));
publicExtras.putBoolean(EXTRA_CHRONOMETER_COUNT_DOWN,
savedBundle.getBoolean(EXTRA_CHRONOMETER_COUNT_DOWN));
+ if (mN.isPromotedOngoing()) {
+ publicExtras.putBoolean(EXTRA_COLORIZED,
+ savedBundle.getBoolean(EXTRA_COLORIZED));
+ }
String appName = savedBundle.getString(EXTRA_SUBSTITUTE_APP_NAME);
if (appName != null) {
publicExtras.putString(EXTRA_SUBSTITUTE_APP_NAME, appName);
@@ -7053,6 +7097,9 @@ public class Notification implements Parcelable
if (isLowPriority) {
params.highlightExpander(false);
}
+ if (!mN.isPromotedOngoing()) {
+ params.disallowColorization();
+ }
view = makeNotificationHeader(params);
view.setBoolean(R.id.notification_header, "setExpandOnlyOnButton", true);
mN.extras = savedBundle;
@@ -7072,7 +7119,7 @@ public class Notification implements Parcelable
* @hide
*/
public RemoteViews makeLowPriorityContentView(boolean useRegularSubtext) {
- StandardTemplateParams p = mParams.reset()
+ StandardTemplateParams p = mParams.reset().disallowColorization()
.viewType(StandardTemplateParams.VIEW_TYPE_MINIMIZED)
.highlightExpander(false)
.fillTextsFrom(this);
diff --git a/core/java/android/app/NotificationChannel.java b/core/java/android/app/NotificationChannel.java
index d88395331656..c1d80c93cfd6 100644
--- a/core/java/android/app/NotificationChannel.java
+++ b/core/java/android/app/NotificationChannel.java
@@ -508,7 +508,6 @@ public final class NotificationChannel implements Parcelable {
/** @hide */
@TestApi
@NonNull
- @FlaggedApi(FLAG_NOTIFICATION_CONVERSATION_CHANNEL_MANAGEMENT)
public NotificationChannel copy() {
NotificationChannel copy = new NotificationChannel(mId, mName, mImportance);
copy.setDescription(mDesc);
diff --git a/core/java/android/app/NotificationChannelGroup.java b/core/java/android/app/NotificationChannelGroup.java
index 92db8b329045..06b492c417d8 100644
--- a/core/java/android/app/NotificationChannelGroup.java
+++ b/core/java/android/app/NotificationChannelGroup.java
@@ -221,7 +221,10 @@ public final class NotificationChannelGroup implements Parcelable {
* @hide
*/
public void setChannels(List<NotificationChannel> channels) {
- mChannels = channels;
+ mChannels.clear();
+ if (channels != null) {
+ mChannels.addAll(channels);
+ }
}
/**
@@ -331,7 +334,9 @@ public final class NotificationChannelGroup implements Parcelable {
NotificationChannelGroup cloned = new NotificationChannelGroup(getId(), getName());
cloned.setDescription(getDescription());
cloned.setBlocked(isBlocked());
- cloned.setChannels(getChannels());
+ for (NotificationChannel c : mChannels) {
+ cloned.addChannel(c.copy());
+ }
cloned.lockFields(mUserLockedFields);
return cloned;
}
diff --git a/core/java/android/app/NotificationManager.java b/core/java/android/app/NotificationManager.java
index 69e3ef9086d5..f24eb0a63b26 100644
--- a/core/java/android/app/NotificationManager.java
+++ b/core/java/android/app/NotificationManager.java
@@ -1317,10 +1317,16 @@ public class NotificationManager {
*/
public List<NotificationChannel> getNotificationChannels() {
if (Flags.nmBinderPerfCacheChannels()) {
- return mNotificationChannelListCache.query(new NotificationChannelQuery(
- mContext.getOpPackageName(),
- mContext.getPackageName(),
- mContext.getUserId()));
+ List<NotificationChannel> channelList = mNotificationChannelListCache.query(
+ new NotificationChannelQuery(mContext.getOpPackageName(),
+ mContext.getPackageName(), mContext.getUserId()));
+ List<NotificationChannel> out = new ArrayList();
+ if (channelList != null) {
+ for (NotificationChannel c : channelList) {
+ out.add(c.copy());
+ }
+ }
+ return out;
} else {
INotificationManager service = service();
try {
@@ -1343,7 +1349,7 @@ public class NotificationManager {
}
for (NotificationChannel channel : channels) {
if (channelId.equals(channel.getId())) {
- return channel;
+ return channel.copy();
}
}
return null;
@@ -1364,12 +1370,12 @@ public class NotificationManager {
for (NotificationChannel channel : channels) {
if (conversationId.equals(channel.getConversationId())
&& channelId.equals(channel.getParentChannelId())) {
- return channel;
+ return channel.copy();
} else if (channelId.equals(channel.getId())) {
parent = channel;
}
}
- return parent;
+ return parent != null ? parent.copy() : null;
}
/**
@@ -1405,8 +1411,9 @@ public class NotificationManager {
new NotificationChannelQuery(pkgName, pkgName, mContext.getUserId()));
Map<String, NotificationChannelGroup> groupHeaders =
mNotificationChannelGroupsCache.query(pkgName);
- return NotificationChannelGroupsHelper.getGroupWithChannels(channelGroupId, channelList,
- groupHeaders, /* includeDeleted= */ false);
+ NotificationChannelGroup ncg = NotificationChannelGroupsHelper.getGroupWithChannels(
+ channelGroupId, channelList, groupHeaders, /* includeDeleted= */ false);
+ return ncg != null ? ncg.clone() : null;
} else {
INotificationManager service = service();
try {
@@ -1428,8 +1435,14 @@ public class NotificationManager {
new NotificationChannelQuery(pkgName, pkgName, mContext.getUserId()));
Map<String, NotificationChannelGroup> groupHeaders =
mNotificationChannelGroupsCache.query(pkgName);
- return NotificationChannelGroupsHelper.getGroupsWithChannels(channelList, groupHeaders,
- NotificationChannelGroupsHelper.Params.forAllGroups());
+ List<NotificationChannelGroup> populatedGroupList =
+ NotificationChannelGroupsHelper.getGroupsWithChannels(channelList, groupHeaders,
+ NotificationChannelGroupsHelper.Params.forAllGroups());
+ List<NotificationChannelGroup> out = new ArrayList<>();
+ for (NotificationChannelGroup g : populatedGroupList) {
+ out.add(g.clone());
+ }
+ return out;
} else {
INotificationManager service = service();
try {
diff --git a/core/java/android/app/jank/JankTracker.java b/core/java/android/app/jank/JankTracker.java
index e3f67811757c..a085701b006a 100644
--- a/core/java/android/app/jank/JankTracker.java
+++ b/core/java/android/app/jank/JankTracker.java
@@ -143,6 +143,13 @@ public class JankTracker {
* stats
*/
public void mergeAppJankStats(AppJankStats appJankStats) {
+ if (appJankStats.getUid() != mAppUid) {
+ if (DEBUG) {
+ Log.d(DEBUG_KEY, "Reported JankStats AppUID does not match AppUID of "
+ + "enclosing activity.");
+ }
+ return;
+ }
getHandler().post(new Runnable() {
@Override
public void run() {
diff --git a/core/java/android/app/supervision/ISupervisionManager.aidl b/core/java/android/app/supervision/ISupervisionManager.aidl
index 2f67a8abcd17..801162f3cbd3 100644
--- a/core/java/android/app/supervision/ISupervisionManager.aidl
+++ b/core/java/android/app/supervision/ISupervisionManager.aidl
@@ -27,4 +27,6 @@ interface ISupervisionManager {
boolean isSupervisionEnabledForUser(int userId);
void setSupervisionEnabledForUser(int userId, boolean enabled);
String getActiveSupervisionAppPackage(int userId);
+ @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.MANAGE_ROLE_HOLDERS)")
+ boolean shouldAllowBypassingSupervisionRoleQualification();
}
diff --git a/core/java/android/app/supervision/SupervisionManager.java b/core/java/android/app/supervision/SupervisionManager.java
index 172ed2358a5d..76a789d3426f 100644
--- a/core/java/android/app/supervision/SupervisionManager.java
+++ b/core/java/android/app/supervision/SupervisionManager.java
@@ -19,6 +19,7 @@ package android.app.supervision;
import static android.Manifest.permission.INTERACT_ACROSS_USERS;
import static android.Manifest.permission.MANAGE_USERS;
import static android.Manifest.permission.QUERY_USERS;
+import static android.permission.flags.Flags.FLAG_ENABLE_SYSTEM_SUPERVISION_ROLE_BEHAVIOR;
import android.annotation.FlaggedApi;
import android.annotation.Nullable;
@@ -193,4 +194,25 @@ public class SupervisionManager {
}
return null;
}
+
+
+ /**
+ * @return {@code true} if bypassing the qualification is allowed for the specified role based
+ * on the current state of the device.
+ *
+ * @hide
+ */
+ @SystemApi
+ @FlaggedApi(FLAG_ENABLE_SYSTEM_SUPERVISION_ROLE_BEHAVIOR)
+ @RequiresPermission(android.Manifest.permission.MANAGE_ROLE_HOLDERS)
+ public boolean shouldAllowBypassingSupervisionRoleQualification() {
+ if (mService != null) {
+ try {
+ return mService.shouldAllowBypassingSupervisionRoleQualification();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return false;
+ }
}
diff --git a/core/java/android/appwidget/AppWidgetHostView.java b/core/java/android/appwidget/AppWidgetHostView.java
index b9b5c6a8bbc3..33326347fda0 100644
--- a/core/java/android/appwidget/AppWidgetHostView.java
+++ b/core/java/android/appwidget/AppWidgetHostView.java
@@ -16,10 +16,15 @@
package android.appwidget;
+import static android.appwidget.flags.Flags.FLAG_ENGAGEMENT_METRICS;
+import static android.appwidget.flags.Flags.engagementMetrics;
+
+import android.annotation.FlaggedApi;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.Activity;
import android.app.ActivityOptions;
+import android.app.PendingIntent;
import android.compat.annotation.UnsupportedAppUsage;
import android.content.ComponentName;
import android.content.Context;
@@ -38,6 +43,7 @@ import android.os.Build;
import android.os.Bundle;
import android.os.CancellationSignal;
import android.os.Parcelable;
+import android.util.ArraySet;
import android.util.AttributeSet;
import android.util.Log;
import android.util.Pair;
@@ -48,6 +54,7 @@ import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.accessibility.AccessibilityNodeInfo;
+import android.widget.AbsListView;
import android.widget.Adapter;
import android.widget.AdapterView;
import android.widget.BaseAdapter;
@@ -57,8 +64,11 @@ import android.widget.RemoteViews.InteractionHandler;
import android.widget.RemoteViewsAdapter.RemoteAdapterConnectionCallback;
import android.widget.TextView;
+import com.android.internal.annotations.VisibleForTesting;
+
import java.util.ArrayList;
import java.util.List;
+import java.util.Set;
import java.util.concurrent.Executor;
/**
@@ -99,7 +109,8 @@ public class AppWidgetHostView extends FrameLayout implements AppWidgetHost.AppW
int mViewMode = VIEW_MODE_NOINIT;
// If true, we should not try to re-apply the RemoteViews on the next inflation.
boolean mColorMappingChanged = false;
- private InteractionHandler mInteractionHandler;
+ @NonNull
+ private InteractionLogger mInteractionLogger = new InteractionLogger();
private boolean mOnLightBackground;
private SizeF mCurrentSize = null;
private RemoteViews.ColorResources mColorResources = null;
@@ -124,7 +135,7 @@ public class AppWidgetHostView extends FrameLayout implements AppWidgetHost.AppW
*/
public AppWidgetHostView(Context context, InteractionHandler handler) {
this(context, android.R.anim.fade_in, android.R.anim.fade_out);
- mInteractionHandler = getHandler(handler);
+ setInteractionHandler(handler);
}
/**
@@ -145,13 +156,29 @@ public class AppWidgetHostView extends FrameLayout implements AppWidgetHost.AppW
/**
* Pass the given handler to RemoteViews when updating this widget. Unless this
- * is done immediatly after construction, a call to {@link #updateAppWidget(RemoteViews)}
+ * is done immediately after construction, a call to {@link #updateAppWidget(RemoteViews)}
* should be made.
*
* @hide
*/
public void setInteractionHandler(InteractionHandler handler) {
- mInteractionHandler = getHandler(handler);
+ if (handler instanceof InteractionLogger logger) {
+ // Nested AppWidgetHostViews should reuse the parent logger instead of wrapping it.
+ mInteractionLogger = logger;
+ } else {
+ mInteractionLogger = new InteractionLogger(handler);
+ }
+ }
+
+ /**
+ * Return the InteractionLogger used by this class.
+ *
+ * @hide
+ */
+ @VisibleForTesting
+ @NonNull
+ public InteractionLogger getInteractionLogger() {
+ return mInteractionLogger;
}
/**
@@ -588,7 +615,7 @@ public class AppWidgetHostView extends FrameLayout implements AppWidgetHost.AppW
if (!mColorMappingChanged && rvToApply.canRecycleView(mView)) {
try {
- rvToApply.reapply(mContext, mView, mInteractionHandler, mCurrentSize,
+ rvToApply.reapply(mContext, mView, mInteractionLogger, mCurrentSize,
mColorResources);
content = mView;
mLastInflatedRemoteViewsId = rvToApply.computeUniqueId(remoteViews);
@@ -602,7 +629,7 @@ public class AppWidgetHostView extends FrameLayout implements AppWidgetHost.AppW
// Try normal RemoteView inflation
if (content == null) {
try {
- content = rvToApply.apply(mContext, this, mInteractionHandler,
+ content = rvToApply.apply(mContext, this, mInteractionLogger,
mCurrentSize, mColorResources);
mLastInflatedRemoteViewsId = rvToApply.computeUniqueId(remoteViews);
if (LOGD) Log.d(TAG, "had to inflate new layout");
@@ -660,7 +687,7 @@ public class AppWidgetHostView extends FrameLayout implements AppWidgetHost.AppW
mView,
mAsyncExecutor,
new ViewApplyListener(remoteViews, layoutId, true),
- mInteractionHandler,
+ mInteractionLogger,
mCurrentSize,
mColorResources);
} catch (Exception e) {
@@ -672,7 +699,7 @@ public class AppWidgetHostView extends FrameLayout implements AppWidgetHost.AppW
this,
mAsyncExecutor,
new ViewApplyListener(remoteViews, layoutId, false),
- mInteractionHandler,
+ mInteractionLogger,
mCurrentSize,
mColorResources);
}
@@ -711,7 +738,7 @@ public class AppWidgetHostView extends FrameLayout implements AppWidgetHost.AppW
AppWidgetHostView.this,
mAsyncExecutor,
new ViewApplyListener(mViews, mLayoutId, false),
- mInteractionHandler,
+ mInteractionLogger,
mCurrentSize);
} else {
applyContent(null, false, e);
@@ -916,21 +943,6 @@ public class AppWidgetHostView extends FrameLayout implements AppWidgetHost.AppW
return null;
}
- private InteractionHandler getHandler(InteractionHandler handler) {
- return (view, pendingIntent, response) -> {
- AppWidgetManager manager = AppWidgetManager.getInstance(mContext);
- if (manager != null) {
- manager.noteAppWidgetTapped(mAppWidgetId);
- }
- if (handler != null) {
- return handler.onInteraction(view, pendingIntent, response);
- } else {
- return RemoteViews.startPendingIntent(view, pendingIntent,
- response.getLaunchOptions(view));
- }
- };
- }
-
/**
* Set the dynamically overloaded color resources.
*
@@ -1016,4 +1028,83 @@ public class AppWidgetHostView extends FrameLayout implements AppWidgetHost.AppW
post(this::handleViewError);
}
}
+
+ /**
+ * This class is used to track user interactions with this widget.
+ * @hide
+ */
+ public class InteractionLogger implements RemoteViews.InteractionHandler {
+ // Max number of clicked and scrolled IDs stored per impression.
+ public static final int MAX_NUM_ITEMS = 10;
+ // Clicked views
+ @NonNull
+ private final Set<Integer> mClickedIds = new ArraySet<>(MAX_NUM_ITEMS);
+ // Scrolled views
+ @NonNull
+ private final Set<Integer> mScrolledIds = new ArraySet<>(MAX_NUM_ITEMS);
+ @Nullable
+ private RemoteViews.InteractionHandler mInteractionHandler = null;
+
+ InteractionLogger() {
+ }
+
+ InteractionLogger(@Nullable InteractionHandler handler) {
+ mInteractionHandler = handler;
+ }
+
+ @VisibleForTesting
+ @NonNull
+ public Set<Integer> getClickedIds() {
+ return mClickedIds;
+ }
+
+ @VisibleForTesting
+ @NonNull
+ public Set<Integer> getScrolledIds() {
+ return mScrolledIds;
+ }
+
+ @Override
+ public boolean onInteraction(View view, PendingIntent pendingIntent,
+ RemoteViews.RemoteResponse response) {
+ if (engagementMetrics() && mClickedIds.size() < MAX_NUM_ITEMS) {
+ mClickedIds.add(getMetricsId(view));
+ }
+ AppWidgetManager manager = AppWidgetManager.getInstance(mContext);
+ if (manager != null) {
+ manager.noteAppWidgetTapped(mAppWidgetId);
+ }
+
+ if (mInteractionHandler != null) {
+ return mInteractionHandler.onInteraction(view, pendingIntent, response);
+ } else {
+ return RemoteViews.startPendingIntent(view, pendingIntent,
+ response.getLaunchOptions(view));
+ }
+ }
+
+ @Override
+ public void onScroll(@NonNull AbsListView view) {
+ if (!engagementMetrics()) return;
+
+ if (mScrolledIds.size() < MAX_NUM_ITEMS) {
+ mScrolledIds.add(getMetricsId(view));
+ }
+
+ if (mInteractionHandler != null) {
+ mInteractionHandler.onScroll(view);
+ }
+ }
+
+ @FlaggedApi(FLAG_ENGAGEMENT_METRICS)
+ private int getMetricsId(@NonNull View view) {
+ int viewId = view.getId();
+ Object metricsTag = view.getTag(com.android.internal.R.id.remoteViewsMetricsId);
+ if (metricsTag instanceof Integer tag) {
+ viewId = tag;
+ }
+ return viewId;
+ }
+ }
}
+
diff --git a/core/java/android/appwidget/AppWidgetManager.java b/core/java/android/appwidget/AppWidgetManager.java
index b54e17beb100..52315d68afda 100644
--- a/core/java/android/appwidget/AppWidgetManager.java
+++ b/core/java/android/appwidget/AppWidgetManager.java
@@ -515,12 +515,13 @@ public class AppWidgetManager {
/**
* This bundle extra describes which views have been clicked during a single impression of the
- * widget. It is an integer array of view IDs of the clicked views.
+ * widget. It is an integer array of view IDs of the clicked views. The array may contain up to
+ * 10 distinct IDs per event.
*
- * Widget providers may set a different ID for event purposes by setting the
- * {@link android.R.id.remoteViewsMetricsId} int tag on the view.
+ * Widget providers may set a different ID for event logging by setting the usage event tag on
+ * the view with {@link RemoteViews#setUsageEventTag}.
*
- * @see android.views.RemoteViews.setIntTag
+ * @see android.widget.RemoteViews#setUsageEventTag
*/
@FlaggedApi(Flags.FLAG_ENGAGEMENT_METRICS)
public static final String EXTRA_EVENT_CLICKED_VIEWS =
@@ -528,12 +529,13 @@ public class AppWidgetManager {
/**
* This bundle extra describes which views have been scrolled during a single impression of the
- * widget. It is an integer array of view IDs of the scrolled views.
+ * widget. It is an integer array of view IDs of the scrolled views. The array may contain up to
+ * 10 distinct IDs per event.
*
- * Widget providers may set a different ID for event purposes by setting the
- * {@link android.R.id.remoteViewsMetricsId} int tag on the view.
+ * Widget providers may set a different ID for event logging by setting the usage event tag on
+ * the view with {@link RemoteViews#setUsageEventTag}.
*
- * @see android.views.RemoteViews.setIntTag
+ * @see android.widget.RemoteViews#setUsageEventTag
*/
@FlaggedApi(Flags.FLAG_ENGAGEMENT_METRICS)
public static final String EXTRA_EVENT_SCROLLED_VIEWS =
diff --git a/core/java/android/companion/virtual/flags/flags.aconfig b/core/java/android/companion/virtual/flags/flags.aconfig
index 161f05bc5139..c29f1528be89 100644
--- a/core/java/android/companion/virtual/flags/flags.aconfig
+++ b/core/java/android/companion/virtual/flags/flags.aconfig
@@ -154,7 +154,7 @@ flag {
flag {
namespace: "virtual_devices"
name: "viewconfiguration_apis"
- description: "APIs for settings ViewConfiguration attributes on virtual devices"
+ description: "APIs for setting ViewConfiguration attributes on virtual devices"
bug: "370720522"
is_exported: true
}
diff --git a/core/java/android/content/om/FabricatedOverlay.java b/core/java/android/content/om/FabricatedOverlay.java
index 64e9c339f2d6..2f93adbb1e8c 100644
--- a/core/java/android/content/om/FabricatedOverlay.java
+++ b/core/java/android/content/om/FabricatedOverlay.java
@@ -490,6 +490,17 @@ public class FabricatedOverlay {
return entry;
}
+ @NonNull
+ private static FabricatedOverlayInternalEntry generateFabricatedOverlayInternalEntry(
+ @NonNull String resourceName, float value, @Nullable String configuration) {
+ final FabricatedOverlayInternalEntry entry = new FabricatedOverlayInternalEntry();
+ entry.resourceName = resourceName;
+ entry.dataType = TypedValue.TYPE_FLOAT;
+ entry.data = Float.floatToIntBits(value);
+ entry.configuration = configuration;
+ return entry;
+ }
+
/**
* Sets the resource value in the fabricated overlay for the integer-like types with the
* configuration.
@@ -621,4 +632,24 @@ public class FabricatedOverlay {
mOverlay.entries.add(generateFabricatedOverlayInternalEntry(resourceName, dimensionValue,
dimensionUnit, configuration));
}
+
+ /**
+ * Sets the resource value in the fabricated overlay for the float type with the
+ * configuration.
+ *
+ * @param resourceName name of the target resource to overlay (in the form
+ * [package]:type/entry)
+ * @param value the float representing the new value
+ * @param configuration The string representation of the config this overlay is enabled for
+ * @throws IllegalArgumentException If the resource name is invalid
+ */
+ @FlaggedApi(android.content.res.Flags.FLAG_DIMENSION_FRRO)
+ public void setResourceValue(
+ @NonNull String resourceName,
+ float value,
+ @Nullable String configuration) {
+ ensureValidResourceName(resourceName);
+ mOverlay.entries.add(generateFabricatedOverlayInternalEntry(resourceName, value,
+ configuration));
+ }
}
diff --git a/core/java/android/content/pm/flags.aconfig b/core/java/android/content/pm/flags.aconfig
index 42bef0e91539..fe47243cd8d3 100644
--- a/core/java/android/content/pm/flags.aconfig
+++ b/core/java/android/content/pm/flags.aconfig
@@ -408,3 +408,10 @@ flag {
bug: "377474232"
is_fixed_read_only: true
}
+
+flag {
+ name: "parallel_package_parsing_across_system_dirs"
+ namespace: "system_performance"
+ description: "Continue parsing packages in system dirs instead of blocking on install completion for each directory."
+ bug: "401622806"
+}
diff --git a/core/java/android/content/res/ApkAssets.java b/core/java/android/content/res/ApkAssets.java
index f538e9ffffdd..3987f3abff0b 100644
--- a/core/java/android/content/res/ApkAssets.java
+++ b/core/java/android/content/res/ApkAssets.java
@@ -408,7 +408,7 @@ public final class ApkAssets {
Objects.requireNonNull(fileName, "fileName");
synchronized (this) {
long nativeXmlPtr = nativeOpenXml(mNativePtr, fileName);
- try (XmlBlock block = new XmlBlock(null, nativeXmlPtr)) {
+ try (XmlBlock block = new XmlBlock(null, nativeXmlPtr, true)) {
XmlResourceParser parser = block.newParser();
// If nativeOpenXml doesn't throw, it will always return a valid native pointer,
// which makes newParser always return non-null. But let's be careful.
diff --git a/core/java/android/content/res/AssetManager.java b/core/java/android/content/res/AssetManager.java
index bcb50881d327..008bf2f522c3 100644
--- a/core/java/android/content/res/AssetManager.java
+++ b/core/java/android/content/res/AssetManager.java
@@ -1190,7 +1190,7 @@ public final class AssetManager implements AutoCloseable {
*/
public @NonNull XmlResourceParser openXmlResourceParser(int cookie, @NonNull String fileName)
throws IOException {
- try (XmlBlock block = openXmlBlockAsset(cookie, fileName)) {
+ try (XmlBlock block = openXmlBlockAsset(cookie, fileName, true)) {
XmlResourceParser parser = block.newParser(ID_NULL, new Validator());
// If openXmlBlockAsset doesn't throw, it will always return an XmlBlock object with
// a valid native pointer, which makes newParser always return non-null. But let's
@@ -1209,7 +1209,7 @@ public final class AssetManager implements AutoCloseable {
* @hide
*/
@NonNull XmlBlock openXmlBlockAsset(@NonNull String fileName) throws IOException {
- return openXmlBlockAsset(0, fileName);
+ return openXmlBlockAsset(0, fileName, true);
}
/**
@@ -1218,9 +1218,11 @@ public final class AssetManager implements AutoCloseable {
*
* @param cookie Identifier of the package to be opened.
* @param fileName Name of the asset to retrieve.
+ * @param usesFeatureFlags Whether the resources uses feature flags
* @hide
*/
- @NonNull XmlBlock openXmlBlockAsset(int cookie, @NonNull String fileName) throws IOException {
+ @NonNull XmlBlock openXmlBlockAsset(int cookie, @NonNull String fileName,
+ boolean usesFeatureFlags) throws IOException {
Objects.requireNonNull(fileName, "fileName");
synchronized (this) {
ensureOpenLocked();
@@ -1229,7 +1231,8 @@ public final class AssetManager implements AutoCloseable {
if (xmlBlock == 0) {
throw new FileNotFoundException("Asset XML file: " + fileName);
}
- final XmlBlock block = new XmlBlock(this, xmlBlock);
+
+ final XmlBlock block = new XmlBlock(this, xmlBlock, usesFeatureFlags);
incRefsLocked(block.hashCode());
return block;
}
diff --git a/core/java/android/content/res/OWNERS b/core/java/android/content/res/OWNERS
index 141d58d51353..f3394c3932ba 100644
--- a/core/java/android/content/res/OWNERS
+++ b/core/java/android/content/res/OWNERS
@@ -2,6 +2,7 @@
patb@google.com
zyy@google.com
-branliu@google.com
+jakmcbane@google.com
+markpun@google.com
-per-file FontScaleConverter*=fuego@google.com \ No newline at end of file
+per-file FontScaleConverter*=fuego@google.com
diff --git a/core/java/android/content/res/Resources.java b/core/java/android/content/res/Resources.java
index 2658efab0e44..92f8bb4e005e 100644
--- a/core/java/android/content/res/Resources.java
+++ b/core/java/android/content/res/Resources.java
@@ -2568,7 +2568,7 @@ public class Resources {
impl.getValue(id, value, true);
if (value.type == TypedValue.TYPE_STRING) {
return loadXmlResourceParser(value.string.toString(), id,
- value.assetCookie, type);
+ value.assetCookie, type, value.usesFeatureFlags);
}
throw new NotFoundException("Resource ID #0x" + Integer.toHexString(id)
+ " type #0x" + Integer.toHexString(value.type) + " is not valid");
@@ -2591,7 +2591,26 @@ public class Resources {
@UnsupportedAppUsage
XmlResourceParser loadXmlResourceParser(String file, int id, int assetCookie,
String type) throws NotFoundException {
- return mResourcesImpl.loadXmlResourceParser(file, id, assetCookie, type);
+ return loadXmlResourceParser(file, id, assetCookie, type, true);
+ }
+
+ /**
+ * Loads an XML parser for the specified file.
+ *
+ * @param file the path for the XML file to parse
+ * @param id the resource identifier for the file
+ * @param assetCookie the asset cookie for the file
+ * @param type the type of resource (used for logging)
+ * @param usesFeatureFlags whether the xml has read/write feature flags
+ * @return a parser for the specified XML file
+ * @throws NotFoundException if the file could not be loaded
+ * @hide
+ */
+ @NonNull
+ @VisibleForTesting
+ public XmlResourceParser loadXmlResourceParser(String file, int id, int assetCookie,
+ String type, boolean usesFeatureFlags) throws NotFoundException {
+ return mResourcesImpl.loadXmlResourceParser(file, id, assetCookie, type, usesFeatureFlags);
}
/**
diff --git a/core/java/android/content/res/ResourcesImpl.java b/core/java/android/content/res/ResourcesImpl.java
index 8c76fd70afd9..6cbad2f0909b 100644
--- a/core/java/android/content/res/ResourcesImpl.java
+++ b/core/java/android/content/res/ResourcesImpl.java
@@ -276,7 +276,8 @@ public class ResourcesImpl {
}
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
- void getValue(@AnyRes int id, TypedValue outValue, boolean resolveRefs)
+ @VisibleForTesting
+ public void getValue(@AnyRes int id, TypedValue outValue, boolean resolveRefs)
throws NotFoundException {
boolean found = mAssets.getResourceValue(id, 0, outValue, resolveRefs);
if (found) {
@@ -1057,8 +1058,8 @@ public class ResourcesImpl {
int id, int density, String file)
throws IOException, XmlPullParserException {
try (
- XmlResourceParser rp =
- loadXmlResourceParser(file, id, value.assetCookie, "drawable")
+ XmlResourceParser rp = loadXmlResourceParser(
+ file, id, value.assetCookie, "drawable", value.usesFeatureFlags)
) {
return Drawable.createFromXmlForDensity(wrapper, rp, density, null);
}
@@ -1092,7 +1093,7 @@ public class ResourcesImpl {
try {
if (file.endsWith("xml")) {
final XmlResourceParser rp = loadXmlResourceParser(
- file, id, value.assetCookie, "font");
+ file, id, value.assetCookie, "font", value.usesFeatureFlags);
final FontResourcesParser.FamilyResourceEntry familyEntry =
FontResourcesParser.parse(rp, wrapper);
if (familyEntry == null) {
@@ -1286,7 +1287,7 @@ public class ResourcesImpl {
if (file.endsWith(".xml")) {
try {
final XmlResourceParser parser = loadXmlResourceParser(
- file, id, value.assetCookie, "ComplexColor");
+ file, id, value.assetCookie, "ComplexColor", value.usesFeatureFlags);
final AttributeSet attrs = Xml.asAttributeSet(parser);
int type;
@@ -1331,12 +1332,13 @@ public class ResourcesImpl {
* @param id the resource identifier for the file
* @param assetCookie the asset cookie for the file
* @param type the type of resource (used for logging)
+ * @param usesFeatureFlags whether the xml has read/write feature flags
* @return a parser for the specified XML file
* @throws NotFoundException if the file could not be loaded
*/
@NonNull
XmlResourceParser loadXmlResourceParser(@NonNull String file, @AnyRes int id, int assetCookie,
- @NonNull String type)
+ @NonNull String type, boolean usesFeatureFlags)
throws NotFoundException {
if (id != 0) {
try {
@@ -1355,7 +1357,8 @@ public class ResourcesImpl {
// Not in the cache, create a new block and put it at
// the next slot in the cache.
- final XmlBlock block = mAssets.openXmlBlockAsset(assetCookie, file);
+ final XmlBlock block =
+ mAssets.openXmlBlockAsset(assetCookie, file, usesFeatureFlags);
if (block != null) {
final int pos = (mLastCachedXmlBlockIndex + 1) % num;
mLastCachedXmlBlockIndex = pos;
diff --git a/core/java/android/content/res/XmlBlock.java b/core/java/android/content/res/XmlBlock.java
index 36fa05905814..b27150b7171f 100644
--- a/core/java/android/content/res/XmlBlock.java
+++ b/core/java/android/content/res/XmlBlock.java
@@ -59,12 +59,14 @@ public final class XmlBlock implements AutoCloseable {
mAssets = null;
mNative = nativeCreate(data, 0, data.length);
mStrings = new StringBlock(nativeGetStringBlock(mNative), false);
+ mUsesFeatureFlags = true;
}
public XmlBlock(byte[] data, int offset, int size) {
mAssets = null;
mNative = nativeCreate(data, offset, size);
mStrings = new StringBlock(nativeGetStringBlock(mNative), false);
+ mUsesFeatureFlags = true;
}
@Override
@@ -346,7 +348,8 @@ public final class XmlBlock implements AutoCloseable {
if (ev == ERROR_BAD_DOCUMENT) {
throw new XmlPullParserException("Corrupt XML binary file");
}
- if (useLayoutReadwrite() && ev == START_TAG) {
+
+ if (useLayoutReadwrite() && mUsesFeatureFlags && ev == START_TAG) {
AconfigFlags flags = ParsingPackageUtils.getAconfigFlags();
if (flags.skipCurrentElement(/* pkg= */ null, this)) {
int depth = 1;
@@ -678,10 +681,11 @@ public final class XmlBlock implements AutoCloseable {
* are doing! The given native object must exist for the entire lifetime
* of this newly creating XmlBlock.
*/
- XmlBlock(@Nullable AssetManager assets, long xmlBlock) {
+ XmlBlock(@Nullable AssetManager assets, long xmlBlock, boolean usesFeatureFlags) {
mAssets = assets;
mNative = xmlBlock;
mStrings = new StringBlock(nativeGetStringBlock(xmlBlock), false);
+ mUsesFeatureFlags = usesFeatureFlags;
}
private @Nullable final AssetManager mAssets;
@@ -690,6 +694,8 @@ public final class XmlBlock implements AutoCloseable {
private boolean mOpen = true;
private int mOpenCount = 1;
+ private final boolean mUsesFeatureFlags;
+
private static final native long nativeCreate(byte[] data,
int offset,
int size);
diff --git a/core/java/android/content/theming/FieldColor.java b/core/java/android/content/theming/FieldColor.java
new file mode 100644
index 000000000000..a06a54f362b5
--- /dev/null
+++ b/core/java/android/content/theming/FieldColor.java
@@ -0,0 +1,80 @@
+/*
+ * 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.content.theming;
+
+import android.annotation.ColorInt;
+import android.annotation.FlaggedApi;
+import android.graphics.Color;
+
+import androidx.annotation.Nullable;
+
+import java.util.function.BiConsumer;
+import java.util.function.Function;
+import java.util.regex.Pattern;
+
+/** @hide */
+@FlaggedApi(android.server.Flags.FLAG_ENABLE_THEME_SERVICE)
+public class FieldColor extends ThemeSettingsField<Integer, String> {
+ private static final Pattern COLOR_PATTERN = Pattern.compile("[0-9a-fA-F]{6,8}");
+
+ public FieldColor(
+ String key,
+ BiConsumer<ThemeSettingsUpdater, Integer> setter,
+ Function<ThemeSettings, Integer> getter,
+ ThemeSettings defaults
+ ) {
+ super(key, setter, getter, defaults);
+ }
+
+ @Override
+ @ColorInt
+ @Nullable
+ public Integer parse(String primitive) {
+ if (primitive == null) {
+ return null;
+ }
+ if (!COLOR_PATTERN.matcher(primitive).matches()) {
+ return null;
+ }
+
+ try {
+ return Color.valueOf(Color.parseColor("#" + primitive)).toArgb();
+ } catch (IllegalArgumentException e) {
+ return null;
+ }
+ }
+
+ @Override
+ public String serialize(@ColorInt Integer value) {
+ return Integer.toHexString(value);
+ }
+
+ @Override
+ public boolean validate(Integer value) {
+ return !value.equals(Color.TRANSPARENT);
+ }
+
+ @Override
+ public Class<Integer> getFieldType() {
+ return Integer.class;
+ }
+
+ @Override
+ public Class<String> getJsonType() {
+ return String.class;
+ }
+}
diff --git a/core/java/android/content/theming/FieldColorBoth.java b/core/java/android/content/theming/FieldColorBoth.java
new file mode 100644
index 000000000000..e4a9f7f716d8
--- /dev/null
+++ b/core/java/android/content/theming/FieldColorBoth.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content.theming;
+
+import android.annotation.FlaggedApi;
+
+import androidx.annotation.Nullable;
+
+import java.util.Objects;
+import java.util.function.BiConsumer;
+import java.util.function.Function;
+
+/** @hide */
+@FlaggedApi(android.server.Flags.FLAG_ENABLE_THEME_SERVICE)
+public class FieldColorBoth extends ThemeSettingsField<Boolean, String> {
+ public FieldColorBoth(
+ String key,
+ BiConsumer<ThemeSettingsUpdater, Boolean> setter,
+ Function<ThemeSettings, Boolean> getter,
+ ThemeSettings defaults
+ ) {
+ super(key, setter, getter, defaults);
+ }
+
+ @Override
+ @Nullable
+ public Boolean parse(String primitive) {
+ return switch (primitive) {
+ case "1" -> true;
+ case "0" -> false;
+ default -> null;
+ };
+ }
+
+ @Override
+ public String serialize(Boolean typedValue) {
+ if (typedValue) return "1";
+ return "0";
+ }
+
+ @Override
+ public boolean validate(Boolean value) {
+ Objects.requireNonNull(value);
+ return true;
+ }
+
+ @Override
+ public Class<Boolean> getFieldType() {
+ return Boolean.class;
+ }
+
+ @Override
+ public Class<String> getJsonType() {
+ return String.class;
+ }
+}
diff --git a/core/java/android/content/theming/FieldColorIndex.java b/core/java/android/content/theming/FieldColorIndex.java
new file mode 100644
index 000000000000..683568a42318
--- /dev/null
+++ b/core/java/android/content/theming/FieldColorIndex.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 android.content.theming;
+
+import android.annotation.FlaggedApi;
+
+import java.util.function.BiConsumer;
+import java.util.function.Function;
+
+/** @hide */
+@FlaggedApi(android.server.Flags.FLAG_ENABLE_THEME_SERVICE)
+public class FieldColorIndex extends ThemeSettingsField<Integer, String> {
+ public FieldColorIndex(
+ String key,
+ BiConsumer<ThemeSettingsUpdater, Integer> setter,
+ Function<ThemeSettings, Integer> getter,
+ ThemeSettings defaults
+ ) {
+ super(key, setter, getter, defaults);
+ }
+
+ @Override
+ public Integer parse(String primitive) {
+ try {
+ return Integer.parseInt(primitive);
+ } catch (NumberFormatException e) {
+ return null;
+ }
+ }
+
+ @Override
+ public String serialize(Integer typedValue) {
+ return typedValue.toString();
+ }
+
+ @Override
+ public boolean validate(Integer value) {
+ return value >= -1;
+ }
+
+ @Override
+ public Class<Integer> getFieldType() {
+ return Integer.class;
+ }
+
+ @Override
+ public Class<String> getJsonType() {
+ return String.class;
+ }
+}
diff --git a/core/java/android/content/theming/FieldColorSource.java b/core/java/android/content/theming/FieldColorSource.java
new file mode 100644
index 000000000000..1ff3aa64fda5
--- /dev/null
+++ b/core/java/android/content/theming/FieldColorSource.java
@@ -0,0 +1,76 @@
+/*
+ * 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.content.theming;
+
+import android.annotation.FlaggedApi;
+import android.annotation.StringDef;
+
+import androidx.annotation.Nullable;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.function.BiConsumer;
+import java.util.function.Function;
+
+/** @hide */
+@FlaggedApi(android.server.Flags.FLAG_ENABLE_THEME_SERVICE)
+public class FieldColorSource extends ThemeSettingsField<String, String> {
+ public FieldColorSource(
+ String key,
+ BiConsumer<ThemeSettingsUpdater, String> setter,
+ Function<ThemeSettings, String> getter,
+ ThemeSettings defaults
+ ) {
+ super(key, setter, getter, defaults);
+ }
+
+ @Override
+ @Nullable
+ @Type
+ public String parse(String primitive) {
+ return primitive;
+ }
+
+ @Override
+ public String serialize(@Type String typedValue) {
+ return typedValue;
+ }
+
+ @Override
+ public boolean validate(String value) {
+ return switch (value) {
+ case "preset", "home_wallpaper", "lock_wallpaper" -> true;
+ default -> false;
+ };
+ }
+
+ @Override
+ public Class<String> getFieldType() {
+ return String.class;
+ }
+
+ @Override
+ public Class<String> getJsonType() {
+ return String.class;
+ }
+
+
+ @StringDef({"preset", "home_wallpaper", "lock_wallpaper"})
+ @Retention(RetentionPolicy.SOURCE)
+ @interface Type {
+ }
+}
diff --git a/core/java/android/content/theming/FieldThemeStyle.java b/core/java/android/content/theming/FieldThemeStyle.java
new file mode 100644
index 000000000000..b433e5b96ec3
--- /dev/null
+++ b/core/java/android/content/theming/FieldThemeStyle.java
@@ -0,0 +1,76 @@
+/*
+ * 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.content.theming;
+
+import android.annotation.FlaggedApi;
+import android.annotation.Nullable;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.function.BiConsumer;
+import java.util.function.Function;
+
+/** @hide */
+@FlaggedApi(android.server.Flags.FLAG_ENABLE_THEME_SERVICE)
+public class FieldThemeStyle extends ThemeSettingsField<Integer, String> {
+ public FieldThemeStyle(
+ String key,
+ BiConsumer<ThemeSettingsUpdater, Integer> setter,
+ Function<ThemeSettings, Integer> getter,
+ ThemeSettings defaults
+ ) {
+ super(key, setter, getter, defaults);
+ }
+
+ private static final @ThemeStyle.Type List<Integer> sValidStyles = Arrays.asList(
+ ThemeStyle.EXPRESSIVE,
+ ThemeStyle.SPRITZ,
+ ThemeStyle.TONAL_SPOT, ThemeStyle.FRUIT_SALAD, ThemeStyle.RAINBOW,
+ ThemeStyle.VIBRANT,
+ ThemeStyle.MONOCHROMATIC);
+
+ @Override
+ public String serialize(@ThemeStyle.Type Integer typedValue) {
+ return ThemeStyle.toString(typedValue);
+ }
+
+ @Override
+ public boolean validate(Integer value) {
+ return sValidStyles.contains(value);
+ }
+
+ @Override
+ @Nullable
+ @ThemeStyle.Type
+ public Integer parse(String primitive) {
+ try {
+ return ThemeStyle.valueOf(primitive);
+ } catch (Exception e) {
+ return null;
+ }
+ }
+
+ @Override
+ public Class<Integer> getFieldType() {
+ return Integer.class;
+ }
+
+ @Override
+ public Class<String> getJsonType() {
+ return String.class;
+ }
+}
diff --git a/core/java/android/content/theming/ThemeSettings.java b/core/java/android/content/theming/ThemeSettings.java
new file mode 100644
index 000000000000..e94c1fef5382
--- /dev/null
+++ b/core/java/android/content/theming/ThemeSettings.java
@@ -0,0 +1,200 @@
+/*
+ * 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.content.theming;
+
+import android.annotation.ColorInt;
+import android.annotation.FlaggedApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import androidx.annotation.NonNull;
+
+import java.util.Objects;
+
+/**
+ * Represents the theme settings for the system.
+ * This class holds various properties related to theming, such as color indices, palettes,
+ * accent colors, color sources, theme styles, and color combinations.
+ *
+ * @hide
+ */
+@FlaggedApi(android.server.Flags.FLAG_ENABLE_THEME_SERVICE)
+public final class ThemeSettings implements Parcelable {
+ private final int mColorIndex;
+ private final int mSystemPalette;
+ private final int mAccentColor;
+ @NonNull
+ private final String mColorSource;
+ private final int mThemeStyle;
+ private final boolean mColorBoth;
+
+ /**
+ * Constructs a new ThemeSettings object.
+ *
+ * @param colorIndex The color index.
+ * @param systemPalette The system palette color.
+ * @param accentColor The accent color.
+ * @param colorSource The color source.
+ * @param themeStyle The theme style.
+ * @param colorBoth The color combination.
+ */
+
+ public ThemeSettings(int colorIndex, @ColorInt int systemPalette,
+ @ColorInt int accentColor, @NonNull String colorSource, int themeStyle,
+ boolean colorBoth) {
+
+ this.mAccentColor = accentColor;
+ this.mColorBoth = colorBoth;
+ this.mColorIndex = colorIndex;
+ this.mColorSource = colorSource;
+ this.mSystemPalette = systemPalette;
+ this.mThemeStyle = themeStyle;
+ }
+
+ /**
+ * Constructs a ThemeSettings object from a Parcel.
+ *
+ * @param in The Parcel to read from.
+ */
+ ThemeSettings(Parcel in) {
+ this.mAccentColor = in.readInt();
+ this.mColorBoth = in.readBoolean();
+ this.mColorIndex = in.readInt();
+ this.mColorSource = Objects.requireNonNullElse(in.readString8(), "s");
+ this.mSystemPalette = in.readInt();
+ this.mThemeStyle = in.readInt();
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeInt(mAccentColor);
+ dest.writeBoolean(mColorBoth);
+ dest.writeInt(mColorIndex);
+ dest.writeString8(mColorSource);
+ dest.writeInt(mSystemPalette);
+ dest.writeInt(mThemeStyle);
+ }
+
+ /**
+ * Gets the color index.
+ *
+ * @return The color index.
+ */
+ public Integer colorIndex() {
+ return mColorIndex;
+ }
+
+ /**
+ * Gets the system palette color.
+ *
+ * @return The system palette color.
+ */
+ @ColorInt
+ public Integer systemPalette() {
+ return mSystemPalette;
+ }
+
+ /**
+ * Gets the accent color.
+ *
+ * @return The accent color.
+ */
+ @ColorInt
+ public Integer accentColor() {
+ return mAccentColor;
+ }
+
+ /**
+ * Gets the color source.
+ *
+ * @return The color source.
+ */
+ @FieldColorSource.Type
+ public String colorSource() {
+ return mColorSource;
+ }
+
+ /**
+ * Gets the theme style.
+ *
+ * @return The theme style.
+ */
+ @ThemeStyle.Type
+ public Integer themeStyle() {
+ return mThemeStyle;
+ }
+
+ /**
+ * Gets the color combination.
+ *
+ * @return The color combination.
+ */
+ public Boolean colorBoth() {
+ return mColorBoth;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+
+ return obj instanceof ThemeSettings other
+ && mColorIndex == other.mColorIndex
+ && mSystemPalette == other.mSystemPalette
+ && mAccentColor == other.mAccentColor
+ && mColorSource.equals(other.mColorSource)
+ && mThemeStyle == other.mThemeStyle
+ && mColorBoth == other.mColorBoth;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mColorIndex, mSystemPalette, mAccentColor, mColorSource, mThemeStyle,
+ mColorBoth);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ /**
+ * Creator for Parcelable interface.
+ */
+ public static final Creator<ThemeSettings> CREATOR = new Creator<>() {
+ @Override
+ public ThemeSettings createFromParcel(Parcel in) {
+ return new ThemeSettings(in);
+ }
+
+ @Override
+ public ThemeSettings[] newArray(int size) {
+ return new ThemeSettings[size];
+ }
+ };
+
+ /**
+ * Creates a new {@link ThemeSettingsUpdater} instance for updating the {@link ThemeSettings}
+ * through the API.
+ *
+ * @return A new {@link ThemeSettingsUpdater} instance.
+ */
+ public static ThemeSettingsUpdater updater() {
+ return new ThemeSettingsUpdater();
+ }
+}
diff --git a/core/java/android/content/theming/ThemeSettingsField.java b/core/java/android/content/theming/ThemeSettingsField.java
new file mode 100644
index 000000000000..1696df4ad0f6
--- /dev/null
+++ b/core/java/android/content/theming/ThemeSettingsField.java
@@ -0,0 +1,287 @@
+/*
+ * 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.content.theming;
+
+
+import android.annotation.FlaggedApi;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+
+import com.android.internal.util.Preconditions;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.function.BiConsumer;
+import java.util.function.Function;
+
+/**
+ * Represents a field within {@link ThemeSettings}, providing methods for parsing, serializing,
+ * managing default values, and validating the field's value.
+ * <p>
+ * This class is designed to be extended by concrete classes that represent specific fields within
+ * {@link ThemeSettings}. Each subclass should define the following methods, where T is the type of
+ * the field's value and J is the type of the field's value stored in JSON:
+ * <ul>
+ * <li>{@link #parse(Object)} to parse a JSON representation into the field's value type.</li>
+ * <li>{@link #serialize(Object)} to serialize the field's value into a JSON representation.</li>
+ * <li>{@link #validate(Object)} to validate the field's value.</li>
+ * <li>{@link #getFieldType()} to return the type of the field's value.</li>
+ * <li>{@link #getJsonType()} to return the type of the field's value stored in JSON.</li>
+ * </ul>
+ * <p>
+ * The {@link #fromJSON(JSONObject, ThemeSettingsUpdater)} and
+ * {@link #toJSON(ThemeSettings, JSONObject)}
+ * methods handle the extraction and serialization of the field's value to and from JSON objects
+ * respectively. The {@link #fallbackParse(Object, Object)} method is used to parse a string
+ * representation of the field's value, falling back to a default value if parsing fails.
+ *
+ * @param <T> The type of the field's value.
+ * @param <J> The type of the JSON property.
+ * @hide
+ */
+@FlaggedApi(android.server.Flags.FLAG_ENABLE_THEME_SERVICE)
+public abstract class ThemeSettingsField<T, J> {
+ private static final String TAG = ThemeSettingsField.class.getSimpleName();
+
+ private static final String KEY_PREFIX = "android.theme.customization.";
+ public static final String OVERLAY_CATEGORY_ACCENT_COLOR = KEY_PREFIX + "accent_color";
+ public static final String OVERLAY_CATEGORY_SYSTEM_PALETTE = KEY_PREFIX + "system_palette";
+ public static final String OVERLAY_CATEGORY_THEME_STYLE = KEY_PREFIX + "theme_style";
+ public static final String OVERLAY_COLOR_SOURCE = KEY_PREFIX + "color_source";
+ public static final String OVERLAY_COLOR_INDEX = KEY_PREFIX + "color_index";
+ public static final String OVERLAY_COLOR_BOTH = KEY_PREFIX + "color_both";
+
+
+ /**
+ * Returns an array of all available {@link ThemeSettingsField} instances.
+ *
+ * @param defaults The default {@link ThemeSettings} object to use for default values.
+ * @return An array of {@link ThemeSettingsField} instances.
+ */
+ public static ThemeSettingsField<?, ?>[] getFields(ThemeSettings defaults) {
+ return new ThemeSettingsField[]{
+ new FieldColorIndex(
+ OVERLAY_COLOR_INDEX,
+ ThemeSettingsUpdater::colorIndex,
+ ThemeSettings::colorIndex,
+ defaults),
+ new FieldColor(
+ OVERLAY_CATEGORY_SYSTEM_PALETTE,
+ ThemeSettingsUpdater::systemPalette,
+ ThemeSettings::systemPalette,
+ defaults),
+ new FieldColor(
+ OVERLAY_CATEGORY_ACCENT_COLOR,
+ ThemeSettingsUpdater::accentColor,
+ ThemeSettings::accentColor,
+ defaults),
+ new FieldColorSource(
+ OVERLAY_COLOR_SOURCE,
+ ThemeSettingsUpdater::colorSource,
+ ThemeSettings::colorSource,
+ defaults),
+ new FieldThemeStyle(
+ OVERLAY_CATEGORY_THEME_STYLE,
+ ThemeSettingsUpdater::themeStyle,
+ ThemeSettings::themeStyle,
+ defaults),
+ new FieldColorBoth(
+ OVERLAY_COLOR_BOTH,
+ ThemeSettingsUpdater::colorBoth,
+ ThemeSettings::colorBoth,
+ defaults)
+ };
+ }
+
+ public final String key;
+ private final BiConsumer<ThemeSettingsUpdater, T> mSetter;
+ private final Function<ThemeSettings, T> mGetter;
+ private final ThemeSettings mDefaults;
+
+ /**
+ * Creates a new {@link ThemeSettingsField}.
+ *
+ * @param key The key to identify the field in JSON objects.
+ * @param setter The setter to update the field's value in a {@link ThemeSettingsUpdater}.
+ * @param getter The getter to retrieve the field's value from a {@link ThemeSettings}
+ * object.
+ * @param defaults The default {@link ThemeSettings} object to provide default values.
+ */
+
+ public ThemeSettingsField(
+ String key,
+ BiConsumer<ThemeSettingsUpdater, T> setter,
+ Function<ThemeSettings, T> getter,
+ ThemeSettings defaults
+ ) {
+ this.key = key;
+ mSetter = setter;
+ mGetter = getter;
+ mDefaults = defaults;
+ }
+
+ /**
+ * Attempts to parse a JSON primitive representation of the field's value. If parsing fails, it
+ * defaults to the field's default value.
+ *
+ * @param primitive The string representation to parse.
+ */
+ private T fallbackParse(Object primitive, T fallbackValue) {
+ if (primitive == null) {
+ Log.w(TAG, "Error, field `" + key + "` was not found, defaulting to " + fallbackValue);
+ return fallbackValue;
+ }
+
+ if (!getJsonType().isInstance(primitive)) {
+ Log.w(TAG, "Error, field `" + key + "` expected to be of type `"
+ + getJsonType().getSimpleName()
+ + "`, got `" + primitive.getClass().getSimpleName() + "`, defaulting to "
+ + fallbackValue);
+ return fallbackValue;
+ }
+
+ // skips parsing if destination json type is already the same as field type
+ T parsedValue = getFieldType() == getJsonType() ? (T) primitive : parse((J) primitive);
+
+ if (parsedValue == null) {
+ Log.w(TAG, "Error parsing JSON field `" + key + "` , defaulting to " + fallbackValue);
+ return fallbackValue;
+ }
+
+ if (!validate(parsedValue)) {
+ Log.w(TAG,
+ "Error validating JSON field `" + key + "` , defaulting to " + fallbackValue);
+ return fallbackValue;
+ }
+
+ if (parsedValue.getClass() != getFieldType()) {
+ Log.w(TAG, "Error: JSON field `" + key + "` expected to be of type `"
+ + getFieldType().getSimpleName()
+ + "`, defaulting to " + fallbackValue);
+ return fallbackValue;
+ }
+
+ return parsedValue;
+ }
+
+
+ /**
+ * Extracts the field's value from a JSON object and sets it in a
+ * {@link ThemeSettingsUpdater}.
+ *
+ * @param source The JSON object containing the field's value.
+ */
+ public void fromJSON(JSONObject source, ThemeSettingsUpdater updater) {
+ Object primitiveStr = source.opt(key);
+ T typedValue = fallbackParse(primitiveStr, getDefaultValue());
+ mSetter.accept(updater, typedValue);
+ }
+
+ /**
+ * Serializes the field's value from a {@link ThemeSettings} object into a JSON object.
+ *
+ * @param source The {@link ThemeSettings} object from which to retrieve the field's
+ * value.
+ * @param destination The JSON object to which the field's value will be added.
+ */
+ public void toJSON(ThemeSettings source, JSONObject destination) {
+ T value = mGetter.apply(source);
+ Preconditions.checkState(value.getClass() == getFieldType());
+
+ J serialized;
+ if (validate(value)) {
+ serialized = serialize(value);
+ } else {
+ T fallbackValue = getDefaultValue();
+ serialized = serialize(fallbackValue);
+ Log.w(TAG, "Invalid value `" + value + "` for key `" + key + "`, defaulting to '"
+ + fallbackValue);
+ }
+
+ try {
+ destination.put(key, serialized);
+ } catch (JSONException e) {
+ Log.d(TAG,
+ "Error writing JSON primitive, skipping field " + key + ", " + e.getMessage());
+ }
+ }
+
+
+ /**
+ * Returns the default value of the field.
+ *
+ * @return The default value.
+ */
+ @VisibleForTesting
+ @NonNull
+ public T getDefaultValue() {
+ return mGetter.apply(mDefaults);
+ }
+
+ /**
+ * Parses a string representation into the field's value type.
+ *
+ * @param primitive The string representation to parse.
+ * @return The parsed value, or null if parsing fails.
+ */
+ @VisibleForTesting
+ @Nullable
+ public abstract T parse(J primitive);
+
+ /**
+ * Serializes the field's value into a primitive type suitable for JSON.
+ *
+ * @param value The value to serialize.
+ * @return The serialized value.
+ */
+ @VisibleForTesting
+ public abstract J serialize(T value);
+
+ /**
+ * Validates the field's value.
+ * This method can be overridden to perform custom validation logic and MUST NOT validate for
+ * nullity.
+ *
+ * @param value The value to validate.
+ * @return {@code true} if the value is valid, {@code false} otherwise.
+ */
+ @VisibleForTesting
+ public abstract boolean validate(T value);
+
+ /**
+ * Returns the type of the field's value.
+ *
+ * @return The type of the field's value.
+ */
+ @VisibleForTesting
+ public abstract Class<T> getFieldType();
+
+ /**
+ * Returns the type of the field's value stored in JSON.
+ *
+ * <p>This method is used to determine the expected type of the field's value when it is
+ * stored in a JSON object.
+ *
+ * @return The type of the field's value stored in JSON.
+ */
+ @VisibleForTesting
+ public abstract Class<J> getJsonType();
+}
diff --git a/core/java/android/content/theming/ThemeSettingsUpdater.java b/core/java/android/content/theming/ThemeSettingsUpdater.java
new file mode 100644
index 000000000000..acd7d356db69
--- /dev/null
+++ b/core/java/android/content/theming/ThemeSettingsUpdater.java
@@ -0,0 +1,244 @@
+/*
+ * 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.content.theming;
+
+
+import android.annotation.ColorInt;
+import android.annotation.FlaggedApi;
+import android.annotation.NonNull;
+import android.annotation.SuppressLint;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import androidx.annotation.VisibleForTesting;
+
+import java.util.Objects;
+
+/**
+ * Updater class for constructing {@link ThemeSettings} objects.
+ * This class provides a fluent interface for setting the various properties of the theme
+ * settings.
+ *
+ * @hide
+ */
+@FlaggedApi(android.server.Flags.FLAG_ENABLE_THEME_SERVICE)
+public class ThemeSettingsUpdater implements Parcelable {
+ @ColorInt
+ private Integer mAccentColor;
+ private Boolean mColorBoth;
+ private Integer mColorIndex;
+ private String mColorSource;
+ @ColorInt
+ private Integer mSystemPalette;
+ private Integer mThemeStyle;
+
+ ThemeSettingsUpdater(Integer colorIndex, @ColorInt Integer systemPalette,
+ @ColorInt Integer accentColor, @FieldColorSource.Type String colorSource,
+ @ThemeStyle.Type Integer themeStyle, Boolean colorBoth) {
+ this.mAccentColor = accentColor;
+ this.mColorBoth = colorBoth;
+ this.mColorIndex = colorIndex;
+ this.mColorSource = colorSource;
+ this.mSystemPalette = systemPalette;
+ this.mThemeStyle = themeStyle;
+ }
+
+ ThemeSettingsUpdater() {
+ }
+
+ // only reading basic JVM types for nullability
+ @SuppressLint("ParcelClassLoader")
+ protected ThemeSettingsUpdater(Parcel in) {
+ mAccentColor = (Integer) in.readValue(null);
+ mColorBoth = (Boolean) in.readValue(null);
+ mColorIndex = (Integer) in.readValue(null);
+ mColorSource = (String) in.readValue(null);
+ mSystemPalette = (Integer) in.readValue(null);
+ mThemeStyle = (Integer) in.readValue(null);
+ }
+
+ // using read/writeValue for nullability support
+ @SuppressWarnings("AndroidFrameworkEfficientParcelable")
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeValue(mAccentColor);
+ dest.writeValue(mColorBoth);
+ dest.writeValue(mColorIndex);
+ dest.writeValue(mColorSource);
+ dest.writeValue(mSystemPalette);
+ dest.writeValue(mThemeStyle);
+ }
+
+ /**
+ * Sets the color index.
+ *
+ * @param colorIndex The color index to set.
+ * @return This {@link ThemeSettingsUpdater} instance.
+ */
+ public ThemeSettingsUpdater colorIndex(int colorIndex) {
+ this.mColorIndex = colorIndex;
+ return this;
+ }
+
+ /**
+ * Returns the color index.
+ *
+ * @return The color index.
+ */
+ @VisibleForTesting
+ public Integer getColorIndex() {
+ return mColorIndex;
+ }
+
+ /**
+ * Sets the system palette color.
+ *
+ * @param systemPalette The system palette color to set.
+ * @return This {@link ThemeSettingsUpdater} instance.
+ */
+ public ThemeSettingsUpdater systemPalette(@ColorInt int systemPalette) {
+ this.mSystemPalette = systemPalette;
+ return this;
+ }
+
+ /**
+ * Returns the system palette color.
+ *
+ * @return The system palette color.
+ */
+ @VisibleForTesting
+ public Integer getSystemPalette() {
+ return mSystemPalette;
+ }
+
+ /**
+ * Sets the accent color.
+ *
+ * @param accentColor The accent color to set.
+ * @return This {@link ThemeSettingsUpdater} instance.
+ */
+ public ThemeSettingsUpdater accentColor(@ColorInt int accentColor) {
+ this.mAccentColor = accentColor;
+ return this;
+ }
+
+ /**
+ * Returns the accent color.
+ *
+ * @return The accent color.
+ */
+ @VisibleForTesting
+ public Integer getAccentColor() {
+ return mAccentColor;
+ }
+
+ /**
+ * Sets the color source.
+ *
+ * @param colorSource The color source to set.
+ * @return This {@link ThemeSettingsUpdater} instance.
+ */
+ public ThemeSettingsUpdater colorSource(@NonNull @FieldColorSource.Type String colorSource) {
+ this.mColorSource = colorSource;
+ return this;
+ }
+
+ /**
+ * Returns the theme style.
+ *
+ * @return The theme style.
+ */
+ @VisibleForTesting
+ public Integer getThemeStyle() {
+ return mThemeStyle;
+ }
+
+ /**
+ * Sets the theme style.
+ *
+ * @param themeStyle The theme style to set.
+ * @return This {@link ThemeSettingsUpdater} instance.
+ */
+ public ThemeSettingsUpdater themeStyle(@ThemeStyle.Type int themeStyle) {
+ this.mThemeStyle = themeStyle;
+ return this;
+ }
+
+ /**
+ * Returns the color source.
+ *
+ * @return The color source.
+ */
+ @VisibleForTesting
+ public String getColorSource() {
+ return mColorSource;
+ }
+
+ /**
+ * Sets the color combination.
+ *
+ * @param colorBoth The color combination to set.
+ * @return This {@link ThemeSettingsUpdater} instance.
+ */
+ public ThemeSettingsUpdater colorBoth(boolean colorBoth) {
+ this.mColorBoth = colorBoth;
+ return this;
+ }
+
+ /**
+ * Returns the color combination.
+ *
+ * @return The color combination.
+ */
+ @VisibleForTesting
+ public Boolean getColorBoth() {
+ return mColorBoth;
+ }
+
+ /**
+ * Constructs a new {@link ThemeSettings} object with the current builder settings.
+ *
+ * @return A new {@link ThemeSettings} object.
+ */
+ public ThemeSettings toThemeSettings(@NonNull ThemeSettings defaults) {
+ return new ThemeSettings(
+ Objects.requireNonNullElse(mColorIndex, defaults.colorIndex()),
+ Objects.requireNonNullElse(mSystemPalette, defaults.systemPalette()),
+ Objects.requireNonNullElse(mAccentColor, defaults.accentColor()),
+ Objects.requireNonNullElse(mColorSource, defaults.colorSource()),
+ Objects.requireNonNullElse(mThemeStyle, defaults.themeStyle()),
+ Objects.requireNonNullElse(mColorBoth, defaults.colorBoth()));
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ public static final Creator<ThemeSettingsUpdater> CREATOR =
+ new Creator<>() {
+ @Override
+ public ThemeSettingsUpdater createFromParcel(Parcel in) {
+ return new ThemeSettingsUpdater(in);
+ }
+
+ @Override
+ public ThemeSettingsUpdater[] newArray(int size) {
+ return new ThemeSettingsUpdater[size];
+ }
+ };
+}
diff --git a/core/java/android/content/theming/ThemeStyle.java b/core/java/android/content/theming/ThemeStyle.java
new file mode 100644
index 000000000000..607896405020
--- /dev/null
+++ b/core/java/android/content/theming/ThemeStyle.java
@@ -0,0 +1,180 @@
+/*
+ * 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.content.theming;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * A class defining the different styles available for theming.
+ * This class replaces the previous enum implementation for improved performance and compatibility.
+ *
+ * @hide
+ */
+public final class ThemeStyle {
+
+ private ThemeStyle() {
+ }
+
+ /**
+ * @hide
+ */
+ @IntDef({
+ SPRITZ,
+ TONAL_SPOT,
+ VIBRANT,
+ EXPRESSIVE,
+ RAINBOW,
+ FRUIT_SALAD,
+ CONTENT,
+ MONOCHROMATIC,
+ CLOCK,
+ CLOCK_VIBRANT
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface Type {
+ }
+
+ /**
+ * Represents the SPRITZ style.
+ */
+ public static final int SPRITZ = 0;
+ /**
+ * Represents the TONAL_SPOT style.
+ */
+ public static final int TONAL_SPOT = 1;
+ /**
+ * Represents the VIBRANT style.
+ */
+ public static final int VIBRANT = 2;
+ /**
+ * Represents the EXPRESSIVE style.
+ */
+ public static final int EXPRESSIVE = 3;
+ /**
+ * Represents the RAINBOW style.
+ */
+ public static final int RAINBOW = 4;
+ /**
+ * Represents the FRUIT_SALAD style.
+ */
+ public static final int FRUIT_SALAD = 5;
+ /**
+ * Represents the CONTENT style.
+ */
+ public static final int CONTENT = 6;
+ /**
+ * Represents the MONOCHROMATIC style.
+ */
+ public static final int MONOCHROMATIC = 7;
+ /**
+ * Represents the CLOCK style.
+ */
+ public static final int CLOCK = 8;
+ /**
+ * Represents the CLOCK_VIBRANT style.
+ */
+ public static final int CLOCK_VIBRANT = 9;
+
+
+ /**
+ * Returns the string representation of the given style.
+ *
+ * @param style The style value.
+ * @return The string representation of the style.
+ * @throws IllegalArgumentException if the style value is invalid.
+ */
+ @NonNull
+ public static String toString(@Nullable @Type Integer style) {
+ // Throw an exception if style is null
+ if (style == null) {
+ throw new IllegalArgumentException("Invalid style value: null");
+ }
+
+ return switch (style) {
+ case SPRITZ -> "SPRITZ";
+ case TONAL_SPOT -> "TONAL_SPOT";
+ case VIBRANT -> "VIBRANT";
+ case EXPRESSIVE -> "EXPRESSIVE";
+ case RAINBOW -> "RAINBOW";
+ case FRUIT_SALAD -> "FRUIT_SALAD";
+ case CONTENT -> "CONTENT";
+ case MONOCHROMATIC -> "MONOCHROMATIC";
+ case CLOCK -> "CLOCK";
+ case CLOCK_VIBRANT -> "CLOCK_VIBRANT";
+ default -> throw new IllegalArgumentException("Invalid style value: " + style);
+ };
+ }
+
+ /**
+ * Returns the style value corresponding to the given style name.
+ *
+ * @param styleName The name of the style.
+ * @return The style value.
+ * @throws IllegalArgumentException if the style name is invalid.
+ */
+ public static @Type int valueOf(@Nullable String styleName) {
+ return switch (styleName) {
+ case "SPRITZ" -> SPRITZ;
+ case "TONAL_SPOT" -> TONAL_SPOT;
+ case "VIBRANT" -> VIBRANT;
+ case "EXPRESSIVE" -> EXPRESSIVE;
+ case "RAINBOW" -> RAINBOW;
+ case "FRUIT_SALAD" -> FRUIT_SALAD;
+ case "CONTENT" -> CONTENT;
+ case "MONOCHROMATIC" -> MONOCHROMATIC;
+ case "CLOCK" -> CLOCK;
+ case "CLOCK_VIBRANT" -> CLOCK_VIBRANT;
+ default -> throw new IllegalArgumentException("Invalid style name: " + styleName);
+ };
+ }
+
+ /**
+ * Returns the name of the given style. This method is equivalent to {@link #toString(int)}.
+ *
+ * @param style The style value.
+ * @return The name of the style.
+ */
+ @NonNull
+ public static String name(@Type int style) {
+ return toString(style);
+ }
+
+ /**
+ * Returns an array containing all the style values.
+ *
+ * @return An array of all style values.
+ */
+ public static int[] values() {
+ return new int[]{
+ SPRITZ,
+ TONAL_SPOT,
+ VIBRANT,
+ EXPRESSIVE,
+ RAINBOW,
+ FRUIT_SALAD,
+ CONTENT,
+ MONOCHROMATIC,
+ CLOCK,
+ CLOCK_VIBRANT
+ };
+ }
+}
diff --git a/core/java/android/hardware/display/DisplayManagerGlobal.java b/core/java/android/hardware/display/DisplayManagerGlobal.java
index bebca57125b6..42df43e4d436 100644
--- a/core/java/android/hardware/display/DisplayManagerGlobal.java
+++ b/core/java/android/hardware/display/DisplayManagerGlobal.java
@@ -475,6 +475,7 @@ public final class DisplayManagerGlobal {
synchronized (mLock) {
if (!mShouldImplicitlyRegisterRrChanges) {
mShouldImplicitlyRegisterRrChanges = true;
+ Slog.i(TAG, "Implicitly registering for refresh rate");
updateCallbackIfNeededLocked();
}
}
@@ -1759,6 +1760,9 @@ public final class DisplayManagerGlobal {
synchronized (mLock) {
mDispatchNativeCallbacks = true;
if (Flags.delayImplicitRrRegistrationUntilRrAccessed()) {
+ if (!mShouldImplicitlyRegisterRrChanges) {
+ Slog.i(TAG, "Choreographer implicitly registered for the refresh rate.");
+ }
mShouldImplicitlyRegisterRrChanges = true;
}
registerCallbackIfNeededLocked();
diff --git a/core/java/android/hardware/input/KeyGestureEvent.java b/core/java/android/hardware/input/KeyGestureEvent.java
index 9dd1fed4a85a..1249af7cc595 100644
--- a/core/java/android/hardware/input/KeyGestureEvent.java
+++ b/core/java/android/hardware/input/KeyGestureEvent.java
@@ -72,7 +72,8 @@ public final class KeyGestureEvent {
public static final int KEY_GESTURE_TYPE_ALL_APPS = 21;
public static final int KEY_GESTURE_TYPE_LAUNCH_SEARCH = 22;
public static final int KEY_GESTURE_TYPE_LANGUAGE_SWITCH = 23;
- public static final int KEY_GESTURE_TYPE_ACCESSIBILITY_ALL_APPS = 24;
+ @Deprecated
+ public static final int DEPRECATED_KEY_GESTURE_TYPE_ACCESSIBILITY_ALL_APPS = 24;
public static final int KEY_GESTURE_TYPE_TOGGLE_CAPS_LOCK = 25;
public static final int KEY_GESTURE_TYPE_SYSTEM_MUTE = 26;
public static final int KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_LEFT = 27;
@@ -167,7 +168,6 @@ public final class KeyGestureEvent {
KEY_GESTURE_TYPE_ALL_APPS,
KEY_GESTURE_TYPE_LAUNCH_SEARCH,
KEY_GESTURE_TYPE_LANGUAGE_SWITCH,
- KEY_GESTURE_TYPE_ACCESSIBILITY_ALL_APPS,
KEY_GESTURE_TYPE_TOGGLE_CAPS_LOCK,
KEY_GESTURE_TYPE_SYSTEM_MUTE,
KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_LEFT,
@@ -525,8 +525,6 @@ public final class KeyGestureEvent {
return FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__LAUNCH_SEARCH;
case KEY_GESTURE_TYPE_LANGUAGE_SWITCH:
return FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__LANGUAGE_SWITCH;
- case KEY_GESTURE_TYPE_ACCESSIBILITY_ALL_APPS:
- return FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__ACCESSIBILITY_ALL_APPS;
case KEY_GESTURE_TYPE_TOGGLE_CAPS_LOCK:
return FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__TOGGLE_CAPS_LOCK;
case KEY_GESTURE_TYPE_SYSTEM_MUTE:
@@ -707,8 +705,6 @@ public final class KeyGestureEvent {
return "KEY_GESTURE_TYPE_LAUNCH_SEARCH";
case KEY_GESTURE_TYPE_LANGUAGE_SWITCH:
return "KEY_GESTURE_TYPE_LANGUAGE_SWITCH";
- case KEY_GESTURE_TYPE_ACCESSIBILITY_ALL_APPS:
- return "KEY_GESTURE_TYPE_ACCESSIBILITY_ALL_APPS";
case KEY_GESTURE_TYPE_TOGGLE_CAPS_LOCK:
return "KEY_GESTURE_TYPE_TOGGLE_CAPS_LOCK";
case KEY_GESTURE_TYPE_SYSTEM_MUTE:
diff --git a/core/java/android/inputmethodservice/InputMethodService.java b/core/java/android/inputmethodservice/InputMethodService.java
index 84d96bd1e155..3d6da5452ad2 100644
--- a/core/java/android/inputmethodservice/InputMethodService.java
+++ b/core/java/android/inputmethodservice/InputMethodService.java
@@ -435,6 +435,11 @@ public class InputMethodService extends AbstractInputMethodService {
}
/**
+ * Cached value of {@link #canImeRenderGesturalNavButtons}, as it doesn't change at runtime.
+ */
+ private final boolean mCanImeRenderGesturalNavButtons = canImeRenderGesturalNavButtons();
+
+ /**
* Allows the system to optimize the back button affordance based on the presence of software
* keyboard.
*
@@ -564,6 +569,9 @@ public class InputMethodService extends AbstractInputMethodService {
private final NavigationBarController mNavigationBarController =
new NavigationBarController(this);
+ /** Whether a custom IME Switcher button was requested to be visible. */
+ private boolean mCustomImeSwitcherButtonRequestedVisible;
+
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
int mTheme = 0;
@@ -783,7 +791,7 @@ public class InputMethodService extends AbstractInputMethodService {
Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "IMS.initializeInternal");
mPrivOps.set(params.privilegedOperations);
InputMethodPrivilegedOperationsRegistry.put(params.token, mPrivOps);
- mNavigationBarController.onNavButtonFlagsChanged(params.navigationBarFlags);
+ onNavButtonFlagsChanged(params.navigationBarFlags);
attachToken(params.token);
Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
}
@@ -893,7 +901,7 @@ public class InputMethodService extends AbstractInputMethodService {
public final void dispatchStartInput(@Nullable InputConnection inputConnection,
@NonNull IInputMethod.StartInputParams params) {
mPrivOps.reportStartInputAsync(params.startInputToken);
- mNavigationBarController.onNavButtonFlagsChanged(params.navigationBarFlags);
+ onNavButtonFlagsChanged(params.navigationBarFlags);
if (params.restarting) {
restartInput(inputConnection, params.editorInfo);
} else {
@@ -918,6 +926,20 @@ public class InputMethodService extends AbstractInputMethodService {
@Override
public void onNavButtonFlagsChanged(@InputMethodNavButtonFlags int navButtonFlags) {
mNavigationBarController.onNavButtonFlagsChanged(navButtonFlags);
+ if (!mCanImeRenderGesturalNavButtons) {
+ final boolean showImeSwitcher = (navButtonFlags
+ & InputMethodNavButtonFlags.SHOW_IME_SWITCHER_WHEN_IME_IS_SHOWN) != 0;
+ // The IME cannot draw the IME nav bar, so this will never be visible. In this case
+ // the system nav bar hosts the IME buttons.
+ // The system nav bar will be hidden when the IME is shown and the config is set.
+ final boolean navBarNotVisible = getApplicationContext().getResources()
+ .getBoolean(com.android.internal.R.bool.config_hideNavBarForKeyboard);
+ final boolean visible = showImeSwitcher && navBarNotVisible;
+ if (visible != mCustomImeSwitcherButtonRequestedVisible) {
+ mCustomImeSwitcherButtonRequestedVisible = visible;
+ onCustomImeSwitcherButtonRequestedVisible(visible);
+ }
+ }
}
/**
@@ -4473,28 +4495,27 @@ public class InputMethodService extends AbstractInputMethodService {
/**
* Called when the requested visibility of a custom IME Switcher button changes.
*
- * <p>When the system provides an IME navigation bar, it may decide to show an IME Switcher
- * button inside this bar. However, the IME can request hiding the bar provided by the system
- * with {@code getWindowInsetsController().hide(captionBar())} (the IME navigation bar provides
- * {@link Type#captionBar() captionBar} insets to the IME window). If the request is successful,
- * then it becomes the IME's responsibility to provide a custom IME Switcher button in its
- * input view, with equivalent functionality.</p>
+ * <p>When this method is called with {@code true} by the system, the IME must show a button
+ * within its UI to switch IMEs. When it is called with {@code false}, it must hide this button.
+ *
+ * <p>Normally, the system provides a button for switching to a different IME when that is
+ * appropriate. Under certain circumstances, namely when the IME successfully asks to hide the
+ * system-provided navigation bar (with {@code getWindowInsetsController().hide(captionBar())}),
+ * providing this button is delegated to the IME through this callback.
*
- * <p>This custom button is only requested to be visible when the system provides the IME
- * navigation bar, both the bar and the IME Switcher button inside it should be visible,
- * but the IME successfully requested to hide the bar. This does not depend on the current
- * visibility of the IME. It could be called with {@code true} while the IME is hidden, in
- * which case the IME should prepare to show the button as soon as the IME itself is shown.</p>
+ * <p>This does not depend on the current visibility of the IME. It could be called with
+ * {@code true} while the IME is hidden, in which case the IME should prepare to show the button
+ * as soon as the IME itself is shown.
*
* <p>This is only called when the requested visibility changes. The default value is
* {@code false} and as such, this will not be called initially if the resulting value is
- * {@code false}.</p>
+ * {@code false}.
*
* <p>This can be called at any time after {@link #onCreate}, even if the IME is not currently
- * visible. However, this is not guaranteed to be called before the IME is shown, as it depends
- * on when the IME requested hiding the IME navigation bar. If the request is sent during
- * the showing flow (e.g. during {@link #onStartInputView}), this will be called shortly after
- * {@link #onWindowShown}, but before the first IME frame is drawn.</p>
+ * visible. However, this is not guaranteed to be called before the IME is shown, as it may
+ * depend on the IME requesting to hide the system-provided navigation bar. If the request is
+ * sent during the showing flow (e.g. during {@link #onStartInputView}), this will be called
+ * shortly after {@link #onWindowShown}, but before the first IME frame is drawn.
*
* @param visible whether the button is requested visible or not.
*/
@@ -4686,6 +4707,8 @@ public class InputMethodService extends AbstractInputMethodService {
+ " touchableRegion=" + mTmpInsets.touchableRegion);
p.println(" mSettingsObserver=" + mSettingsObserver);
p.println(" mNavigationBarController=" + mNavigationBarController.toDebugString());
+ p.println(" mCustomImeSwitcherButtonRequestedVisible="
+ + mCustomImeSwitcherButtonRequestedVisible);
}
private final ImeTracing.ServiceDumper mDumper = new ImeTracing.ServiceDumper() {
diff --git a/core/java/android/inputmethodservice/NavigationBarController.java b/core/java/android/inputmethodservice/NavigationBarController.java
index 7da053d0010e..f1dee89b0b1d 100644
--- a/core/java/android/inputmethodservice/NavigationBarController.java
+++ b/core/java/android/inputmethodservice/NavigationBarController.java
@@ -170,6 +170,9 @@ final class NavigationBarController {
private boolean mShouldShowImeSwitcherWhenImeIsShown;
+ /** Whether a custom IME Switcher button should be visible. */
+ private boolean mCustomImeSwitcherButtonRequestedVisible;
+
@Appearance
private int mAppearance;
@@ -181,9 +184,6 @@ final class NavigationBarController {
private boolean mDrawLegacyNavigationBarBackground;
- /** Whether a custom IME Switcher button should be visible. */
- private boolean mCustomImeSwitcherVisible;
-
private final Rect mTempRect = new Rect();
private final int[] mTempPos = new int[2];
@@ -275,7 +275,9 @@ final class NavigationBarController {
// IME navigation bar.
boolean visible = insets.isVisible(captionBar());
mNavigationBarFrame.setVisibility(visible ? View.VISIBLE : View.GONE);
- checkCustomImeSwitcherVisibility();
+ checkCustomImeSwitcherButtonRequestedVisible(
+ mShouldShowImeSwitcherWhenImeIsShown, mImeDrawsImeNavBar,
+ !visible /* imeNavBarNotVisible */);
}
return view.onApplyWindowInsets(insets);
});
@@ -502,33 +504,31 @@ final class NavigationBarController {
mShouldShowImeSwitcherWhenImeIsShown;
mShouldShowImeSwitcherWhenImeIsShown = shouldShowImeSwitcherWhenImeIsShown;
- checkCustomImeSwitcherVisibility();
-
mService.mWindow.getWindow().getDecorView().getWindowInsetsController()
.setImeCaptionBarInsetsHeight(getImeCaptionBarHeight(imeDrawsImeNavBar));
if (imeDrawsImeNavBar) {
installNavigationBarFrameIfNecessary();
- if (mNavigationBarFrame == null) {
- return;
- }
- if (mShouldShowImeSwitcherWhenImeIsShown
- == prevShouldShowImeSwitcherWhenImeIsShown) {
- return;
- }
- final NavigationBarView navigationBarView = mNavigationBarFrame.findViewByPredicate(
- NavigationBarView.class::isInstance);
- if (navigationBarView != null) {
- // TODO(b/213337792): Support InputMethodService#setBackDisposition().
- // TODO(b/213337792): Set NAVBAR_IME_VISIBLE only when necessary.
- final int flags = NAVBAR_BACK_DISMISS_IME | NAVBAR_IME_VISIBLE
- | (mShouldShowImeSwitcherWhenImeIsShown
- ? NAVBAR_IME_SWITCHER_BUTTON_VISIBLE : 0);
- navigationBarView.setNavbarFlags(flags);
+ if (mNavigationBarFrame != null && mShouldShowImeSwitcherWhenImeIsShown
+ != prevShouldShowImeSwitcherWhenImeIsShown) {
+ final NavigationBarView navigationBarView = mNavigationBarFrame
+ .findViewByPredicate(NavigationBarView.class::isInstance);
+ if (navigationBarView != null) {
+ // TODO(b/213337792): Support InputMethodService#setBackDisposition().
+ // TODO(b/213337792): Set NAVBAR_IME_VISIBLE only when necessary.
+ final int flags = NAVBAR_BACK_DISMISS_IME | NAVBAR_IME_VISIBLE
+ | (mShouldShowImeSwitcherWhenImeIsShown
+ ? NAVBAR_IME_SWITCHER_BUTTON_VISIBLE : 0);
+ navigationBarView.setNavbarFlags(flags);
+ }
}
} else {
uninstallNavigationBarFrameIfNecessary();
}
+
+ // Check custom IME Switcher button visibility after (un)installing nav bar frame.
+ checkCustomImeSwitcherButtonRequestedVisible(shouldShowImeSwitcherWhenImeIsShown,
+ imeDrawsImeNavBar, !isShown() /* imeNavBarNotVisible */);
}
@Override
@@ -631,22 +631,29 @@ final class NavigationBarController {
}
/**
- * Checks if a custom IME Switcher button should be visible, and notifies the IME when this
- * state changes. This can only be {@code true} if three conditions are met:
+ * Checks if a custom IME Switcher button should be requested visible, and notifies the IME
+ * when this state changes. This is only {@code true} when the IME Switcher button is
+ * requested visible, and the navigation bar is not requested visible.
*
- * <li>The IME should draw the IME navigation bar.</li>
- * <li>The IME Switcher button should be visible when the IME is visible.</li>
- * <li>The IME navigation bar should be visible, but was requested hidden by the IME.</li>
+ * @param buttonVisible whether the IME Switcher button is requested visible.
+ * @param shouldDrawImeNavBar whether the IME navigation bar should be drawn.
+ * @param imeNavBarNotVisible whether the IME navigation bar is not requested visible. This
+ * will be {@code true} if it is requested hidden or not
+ * installed.
*/
- private void checkCustomImeSwitcherVisibility() {
+ private void checkCustomImeSwitcherButtonRequestedVisible(boolean buttonVisible,
+ boolean shouldDrawImeNavBar, boolean imeNavBarNotVisible) {
if (!Flags.imeSwitcherRevampApi()) {
return;
}
- final boolean visible = mImeDrawsImeNavBar && mShouldShowImeSwitcherWhenImeIsShown
- && mNavigationBarFrame != null && !isShown();
- if (visible != mCustomImeSwitcherVisible) {
- mCustomImeSwitcherVisible = visible;
- mService.onCustomImeSwitcherButtonRequestedVisible(mCustomImeSwitcherVisible);
+ // The system nav bar will be hidden when the IME is shown and the config is set.
+ final boolean navBarNotVisible = shouldDrawImeNavBar ? imeNavBarNotVisible
+ : mService.getResources().getBoolean(
+ com.android.internal.R.bool.config_hideNavBarForKeyboard);
+ final boolean visible = buttonVisible && navBarNotVisible;
+ if (visible != mCustomImeSwitcherButtonRequestedVisible) {
+ mCustomImeSwitcherButtonRequestedVisible = visible;
+ mService.onCustomImeSwitcherButtonRequestedVisible(visible);
}
}
@@ -656,7 +663,8 @@ final class NavigationBarController {
+ " mNavigationBarFrame=" + mNavigationBarFrame
+ " mShouldShowImeSwitcherWhenImeIsShown="
+ mShouldShowImeSwitcherWhenImeIsShown
- + " mCustomImeSwitcherVisible=" + mCustomImeSwitcherVisible
+ + " mCustomImeSwitcherButtonRequestedVisible="
+ + mCustomImeSwitcherButtonRequestedVisible
+ " mAppearance=0x" + Integer.toHexString(mAppearance)
+ " mDarkIntensity=" + mDarkIntensity
+ " mDrawLegacyNavigationBarBackground=" + mDrawLegacyNavigationBarBackground
diff --git a/core/java/android/os/BatteryUsageStats.java b/core/java/android/os/BatteryUsageStats.java
index 739908ef0dfc..b4ca217539a3 100644
--- a/core/java/android/os/BatteryUsageStats.java
+++ b/core/java/android/os/BatteryUsageStats.java
@@ -129,12 +129,6 @@ public final class BatteryUsageStats implements Parcelable, Closeable {
// Max window size. CursorWindow uses only as much memory as needed.
private static final long BATTERY_CONSUMER_CURSOR_WINDOW_SIZE = 20_000_000; // bytes
- /**
- * Used by tests to ensure all BatteryUsageStats instances are closed.
- */
- @VisibleForTesting
- public static boolean DEBUG_INSTANCE_COUNT;
-
private static final int STATSD_PULL_ATOM_MAX_BYTES = 45000;
private static final int[] UID_USAGE_TIME_PROCESS_STATES = {
@@ -1267,11 +1261,16 @@ public final class BatteryUsageStats implements Parcelable, Closeable {
}
}
+ /*
+ * Used by tests to ensure all BatteryUsageStats instances are closed.
+ */
+ private static volatile boolean sInstanceLeakDetectionEnabled;
+
@GuardedBy("BatteryUsageStats.class")
private static Map<CursorWindow, Exception> sInstances;
private static void onCursorWindowAllocated(CursorWindow window) {
- if (!DEBUG_INSTANCE_COUNT) {
+ if (!sInstanceLeakDetectionEnabled) {
return;
}
@@ -1284,7 +1283,7 @@ public final class BatteryUsageStats implements Parcelable, Closeable {
}
private static void onCursorWindowReleased(CursorWindow window) {
- if (!DEBUG_INSTANCE_COUNT) {
+ if (!sInstanceLeakDetectionEnabled) {
return;
}
@@ -1294,12 +1293,26 @@ public final class BatteryUsageStats implements Parcelable, Closeable {
}
/**
+ * Enables detection of leaked BatteryUsageStats instances, meaning instances that are created
+ * but not closed during the test execution.
+ */
+ @VisibleForTesting
+ public static void enableInstanceLeakDetection() {
+ sInstanceLeakDetectionEnabled = true;
+ synchronized (BatteryUsageStats.class) {
+ if (sInstances != null) {
+ sInstances.clear();
+ }
+ }
+ }
+
+ /**
* Used by tests to ensure all BatteryUsageStats instances are closed.
*/
@VisibleForTesting
public static void assertAllInstancesClosed() {
- if (!DEBUG_INSTANCE_COUNT) {
- throw new IllegalStateException("DEBUG_INSTANCE_COUNT is false");
+ if (!sInstanceLeakDetectionEnabled) {
+ throw new IllegalStateException("Instance leak detection is not enabled");
}
synchronized (BatteryUsageStats.class) {
diff --git a/core/java/android/os/Parcel.java b/core/java/android/os/Parcel.java
index 4a9928532b93..8a64dd67ace9 100644
--- a/core/java/android/os/Parcel.java
+++ b/core/java/android/os/Parcel.java
@@ -4702,11 +4702,9 @@ public final class Parcel {
object = readValue(type, loader, clazz, itemTypes);
int actual = dataPosition() - start;
if (actual != length) {
- String error = "Unparcelling of " + object + " of type "
- + Parcel.valueTypeToString(type) + " consumed " + actual
- + " bytes, but " + length + " expected.";
- Slog.wtfStack(TAG, error);
- throw new BadParcelableException(error);
+ Slog.wtfStack(TAG,
+ "Unparcelling of " + object + " of type " + Parcel.valueTypeToString(type)
+ + " consumed " + actual + " bytes, but " + length + " expected.");
}
} else {
object = readValue(type, loader, clazz, itemTypes);
diff --git a/core/java/android/permission/flags.aconfig b/core/java/android/permission/flags.aconfig
index 34272b17cf54..ef6f37ac6f9c 100644
--- a/core/java/android/permission/flags.aconfig
+++ b/core/java/android/permission/flags.aconfig
@@ -554,3 +554,12 @@ flag {
description: "This flag is used to add role protection to READ_BLOCKED_NUMBERS for SYSTEM_UI_INTELLIGENCE"
bug: "354758615"
}
+
+flag {
+ name: "enable_system_supervision_role_behavior"
+ is_fixed_read_only: true
+ is_exported: true
+ namespace: "supervision"
+ description: "This flag is used to enable the role behavior for the system supervision role"
+ bug: "378102594"
+}
diff --git a/core/java/android/security/flags.aconfig b/core/java/android/security/flags.aconfig
index 09137c3a7b65..a2403826fe32 100644
--- a/core/java/android/security/flags.aconfig
+++ b/core/java/android/security/flags.aconfig
@@ -52,13 +52,6 @@ flag {
}
flag {
- name: "binary_transparency_sepolicy_hash"
- namespace: "hardware_backed_security"
- description: "Collect sepolicy hash from sysfs"
- bug: "308471499"
-}
-
-flag {
name: "frp_enforcement"
is_exported: true
namespace: "hardware_backed_security"
@@ -75,13 +68,6 @@ flag {
}
flag {
- name: "dump_attestation_verifications"
- namespace: "hardware_backed_security"
- description: "Add a dump capability for attestation_verification service"
- bug: "335498868"
-}
-
-flag {
name: "should_trust_manager_listen_for_primary_auth"
namespace: "biometrics"
description: "Causes TrustManagerService to listen for credential attempts and ignore reports from upstream"
diff --git a/core/java/android/service/dreams/flags.aconfig b/core/java/android/service/dreams/flags.aconfig
index 971942ecfe8b..82b035c8ebfd 100644
--- a/core/java/android/service/dreams/flags.aconfig
+++ b/core/java/android/service/dreams/flags.aconfig
@@ -90,5 +90,5 @@ flag {
namespace: "systemui"
description: "Enables various improvements to the dream experience "
"such as new triggers and various bug fixes"
- bug: "375689917"
+ bug: "403579494"
}
diff --git a/core/java/android/text/Layout.java b/core/java/android/text/Layout.java
index 0152c52a6753..ebd6efac3d96 100644
--- a/core/java/android/text/Layout.java
+++ b/core/java/android/text/Layout.java
@@ -1074,15 +1074,15 @@ public abstract class Layout {
public void onCharacterBounds(int index, int lineNum, float left, float top,
float right, float bottom) {
- var newBackground = determineContrastingBackgroundColor(index);
- var hasBgColorChanged = newBackground != bgPaint.getColor();
-
// Skip processing if the character is a space or a tap to avoid
// rendering an abrupt, empty rectangle.
if (TextLine.isLineEndSpace(mText.charAt(index))) {
return;
}
+ var newBackground = determineContrastingBackgroundColor(index);
+ var hasBgColorChanged = newBackground != bgPaint.getColor();
+
// To avoid highlighting emoji sequences, we use Extended_Pictgraphs as a
// heuristic. Highlighting is skipped based on code points, not glyph type
// (text vs. color), so emojis with default text presentation are
diff --git a/core/java/android/util/TypedValue.java b/core/java/android/util/TypedValue.java
index 26ab5885c9ea..11f3f8f68dd6 100644
--- a/core/java/android/util/TypedValue.java
+++ b/core/java/android/util/TypedValue.java
@@ -247,6 +247,12 @@ public class TypedValue {
*/
public int sourceResourceId;
+ /**
+ * Whether the value uses feature flags that need to be evaluated at runtime.
+ * @hide
+ */
+ public boolean usesFeatureFlags = false;
+
/* ------------------------------------------------------------ */
/** Return the data for this value as a float. Only use for values
diff --git a/core/java/android/view/IWindowManager.aidl b/core/java/android/view/IWindowManager.aidl
index 237d8f96496f..4bf64954a380 100644
--- a/core/java/android/view/IWindowManager.aidl
+++ b/core/java/android/view/IWindowManager.aidl
@@ -135,8 +135,29 @@ interface IWindowManager
int getDisplayIdByUniqueId(String uniqueId);
@EnforcePermission("WRITE_SECURE_SETTINGS")
void setForcedDisplayDensityForUser(int displayId, int density, int userId);
+ /**
+ * Clears forced density and forced density ratio in DisplayWindowSettings for the given
+ * displayId.
+ *
+ * @param displayId Id of the display.
+ * @param userId Id of the user.
+ */
@EnforcePermission("WRITE_SECURE_SETTINGS")
void clearForcedDisplayDensityForUser(int displayId, int userId);
+ /**
+ * Sets display forced density ratio and forced density in DisplayWindowSettings for
+ * the given displayId. Ratio is used to update forced density to persist display size when
+ * resolution change happens. Use {@link #setForcedDisplayDensityForUser} when there is no need
+ * to handle resolution changes for the display. If setForcedDisplayDensityForUser is used after,
+ * this the ratio will be updated to use the last set forced density. Use
+ * {@link #clearForcedDisplayDensityForUser} to reset.
+ *
+ * @param displayId Id of the display.
+ * @param ratio The ratio of forced density to the default density.
+ * @param userId Id of the user.
+ */
+ @EnforcePermission("WRITE_SECURE_SETTINGS")
+ void setForcedDisplayDensityRatio(int displayId, float ratio, int userId);
/**
* Sets settings for a specific user in a batch to minimize configuration updates.
diff --git a/core/java/android/view/InsetsAnimationControlImpl.java b/core/java/android/view/InsetsAnimationControlImpl.java
index 6decd6d3a603..0558858895b8 100644
--- a/core/java/android/view/InsetsAnimationControlImpl.java
+++ b/core/java/android/view/InsetsAnimationControlImpl.java
@@ -257,6 +257,11 @@ public class InsetsAnimationControlImpl implements InternalInsetsAnimationContro
}
@Override
+ public boolean willUpdateSurface() {
+ return !mFinished && !mCancelled;
+ }
+
+ @Override
public @AnimationType int getAnimationType() {
return mAnimationType;
}
diff --git a/core/java/android/view/InsetsAnimationControlRunner.java b/core/java/android/view/InsetsAnimationControlRunner.java
index 4f102da4692a..968181b1723d 100644
--- a/core/java/android/view/InsetsAnimationControlRunner.java
+++ b/core/java/android/view/InsetsAnimationControlRunner.java
@@ -55,6 +55,12 @@ public interface InsetsAnimationControlRunner {
void updateSurfacePosition(SparseArray<InsetsSourceControl> controls);
/**
+ * Returns {@code true} if this runner will keep playing the animation and updating the surface.
+ * {@code false} otherwise.
+ */
+ boolean willUpdateSurface();
+
+ /**
* Cancels the animation.
*/
void cancel();
diff --git a/core/java/android/view/InsetsAnimationThreadControlRunner.java b/core/java/android/view/InsetsAnimationThreadControlRunner.java
index 8c2c4951a9f7..8acb46dcc0a4 100644
--- a/core/java/android/view/InsetsAnimationThreadControlRunner.java
+++ b/core/java/android/view/InsetsAnimationThreadControlRunner.java
@@ -89,7 +89,7 @@ public class InsetsAnimationThreadControlRunner implements InsetsAnimationContro
}
};
- private SurfaceParamsApplier mSurfaceParamsApplier = new SurfaceParamsApplier() {
+ private final SurfaceParamsApplier mSurfaceParamsApplier = new SurfaceParamsApplier() {
private final float[] mTmpFloat9 = new float[9];
@@ -170,6 +170,17 @@ public class InsetsAnimationThreadControlRunner implements InsetsAnimationContro
@Override
@UiThread
+ public boolean willUpdateSurface() {
+ synchronized (mControl) {
+ // This is called from the UI thread, however, applyChangeInsets would be called on the
+ // animation thread, so we need this critical section to ensure this is not called
+ // during applyChangeInsets. See: scheduleApplyChangeInsets.
+ return mControl.willUpdateSurface();
+ }
+ }
+
+ @Override
+ @UiThread
public void cancel() {
InsetsAnimationThread.getHandler().post(mControl::cancel);
}
diff --git a/core/java/android/view/InsetsController.java b/core/java/android/view/InsetsController.java
index 4c578fb93600..f7ffc1e1a103 100644
--- a/core/java/android/view/InsetsController.java
+++ b/core/java/android/view/InsetsController.java
@@ -1747,9 +1747,9 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation
mTypesBeingCancelled |= types;
try {
for (int i = mRunningAnimations.size() - 1; i >= 0; i--) {
- InsetsAnimationControlRunner control = mRunningAnimations.get(i).runner;
- if ((control.getTypes() & types) != 0) {
- cancelAnimation(control, true /* invokeCallback */);
+ final InsetsAnimationControlRunner runner = mRunningAnimations.get(i).runner;
+ if ((runner.getTypes() & types) != 0) {
+ cancelAnimation(runner, true /* invokeCallback */);
}
}
if ((types & ime()) != 0) {
@@ -1788,11 +1788,11 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation
ImeTracker.PHASE_CLIENT_ANIMATION_FINISHED_SHOW);
ImeTracker.forLogging().onShown(statsToken);
} else {
- ImeTracker.forLogging().onProgress(statsToken,
- ImeTracker.PHASE_CLIENT_ANIMATION_FINISHED_HIDE);
// The requestedVisibleTypes are only send at the end of the hide animation.
// Therefore, the requested is not finished at this point.
if (!Flags.refactorInsetsController()) {
+ ImeTracker.forLogging().onProgress(statsToken,
+ ImeTracker.PHASE_CLIENT_ANIMATION_FINISHED_HIDE);
ImeTracker.forLogging().onHidden(statsToken);
}
}
@@ -1807,10 +1807,10 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation
void notifyControlRevoked(InsetsSourceConsumer consumer) {
final @InsetsType int type = consumer.getType();
for (int i = mRunningAnimations.size() - 1; i >= 0; i--) {
- InsetsAnimationControlRunner control = mRunningAnimations.get(i).runner;
- control.notifyControlRevoked(type);
- if (control.getControllingTypes() == 0) {
- cancelAnimation(control, true /* invokeCallback */);
+ final InsetsAnimationControlRunner runner = mRunningAnimations.get(i).runner;
+ runner.notifyControlRevoked(type);
+ if (runner.getControllingTypes() == 0) {
+ cancelAnimation(runner, true /* invokeCallback */);
}
}
if (type == ime()) {
@@ -1823,38 +1823,38 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation
}
}
- private void cancelAnimation(InsetsAnimationControlRunner control, boolean invokeCallback) {
+ private void cancelAnimation(InsetsAnimationControlRunner runner, boolean invokeCallback) {
if (invokeCallback) {
- ImeTracker.forLogging().onCancelled(control.getStatsToken(),
+ ImeTracker.forLogging().onCancelled(runner.getStatsToken(),
ImeTracker.PHASE_CLIENT_ANIMATION_CANCEL);
- control.cancel();
+ runner.cancel();
} else {
// Succeeds if invokeCallback is false (i.e. when called from notifyFinished).
- ImeTracker.forLogging().onProgress(control.getStatsToken(),
+ ImeTracker.forLogging().onProgress(runner.getStatsToken(),
ImeTracker.PHASE_CLIENT_ANIMATION_CANCEL);
}
if (DEBUG) {
Log.d(TAG, TextUtils.formatSimple(
"cancelAnimation of types: %d, animType: %d, host: %s",
- control.getTypes(), control.getAnimationType(), mHost.getRootViewTitle()));
+ runner.getTypes(), runner.getAnimationType(), mHost.getRootViewTitle()));
}
@InsetsType int removedTypes = 0;
for (int i = mRunningAnimations.size() - 1; i >= 0; i--) {
RunningAnimation runningAnimation = mRunningAnimations.get(i);
- if (runningAnimation.runner == control) {
+ if (runningAnimation.runner == runner) {
mRunningAnimations.remove(i);
- removedTypes = control.getTypes();
+ removedTypes = runner.getTypes();
if (invokeCallback) {
dispatchAnimationEnd(runningAnimation.runner.getAnimation());
} else {
if (Flags.refactorInsetsController()) {
if ((removedTypes & ime()) != 0
- && control.getAnimationType() == ANIMATION_TYPE_HIDE) {
+ && runner.getAnimationType() == ANIMATION_TYPE_HIDE) {
if (mHost != null) {
// if the (hide) animation is cancelled, the
// requestedVisibleTypes should be reported at this point.
reportRequestedVisibleTypes(!Flags.reportAnimatingInsetsTypes()
- ? control.getStatsToken() : null);
+ ? runner.getStatsToken() : null);
mHost.getInputMethodManager().removeImeSurface(
mHost.getWindowToken());
}
@@ -1869,9 +1869,9 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation
if (mHost != null) {
final boolean dispatchStatsToken =
Flags.reportAnimatingInsetsTypes() && (removedTypes & ime()) != 0
- && control.getAnimationType() == ANIMATION_TYPE_HIDE;
+ && runner.getAnimationType() == ANIMATION_TYPE_HIDE;
mHost.updateAnimatingTypes(mAnimatingTypes,
- dispatchStatsToken ? control.getStatsToken() : null);
+ dispatchStatsToken ? runner.getStatsToken() : null);
}
}
@@ -1959,14 +1959,30 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation
@VisibleForTesting(visibility = PACKAGE)
public @AnimationType int getAnimationType(@InsetsType int type) {
for (int i = mRunningAnimations.size() - 1; i >= 0; i--) {
- InsetsAnimationControlRunner control = mRunningAnimations.get(i).runner;
- if (control.controlsType(type)) {
+ final InsetsAnimationControlRunner runner = mRunningAnimations.get(i).runner;
+ if (runner.controlsType(type)) {
return mRunningAnimations.get(i).type;
}
}
return ANIMATION_TYPE_NONE;
}
+ /**
+ * Returns {@code true} if there is an animation which controls the given {@link InsetsType} and
+ * the runner is still playing the surface animation.
+ *
+ * @see InsetsAnimationControlRunner#willUpdateSurface()
+ */
+ boolean hasSurfaceAnimation(@InsetsType int type) {
+ for (int i = mRunningAnimations.size() - 1; i >= 0; i--) {
+ final InsetsAnimationControlRunner runner = mRunningAnimations.get(i).runner;
+ if (runner.controlsType(type) && runner.willUpdateSurface()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
@VisibleForTesting(visibility = PACKAGE)
public void setRequestedVisibleTypes(@InsetsType int visibleTypes, @InsetsType int mask) {
final @InsetsType int requestedVisibleTypes =
diff --git a/core/java/android/view/InsetsResizeAnimationRunner.java b/core/java/android/view/InsetsResizeAnimationRunner.java
index 5262751cc6ed..6356be262cc4 100644
--- a/core/java/android/view/InsetsResizeAnimationRunner.java
+++ b/core/java/android/view/InsetsResizeAnimationRunner.java
@@ -233,6 +233,11 @@ public class InsetsResizeAnimationRunner implements InsetsAnimationControlRunner
}
@Override
+ public boolean willUpdateSurface() {
+ return false;
+ }
+
+ @Override
public boolean hasZeroInsetsIme() {
return false;
}
diff --git a/core/java/android/view/InsetsSourceConsumer.java b/core/java/android/view/InsetsSourceConsumer.java
index 945975a88cd5..1a750a3f89c4 100644
--- a/core/java/android/view/InsetsSourceConsumer.java
+++ b/core/java/android/view/InsetsSourceConsumer.java
@@ -17,7 +17,6 @@
package android.view;
import static android.view.InsetsController.ANIMATION_TYPE_NONE;
-import static android.view.InsetsController.ANIMATION_TYPE_RESIZE;
import static android.view.InsetsController.AnimationType;
import static android.view.InsetsController.DEBUG;
import static android.view.InsetsSourceConsumerProto.ANIMATION_STATE;
@@ -201,9 +200,8 @@ public class InsetsSourceConsumer {
}
// If there is no animation controlling the leash, make sure the visibility and the
- // position is up-to-date. Note: ANIMATION_TYPE_RESIZE doesn't control the leash.
- final int animType = mController.getAnimationType(mType);
- if (animType == ANIMATION_TYPE_NONE || animType == ANIMATION_TYPE_RESIZE) {
+ // position is up-to-date.
+ if (!mController.hasSurfaceAnimation(mType)) {
applyRequestedVisibilityAndPositionToControl();
}
diff --git a/core/java/android/view/RoundScrollbarRenderer.java b/core/java/android/view/RoundScrollbarRenderer.java
index 331e34526ae8..a592e1f0a874 100644
--- a/core/java/android/view/RoundScrollbarRenderer.java
+++ b/core/java/android/view/RoundScrollbarRenderer.java
@@ -45,8 +45,8 @@ public class RoundScrollbarRenderer {
private static final float MIN_SCROLLBAR_ANGLE_SWIPE = 0.3f * SCROLLBAR_ANGLE_RANGE;
private static final float GAP_BETWEEN_TRACK_AND_THUMB_DP = 3f;
private static final float OUTER_PADDING_DP = 2f;
- private static final int DEFAULT_THUMB_COLOR = 0xFFFFFFFF;
- private static final int DEFAULT_TRACK_COLOR = 0x4CFFFFFF;
+ private static final int DEFAULT_THUMB_COLOR = 0xFFC6C6C7;
+ private static final int DEFAULT_TRACK_COLOR = 0xFF2F3131;
// Rate at which the scrollbar will resize itself when the size of the view changes
private static final float RESIZING_RATE = 0.8f;
diff --git a/core/java/android/view/ScrollCaptureConnection.java b/core/java/android/view/ScrollCaptureConnection.java
index f0c7909647ce..0abb8b6c9a5a 100644
--- a/core/java/android/view/ScrollCaptureConnection.java
+++ b/core/java/android/view/ScrollCaptureConnection.java
@@ -20,8 +20,9 @@ import static android.os.Trace.TRACE_TAG_GRAPHICS;
import static java.util.Objects.requireNonNull;
-import android.annotation.BinderThread;
+import android.annotation.AnyThread;
import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.annotation.UiThread;
import android.graphics.Point;
import android.graphics.Rect;
@@ -64,9 +65,13 @@ public class ScrollCaptureConnection extends IScrollCaptureConnection.Stub imple
private final Executor mUiThread;
private final CloseGuard mCloseGuard = new CloseGuard();
+ @Nullable
private ScrollCaptureCallback mLocal;
+ @Nullable
private IScrollCaptureCallbacks mRemote;
+ @Nullable
private ScrollCaptureSession mSession;
+ @Nullable
private CancellationSignal mCancellation;
private volatile boolean mActive;
@@ -92,7 +97,7 @@ public class ScrollCaptureConnection extends IScrollCaptureConnection.Stub imple
mPositionInWindow = new Point(selectedTarget.getPositionInWindow());
}
- @BinderThread
+ @AnyThread
@Override
public ICancellationSignal startCapture(@NonNull Surface surface,
@NonNull IScrollCaptureCallbacks remote) throws RemoteException {
@@ -115,7 +120,11 @@ public class ScrollCaptureConnection extends IScrollCaptureConnection.Stub imple
Runnable listener =
SafeCallback.create(mCancellation, mUiThread, this::onStartCaptureCompleted);
// -> UiThread
- mUiThread.execute(() -> mLocal.onScrollCaptureStart(mSession, mCancellation, listener));
+ mUiThread.execute(() -> {
+ if (mLocal != null && mCancellation != null) {
+ mLocal.onScrollCaptureStart(mSession, mCancellation, listener);
+ }
+ });
return cancellation;
}
@@ -123,7 +132,11 @@ public class ScrollCaptureConnection extends IScrollCaptureConnection.Stub imple
private void onStartCaptureCompleted() {
mActive = true;
try {
- mRemote.onCaptureStarted();
+ if (mRemote != null) {
+ mRemote.onCaptureStarted();
+ } else {
+ close();
+ }
} catch (RemoteException e) {
Log.w(TAG, "Shutting down due to error: ", e);
close();
@@ -132,7 +145,7 @@ public class ScrollCaptureConnection extends IScrollCaptureConnection.Stub imple
Trace.asyncTraceForTrackEnd(TRACE_TAG_GRAPHICS, TRACE_TRACK, mTraceId);
}
- @BinderThread
+ @AnyThread
@Override
public ICancellationSignal requestImage(Rect requestRect) throws RemoteException {
Trace.asyncTraceForTrackBegin(TRACE_TAG_GRAPHICS, TRACE_TRACK, REQUEST_IMAGE, mTraceId);
@@ -145,7 +158,7 @@ public class ScrollCaptureConnection extends IScrollCaptureConnection.Stub imple
SafeCallback.create(mCancellation, mUiThread, this::onImageRequestCompleted);
// -> UiThread
mUiThread.execute(() -> {
- if (mLocal != null) {
+ if (mLocal != null && mSession != null && mCancellation != null) {
mLocal.onScrollCaptureImageRequest(
mSession, mCancellation, new Rect(requestRect), listener);
}
@@ -157,7 +170,11 @@ public class ScrollCaptureConnection extends IScrollCaptureConnection.Stub imple
@UiThread
void onImageRequestCompleted(Rect capturedArea) {
try {
- mRemote.onImageRequestCompleted(0, capturedArea);
+ if (mRemote != null) {
+ mRemote.onImageRequestCompleted(0, capturedArea);
+ } else {
+ close();
+ }
} catch (RemoteException e) {
Log.w(TAG, "Shutting down due to error: ", e);
close();
@@ -167,7 +184,7 @@ public class ScrollCaptureConnection extends IScrollCaptureConnection.Stub imple
Trace.asyncTraceForTrackEnd(TRACE_TAG_GRAPHICS, TRACE_TRACK, mTraceId);
}
- @BinderThread
+ @AnyThread
@Override
public ICancellationSignal endCapture() throws RemoteException {
Trace.asyncTraceForTrackBegin(TRACE_TAG_GRAPHICS, TRACE_TRACK, END_CAPTURE, mTraceId);
@@ -212,7 +229,7 @@ public class ScrollCaptureConnection extends IScrollCaptureConnection.Stub imple
}
- @BinderThread
+ @AnyThread
@Override
public synchronized void close() {
Trace.instantForTrack(TRACE_TAG_GRAPHICS, TRACE_TRACK, "close");
@@ -220,7 +237,11 @@ public class ScrollCaptureConnection extends IScrollCaptureConnection.Stub imple
Log.w(TAG, "close(): capture session still active! Ending now.");
cancelPendingAction();
final ScrollCaptureCallback callback = mLocal;
- mUiThread.execute(() -> callback.onScrollCaptureEnd(() -> { /* ignore */ }));
+ mUiThread.execute(() -> {
+ if (callback != null) {
+ callback.onScrollCaptureEnd(() -> { /* ignore */ });
+ }
+ });
mActive = false;
}
if (mRemote != null) {
@@ -297,10 +318,13 @@ public class ScrollCaptureConnection extends IScrollCaptureConnection.Stub imple
protected final void maybeAccept(Consumer<T> consumer) {
T value = mValue.getAndSet(null);
if (mSignal.isCanceled()) {
+ Log.w(TAG, "callback ignored, operation already cancelled");
return;
}
if (value != null) {
mExecutor.execute(() -> consumer.accept(value));
+ } else {
+ Log.w(TAG, "callback ignored, value already delivered");
}
}
diff --git a/core/java/android/view/SurfaceControl.java b/core/java/android/view/SurfaceControl.java
index c7ae3283c46c..f9d7a672f43a 100644
--- a/core/java/android/view/SurfaceControl.java
+++ b/core/java/android/view/SurfaceControl.java
@@ -2592,7 +2592,7 @@ public final class SurfaceControl implements Parcelable {
int[] dataspaces = nativeGetCompositionDataspaces();
ColorSpace srgb = ColorSpace.get(ColorSpace.Named.SRGB);
ColorSpace[] colorSpaces = { srgb, srgb };
- if (dataspaces.length == 2) {
+ if (dataspaces != null && dataspaces.length == 2) {
for (int i = 0; i < 2; ++i) {
ColorSpace cs = ColorSpace.getFromDataSpace(dataspaces[i]);
if (cs != null) {
diff --git a/core/java/android/view/SyncRtSurfaceTransactionApplier.java b/core/java/android/view/SyncRtSurfaceTransactionApplier.java
index e9c937cc0f9b..b2f0bd931174 100644
--- a/core/java/android/view/SyncRtSurfaceTransactionApplier.java
+++ b/core/java/android/view/SyncRtSurfaceTransactionApplier.java
@@ -16,6 +16,7 @@
package android.view;
+import android.annotation.SuppressLint;
import android.graphics.Matrix;
import android.graphics.Rect;
import android.view.SurfaceControl.Transaction;
@@ -39,6 +40,9 @@ public class SyncRtSurfaceTransactionApplier {
public static final int FLAG_BACKGROUND_BLUR_RADIUS = 1 << 5;
public static final int FLAG_VISIBILITY = 1 << 6;
public static final int FLAG_TRANSACTION = 1 << 7;
+ public static final int FLAG_EARLY_WAKEUP_START = 1 << 8;
+ public static final int FLAG_EARLY_WAKEUP_END = 1 << 9;
+ public static final int FLAG_OPAQUE = 1 << 10;
private SurfaceControl mTargetSc;
private final ViewRootImpl mTargetViewRootImpl;
@@ -99,6 +103,7 @@ public class SyncRtSurfaceTransactionApplier {
}
}
+ @SuppressLint("MissingPermission")
public static void applyParams(Transaction t, SurfaceParams params, float[] tmpFloat9) {
if ((params.flags & FLAG_TRANSACTION) != 0) {
t.merge(params.mergeTransaction);
@@ -129,6 +134,15 @@ public class SyncRtSurfaceTransactionApplier {
t.hide(params.surface);
}
}
+ if ((params.flags & FLAG_EARLY_WAKEUP_START) != 0) {
+ t.setEarlyWakeupStart();
+ }
+ if ((params.flags & FLAG_EARLY_WAKEUP_END) != 0) {
+ t.setEarlyWakeupEnd();
+ }
+ if ((params.flags & FLAG_OPAQUE) != 0) {
+ t.setOpaque(params.surface, params.opaque);
+ }
}
/**
@@ -172,6 +186,7 @@ public class SyncRtSurfaceTransactionApplier {
Rect windowCrop;
int layer;
boolean visible;
+ boolean opaque;
Transaction mergeTransaction;
/**
@@ -263,17 +278,48 @@ public class SyncRtSurfaceTransactionApplier {
}
/**
+ * Provides a hint to SurfaceFlinger to change its offset so that SurfaceFlinger
+ * wakes up earlier to compose surfaces.
+ * @return this Builder
+ */
+ public Builder withEarlyWakeupStart() {
+ flags |= FLAG_EARLY_WAKEUP_START;
+ return this;
+ }
+
+ /**
+ * Removes the early wake up hint set by earlyWakeupStart.
+ * @return this Builder
+ */
+ public Builder withEarlyWakeupEnd() {
+ flags |= FLAG_EARLY_WAKEUP_END;
+ return this;
+ }
+
+ /**
+ * @param opaque Indicates weather the surface must be considered opaque.
+ * @return this Builder
+ */
+ public Builder withOpaque(boolean opaque) {
+ this.opaque = opaque;
+ flags |= FLAG_OPAQUE;
+ return this;
+ }
+
+ /**
* @return a new SurfaceParams instance
*/
public SurfaceParams build() {
return new SurfaceParams(surface, flags, alpha, matrix, windowCrop, layer,
- cornerRadius, backgroundBlurRadius, visible, mergeTransaction);
+ cornerRadius, backgroundBlurRadius, visible, mergeTransaction,
+ opaque);
}
}
private SurfaceParams(SurfaceControl surface, int params, float alpha, Matrix matrix,
Rect windowCrop, int layer, float cornerRadius,
- int backgroundBlurRadius, boolean visible, Transaction mergeTransaction) {
+ int backgroundBlurRadius, boolean visible,
+ Transaction mergeTransaction, boolean opaque) {
this.flags = params;
this.surface = surface;
this.alpha = alpha;
@@ -284,6 +330,7 @@ public class SyncRtSurfaceTransactionApplier {
this.backgroundBlurRadius = backgroundBlurRadius;
this.visible = visible;
this.mergeTransaction = mergeTransaction;
+ this.opaque = opaque;
}
private final int flags;
@@ -312,5 +359,6 @@ public class SyncRtSurfaceTransactionApplier {
public final boolean visible;
public final Transaction mergeTransaction;
+ public final boolean opaque;
}
}
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index f32ce6f1d6e4..1213d173ab3b 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -5179,9 +5179,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
* This lives here since it's only valid for interactive views. This list is null
* until its first use.
*/
- private List<Rect> mSystemGestureExclusionRects = null;
- private List<Rect> mKeepClearRects = null;
- private List<Rect> mUnrestrictedKeepClearRects = null;
+ private ArrayList<Rect> mSystemGestureExclusionRects = null;
+ private ArrayList<Rect> mKeepClearRects = null;
+ private ArrayList<Rect> mUnrestrictedKeepClearRects = null;
private boolean mPreferKeepClear = false;
private Rect mHandwritingArea = null;
@@ -12891,21 +12891,34 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
final ListenerInfo info = getListenerInfo();
final boolean rectsChanged = !reduceChangedExclusionRectsMsgs()
- || !Objects.equals(info.mSystemGestureExclusionRects, rects);
- if (info.mSystemGestureExclusionRects != null) {
- if (rectsChanged) {
- info.mSystemGestureExclusionRects.clear();
- info.mSystemGestureExclusionRects.addAll(rects);
- }
- } else {
- info.mSystemGestureExclusionRects = new ArrayList<>(rects);
+ || !Objects.deepEquals(info.mSystemGestureExclusionRects, rects);
+ if (info.mSystemGestureExclusionRects == null) {
+ info.mSystemGestureExclusionRects = new ArrayList<>();
}
if (rectsChanged) {
+ deepCopyRectsObjectRecycling(info.mSystemGestureExclusionRects, rects);
updatePositionUpdateListener();
postUpdate(this::updateSystemGestureExclusionRects);
}
}
+ private void deepCopyRectsObjectRecycling(@NonNull ArrayList<Rect> dest, List<Rect> src) {
+ dest.ensureCapacity(src.size());
+ for (int i = 0; i < src.size(); i++) {
+ if (i < dest.size()) {
+ // Replace if there is an old rect to refresh
+ dest.get(i).set(src.get(i));
+ } else {
+ // Add a rect if the list enlarged
+ dest.add(Rect.copyOrNull(src.get(i)));
+ }
+ }
+ while (dest.size() > src.size()) {
+ // Remove elements if the list shrank
+ dest.removeLast();
+ }
+ }
+
private void updatePositionUpdateListener() {
final ListenerInfo info = getListenerInfo();
if (getSystemGestureExclusionRects().isEmpty()
@@ -13031,14 +13044,16 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
*/
public final void setPreferKeepClearRects(@NonNull List<Rect> rects) {
final ListenerInfo info = getListenerInfo();
- if (info.mKeepClearRects != null) {
- info.mKeepClearRects.clear();
- info.mKeepClearRects.addAll(rects);
- } else {
- info.mKeepClearRects = new ArrayList<>(rects);
+ final boolean rectsChanged = !reduceChangedExclusionRectsMsgs()
+ || !Objects.deepEquals(info.mKeepClearRects, rects);
+ if (info.mKeepClearRects == null) {
+ info.mKeepClearRects = new ArrayList<>();
+ }
+ if (rectsChanged) {
+ deepCopyRectsObjectRecycling(info.mKeepClearRects, rects);
+ updatePositionUpdateListener();
+ postUpdate(this::updateKeepClearRects);
}
- updatePositionUpdateListener();
- postUpdate(this::updateKeepClearRects);
}
/**
@@ -13076,14 +13091,16 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
@RequiresPermission(android.Manifest.permission.SET_UNRESTRICTED_KEEP_CLEAR_AREAS)
public final void setUnrestrictedPreferKeepClearRects(@NonNull List<Rect> rects) {
final ListenerInfo info = getListenerInfo();
- if (info.mUnrestrictedKeepClearRects != null) {
- info.mUnrestrictedKeepClearRects.clear();
- info.mUnrestrictedKeepClearRects.addAll(rects);
- } else {
- info.mUnrestrictedKeepClearRects = new ArrayList<>(rects);
+ final boolean rectsChanged = !reduceChangedExclusionRectsMsgs()
+ || !Objects.deepEquals(info.mUnrestrictedKeepClearRects, rects);
+ if (info.mUnrestrictedKeepClearRects == null) {
+ info.mUnrestrictedKeepClearRects = new ArrayList<>();
+ }
+ if (rectsChanged) {
+ deepCopyRectsObjectRecycling(info.mUnrestrictedKeepClearRects, rects);
+ updatePositionUpdateListener();
+ postUpdate(this::updateKeepClearRects);
}
- updatePositionUpdateListener();
- postUpdate(this::updateKeepClearRects);
}
/**
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index 4e3ff9063179..b1676dde3b70 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -138,6 +138,7 @@ import static com.android.window.flags.Flags.enableWindowContextResourcesUpdateO
import static com.android.window.flags.Flags.predictiveBackSwipeEdgeNoneApi;
import static com.android.window.flags.Flags.reduceChangedExclusionRectsMsgs;
import static com.android.window.flags.Flags.setScPropertiesInClient;
+import static com.android.window.flags.Flags.fixViewRootCallTrace;
import android.Manifest;
import android.accessibilityservice.AccessibilityService;
@@ -189,6 +190,7 @@ import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Region;
import android.graphics.RenderNode;
+import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.GradientDrawable;
import android.hardware.SyncFence;
@@ -292,6 +294,7 @@ import com.android.internal.os.SomeArgs;
import com.android.internal.policy.DecorView;
import com.android.internal.policy.PhoneFallbackEventHandler;
import com.android.internal.protolog.ProtoLog;
+import com.android.internal.util.ContrastColorUtil;
import com.android.internal.util.FastPrintWriter;
import com.android.internal.view.BaseSurfaceHolder;
import com.android.internal.view.RootViewSurfaceTaker;
@@ -1588,7 +1591,9 @@ public final class ViewRootImpl implements ViewParent,
mAttachInfo.mPanelParentWindowToken
= panelParentView.getApplicationWindowToken();
}
- mAdded = true;
+ if (!fixViewRootCallTrace()) {
+ mAdded = true;
+ }
int res; /* = WindowManagerImpl.ADD_OKAY; */
// Schedule the first layout -before- adding to the window
@@ -1639,7 +1644,9 @@ public final class ViewRootImpl implements ViewParent,
mTmpFrames.compatScale = compatScale[0];
mInvCompatScale = 1f / compatScale[0];
} catch (RemoteException | RuntimeException e) {
- mAdded = false;
+ if (!fixViewRootCallTrace()) {
+ mAdded = false;
+ }
mView = null;
mAttachInfo.mRootView = null;
mFallbackEventHandler.setView(null);
@@ -1670,7 +1677,9 @@ public final class ViewRootImpl implements ViewParent,
if (DEBUG_LAYOUT) Log.v(mTag, "Added window " + mWindow);
if (res < WindowManagerGlobal.ADD_OKAY) {
mAttachInfo.mRootView = null;
- mAdded = false;
+ if (!fixViewRootCallTrace()) {
+ mAdded = false;
+ }
mFallbackEventHandler.setView(null);
unscheduleTraversals();
setAccessibilityFocus(null, null);
@@ -1779,6 +1788,9 @@ public final class ViewRootImpl implements ViewParent,
mFirstInputStage = nativePreImeStage;
mFirstPostImeInputStage = earlyPostImeStage;
mPendingInputEventQueueLengthCounterName = "aq:pending:" + counterSuffix;
+ if (fixViewRootCallTrace()) {
+ mAdded = true;
+ }
if (!mRemoved || !mAppVisible) {
AnimationHandler.requestAnimatorsEnabled(mAppVisible, this);
@@ -2078,12 +2090,21 @@ public final class ViewRootImpl implements ViewParent,
// preference for dark mode in configuration.uiMode. Instead, we assume that both
// force invert and the system's dark theme are enabled.
if (shouldApplyForceInvertDark()) {
- final boolean isLightTheme =
- a.getBoolean(R.styleable.Theme_isLightTheme, false);
- // TODO: b/372558459 - Also check the background ColorDrawable color lightness
// TODO: b/368725782 - Use hwui color area detection instead of / in
// addition to these heuristics.
- if (isLightTheme) {
+ final boolean isLightTheme =
+ a.getBoolean(R.styleable.Theme_isLightTheme, false);
+ final boolean isBackgroundColorLight;
+ if (mView != null && mView.getBackground()
+ instanceof ColorDrawable colorDrawable) {
+ isBackgroundColorLight =
+ !ContrastColorUtil.isColorDarkLab(colorDrawable.getColor());
+ } else {
+ // Treat unknown as light, so that only isLightTheme is used to determine
+ // force dark treatment.
+ isBackgroundColorLight = true;
+ }
+ if (isLightTheme && isBackgroundColorLight) {
return ForceDarkType.FORCE_INVERT_COLOR_DARK;
} else {
return ForceDarkType.NONE;
@@ -10270,6 +10291,8 @@ public final class ViewRootImpl implements ViewParent,
try {
mWindowSession.notifyImeWindowVisibilityChangedFromClient(mWindow, visible, statsToken);
} catch (RemoteException e) {
+ ImeTracker.forLogging().onFailed(statsToken,
+ ImeTracker.PHASE_CLIENT_NOTIFY_IME_VISIBILITY_CHANGED);
e.rethrowFromSystemServer();
}
}
@@ -11505,12 +11528,24 @@ public final class ViewRootImpl implements ViewParent,
// Search through View-tree
View rootView = getView();
- if (rootView != null) {
- Point point = new Point();
- Rect rect = new Rect(0, 0, rootView.getWidth(), rootView.getHeight());
- getChildVisibleRect(rootView, rect, point);
- rootView.dispatchScrollCaptureSearch(rect, point, results::addTarget);
+ if (rootView == null) {
+ ScrollCaptureResponse.Builder response = new ScrollCaptureResponse.Builder();
+ response.setWindowTitle(getTitle().toString());
+ response.setPackageName(mContext.getPackageName());
+ response.setDescription("The root view was null");
+ try {
+ listener.onScrollCaptureResponse(response.build());
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to send scroll capture search result", e);
+ }
+ return;
}
+
+ Point point = new Point();
+ Rect rect = new Rect(0, 0, rootView.getWidth(), rootView.getHeight());
+ getChildVisibleRect(rootView, rect, point);
+ rootView.dispatchScrollCaptureSearch(rect, point, results::addTarget);
+
Runnable onComplete = () -> dispatchScrollCaptureSearchResponse(listener, results);
results.setOnCompleteListener(onComplete);
if (!results.isComplete()) {
@@ -11535,6 +11570,16 @@ public final class ViewRootImpl implements ViewParent,
pw.flush();
response.addMessage(writer.toString());
+ if (mView == null) {
+ response.setDescription("The root view disappeared!");
+ try {
+ listener.onScrollCaptureResponse(response.build());
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to send scroll capture search result", e);
+ }
+ return;
+ }
+
if (selectedTarget == null) {
response.setDescription("No scrollable targets found in window");
try {
@@ -11561,6 +11606,7 @@ public final class ViewRootImpl implements ViewParent,
boundsOnScreen.set(0, 0, mView.getWidth(), mView.getHeight());
boundsOnScreen.offset(mAttachInfo.mTmpLocation[0], mAttachInfo.mTmpLocation[1]);
response.setWindowBounds(boundsOnScreen);
+ Log.d(TAG, "ScrollCaptureSearchResponse: " + response);
// Create a connection and return it to the caller
ScrollCaptureConnection connection = new ScrollCaptureConnection(
diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java
index 9d21f1aff0c3..1ba3a74b8b2b 100644
--- a/core/java/android/view/WindowManager.java
+++ b/core/java/android/view/WindowManager.java
@@ -80,9 +80,6 @@ import static android.view.WindowLayoutParamsProto.WINDOW_ANIMATIONS;
import static android.view.WindowLayoutParamsProto.X;
import static android.view.WindowLayoutParamsProto.Y;
-import static com.android.hardware.input.Flags.FLAG_OVERRIDE_POWER_KEY_BEHAVIOR_IN_FOCUSED_WINDOW;
-import static com.android.hardware.input.Flags.overridePowerKeyBehaviorInFocusedWindow;
-
import android.Manifest.permission;
import android.annotation.CallbackExecutor;
import android.annotation.FlaggedApi;
@@ -4549,29 +4546,6 @@ public interface WindowManager extends ViewManager {
public static final int INPUT_FEATURE_SENSITIVE_FOR_PRIVACY = 1 << 3;
/**
- * Input feature used to indicate that the system should send power key events to this
- * window when it's in the foreground. The window can override the double press power key
- * gesture behavior.
- *
- * A double press gesture is defined as two
- * {@link KeyEvent.Callback#onKeyDown(int, KeyEvent)} events within a time span defined by
- * {@link ViewConfiguration#getMultiPressTimeout()}.
- *
- * Note: While the window may receive all power key {@link KeyEvent}s, it can only
- * override the double press gesture behavior. The system will perform default behavior for
- * single, long-press and other multi-press gestures, regardless of if the app handles the
- * key or not.
- *
- * To override the default behavior for double press, the app must return true for the
- * second {@link KeyEvent.Callback#onKeyDown(int, KeyEvent)}. If the app returns false, the
- * system behavior will be performed for double press.
- * @hide
- */
- @RequiresPermission(permission.OVERRIDE_SYSTEM_KEY_BEHAVIOR_IN_FOCUSED_WINDOW)
- public static final int
- INPUT_FEATURE_RECEIVE_POWER_KEY_DOUBLE_PRESS = 1 << 4;
-
- /**
* An internal annotation for flags that can be specified to {@link #inputFeatures}.
*
* NOTE: These are not the same as {@link android.os.InputConfig} flags.
@@ -4583,8 +4557,7 @@ public interface WindowManager extends ViewManager {
INPUT_FEATURE_NO_INPUT_CHANNEL,
INPUT_FEATURE_DISABLE_USER_ACTIVITY,
INPUT_FEATURE_SPY,
- INPUT_FEATURE_SENSITIVE_FOR_PRIVACY,
- INPUT_FEATURE_RECEIVE_POWER_KEY_DOUBLE_PRESS
+ INPUT_FEATURE_SENSITIVE_FOR_PRIVACY
})
public @interface InputFeatureFlags {
}
@@ -4874,44 +4847,6 @@ public interface WindowManager extends ViewManager {
}
/**
- * Specifies if the system should send power key events to this window when it's in the
- * foreground, with only the double tap gesture behavior being overrideable.
- *
- * @param enabled if true, the system should send power key events to this window when it's
- * in the foreground, with only the power key double tap gesture being
- * overrideable.
- * @hide
- */
- @SystemApi
- @RequiresPermission(permission.OVERRIDE_SYSTEM_KEY_BEHAVIOR_IN_FOCUSED_WINDOW)
- @FlaggedApi(FLAG_OVERRIDE_POWER_KEY_BEHAVIOR_IN_FOCUSED_WINDOW)
- public void setReceivePowerKeyDoublePressEnabled(boolean enabled) {
- if (enabled) {
- inputFeatures
- |= INPUT_FEATURE_RECEIVE_POWER_KEY_DOUBLE_PRESS;
- } else {
- inputFeatures
- &= ~INPUT_FEATURE_RECEIVE_POWER_KEY_DOUBLE_PRESS;
- }
- }
-
- /**
- * Returns whether or not the system should send power key events to this window when it's
- * in the foreground, with only the double tap gesture being overrideable.
- *
- * @return if the system should send power key events to this window when it's in the
- * foreground, with only the double tap gesture being overrideable.
- * @hide
- */
- @SystemApi
- @RequiresPermission(permission.OVERRIDE_SYSTEM_KEY_BEHAVIOR_IN_FOCUSED_WINDOW)
- @FlaggedApi(FLAG_OVERRIDE_POWER_KEY_BEHAVIOR_IN_FOCUSED_WINDOW)
- public boolean isReceivePowerKeyDoublePressEnabled() {
- return (inputFeatures
- & INPUT_FEATURE_RECEIVE_POWER_KEY_DOUBLE_PRESS) != 0;
- }
-
- /**
* Specifies that the window should be considered a trusted system overlay. Trusted system
* overlays are ignored when considering whether windows are obscured during input
* dispatch. Requires the {@link android.Manifest.permission#INTERNAL_SYSTEM_WINDOW}
@@ -6312,16 +6247,6 @@ public interface WindowManager extends ViewManager {
inputFeatures &= ~INPUT_FEATURE_SPY;
features.add("INPUT_FEATURE_SPY");
}
- if (overridePowerKeyBehaviorInFocusedWindow()) {
- if ((inputFeatures
- & INPUT_FEATURE_RECEIVE_POWER_KEY_DOUBLE_PRESS)
- != 0) {
- inputFeatures
- &=
- ~INPUT_FEATURE_RECEIVE_POWER_KEY_DOUBLE_PRESS;
- features.add("INPUT_FEATURE_RECEIVE_POWER_KEY_DOUBLE_PRESS");
- }
- }
if (inputFeatures != 0) {
features.add(Integer.toHexString(inputFeatures));
}
diff --git a/core/java/android/view/XrWindowProperties.java b/core/java/android/view/XrWindowProperties.java
index 23021a563393..c02d7a9bb8c5 100644
--- a/core/java/android/view/XrWindowProperties.java
+++ b/core/java/android/view/XrWindowProperties.java
@@ -27,7 +27,7 @@ public final class XrWindowProperties {
private XrWindowProperties() {}
/**
- * Both Application and activity level
+ * Application and Activity level
* {@link android.content.pm.PackageManager.Property PackageManager.Property} for an app to
* inform the system of the activity launch mode in XR. When it is declared at the application
* level, all activities are set to the defined value, unless it is overridden at the activity
@@ -105,7 +105,7 @@ public final class XrWindowProperties {
"XR_ACTIVITY_START_MODE_HOME_SPACE";
/**
- * Both Application and activity level
+ * Application and Activity level
* {@link android.content.pm.PackageManager.Property PackageManager.Property} for an app to
* inform the system of the type of safety boundary recommended for the activity. When it is
* declared at the application level, all activities are set to the defined value, unless it is
@@ -156,4 +156,30 @@ public final class XrWindowProperties {
*/
@FlaggedApi(android.xr.Flags.FLAG_XR_MANIFEST_ENTRIES)
public static final String XR_BOUNDARY_TYPE_LARGE = "XR_BOUNDARY_TYPE_LARGE";
+
+ /**
+ * Application and Activity level
+ * {@link android.content.pm.PackageManager.Property PackageManager.Property} to inform the
+ * system if it should play a system provided default animation when the app requests to enter
+ * or exit <a
+ * href="https://developer.android.com/develop/xr/jetpack-xr-sdk/transition-home-space-to-full-space">managed
+ * full space mode</a> in XR. When set to {@code true}, the system provided default animation is
+ * not played and the app is responsible for playing a custom enter or exit animation. When it
+ * is declared at the application level, all activities are set to the defined value, unless it
+ * is overridden at the activity level.
+ *
+ * <p>The default value is {@code false}.
+ *
+ * <p><b>Syntax:</b>
+ * <pre>
+ * &lt;application&gt;
+ * &lt;property
+ * android:name="android.window.PROPERTY_XR_USES_CUSTOM_FULL_SPACE_MANAGED_ANIMATION"
+ * android:value="false|true"/&gt;
+ * &lt;/application&gt;
+ * </pre>
+ */
+ @FlaggedApi(android.xr.Flags.FLAG_XR_MANIFEST_ENTRIES)
+ public static final String PROPERTY_XR_USES_CUSTOM_FULL_SPACE_MANAGED_ANIMATION =
+ "android.window.PROPERTY_XR_USES_CUSTOM_FULL_SPACE_MANAGED_ANIMATION";
}
diff --git a/core/java/android/view/autofill/AutofillFeatureFlags.java b/core/java/android/view/autofill/AutofillFeatureFlags.java
index 0814e23eea87..816d2debf87e 100644
--- a/core/java/android/view/autofill/AutofillFeatureFlags.java
+++ b/core/java/android/view/autofill/AutofillFeatureFlags.java
@@ -422,7 +422,7 @@ public class AutofillFeatureFlags {
*
* @hide
*/
- public static final boolean DEFAULT_SESSION_FILL_EVENT_HISTORY_ENABLED = false;
+ public static final boolean DEFAULT_SESSION_FILL_EVENT_HISTORY_ENABLED = true;
/**
* @hide
diff --git a/core/java/android/view/inputmethod/ImeTracker.java b/core/java/android/view/inputmethod/ImeTracker.java
index b1ba8b32d2f4..64f41c7a2987 100644
--- a/core/java/android/view/inputmethod/ImeTracker.java
+++ b/core/java/android/view/inputmethod/ImeTracker.java
@@ -232,6 +232,8 @@ public interface ImeTracker {
PHASE_WM_NOTIFY_HIDE_ANIMATION_FINISHED,
PHASE_WM_UPDATE_DISPLAY_WINDOW_ANIMATING_TYPES,
PHASE_CLIENT_ON_CONTROLS_CHANGED,
+ PHASE_SERVER_IME_INVOKER,
+ PHASE_SERVER_CLIENT_INVOKER,
})
@Retention(RetentionPolicy.SOURCE)
@interface Phase {}
@@ -473,6 +475,10 @@ public interface ImeTracker {
/** InsetsController received a control for the IME. */
int PHASE_CLIENT_ON_CONTROLS_CHANGED =
ImeProtoEnums.PHASE_CLIENT_ON_CONTROLS_CHANGED;
+ /** Reached the IME invoker on the server. */
+ int PHASE_SERVER_IME_INVOKER = ImeProtoEnums.PHASE_SERVER_IME_INVOKER;
+ /** Reached the IME client invoker on the server. */
+ int PHASE_SERVER_CLIENT_INVOKER = ImeProtoEnums.PHASE_SERVER_CLIENT_INVOKER;
/**
* Called when an IME request is started.
diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java
index b3bd89c2a87d..7d7570d48970 100644
--- a/core/java/android/view/inputmethod/InputMethodManager.java
+++ b/core/java/android/view/inputmethod/InputMethodManager.java
@@ -620,6 +620,12 @@ public final class InputMethodManager {
@GuardedBy("mH")
private CompletionInfo[] mCompletions;
+ /**
+ * Tracks last pending {@link #startInputInner(int, IBinder, int, int, int)} sequenceId.
+ */
+ @GuardedBy("mH")
+ private int mLastPendingStartSeqId = INVALID_SEQ_ID;
+
// Cursor position on the screen.
@GuardedBy("mH")
@UnsupportedAppUsage
@@ -652,6 +658,8 @@ public final class InputMethodManager {
private static final String CACHE_KEY_CONNECTIONLESS_STYLUS_HANDWRITING_PROPERTY =
"cache_key.system_server.connectionless_stylus_handwriting";
+ static final int INVALID_SEQ_ID = -1;
+
@GuardedBy("mH")
private int mCursorSelStart;
@GuardedBy("mH")
@@ -1193,12 +1201,18 @@ public final class InputMethodManager {
case MSG_START_INPUT_RESULT: {
final InputBindResult res = (InputBindResult) msg.obj;
final int startInputSeq = msg.arg1;
- if (res == null) {
- // IMMS logs .wtf already.
- return;
- }
- if (DEBUG) Log.v(TAG, "Starting input: Bind result=" + res);
synchronized (mH) {
+ if (mLastPendingStartSeqId == startInputSeq) {
+ // last pending startInput has been completed. reset.
+ mLastPendingStartSeqId = INVALID_SEQ_ID;
+ }
+
+ if (res == null) {
+ // IMMS logs .wtf already.
+ return;
+ }
+
+ if (DEBUG) Log.v(TAG, "Starting input: Bind result=" + res);
if (res.id != null) {
updateInputChannelLocked(res.channel);
mCurMethod = res.method; // for @UnsupportedAppUsage
@@ -2220,6 +2234,7 @@ public final class InputMethodManager {
}
mCompletions = null;
mServedConnecting = false;
+ mLastPendingStartSeqId = INVALID_SEQ_ID;
clearConnectionLocked();
}
mReportInputConnectionOpenedRunner = null;
@@ -3274,6 +3289,9 @@ public final class InputMethodManager {
* @param view The view whose text has changed.
*/
public void restartInput(View view) {
+ if (DEBUG) {
+ Log.d(TAG, "restartInput()");
+ }
// Re-dispatch if there is a context mismatch.
final InputMethodManager fallbackImm = getFallbackInputMethodManagerIfNecessary(view);
if (fallbackImm != null) {
@@ -3351,6 +3369,9 @@ public final class InputMethodManager {
*/
public void invalidateInput(@NonNull View view) {
Objects.requireNonNull(view);
+ if (DEBUG) {
+ Log.d(TAG, "IMM#invaldateInput()");
+ }
// Re-dispatch if there is a context mismatch.
final InputMethodManager fallbackImm = getFallbackInputMethodManagerIfNecessary(view);
@@ -3363,7 +3384,8 @@ public final class InputMethodManager {
if (mServedInputConnection == null || getServedViewLocked() != view) {
return;
}
- mServedInputConnection.scheduleInvalidateInput();
+ mServedInputConnection.scheduleInvalidateInput(
+ mLastPendingStartSeqId != INVALID_SEQ_ID);
}
}
@@ -3532,7 +3554,7 @@ public final class InputMethodManager {
? editorInfo.targetInputMethodUser.getIdentifier() : UserHandle.myUserId();
Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "IMM.startInputOrWindowGainedFocus");
- int startInputSeq = -1;
+ int startInputSeq = INVALID_SEQ_ID;
if (Flags.useZeroJankProxy()) {
// async result delivered via MSG_START_INPUT_RESULT.
startInputSeq = IInputMethodManagerGlobalInvoker.startInputOrWindowGainedFocusAsync(
@@ -3557,6 +3579,9 @@ public final class InputMethodManager {
// initialized and ready for use.
if (ic != null) {
final int seqId = startInputSeq;
+ if (Flags.invalidateInputCallsRestart()) {
+ mLastPendingStartSeqId = seqId;
+ }
mReportInputConnectionOpenedRunner =
new ReportInputConnectionOpenedRunner(startInputSeq) {
@Override
@@ -5047,6 +5072,7 @@ public final class InputMethodManager {
}
p.println(" mServedInputConnection=" + mServedInputConnection);
p.println(" mServedInputConnectionHandler=" + mServedInputConnectionHandler);
+ p.println(" mLastPendingStartSeqId=" + mLastPendingStartSeqId);
p.println(" mCompletions=" + Arrays.toString(mCompletions));
p.println(" mCursorRect=" + mCursorRect);
p.println(" mCursorSelStart=" + mCursorSelStart
diff --git a/core/java/android/view/inputmethod/RemoteInputConnectionImpl.java b/core/java/android/view/inputmethod/RemoteInputConnectionImpl.java
index ced27d6d4886..3e8575324352 100644
--- a/core/java/android/view/inputmethod/RemoteInputConnectionImpl.java
+++ b/core/java/android/view/inputmethod/RemoteInputConnectionImpl.java
@@ -16,6 +16,8 @@
package android.view.inputmethod;
+import static android.view.inputmethod.InputMethodManager.INVALID_SEQ_ID;
+
import static com.android.internal.inputmethod.InputConnectionProtoDumper.buildGetCursorCapsModeProto;
import static com.android.internal.inputmethod.InputConnectionProtoDumper.buildGetExtractedTextProto;
import static com.android.internal.inputmethod.InputConnectionProtoDumper.buildGetSelectedTextProto;
@@ -276,8 +278,19 @@ final class RemoteInputConnectionImpl extends IRemoteInputConnection.Stub {
* make sure that application code is not modifying text context in a reentrant manner.</p>
*/
public void scheduleInvalidateInput() {
+ scheduleInvalidateInput(false /* isRestarting */);
+ }
+
+ /**
+ * @see #scheduleInvalidateInput()
+ * @param isRestarting when {@code true}, there is an in-progress restartInput that could race
+ * with {@link InputMethodManager#invalidateInput(View)}. To prevent race,
+ * fallback to calling {@link InputMethodManager#restartInput(View)}.
+ */
+ void scheduleInvalidateInput(boolean isRestarting) {
if (mHasPendingInvalidation.compareAndSet(false, true)) {
- final int nextSessionId = mCurrentSessionId.incrementAndGet();
+ final int nextSessionId =
+ isRestarting ? INVALID_SEQ_ID : mCurrentSessionId.incrementAndGet();
// By calling InputConnection#takeSnapshot() directly from the message loop, we can make
// sure that application code is not modifying text context in a reentrant manner.
// e.g. We may see methods like EditText#setText() in the callstack here.
@@ -330,6 +343,14 @@ final class RemoteInputConnectionImpl extends IRemoteInputConnection.Stub {
}
}
}
+ if (isRestarting) {
+ if (DEBUG) {
+ Log.d(TAG, "scheduleInvalidateInput called with ongoing restartInput."
+ + " Fallback to calling restartInput().");
+ }
+ mParentInputMethodManager.restartInput(view);
+ return;
+ }
if (!alwaysTrueEndBatchEditDetected) {
final TextSnapshot textSnapshot = ic.takeSnapshot();
diff --git a/core/java/android/widget/RemoteViews.java b/core/java/android/widget/RemoteViews.java
index 3dfbc2517986..1e0c4906e6ed 100644
--- a/core/java/android/widget/RemoteViews.java
+++ b/core/java/android/widget/RemoteViews.java
@@ -548,6 +548,25 @@ public class RemoteViews implements Parcelable, Filter {
}
/**
+ * Set a view tag associating a View with an ID to be used for widget interaction usage events
+ * ({@link android.app.usage.UsageEvents.Event}). When this RemoteViews is applied to a bound
+ * widget, any clicks or scrolls on the tagged view will be reported to
+ * {@link android.app.usage.UsageStatsManager} using this tag.
+ *
+ * @param viewId ID of the View whose tag will be set
+ * @param tag The integer tag to use for the event
+ *
+ * @see android.appwidget.AppWidgetManager#EVENT_TYPE_WIDGET_INTERACTION
+ * @see android.appwidget.AppWidgetManager#EXTRA_EVENT_CLICKED_VIEWS
+ * @see android.appwidget.AppWidgetManager#EXTRA_EVENT_SCROLLED_VIEWS
+ * @see android.app.usage.UsageStatsManager#queryEventsForSelf
+ */
+ @FlaggedApi(Flags.FLAG_ENGAGEMENT_METRICS)
+ public void setUsageEventTag(@IdRes int viewId, int tag) {
+ addAction(new SetIntTagAction(viewId, com.android.internal.R.id.remoteViewsMetricsId, tag));
+ }
+
+ /**
* Set that it is disallowed to reapply another remoteview with the same layout as this view.
* This should be done if an action is destroying the view tree of the base layout.
*
@@ -666,6 +685,14 @@ public class RemoteViews implements Parcelable, Filter {
View view,
PendingIntent pendingIntent,
RemoteResponse response);
+
+ /**
+ * Invoked when an AbsListView is scrolled.
+ * @param view view that was scrolled
+ *
+ * @hide
+ */
+ default void onScroll(@NonNull AbsListView view) {}
}
/**
@@ -1313,6 +1340,21 @@ public class RemoteViews implements Parcelable, Filter {
// a type error.
throw new ActionException(throwable);
}
+ if (adapterView instanceof AbsListView listView) {
+ listView.setOnScrollListener(new AbsListView.OnScrollListener() {
+ @Override
+ public void onScrollStateChanged(AbsListView view, int scrollState) {
+ if (scrollState != SCROLL_STATE_IDLE) {
+ params.handler.onScroll(view);
+ }
+ }
+
+ @Override
+ public void onScroll(AbsListView view, int firstVisibleItem,
+ int visibleItemCount, int totalItemCount) {
+ }
+ });
+ }
}
@Override
@@ -1804,6 +1846,19 @@ public class RemoteViews implements Parcelable, Filter {
AbsListView v = (AbsListView) target;
v.setRemoteViewsAdapter(mIntent, mIsAsync);
v.setRemoteViewsInteractionHandler(params.handler);
+ v.setOnScrollListener(new AbsListView.OnScrollListener() {
+ @Override
+ public void onScrollStateChanged(AbsListView view, int scrollState) {
+ if (scrollState != SCROLL_STATE_IDLE) {
+ params.handler.onScroll(view);
+ }
+ }
+
+ @Override
+ public void onScroll(AbsListView view, int firstVisibleItem,
+ int visibleItemCount, int totalItemCount) {
+ }
+ });
} else if (target instanceof AdapterViewAnimator) {
AdapterViewAnimator v = (AdapterViewAnimator) target;
v.setRemoteViewsAdapter(mIntent, mIsAsync);
@@ -1894,7 +1949,8 @@ public class RemoteViews implements Parcelable, Filter {
target.setTagInternal(com.android.internal.R.id.fillInIntent, null);
return;
}
- target.setOnClickListener(v -> mResponse.handleViewInteraction(v, params.handler));
+ target.setOnClickListener(v ->
+ mResponse.handleViewInteraction(v, params.handler));
}
@Override
diff --git a/core/java/android/window/DesktopModeFlags.java b/core/java/android/window/DesktopModeFlags.java
index fdaa41c63343..1ea4ce10c60e 100644
--- a/core/java/android/window/DesktopModeFlags.java
+++ b/core/java/android/window/DesktopModeFlags.java
@@ -55,16 +55,16 @@ public enum DesktopModeFlags {
ENABLE_CAPTION_COMPAT_INSET_FORCE_CONSUMPTION_ALWAYS(
Flags::enableCaptionCompatInsetForceConsumptionAlways, true),
ENABLE_CASCADING_WINDOWS(Flags::enableCascadingWindows, true),
- ENABLE_DESKTOP_APP_HANDLE_ANIMATION(Flags::enableDesktopAppHandleAnimation, false),
+ ENABLE_DESKTOP_APP_HANDLE_ANIMATION(Flags::enableDesktopAppHandleAnimation, true),
ENABLE_DESKTOP_APP_LAUNCH_ALTTAB_TRANSITIONS_BUGFIX(
Flags::enableDesktopAppLaunchAlttabTransitionsBugfix, true),
ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS_BUGFIX(Flags::enableDesktopAppLaunchTransitionsBugfix,
true),
ENABLE_DESKTOP_CLOSE_SHORTCUT_BUGFIX(Flags::enableDesktopCloseShortcutBugfix, false),
ENABLE_DESKTOP_COMPAT_UI_VISIBILITY_STATUS(Flags::enableCompatUiVisibilityStatus, true),
- ENABLE_DESKTOP_IMMERSIVE_DRAG_BUGFIX(Flags::enableDesktopImmersiveDragBugfix, false),
+ ENABLE_DESKTOP_IMMERSIVE_DRAG_BUGFIX(Flags::enableDesktopImmersiveDragBugfix, true),
ENABLE_DESKTOP_INDICATOR_IN_SEPARATE_THREAD_BUGFIX(
- Flags::enableDesktopIndicatorInSeparateThreadBugfix, false),
+ Flags::enableDesktopIndicatorInSeparateThreadBugfix, true),
ENABLE_DESKTOP_OPENING_DEEPLINK_MINIMIZE_ANIMATION_BUGFIX(
Flags::enableDesktopOpeningDeeplinkMinimizeAnimationBugfix, true),
ENABLE_DESKTOP_RECENTS_TRANSITIONS_CORNERS_BUGFIX(
@@ -111,13 +111,13 @@ public enum DesktopModeFlags {
ENABLE_FULLY_IMMERSIVE_IN_DESKTOP(Flags::enableFullyImmersiveInDesktop, true),
ENABLE_HANDLE_INPUT_FIX(Flags::enableHandleInputFix, true),
ENABLE_HOLD_TO_DRAG_APP_HANDLE(Flags::enableHoldToDragAppHandle, true),
- ENABLE_INPUT_LAYER_TRANSITION_FIX(Flags::enableInputLayerTransitionFix, false),
+ ENABLE_INPUT_LAYER_TRANSITION_FIX(Flags::enableInputLayerTransitionFix, true),
ENABLE_MINIMIZE_BUTTON(Flags::enableMinimizeButton, true),
ENABLE_MODALS_FULLSCREEN_WITH_PERMISSIONS(Flags::enableModalsFullscreenWithPermission, true),
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_REQUEST_FULLSCREEN_BUGFIX(Flags::enableRequestFullscreenBugfix, true),
ENABLE_RESIZING_METRICS(Flags::enableResizingMetrics, true),
ENABLE_RESTORE_TO_PREVIOUS_SIZE_FROM_DESKTOP_IMMERSIVE(
Flags::enableRestoreToPreviousSizeFromDesktopImmersive, true),
diff --git a/core/java/android/window/flags/lse_desktop_experience.aconfig b/core/java/android/window/flags/lse_desktop_experience.aconfig
index 0d87b73a5e03..07882b6e5c67 100644
--- a/core/java/android/window/flags/lse_desktop_experience.aconfig
+++ b/core/java/android/window/flags/lse_desktop_experience.aconfig
@@ -1006,3 +1006,23 @@ flag {
purpose: PURPOSE_BUGFIX
}
}
+
+flag {
+ name: "enable_desktop_swipe_back_minimize_animation_bugfix"
+ namespace: "lse_desktop_experience"
+ description: "Enabling a minimize animation when a window is minimized via a swipe-back navigation gesture in Desktop Windowing mode."
+ bug: "359343764"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+flag {
+ name: "enable_desktop_close_task_animation_in_dtc_bugfix"
+ namespace: "lse_desktop_experience"
+ description: "Enables bugfix to handle close task animation within DesktopTasksController."
+ bug: "403345083"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
diff --git a/core/java/android/window/flags/windowing_frontend.aconfig b/core/java/android/window/flags/windowing_frontend.aconfig
index 59dd32258d8c..99f9929e1071 100644
--- a/core/java/android/window/flags/windowing_frontend.aconfig
+++ b/core/java/android/window/flags/windowing_frontend.aconfig
@@ -388,6 +388,17 @@ flag {
}
flag {
+ name: "remove_depart_target_from_motion"
+ namespace: "windowing_frontend"
+ description: "Remove DepartingAnimationTarget from BackMotionEvent"
+ bug: "395035430"
+ is_fixed_read_only: true
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+flag {
name: "predictive_back_default_enable_sdk_36"
namespace: "systemui"
description: "Enable Predictive Back by default with targetSdk>=36"
diff --git a/core/java/android/window/flags/windowing_sdk.aconfig b/core/java/android/window/flags/windowing_sdk.aconfig
index e2eb193293c9..f162b1f40d8e 100644
--- a/core/java/android/window/flags/windowing_sdk.aconfig
+++ b/core/java/android/window/flags/windowing_sdk.aconfig
@@ -163,3 +163,25 @@ flag {
purpose: PURPOSE_BUGFIX
}
}
+
+flag {
+ namespace: "windowing_sdk"
+ name: "exclude_task_from_recents"
+ description: "Enables WCT to set whether the task should be excluded from the Recents list"
+ bug: "404726350"
+ is_fixed_read_only: true
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+flag {
+ namespace: "windowing_sdk"
+ name: "fix_view_root_call_trace"
+ description: "Do not set mAdded=true unless #setView finished successfully"
+ bug: "385705687"
+ is_fixed_read_only: true
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
diff --git a/core/java/com/android/internal/content/FileSystemProvider.java b/core/java/com/android/internal/content/FileSystemProvider.java
index 0801dd8c0bd8..fc74a179f66e 100644
--- a/core/java/com/android/internal/content/FileSystemProvider.java
+++ b/core/java/com/android/internal/content/FileSystemProvider.java
@@ -119,7 +119,7 @@ public abstract class FileSystemProvider extends DocumentsProvider {
* Callback indicating that the given document has been deleted or moved. This gives
* the provider a hook to revoke the uri permissions.
*/
- protected void onDocIdDeleted(String docId) {
+ protected void onDocIdDeleted(String docId, boolean shouldRevokeUriPermission) {
// Default is no-op
}
@@ -292,7 +292,6 @@ public abstract class FileSystemProvider extends DocumentsProvider {
final String afterDocId = getDocIdForFile(after);
onDocIdChanged(docId);
- onDocIdDeleted(docId);
onDocIdChanged(afterDocId);
final File afterVisibleFile = getFileForDocId(afterDocId, true);
@@ -301,6 +300,10 @@ public abstract class FileSystemProvider extends DocumentsProvider {
updateMediaStore(getContext(), afterVisibleFile);
if (!TextUtils.equals(docId, afterDocId)) {
+ // DocumentsProvider handles the revoking / granting uri permission for the docId and
+ // the afterDocId in the renameDocument case. Don't need to call revokeUriPermission
+ // for the docId here.
+ onDocIdDeleted(docId, /* shouldRevokeUriPermission */ false);
return afterDocId;
} else {
return null;
@@ -324,7 +327,7 @@ public abstract class FileSystemProvider extends DocumentsProvider {
final String docId = getDocIdForFile(after);
onDocIdChanged(sourceDocumentId);
- onDocIdDeleted(sourceDocumentId);
+ onDocIdDeleted(sourceDocumentId, /* shouldRevokeUriPermission */ true);
onDocIdChanged(docId);
// update the database
updateMediaStore(getContext(), visibleFileBefore);
@@ -362,7 +365,7 @@ public abstract class FileSystemProvider extends DocumentsProvider {
}
onDocIdChanged(docId);
- onDocIdDeleted(docId);
+ onDocIdDeleted(docId, /* shouldRevokeUriPermission */ true);
updateMediaStore(getContext(), visibleFile);
}
diff --git a/core/java/com/android/internal/inputmethod/InputMethodDebug.java b/core/java/com/android/internal/inputmethod/InputMethodDebug.java
index 4d5e67ab8fde..9cdfc02e2e28 100644
--- a/core/java/com/android/internal/inputmethod/InputMethodDebug.java
+++ b/core/java/com/android/internal/inputmethod/InputMethodDebug.java
@@ -305,6 +305,8 @@ public final class InputMethodDebug {
return "HIDE_INPUT_TARGET_CHANGED";
case SoftInputShowHideReason.HIDE_WINDOW_LOST_FOCUS:
return "HIDE_WINDOW_LOST_FOCUS";
+ case SoftInputShowHideReason.IME_REQUESTED_CHANGED_LISTENER:
+ return "IME_REQUESTED_CHANGED_LISTENER";
default:
return "Unknown=" + reason;
}
diff --git a/core/java/com/android/internal/inputmethod/SoftInputShowHideReason.java b/core/java/com/android/internal/inputmethod/SoftInputShowHideReason.java
index cf0580c2f021..8b4371ea8478 100644
--- a/core/java/com/android/internal/inputmethod/SoftInputShowHideReason.java
+++ b/core/java/com/android/internal/inputmethod/SoftInputShowHideReason.java
@@ -92,6 +92,7 @@ import java.lang.annotation.Retention;
SoftInputShowHideReason.SHOW_INPUT_TARGET_CHANGED,
SoftInputShowHideReason.HIDE_INPUT_TARGET_CHANGED,
SoftInputShowHideReason.HIDE_WINDOW_LOST_FOCUS,
+ SoftInputShowHideReason.IME_REQUESTED_CHANGED_LISTENER,
})
public @interface SoftInputShowHideReason {
/** Default, undefined reason. */
@@ -422,4 +423,10 @@ public @interface SoftInputShowHideReason {
/** Hide soft input when the window lost focus. */
int HIDE_WINDOW_LOST_FOCUS = ImeProtoEnums.REASON_HIDE_WINDOW_LOST_FOCUS;
+
+ /**
+ * Show / Hide soft input by
+ * {@link com.android.server.wm.WindowManagerInternal.OnImeRequestedChangedListener}
+ */
+ int IME_REQUESTED_CHANGED_LISTENER = ImeProtoEnums.REASON_IME_REQUESTED_CHANGED_LISTENER;
}
diff --git a/core/java/com/android/internal/policy/DecorView.java b/core/java/com/android/internal/policy/DecorView.java
index 3d81e4fc7acd..e20a52b24485 100644
--- a/core/java/com/android/internal/policy/DecorView.java
+++ b/core/java/com/android/internal/policy/DecorView.java
@@ -120,7 +120,6 @@ import com.android.internal.view.menu.MenuHelper;
import com.android.internal.widget.ActionBarContextView;
import com.android.internal.widget.BackgroundFallback;
import com.android.internal.widget.floatingtoolbar.FloatingToolbar;
-import com.android.window.flags.Flags;
import java.util.List;
import java.util.concurrent.Executor;
@@ -1004,8 +1003,7 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind
public void onWindowSystemUiVisibilityChanged(int visible) {
updateColorViews(null /* insets */, true /* animate */);
- if (!Flags.actionModeEdgeToEdge()
- && mStatusGuard != null && mStatusGuard.getVisibility() == VISIBLE) {
+ if (mStatusGuard != null && mStatusGuard.getVisibility() == VISIBLE) {
updateStatusGuardColor();
}
}
@@ -1042,7 +1040,7 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind
}
mFrameOffsets.set(insets.getSystemWindowInsetsAsRect());
insets = updateColorViews(insets, true /* animate */);
- insets = updateActionModeInsets(insets);
+ insets = updateStatusGuard(insets);
if (getForeground() != null) {
drawableChanged();
}
@@ -1594,7 +1592,7 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind
}
}
- private WindowInsets updateActionModeInsets(WindowInsets insets) {
+ private WindowInsets updateStatusGuard(WindowInsets insets) {
boolean showStatusGuard = false;
// Show the status guard when the non-overlay contextual action bar is showing
if (mPrimaryActionModeView != null) {
@@ -1610,78 +1608,54 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind
final Rect rect = mTempRect;
// Apply the insets that have not been applied by the contentParent yet.
- final WindowInsets innerInsets =
+ WindowInsets innerInsets =
mWindow.mContentParent.computeSystemWindowInsets(insets, rect);
- final boolean consumesSystemWindowInsetsTop;
- if (Flags.actionModeEdgeToEdge()) {
- final Insets newPadding = innerInsets.getSystemWindowInsets();
- final Insets newMargin = innerInsets.getInsets(
- WindowInsets.Type.navigationBars());
-
- // Don't extend into navigation bar area so the width can align with status
- // bar color view.
- if (mlp.leftMargin != newMargin.left
- || mlp.rightMargin != newMargin.right) {
- mlpChanged = true;
- mlp.leftMargin = newMargin.left;
- mlp.rightMargin = newMargin.right;
- }
-
- mPrimaryActionModeView.setPadding(
- newPadding.left - newMargin.left,
- newPadding.top,
- newPadding.right - newMargin.right,
- 0);
- consumesSystemWindowInsetsTop = newPadding.top > 0;
- } else {
- int newTopMargin = innerInsets.getSystemWindowInsetTop();
- int newLeftMargin = innerInsets.getSystemWindowInsetLeft();
- int newRightMargin = innerInsets.getSystemWindowInsetRight();
-
- // Must use root window insets for the guard, because the color views
- // consume the navigation bar inset if the window does not request
- // LAYOUT_HIDE_NAV - but the status guard is attached at the root.
- WindowInsets rootInsets = getRootWindowInsets();
- int newGuardLeftMargin = rootInsets.getSystemWindowInsetLeft();
- int newGuardRightMargin = rootInsets.getSystemWindowInsetRight();
-
- if (mlp.topMargin != newTopMargin || mlp.leftMargin != newLeftMargin
- || mlp.rightMargin != newRightMargin) {
- mlpChanged = true;
- mlp.topMargin = newTopMargin;
- mlp.leftMargin = newLeftMargin;
- mlp.rightMargin = newRightMargin;
- }
+ int newTopMargin = innerInsets.getSystemWindowInsetTop();
+ int newLeftMargin = innerInsets.getSystemWindowInsetLeft();
+ int newRightMargin = innerInsets.getSystemWindowInsetRight();
+
+ // Must use root window insets for the guard, because the color views consume
+ // the navigation bar inset if the window does not request LAYOUT_HIDE_NAV - but
+ // the status guard is attached at the root.
+ WindowInsets rootInsets = getRootWindowInsets();
+ int newGuardLeftMargin = rootInsets.getSystemWindowInsetLeft();
+ int newGuardRightMargin = rootInsets.getSystemWindowInsetRight();
+
+ if (mlp.topMargin != newTopMargin || mlp.leftMargin != newLeftMargin
+ || mlp.rightMargin != newRightMargin) {
+ mlpChanged = true;
+ mlp.topMargin = newTopMargin;
+ mlp.leftMargin = newLeftMargin;
+ mlp.rightMargin = newRightMargin;
+ }
- if (newTopMargin > 0 && mStatusGuard == null) {
- mStatusGuard = new View(mContext);
- mStatusGuard.setVisibility(GONE);
- final LayoutParams lp = new LayoutParams(MATCH_PARENT,
- mlp.topMargin, Gravity.LEFT | Gravity.TOP);
+ if (newTopMargin > 0 && mStatusGuard == null) {
+ mStatusGuard = new View(mContext);
+ mStatusGuard.setVisibility(GONE);
+ final LayoutParams lp = new LayoutParams(MATCH_PARENT,
+ mlp.topMargin, Gravity.LEFT | Gravity.TOP);
+ lp.leftMargin = newGuardLeftMargin;
+ lp.rightMargin = newGuardRightMargin;
+ addView(mStatusGuard, indexOfChild(mStatusColorViewState.view), lp);
+ } else if (mStatusGuard != null) {
+ final LayoutParams lp = (LayoutParams)
+ mStatusGuard.getLayoutParams();
+ if (lp.height != mlp.topMargin || lp.leftMargin != newGuardLeftMargin
+ || lp.rightMargin != newGuardRightMargin) {
+ lp.height = mlp.topMargin;
lp.leftMargin = newGuardLeftMargin;
lp.rightMargin = newGuardRightMargin;
- addView(mStatusGuard, indexOfChild(mStatusColorViewState.view), lp);
- } else if (mStatusGuard != null) {
- final LayoutParams lp = (LayoutParams)
- mStatusGuard.getLayoutParams();
- if (lp.height != mlp.topMargin || lp.leftMargin != newGuardLeftMargin
- || lp.rightMargin != newGuardRightMargin) {
- lp.height = mlp.topMargin;
- lp.leftMargin = newGuardLeftMargin;
- lp.rightMargin = newGuardRightMargin;
- mStatusGuard.setLayoutParams(lp);
- }
+ mStatusGuard.setLayoutParams(lp);
}
+ }
- // The action mode's theme may differ from the app, so
- // always show the status guard above it if we have one.
- showStatusGuard = mStatusGuard != null;
+ // The action mode's theme may differ from the app, so
+ // always show the status guard above it if we have one.
+ showStatusGuard = mStatusGuard != null;
- if (showStatusGuard && mStatusGuard.getVisibility() != VISIBLE) {
- // If it wasn't previously shown, the color may be stale
- updateStatusGuardColor();
- }
- consumesSystemWindowInsetsTop = showStatusGuard;
+ if (showStatusGuard && mStatusGuard.getVisibility() != VISIBLE) {
+ // If it wasn't previously shown, the color may be stale
+ updateStatusGuardColor();
}
// We only need to consume the insets if the action
@@ -1690,16 +1664,14 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind
// screen_simple_overlay_action_mode.xml).
final boolean nonOverlay = (mWindow.getLocalFeaturesPrivate()
& (1 << Window.FEATURE_ACTION_MODE_OVERLAY)) == 0;
- if (nonOverlay && consumesSystemWindowInsetsTop) {
+ if (nonOverlay && showStatusGuard) {
insets = insets.inset(0, insets.getSystemWindowInsetTop(), 0, 0);
}
} else {
- if (!Flags.actionModeEdgeToEdge()) {
- // reset top margin
- if (mlp.topMargin != 0 || mlp.leftMargin != 0 || mlp.rightMargin != 0) {
- mlpChanged = true;
- mlp.topMargin = 0;
- }
+ // reset top margin
+ if (mlp.topMargin != 0 || mlp.leftMargin != 0 || mlp.rightMargin != 0) {
+ mlpChanged = true;
+ mlp.topMargin = 0;
}
}
if (mlpChanged) {
@@ -1707,7 +1679,7 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind
}
}
}
- if (!Flags.actionModeEdgeToEdge() && mStatusGuard != null) {
+ if (mStatusGuard != null) {
mStatusGuard.setVisibility(showStatusGuard ? VISIBLE : GONE);
}
return insets;
@@ -2211,7 +2183,7 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind
for (int i = getChildCount() - 1; i >= 0; i--) {
View v = getChildAt(i);
if (v != mStatusColorViewState.view && v != mNavigationColorViewState.view
- && (Flags.actionModeEdgeToEdge() || v != mStatusGuard)) {
+ && v != mStatusGuard) {
removeViewAt(i);
}
}
diff --git a/core/java/com/android/internal/policy/KeyInterceptionInfo.java b/core/java/com/android/internal/policy/KeyInterceptionInfo.java
index fed8fe3b4cc0..b20f6d225b69 100644
--- a/core/java/com/android/internal/policy/KeyInterceptionInfo.java
+++ b/core/java/com/android/internal/policy/KeyInterceptionInfo.java
@@ -27,13 +27,11 @@ public class KeyInterceptionInfo {
// Debug friendly name to help identify the window
public final String windowTitle;
public final int windowOwnerUid;
- public final int inputFeaturesFlags;
- public KeyInterceptionInfo(int type, int flags, String title, int uid, int inputFeaturesFlags) {
+ public KeyInterceptionInfo(int type, int flags, String title, int uid) {
layoutParamsType = type;
layoutParamsPrivateFlags = flags;
windowTitle = title;
windowOwnerUid = uid;
- this.inputFeaturesFlags = inputFeaturesFlags;
}
}
diff --git a/core/java/com/android/internal/policy/SystemBarUtils.java b/core/java/com/android/internal/policy/SystemBarUtils.java
index 783c68695fb3..e5badc87fb13 100644
--- a/core/java/com/android/internal/policy/SystemBarUtils.java
+++ b/core/java/com/android/internal/policy/SystemBarUtils.java
@@ -66,12 +66,14 @@ public final class SystemBarUtils {
display.getDisplayInfo(info);
Insets insets;
Insets waterfallInsets;
+ final int localWidth = context.getResources().getDisplayMetrics().widthPixels;
+ final int localHeight = context.getResources().getDisplayMetrics().heightPixels;
if (cutout == null) {
insets = Insets.NONE;
waterfallInsets = Insets.NONE;
} else {
DisplayCutout rotated =
- cutout.getRotated(info.logicalWidth, info.logicalHeight, rotation, targetRot);
+ cutout.getRotated(localWidth, localHeight, rotation, targetRot);
insets = Insets.of(rotated.getSafeInsets());
waterfallInsets = rotated.getWaterfallInsets();
}
diff --git a/core/java/com/android/internal/protolog/ProtoLogCommandHandler.java b/core/java/com/android/internal/protolog/ProtoLogCommandHandler.java
index 6d4a40899a65..e723499b283f 100644
--- a/core/java/com/android/internal/protolog/ProtoLogCommandHandler.java
+++ b/core/java/com/android/internal/protolog/ProtoLogCommandHandler.java
@@ -61,6 +61,18 @@ public class ProtoLogCommandHandler extends ShellCommand {
}
@Override
+ public int handleDefaultCommands(String cmd) {
+ if (cmd == null || "help".equals(cmd) || "-h".equals(cmd)) {
+ onHelp();
+ return 0;
+ } else {
+ getOutPrintWriter().println("Unknown command: " + cmd +
+ " (use 'help' command for guidance)");
+ return -1;
+ }
+ }
+
+ @Override
public void onHelp() {
PrintWriter pw = getOutPrintWriter();
pw.println("ProtoLog commands:");
diff --git a/core/java/com/android/internal/util/ContrastColorUtil.java b/core/java/com/android/internal/util/ContrastColorUtil.java
index 0fd139188665..c68f107951ac 100644
--- a/core/java/com/android/internal/util/ContrastColorUtil.java
+++ b/core/java/com/android/internal/util/ContrastColorUtil.java
@@ -41,8 +41,6 @@ import android.text.style.TextAppearanceSpan;
import android.util.Log;
import android.util.Pair;
-import com.android.internal.annotations.VisibleForTesting;
-
import java.util.Arrays;
import java.util.WeakHashMap;
@@ -381,6 +379,13 @@ public class ContrastColorUtil {
return calculateLuminance(color) <= 0.17912878474;
}
+ /** Like {@link #isColorDark(int)} but converts to LAB before checking the L component. */
+ public static boolean isColorDarkLab(int color) {
+ final double[] result = ColorUtilsFromCompat.getTempDouble3Array();
+ ColorUtilsFromCompat.colorToLAB(color, result);
+ return result[0] < 50;
+ }
+
private int processColor(int color) {
return Color.argb(Color.alpha(color),
255 - Color.red(color),
diff --git a/core/java/com/android/internal/widget/ActionBarContextView.java b/core/java/com/android/internal/widget/ActionBarContextView.java
index d5bb51187ba4..80fc218839d5 100644
--- a/core/java/com/android/internal/widget/ActionBarContextView.java
+++ b/core/java/com/android/internal/widget/ActionBarContextView.java
@@ -34,7 +34,6 @@ import android.widget.TextView;
import com.android.internal.R;
import com.android.internal.view.menu.MenuBuilder;
-import com.android.window.flags.Flags;
/**
* @hide
@@ -316,14 +315,12 @@ public class ActionBarContextView extends AbsActionBarView {
final int contentWidth = MeasureSpec.getSize(widthMeasureSpec);
- final int maxHeight = !Flags.actionModeEdgeToEdge() && mContentHeight > 0
- ? mContentHeight : MeasureSpec.getSize(heightMeasureSpec);
+ int maxHeight = mContentHeight > 0 ?
+ mContentHeight : MeasureSpec.getSize(heightMeasureSpec);
final int verticalPadding = getPaddingTop() + getPaddingBottom();
int availableWidth = contentWidth - getPaddingLeft() - getPaddingRight();
- final int height = Flags.actionModeEdgeToEdge()
- ? mContentHeight > 0 ? mContentHeight : maxHeight
- : maxHeight - verticalPadding;
+ final int height = maxHeight - verticalPadding;
final int childSpecHeight = MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST);
if (mClose != null) {
@@ -379,8 +376,7 @@ public class ActionBarContextView extends AbsActionBarView {
}
setMeasuredDimension(contentWidth, measuredHeight);
} else {
- setMeasuredDimension(contentWidth, Flags.actionModeEdgeToEdge()
- ? mContentHeight + verticalPadding : maxHeight);
+ setMeasuredDimension(contentWidth, maxHeight);
}
}
diff --git a/core/java/com/android/internal/widget/ActionBarOverlayLayout.java b/core/java/com/android/internal/widget/ActionBarOverlayLayout.java
index 362b79db4003..ff57fd4fe2ce 100644
--- a/core/java/com/android/internal/widget/ActionBarOverlayLayout.java
+++ b/core/java/com/android/internal/widget/ActionBarOverlayLayout.java
@@ -294,24 +294,54 @@ public class ActionBarOverlayLayout extends ViewGroup implements DecorContentPar
}
}
- private boolean setMargin(View view, int left, int top, int right, int bottom) {
+ private boolean applyInsets(View view, Rect insets, boolean toPadding,
+ boolean left, boolean top, boolean right, boolean bottom) {
+ boolean changed;
+ if (toPadding) {
+ changed = setMargin(view, EMPTY_RECT, left, top, right, bottom);
+ changed |= setPadding(view, insets, left, top, right, bottom);
+ } else {
+ changed = setPadding(view, EMPTY_RECT, left, top, right, bottom);
+ changed |= setMargin(view, insets, left, top, right, bottom);
+ }
+ return changed;
+ }
+
+ private boolean setPadding(View view, Rect insets,
+ boolean left, boolean top, boolean right, boolean bottom) {
+ if ((left && view.getPaddingLeft() != insets.left)
+ || (top && view.getPaddingTop() != insets.top)
+ || (right && view.getPaddingRight() != insets.right)
+ || (bottom && view.getPaddingBottom() != insets.bottom)) {
+ view.setPadding(
+ left ? insets.left : view.getPaddingLeft(),
+ top ? insets.top : view.getPaddingTop(),
+ right ? insets.right : view.getPaddingRight(),
+ bottom ? insets.bottom : view.getPaddingBottom());
+ return true;
+ }
+ return false;
+ }
+
+ private boolean setMargin(View view, Rect insets,
+ boolean left, boolean top, boolean right, boolean bottom) {
final LayoutParams lp = (LayoutParams) view.getLayoutParams();
boolean changed = false;
- if (lp.leftMargin != left) {
+ if (left && lp.leftMargin != insets.left) {
changed = true;
- lp.leftMargin = left;
+ lp.leftMargin = insets.left;
}
- if (lp.topMargin != top) {
+ if (top && lp.topMargin != insets.top) {
changed = true;
- lp.topMargin = top;
+ lp.topMargin = insets.top;
}
- if (lp.rightMargin != right) {
+ if (right && lp.rightMargin != insets.right) {
changed = true;
- lp.rightMargin = right;
+ lp.rightMargin = insets.right;
}
- if (lp.bottomMargin != bottom) {
+ if (bottom && lp.bottomMargin != insets.bottom) {
changed = true;
- lp.bottomMargin = bottom;
+ lp.bottomMargin = insets.bottom;
}
return changed;
}
@@ -337,30 +367,12 @@ public class ActionBarOverlayLayout extends ViewGroup implements DecorContentPar
final Insets sysInsets = insets.getSystemWindowInsets();
mSystemInsets.set(sysInsets.left, sysInsets.top, sysInsets.right, sysInsets.bottom);
- boolean changed = false;
- if (mActionBarExtendsIntoSystemInsets) {
- // Don't extend into navigation bar area so the width can align with status bar
- // color view.
- final Insets navBarInsets = insets.getInsets(WindowInsets.Type.navigationBars());
- final int paddingLeft = sysInsets.left - navBarInsets.left;
- final int paddingRight = sysInsets.right - navBarInsets.right;
- mActionBarTop.setPadding(paddingLeft, sysInsets.top, paddingRight, 0);
- changed |= setMargin(
- mActionBarTop, navBarInsets.left, 0, navBarInsets.right, 0);
- if (mActionBarBottom != null) {
- mActionBarBottom.setPadding(paddingLeft, 0, paddingRight, sysInsets.bottom);
- changed |= setMargin(
- mActionBarBottom, navBarInsets.left, 0, navBarInsets.right, 0);
- }
- } else {
- mActionBarTop.setPadding(0, 0, 0, 0);
- changed |= setMargin(
- mActionBarTop, sysInsets.left, sysInsets.top, sysInsets.right, 0);
- if (mActionBarBottom != null) {
- mActionBarBottom.setPadding(0, 0, 0, 0);
- changed |= setMargin(
- mActionBarTop, sysInsets.left, 0, sysInsets.right, sysInsets.bottom);
- }
+ // The top and bottom action bars are always within the content area.
+ boolean changed = applyInsets(mActionBarTop, mSystemInsets,
+ mActionBarExtendsIntoSystemInsets, true, true, true, false);
+ if (mActionBarBottom != null) {
+ changed |= applyInsets(mActionBarBottom, mSystemInsets,
+ mActionBarExtendsIntoSystemInsets, true, false, true, true);
}
// Cannot use the result of computeSystemWindowInsets, because that consumes the
@@ -509,12 +521,7 @@ public class ActionBarOverlayLayout extends ViewGroup implements DecorContentPar
);
}
}
- setMargin(
- mContent,
- mContentInsets.left,
- mContentInsets.top,
- mContentInsets.right,
- mContentInsets.bottom);
+ setMargin(mContent, mContentInsets, true, true, true, true);
if (!mLastInnerInsets.equals(mInnerInsets)) {
// If the inner insets have changed, we need to dispatch this down to
diff --git a/core/java/com/android/internal/widget/LockPatternUtils.java b/core/java/com/android/internal/widget/LockPatternUtils.java
index d35072fc10c3..6f1d72944a55 100644
--- a/core/java/com/android/internal/widget/LockPatternUtils.java
+++ b/core/java/com/android/internal/widget/LockPatternUtils.java
@@ -41,7 +41,6 @@ import android.compat.annotation.UnsupportedAppUsage;
import android.content.ComponentName;
import android.content.ContentResolver;
import android.content.Context;
-import android.content.pm.PackageManager;
import android.content.pm.UserInfo;
import android.hardware.input.InputManagerGlobal;
import android.os.Build;
@@ -77,7 +76,6 @@ import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
-import java.util.HashMap;
import java.util.List;
/**
@@ -240,8 +238,6 @@ public class LockPatternUtils {
private final SparseLongArray mLockoutDeadlines = new SparseLongArray();
private Boolean mHasSecureLockScreen;
- private HashMap<UserHandle, UserManager> mUserManagerCache = new HashMap<>();
-
/**
* Use {@link TrustManager#isTrustUsuallyManaged(int)}.
*
@@ -363,22 +359,6 @@ public class LockPatternUtils {
return mUserManager;
}
- private UserManager getUserManager(int userId) {
- UserHandle userHandle = UserHandle.of(userId);
- if (mUserManagerCache.containsKey(userHandle)) {
- return mUserManagerCache.get(userHandle);
- }
-
- try {
- Context userContext = mContext.createPackageContextAsUser("system", 0, userHandle);
- UserManager userManager = userContext.getSystemService(UserManager.class);
- mUserManagerCache.put(userHandle, userManager);
- return userManager;
- } catch (PackageManager.NameNotFoundException e) {
- throw new RuntimeException("Failed to create context for user " + userHandle, e);
- }
- }
-
private TrustManager getTrustManager() {
TrustManager trust = (TrustManager) mContext.getSystemService(Context.TRUST_SERVICE);
if (trust == null) {
@@ -966,7 +946,7 @@ public class LockPatternUtils {
*/
public void setSeparateProfileChallengeEnabled(int userHandle, boolean enabled,
LockscreenCredential profilePassword) {
- if (!isCredentialSharableWithParent(userHandle)) {
+ if (!isCredentialShareableWithParent(userHandle)) {
return;
}
try {
@@ -985,7 +965,7 @@ public class LockPatternUtils {
* credential is not shareable with its parent, or a non-profile user.
*/
public boolean isSeparateProfileChallengeEnabled(int userHandle) {
- return isCredentialSharableWithParent(userHandle) && hasSeparateChallenge(userHandle);
+ return isCredentialShareableWithParent(userHandle) && hasSeparateChallenge(userHandle);
}
/**
@@ -995,7 +975,7 @@ public class LockPatternUtils {
* credential is not shareable with its parent, or a non-profile user.
*/
public boolean isProfileWithUnifiedChallenge(int userHandle) {
- return isCredentialSharableWithParent(userHandle) && !hasSeparateChallenge(userHandle);
+ return isCredentialShareableWithParent(userHandle) && !hasSeparateChallenge(userHandle);
}
/**
@@ -1020,8 +1000,13 @@ public class LockPatternUtils {
return info != null && info.isManagedProfile();
}
- private boolean isCredentialSharableWithParent(int userHandle) {
- return getUserManager(userHandle).isCredentialSharableWithParent();
+ private boolean isCredentialShareableWithParent(int userHandle) {
+ try {
+ return getUserManager().getUserProperties(UserHandle.of(userHandle))
+ .isCredentialShareableWithParent();
+ } catch (IllegalArgumentException e) {
+ return false;
+ }
}
/**
diff --git a/core/java/com/android/internal/widget/NotificationProgressBar.java b/core/java/com/android/internal/widget/NotificationProgressBar.java
index f6e2a4df8cca..c484a8851dfd 100644
--- a/core/java/com/android/internal/widget/NotificationProgressBar.java
+++ b/core/java/com/android/internal/widget/NotificationProgressBar.java
@@ -26,6 +26,8 @@ import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Rect;
+import android.graphics.drawable.Animatable2;
+import android.graphics.drawable.AnimatedVectorDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.Icon;
import android.graphics.drawable.LayerDrawable;
@@ -66,6 +68,8 @@ public final class NotificationProgressBar extends ProgressBar implements
private static final boolean DEBUG = false;
private static final float FADED_OPACITY = 0.5f;
+ private Animatable2.AnimationCallback mIndeterminateAnimationCallback = null;
+
private NotificationProgressDrawable mNotificationProgressDrawable;
private final Rect mProgressDrawableBounds = new Rect();
@@ -150,6 +154,38 @@ public final class NotificationProgressBar extends ProgressBar implements
0);
}
+ @Override
+ public void setIndeterminateDrawable(Drawable d) {
+ final Drawable oldDrawable = getIndeterminateDrawable();
+ if (oldDrawable != d) {
+ if (mIndeterminateAnimationCallback != null) {
+ ((AnimatedVectorDrawable) oldDrawable).unregisterAnimationCallback(
+ mIndeterminateAnimationCallback);
+ mIndeterminateAnimationCallback = null;
+ }
+ if (d instanceof AnimatedVectorDrawable) {
+ mIndeterminateAnimationCallback = new Animatable2.AnimationCallback() {
+ @Override
+ public void onAnimationEnd(Drawable drawable) {
+ super.onAnimationEnd(drawable);
+
+ if (shouldLoopIndeterminateAnimation()) {
+ ((AnimatedVectorDrawable) drawable).start();
+ }
+ }
+ };
+ ((AnimatedVectorDrawable) d).registerAnimationCallback(
+ mIndeterminateAnimationCallback);
+ }
+ }
+
+ super.setIndeterminateDrawable(d);
+ }
+
+ private boolean shouldLoopIndeterminateAnimation() {
+ return isIndeterminate() && isAttachedToWindow() && isAggregatedVisible();
+ }
+
/**
* Setter for the notification progress model.
*
diff --git a/core/jni/Android.bp b/core/jni/Android.bp
index 7ed73d7668b9..40f6acceecb1 100644
--- a/core/jni/Android.bp
+++ b/core/jni/Android.bp
@@ -488,6 +488,7 @@ cc_library_shared_for_libandroid_runtime {
"libbinder",
"libbinder_ndk",
"libhidlbase", // libhwbinder is in here
+ "libaconfig_storage_read_api_cc",
],
version_script: "platform/linux/libandroid_runtime_export.txt",
},
diff --git a/core/jni/android_tracing_PerfettoDataSource.cpp b/core/jni/android_tracing_PerfettoDataSource.cpp
index ea896e1678a7..56ee3dcf7ca5 100644
--- a/core/jni/android_tracing_PerfettoDataSource.cpp
+++ b/core/jni/android_tracing_PerfettoDataSource.cpp
@@ -96,6 +96,9 @@ jobject PerfettoDataSource::newInstance(JNIEnv* env, void* ds_config, size_t ds_
bool PerfettoDataSource::TraceIterateBegin() {
if (gInIteration) {
+ ALOG(LOG_ERROR, LOG_TAG,
+ "Tried calling TraceIterateBegin with an already active iterator for datasource %s.",
+ dataSourceName.c_str());
return false;
}
diff --git a/core/jni/android_util_AssetManager.cpp b/core/jni/android_util_AssetManager.cpp
index 1394b9f8781a..e1c72c34d63f 100644
--- a/core/jni/android_util_AssetManager.cpp
+++ b/core/jni/android_util_AssetManager.cpp
@@ -67,6 +67,7 @@ static struct typedvalue_offsets_t {
jfieldID mResourceId;
jfieldID mChangingConfigurations;
jfieldID mDensity;
+ jfieldID mUsesFeatureFlags;
} gTypedValueOffsets;
// This is also used by asset_manager.cpp.
@@ -137,6 +138,8 @@ static jint CopyValue(JNIEnv* env, const AssetManager2::SelectedValue& value,
env->SetIntField(out_typed_value, gTypedValueOffsets.mResourceId, value.resid);
env->SetIntField(out_typed_value, gTypedValueOffsets.mChangingConfigurations, value.flags);
env->SetIntField(out_typed_value, gTypedValueOffsets.mDensity, value.config.density);
+ env->SetBooleanField(out_typed_value, gTypedValueOffsets.mUsesFeatureFlags,
+ value.entry_flags & ResTable_entry::FLAG_USES_FEATURE_FLAGS);
return static_cast<jint>(ApkAssetsCookieToJavaCookie(value.cookie));
}
@@ -1074,8 +1077,8 @@ static jstring NativeGetLastResourceResolution(JNIEnv* env,
static jobjectArray NativeGetLocales(JNIEnv* env, jclass /*class*/, jlong ptr,
jboolean exclude_system) {
auto assetmanager = LockAndStartAssetManager(ptr);
- std::set<std::string> locales =
- assetmanager->GetResourceLocales(exclude_system, true /*merge_equivalent_languages*/);
+ auto locales =
+ assetmanager->GetResourceLocales(exclude_system, true /*merge_equivalent_languages*/);
jobjectArray array = env->NewObjectArray(locales.size(), g_stringClass, nullptr);
if (array == nullptr) {
@@ -1664,6 +1667,7 @@ int register_android_content_AssetManager(JNIEnv* env) {
gTypedValueOffsets.mChangingConfigurations =
GetFieldIDOrDie(env, typedValue, "changingConfigurations", "I");
gTypedValueOffsets.mDensity = GetFieldIDOrDie(env, typedValue, "density", "I");
+ gTypedValueOffsets.mUsesFeatureFlags = GetFieldIDOrDie(env, typedValue, "usesFeatureFlags", "Z");
jclass assetManager = FindClassOrDie(env, "android/content/res/AssetManager");
gAssetManagerOffsets.mObject = GetFieldIDOrDie(env, assetManager, "mObject", "J");
diff --git a/core/jni/fd_utils.cpp b/core/jni/fd_utils.cpp
index 5225ce878310..0eb7c4aee287 100644
--- a/core/jni/fd_utils.cpp
+++ b/core/jni/fd_utils.cpp
@@ -51,8 +51,7 @@ static const char* kPathAllowlist[] = {
"/dev/blkio/tasks",
"/metadata/aconfig/maps/system.package.map",
"/metadata/aconfig/maps/system.flag.map",
- "/metadata/aconfig/boot/system.val",
- "/metadata/libprocessgroup/memcg_v2_max_activation_depth" // TODO Revert after go/android-memcgv2-exp b/386797433
+ "/metadata/aconfig/boot/system.val"
};
static const char kFdPath[] = "/proc/self/fd";
diff --git a/core/proto/android/server/windowmanagerservice.proto b/core/proto/android/server/windowmanagerservice.proto
index 5820c8e947c2..3ebb48041ecd 100644
--- a/core/proto/android/server/windowmanagerservice.proto
+++ b/core/proto/android/server/windowmanagerservice.proto
@@ -471,6 +471,8 @@ message WindowStateProto {
repeated .android.view.InsetsSourceProto mergedLocalInsetsSources = 47;
optional int32 requested_visible_types = 48;
optional .android.graphics.RectProto dim_bounds = 49;
+ optional int32 prepare_sync_seq_id = 50;
+ optional int32 sync_seq_id = 51;
}
message IdentifierProto {
diff --git a/core/res/Android.bp b/core/res/Android.bp
index 1199d77d04c6..29da0d6f67ae 100644
--- a/core/res/Android.bp
+++ b/core/res/Android.bp
@@ -181,6 +181,7 @@ android_app {
"ranging_aconfig_flags",
"aconfig_settingslib_flags",
"telephony_flags",
+ "update_engine_aconfig_declarations",
],
}
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index fcf5a88cd9e8..636968dd1152 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -5180,6 +5180,13 @@
<permission android:name="android.permission.READ_LOGS"
android:protectionLevel="signature|privileged|development" />
+ <!-- Allows an application to read the update_engine logs
+ <p>Not for use by third-party applications.
+ @FlaggedApi("com.android.update_engine.minor_changes_2025q4") -->
+ <permission android:name="android.permission.READ_UPDATE_ENGINE_LOGS"
+ android:protectionLevel="signature|privileged|development"
+ android:featureFlag="com.android.update_engine.minor_changes_2025q4" />
+
<!-- Configure an application for debugging.
<p>Not for use by third-party applications. -->
<permission android:name="android.permission.SET_DEBUG_APP"
@@ -9287,11 +9294,15 @@
<receiver android:name="com.android.server.updates.CertPinInstallReceiver"
android:exported="true"
+ android:systemUserOnly="true"
android:permission="android.permission.UPDATE_CONFIG">
<intent-filter>
<action android:name="android.intent.action.UPDATE_PINS" />
<data android:scheme="content" android:host="*" android:mimeType="*/*" />
</intent-filter>
+ <intent-filter>
+ <action android:name="android.intent.action.BOOT_COMPLETED" />
+ </intent-filter>
</receiver>
<receiver android:name="com.android.server.updates.IntentFirewallInstallReceiver"
diff --git a/core/res/res/drawable/ic_standby.xml b/core/res/res/drawable/ic_standby.xml
new file mode 100644
index 000000000000..6736f14f1377
--- /dev/null
+++ b/core/res/res/drawable/ic_standby.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="M480,600Q530,600 565,565Q600,530 600,480Q600,430 565,395Q530,360 480,360Q430,360 395,395Q360,430 360,480Q360,530 395,565Q430,600 480,600ZM480,880Q397,880 324,848.5Q251,817 197,763Q143,709 111.5,636Q80,563 80,480Q80,397 111.5,324Q143,251 197,197Q251,143 324,111.5Q397,80 480,80Q563,80 636,111.5Q709,143 763,197Q817,251 848.5,324Q880,397 880,480Q880,563 848.5,636Q817,709 763,763Q709,817 636,848.5Q563,880 480,880ZM480,800Q614,800 707,707Q800,614 800,480Q800,346 707,253Q614,160 480,160Q346,160 253,253Q160,346 160,480Q160,614 253,707Q346,800 480,800ZM480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Z"/>
+</vector>
diff --git a/core/res/res/drawable/notification_progress_indeterminate_horizontal_material.xml b/core/res/res/drawable/notification_progress_indeterminate_horizontal_material.xml
new file mode 100644
index 000000000000..9b0405ad264f
--- /dev/null
+++ b/core/res/res/drawable/notification_progress_indeterminate_horizontal_material.xml
@@ -0,0 +1,167 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2025 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:aapt="http://schemas.android.com/aapt"
+ android:drawable="@drawable/vector_notification_progress_indeterminate_horizontal">
+ <target android:name="track_behind">
+ <aapt:attr name="android:animation">
+ <set android:ordering="sequentially">
+ <objectAnimator
+ android:duration="800"
+ android:propertyName="trimPathEnd"
+ android:startOffset="400"
+ android:valueFrom="0"
+ android:valueTo="0.51"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.3,0 0.8,0.15 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ <objectAnimator
+ android:duration="200"
+ android:propertyName="trimPathEnd"
+ android:valueFrom="0.51"
+ android:valueTo="0.98"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.001,0 0.2,1 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ <objectAnimator
+ android:duration="100"
+ android:propertyName="trimPathEnd"
+ android:valueFrom="0.98"
+ android:valueTo="1"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.5 0.833,0.833 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="progress">
+ <aapt:attr name="android:animation">
+ <set android:ordering="sequentially">
+ <objectAnimator
+ android:duration="200"
+ android:propertyName="trimPathStart"
+ android:startOffset="200"
+ android:valueFrom="0"
+ android:valueTo="0.02"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.963,0.8 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ <objectAnimator
+ android:duration="800"
+ android:propertyName="trimPathStart"
+ android:valueFrom="0.02"
+ android:valueTo="0.53"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.3,0 0.8,0.15 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ <objectAnimator
+ android:duration="200"
+ android:propertyName="trimPathStart"
+ android:valueFrom="0.53"
+ android:valueTo="1"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.001,0 0.2,1 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="progress">
+ <aapt:attr name="android:animation">
+ <set android:ordering="sequentially">
+ <objectAnimator
+ android:duration="200"
+ android:propertyName="trimPathEnd"
+ android:startOffset="200"
+ android:valueFrom="0"
+ android:valueTo="0.024"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.963,0.834 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ <objectAnimator
+ android:duration="800"
+ android:propertyName="trimPathEnd"
+ android:valueFrom="0.024"
+ android:valueTo="0.98"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.3,0 0.8,0.15 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ <objectAnimator
+ android:duration="30"
+ android:propertyName="trimPathEnd"
+ android:valueFrom="0.98"
+ android:valueTo="1"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="track_ahead">
+ <aapt:attr name="android:animation">
+ <set android:ordering="sequentially">
+ <objectAnimator
+ android:duration="200"
+ android:propertyName="trimPathStart"
+ android:valueFrom="0"
+ android:valueTo="0.02"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ <objectAnimator
+ android:duration="200"
+ android:propertyName="trimPathStart"
+ android:valueFrom="0.02"
+ android:valueTo="0.044000000000000004"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.963,0.834 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ <objectAnimator
+ android:duration="800"
+ android:propertyName="trimPathStart"
+ android:valueFrom="0.044000000000000004"
+ android:valueTo="1"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.3,0 0.8,0.15 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+</animated-vector> \ No newline at end of file
diff --git a/core/res/res/drawable/vector_notification_progress_indeterminate_horizontal.xml b/core/res/res/drawable/vector_notification_progress_indeterminate_horizontal.xml
new file mode 100644
index 000000000000..fe81b79e481c
--- /dev/null
+++ b/core/res/res/drawable/vector_notification_progress_indeterminate_horizontal.xml
@@ -0,0 +1,59 @@
+<?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="300dp"
+ android:height="20dp"
+ android:viewportHeight="16"
+ android:viewportWidth="300">
+ <group
+ android:name="root"
+ android:translateX="150"
+ android:translateY="8">
+ <path
+ android:name="track_ahead"
+ android:pathData="M -147,0.0 L 147,0.0"
+ android:strokeAlpha="0.5"
+ android:strokeColor="?attr/colorControlActivated"
+ android:strokeLineCap="round"
+ android:strokeLineJoin="round"
+ android:strokeWidth="2"
+ android:trimPathEnd="1"
+ android:trimPathOffset="0"
+ android:trimPathStart="0" />
+ <path
+ android:name="progress"
+ android:pathData="M -147,0.0 L 147,0.0"
+ android:strokeColor="?attr/colorControlActivated"
+ android:strokeLineCap="round"
+ android:strokeLineJoin="round"
+ android:strokeWidth="6"
+ android:trimPathEnd="0"
+ android:trimPathOffset="0"
+ android:trimPathStart="0" />
+ <path
+ android:name="track_behind"
+ android:pathData="M -147,0.0 L 147,0.0"
+ android:strokeAlpha="0.5"
+ android:strokeColor="?attr/colorControlActivated"
+ android:strokeLineCap="round"
+ android:strokeLineJoin="round"
+ android:strokeWidth="2"
+ android:trimPathEnd="0"
+ android:trimPathOffset="0"
+ android:trimPathStart="0" />
+ </group>
+</vector> \ No newline at end of file
diff --git a/core/res/res/values-gl/strings.xml b/core/res/res/values-gl/strings.xml
index d79e3c63e084..ca7773321992 100644
--- a/core/res/res/values-gl/strings.xml
+++ b/core/res/res/values-gl/strings.xml
@@ -2273,8 +2273,8 @@
<string name="accessibility_autoclick_scroll" msgid="3499385943728726933">"Desprazar"</string>
<string name="accessibility_autoclick_pause" msgid="3272200156172573568">"Pausa"</string>
<string name="accessibility_autoclick_position" msgid="2933660969907663545">"Posición"</string>
- <string name="accessibility_autoclick_scroll_up" msgid="2044948780797117443">"Desprazarse cara arriba"</string>
- <string name="accessibility_autoclick_scroll_down" msgid="3733401063292018116">"Desprazarse cara abaixo"</string>
+ <string name="accessibility_autoclick_scroll_up" msgid="2044948780797117443">"Desprazar cara arriba"</string>
+ <string name="accessibility_autoclick_scroll_down" msgid="3733401063292018116">"Desprazar cara abaixo"</string>
<string name="accessibility_autoclick_scroll_left" msgid="8564421367992824198">"Desprazar cara á esquerda"</string>
<string name="accessibility_autoclick_scroll_right" msgid="8932417330753984265">"Desprazar cara á dereita"</string>
<string name="accessibility_autoclick_scroll_exit" msgid="3788610039146769696">"Saír do modo de desprazamento"</string>
diff --git a/core/res/res/values-ko/strings.xml b/core/res/res/values-ko/strings.xml
index bd48cb3e3bf8..6a0ba83fa456 100644
--- a/core/res/res/values-ko/strings.xml
+++ b/core/res/res/values-ko/strings.xml
@@ -2278,7 +2278,7 @@
<string name="accessibility_autoclick_scroll_left" msgid="8564421367992824198">"왼쪽으로 스크롤"</string>
<string name="accessibility_autoclick_scroll_right" msgid="8932417330753984265">"오른쪽으로 스크롤"</string>
<string name="accessibility_autoclick_scroll_exit" msgid="3788610039146769696">"스크롤 모드 종료"</string>
- <string name="accessibility_autoclick_scroll_panel_title" msgid="7120598166296447036">"패널 스크롤"</string>
+ <string name="accessibility_autoclick_scroll_panel_title" msgid="7120598166296447036">"스크롤 패널"</string>
<string name="as_app_forced_to_restricted_bucket" msgid="8233871289353898964">"<xliff:g id="PACKAGE_NAME">%1$s</xliff:g> 항목이 RESTRICTED 버킷으로 이동함"</string>
<string name="conversation_single_line_name_display" msgid="8958948312915255999">"<xliff:g id="SENDER_NAME">%1$s</xliff:g>:"</string>
<string name="conversation_single_line_image_placeholder" msgid="6983271082911936900">"이미지 보냄"</string>
diff --git a/core/res/res/values-ne/strings.xml b/core/res/res/values-ne/strings.xml
index b3ce1c8eb9b6..b550c33acb88 100644
--- a/core/res/res/values-ne/strings.xml
+++ b/core/res/res/values-ne/strings.xml
@@ -2277,7 +2277,7 @@
<string name="accessibility_autoclick_scroll_down" msgid="3733401063292018116">"तलतिर स्क्रोल गर्नुहोस्"</string>
<string name="accessibility_autoclick_scroll_left" msgid="8564421367992824198">"बायाँतिर स्क्रोल गर्नुहोस्"</string>
<string name="accessibility_autoclick_scroll_right" msgid="8932417330753984265">"दायाँतिर स्क्रोल गर्नुहोस्"</string>
- <string name="accessibility_autoclick_scroll_exit" msgid="3788610039146769696">"स्क्रोल गर्नुहोस् मोडबाट बाहिरिनुहोस्"</string>
+ <string name="accessibility_autoclick_scroll_exit" msgid="3788610039146769696">"स्क्रोल मोडबाट बाहिरिनुहोस्"</string>
<string name="accessibility_autoclick_scroll_panel_title" msgid="7120598166296447036">"प्यानल स्क्रोल गर्नुहोस्"</string>
<string name="as_app_forced_to_restricted_bucket" msgid="8233871289353898964">"<xliff:g id="PACKAGE_NAME">%1$s</xliff:g> लाई प्रतिबन्धित बाल्टीमा राखियो"</string>
<string name="conversation_single_line_name_display" msgid="8958948312915255999">"<xliff:g id="SENDER_NAME">%1$s</xliff:g>:"</string>
diff --git a/core/res/res/values-tl/strings.xml b/core/res/res/values-tl/strings.xml
index 9b6ecd30b233..bd6ce2d318f8 100644
--- a/core/res/res/values-tl/strings.xml
+++ b/core/res/res/values-tl/strings.xml
@@ -2507,7 +2507,7 @@
<string name="satellite_manual_selection_state_popup_ok" msgid="2459664752624985095">"I-on"</string>
<string name="satellite_manual_selection_state_popup_cancel" msgid="973605633339469252">"Bumalik"</string>
<string name="unarchival_session_app_label" msgid="6811856981546348205">"Nakabinbin..."</string>
- <string name="satellite_sos_available_notification_title" msgid="5396708154268096124">"Available na ang SOS gamit ang Satellite"</string>
+ <string name="satellite_sos_available_notification_title" msgid="5396708154268096124">"Available na ang SOS gamit ang satellite"</string>
<string name="satellite_sos_available_notification_summary" msgid="1727088812951848330">"Puwede kang magpadala ng mensahe sa mga serbisyong pang-emergency kung walang mobile o Wi-Fi network. Dapat Google Messages ang default mong app sa pagmemensahe."</string>
<string name="satellite_sos_not_supported_notification_title" msgid="2659100983227637285">"Hindi sinusuportahan ang SOS gamit ang satellite"</string>
<string name="satellite_sos_not_supported_notification_summary" msgid="1071762454665310549">"Hindi sinusuportahan ang SOS gamit ang satellite sa device na ito"</string>
diff --git a/core/res/res/values-watch/config.xml b/core/res/res/values-watch/config.xml
index 57a09ea34ba1..42210c40e7ba 100644
--- a/core/res/res/values-watch/config.xml
+++ b/core/res/res/values-watch/config.xml
@@ -31,7 +31,7 @@
<dimen name="config_viewMinFlingVelocity">500dp</dimen>
<!-- Maximum velocity to initiate a fling, as measured in dips per second. -->
- <dimen name="config_viewMaxFlingVelocity">8000dp</dimen>
+ <dimen name="config_viewMaxFlingVelocity">2750dp</dimen>
<!-- Minimum velocity (absolute value) to initiate a fling from a rotary encoder device, as
measured in dips per second. Setting this to -1dp disables rotary encoder fling. -->
@@ -39,7 +39,7 @@
<!-- Maximum velocity (absolute value) to initiate a fling from a rotary encoder device, as
measured in dips per second. Setting this to -1dp disables rotary encoder fling. -->
- <dimen name="config_viewMaxRotaryEncoderFlingVelocity">8000dp</dimen>
+ <dimen name="config_viewMaxRotaryEncoderFlingVelocity">2750dp</dimen>
<!-- Whether the View-based scroll haptic feedback implementation is enabled for
{@link InputDevice#SOURCE_ROTARY_ENCODER}s. -->
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 828461c66a1f..59c7fd027a79 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -3836,6 +3836,7 @@
"lockdown" = Lock down device until the user authenticates
"logout" = Logout the current user
"system_update" = Launch System Update screen
+ "standby" = Bring the device to standby
-->
<string-array translatable="false" name="config_globalActionsList">
<item>emergency</item>
@@ -4087,6 +4088,11 @@
<item>-44</item>
</integer-array>
+ <!-- Provides default value for operator name in status bar option in setting:
+ 0 - Hide operator name in status bar
+ 1 - Show operator name in status bar (default) -->
+ <integer name="config_showOperatorNameDefault">1</integer>
+
<!-- Enabled built-in zen mode condition providers -->
<string-array translatable="false" name="config_system_condition_providers">
<item>countdown</item>
@@ -7462,4 +7468,8 @@
<!-- By default ActivityOptions#makeScaleUpAnimation is only used between activities. This
config enables OEMs to support its usage across tasks.-->
<bool name="config_enableCrossTaskScaleUpAnimation">false</bool>
+
+ <!-- Biometrics fingerprint frr notification target activity component name, it shall be customized by OEMs -->
+ <string translatable="false" name="config_fingerprintFrrTargetComponent"></string>
+
</resources>
diff --git a/core/res/res/values/public-staging.xml b/core/res/res/values/public-staging.xml
index ac1e841d3143..ed524054a5d4 100644
--- a/core/res/res/values/public-staging.xml
+++ b/core/res/res/values/public-staging.xml
@@ -128,7 +128,7 @@
<staging-public-group type="id" first-id="0x01b20000">
<!-- @FlaggedApi(android.appwidget.flags.Flags.FLAG_ENGAGEMENT_METRICS) -->
- <public name="remoteViewsMetricsId"/>
+ <public name="removed_remoteViewsMetricsId"/>
<!-- @FlaggedApi("android.view.accessibility.a11y_selection_api") -->
<public name="accessibilityActionSetExtendedSelection"/>
</staging-public-group>
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index d94d659446ac..43ba327f4069 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -739,6 +739,9 @@
<!-- label for screenshot item in power menu [CHAR LIMIT=24]-->
<string name="global_action_screenshot">Screenshot</string>
+ <!-- label for standby item in power menu [CHAR LIMIT=24]-->
+ <string name="global_action_standby">Standby</string>
+
<!-- Take bug report menu title [CHAR LIMIT=30] -->
<string name="bugreport_title">Bug report</string>
<!-- Message in bugreport dialog describing what it does [CHAR LIMIT=NONE] -->
@@ -6864,4 +6867,10 @@ ul.</string>
<string name="usb_apm_usb_plugged_in_when_locked_notification_text">USB device is plugged in when Android is locked. To use device, please unlock Android first and then reinsert USB device to use it.</string>
<string name="usb_apm_usb_suspicious_activity_notification_title">Suspicious USB activity</string>
<string name="usb_apm_usb_suspicious_activity_notification_text">USB data signal has been disabled.</string>
+
+ <!-- Fingerprint failed rate too high notification title -->
+ <string name="fingerprint_frr_notification_title">Having trouble with Fingerprint Unlock?</string>
+ <!-- Fingerprint failed rate too high notification msg -->
+ <string name="fingerprint_frr_notification_msg">Tap to review tips to improve your unlocking experience</string>
+
</resources>
diff --git a/core/res/res/values/styles_material.xml b/core/res/res/values/styles_material.xml
index 8f13ee1ccb49..b6142592296c 100644
--- a/core/res/res/values/styles_material.xml
+++ b/core/res/res/values/styles_material.xml
@@ -508,6 +508,7 @@ please see styles_device_defaults.xml.
<item name="segPointGap">@dimen/notification_progress_segPoint_gap</item>
<item name="progressDrawable">@drawable/notification_progress</item>
<item name="trackerHeight">@dimen/notification_progress_tracker_height</item>
+ <item name="indeterminateDrawable">@drawable/notification_progress_indeterminate_horizontal_material</item>
</style>
<style name="Widget.Material.Notification.Text" parent="Widget.Material.Light.TextView">
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 219ac3f89997..70e3356a46fe 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -1958,6 +1958,7 @@
<java-symbol type="string" name="global_action_voice_assist" />
<java-symbol type="string" name="global_action_assist" />
<java-symbol type="string" name="global_action_screenshot" />
+ <java-symbol type="string" name="global_action_standby" />
<java-symbol type="string" name="invalidPuk" />
<java-symbol type="string" name="lockscreen_carrier_default" />
<java-symbol type="style" name="Animation.LockScreen" />
@@ -2717,6 +2718,9 @@
<java-symbol type="string" name="muted_by" />
<java-symbol type="string" name="zen_mode_alarm" />
+ <!-- For default state of operator name in status bar -->
+ <java-symbol type="integer" name="config_showOperatorNameDefault"/>
+
<java-symbol type="string" name="select_day" />
<java-symbol type="string" name="select_year" />
@@ -3721,6 +3725,7 @@
<java-symbol type="drawable" name="ic_screenshot" />
<java-symbol type="drawable" name="ic_faster_emergency" />
<java-symbol type="drawable" name="ic_media_seamless" />
+ <java-symbol type="drawable" name="ic_standby" />
<java-symbol type="drawable" name="emergency_icon" />
<java-symbol type="array" name="config_convert_to_emergency_number_map" />
@@ -5224,6 +5229,7 @@
<java-symbol type="id" name="remote_views_next_child" />
<java-symbol type="id" name="remote_views_stable_id" />
<java-symbol type="id" name="remote_views_override_id" />
+ <java-symbol type="id" name="remoteViewsMetricsId" />
<!-- View and control prompt -->
<java-symbol type="drawable" name="ic_accessibility_24dp" />
@@ -5988,4 +5994,10 @@
<!-- Default height of desktop view header for freeform tasks on launch. -->
<java-symbol type="dimen" name="desktop_view_default_header_height" />
+
+ <!-- Enable OEMs to support different frr notification target component activity -->
+ <java-symbol type="string" name="config_fingerprintFrrTargetComponent" />
+ <java-symbol type="string" name="fingerprint_frr_notification_title" />
+ <java-symbol type="string" name="fingerprint_frr_notification_msg" />
+
</resources>
diff --git a/core/res/res/xml/sms_short_codes.xml b/core/res/res/xml/sms_short_codes.xml
index 36564cd90d05..9b3a6cba5f23 100644
--- a/core/res/res/xml/sms_short_codes.xml
+++ b/core/res/res/xml/sms_short_codes.xml
@@ -358,7 +358,7 @@
<!-- USA: 5-6 digits (premium codes from https://www.premiumsmsrefunds.com/ShortCodes.htm),
visual voicemail code for T-Mobile: 122 -->
- <shortcode country="us" pattern="\\d{5,6}" free="122|\\d{5,6}" />
+ <shortcode country="us" pattern="\\d{5,6}" premium="20433|21(?:344|472)|22715|23(?:333|847)|24(?:15|28)0|25209|27(?:449|606|663)|28498|305(?:00|83)|32(?:340|941)|33(?:166|786|849)|34746|35(?:182|564)|37975|38(?:135|146|254)|41(?:366|463)|42335|43(?:355|500)|44(?:578|711|811)|45814|46(?:157|173|327)|46666|47553|48(?:221|277|669)|50(?:844|920)|51(?:062|368)|52944|54(?:723|892)|55928|56483|57370|59(?:182|187|252|342)|60339|61(?:266|982)|62478|64(?:219|898)|65(?:108|500)|69(?:208|388)|70877|71851|72(?:078|087|465)|73(?:288|588|882|909|997)|74(?:034|332|815)|76426|79213|81946|83177|84(?:103|685)|85797|86(?:234|236|666)|89616|90(?:715|842|938)|91(?:362|958)|94719|95297|96(?:040|666|835|969)|97(?:142|294|688)|99(?:689|796|807)" standard="44567|244444" free="122|87902|21696|24614|28003|30356|33669|40196|41064|41270|43753|44034|46645|52413|56139|57969|61785|66975|75136|76227|81398|83952|85140|86566|86799|95737|96684|99245|611611|96831|10907" />
<!--Uruguay : 1-6 digits (standard system default, not country specific) -->
<shortcode country="uy" pattern="\\d{1,6}" free="55002|191289" />
diff --git a/core/tests/coretests/res/xml/flags.xml b/core/tests/coretests/res/xml/flags.xml
new file mode 100644
index 000000000000..e580ea5dea00
--- /dev/null
+++ b/core/tests/coretests/res/xml/flags.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<first xmlns:android="http://schemas.android.com/apk/res/android">
+ <second android:featureFlag="android.content.res.always_false"/>
+</first> \ No newline at end of file
diff --git a/core/tests/coretests/src/android/app/NotificationManagerTest.java b/core/tests/coretests/src/android/app/NotificationManagerTest.java
index 250b9ce8d89d..001eb620dd0f 100644
--- a/core/tests/coretests/src/android/app/NotificationManagerTest.java
+++ b/core/tests/coretests/src/android/app/NotificationManagerTest.java
@@ -442,6 +442,44 @@ public class NotificationManagerTest {
@Test
@EnableFlags(Flags.FLAG_NM_BINDER_PERF_CACHE_CHANNELS)
+ public void getNotificationChannel_localModificationDoesNotChangeCache() throws Exception {
+ NotificationManager.invalidateNotificationChannelCache();
+ NotificationChannel original = new NotificationChannel("id", "name",
+ NotificationManager.IMPORTANCE_DEFAULT);
+ NotificationChannel originalConv = new NotificationChannel("", "name_conversation",
+ NotificationManager.IMPORTANCE_DEFAULT);
+ originalConv.setConversationId("id", "id_conversation");
+ when(mNotificationManager.mBackendService.getNotificationChannels(any(), any(),
+ anyInt())).thenReturn(new ParceledListSlice<>(
+ List.of(original.copy(), originalConv.copy())));
+
+ // modify the output channel, but only locally
+ NotificationChannel out = mNotificationManager.getNotificationChannel("id");
+ out.setName("modified");
+
+ // This should not change the result of getNotificationChannel
+ assertThat(mNotificationManager.getNotificationChannel("id")).isEqualTo(original);
+ assertThat(mNotificationManager.getNotificationChannel("id")).isNotEqualTo(out);
+
+ // and also check the conversation channel
+ NotificationChannel outConv = mNotificationManager.getNotificationChannel("id",
+ "id_conversation");
+ outConv.setName("conversation_modified");
+ assertThat(mNotificationManager.getNotificationChannel("id", "id_conversation")).isEqualTo(
+ originalConv);
+ assertThat(
+ mNotificationManager.getNotificationChannel("id", "id_conversation")).isNotEqualTo(
+ outConv);
+
+ // nonexistent conversation returns the (not modified) parent channel
+ assertThat(mNotificationManager.getNotificationChannel("id", "nonexistent")).isEqualTo(
+ original);
+ assertThat(mNotificationManager.getNotificationChannel("id", "nonexistent")).isNotEqualTo(
+ out);
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_NM_BINDER_PERF_CACHE_CHANNELS)
public void getNotificationChannelGroup_cachedUntilInvalidated() throws Exception {
// Data setup: group has some channels in it
NotificationChannelGroup g1 = new NotificationChannelGroup("g1", "group one");
@@ -521,6 +559,37 @@ public class NotificationManagerTest {
}
@Test
+ @EnableFlags(Flags.FLAG_NM_BINDER_PERF_CACHE_CHANNELS)
+ public void getNotificationChannelGroup_localModificationDoesNotChangeCache() throws Exception {
+ // Group setup
+ NotificationChannelGroup g1 = new NotificationChannelGroup("g1", "group one");
+ NotificationChannel nc1 = new NotificationChannel("nc1", "channel one",
+ NotificationManager.IMPORTANCE_DEFAULT);
+ nc1.setGroup("g1");
+ NotificationChannel nc2 = new NotificationChannel("nc2", "channel two",
+ NotificationManager.IMPORTANCE_DEFAULT);
+ nc2.setGroup("g1");
+
+ NotificationManager.invalidateNotificationChannelCache();
+ NotificationManager.invalidateNotificationChannelGroupCache();
+ when(mNotificationManager.mBackendService.getNotificationChannelGroupsWithoutChannels(
+ any())).thenReturn(new ParceledListSlice<>(List.of(g1.clone())));
+ when(mNotificationManager.mBackendService.getNotificationChannels(any(), any(), anyInt()))
+ .thenReturn(new ParceledListSlice<>(List.of(nc1.copy(), nc2.copy())));
+
+ NotificationChannelGroup g1result = mNotificationManager.getNotificationChannelGroup("g1");
+ g1result.setDescription("something different!");
+ for (NotificationChannel c : g1result.getChannels()) {
+ c.setDescription("also something different");
+ }
+
+ // expected output equivalent to original, unchanged
+ NotificationChannelGroup expectedG1 = g1.clone();
+ expectedG1.setChannels(List.of(nc1, nc2));
+ assertThat(mNotificationManager.getNotificationChannelGroup("g1")).isEqualTo(expectedG1);
+ }
+
+ @Test
@EnableFlags(Flags.FLAG_MODES_UI)
public void areAutomaticZenRulesUserManaged_handheld_isTrue() {
PackageManager pm = mock(PackageManager.class);
diff --git a/core/tests/coretests/src/android/app/NotificationTest.java b/core/tests/coretests/src/android/app/NotificationTest.java
index 157c74abc5de..0287956bd07f 100644
--- a/core/tests/coretests/src/android/app/NotificationTest.java
+++ b/core/tests/coretests/src/android/app/NotificationTest.java
@@ -462,7 +462,7 @@ public class NotificationTest {
@Test
@EnableFlags(Flags.FLAG_UI_RICH_ONGOING)
- public void testHasPromotableCharacteristics() {
+ public void testHasPromotableCharacteristics_bigText_bigTitle() {
Notification n = new Notification.Builder(mContext, "test")
.setSmallIcon(android.R.drawable.sym_def_app_icon)
.setStyle(new Notification.BigTextStyle().setBigContentTitle("BIG"))
@@ -475,6 +475,20 @@ public class NotificationTest {
@Test
@EnableFlags(Flags.FLAG_UI_RICH_ONGOING)
+ public void testHasPromotableCharacteristics_bigText_normalTitle() {
+ Notification n = new Notification.Builder(mContext, "test")
+ .setSmallIcon(android.R.drawable.sym_def_app_icon)
+ .setStyle(new Notification.BigTextStyle())
+ .setContentTitle("TITLE")
+ .setColor(Color.WHITE)
+ .setColorized(true)
+ .setOngoing(true)
+ .build();
+ assertThat(n.hasPromotableCharacteristics()).isTrue();
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_UI_RICH_ONGOING)
public void testHasPromotableCharacteristics_notOngoing() {
Notification n = new Notification.Builder(mContext, "test")
.setSmallIcon(android.R.drawable.sym_def_app_icon)
@@ -526,6 +540,51 @@ public class NotificationTest {
@Test
@EnableFlags(Flags.FLAG_UI_RICH_ONGOING)
+ public void testHasPromotableCharacteristics_noStyle_onlyBigTitle() {
+ Bundle extras = new Bundle();
+ extras.putString(Notification.EXTRA_TITLE_BIG, "BIG");
+ Notification n = new Notification.Builder(mContext, "test")
+ .setSmallIcon(android.R.drawable.sym_def_app_icon)
+ .setColor(Color.WHITE)
+ .setColorized(true)
+ .setOngoing(true)
+ .addExtras(extras)
+ .build();
+ assertThat(n.hasPromotableCharacteristics()).isFalse();
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_UI_RICH_ONGOING)
+ public void testHasPromotableCharacteristics_ongoingCallStyle_notColorized() {
+ PendingIntent intent = PendingIntent.getActivity(
+ mContext, 0, new Intent("test1"), PendingIntent.FLAG_IMMUTABLE);
+ Person person = new Person.Builder().setName("Caller").build();
+ Notification n = new Notification.Builder(mContext, "test")
+ .setSmallIcon(android.R.drawable.sym_def_app_icon)
+ .setStyle(Notification.CallStyle.forOngoingCall(person, intent))
+ .setColor(Color.WHITE)
+ .setOngoing(true)
+ .build();
+ assertThat(n.hasPromotableCharacteristics()).isTrue();
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_UI_RICH_ONGOING)
+ public void testHasPromotableCharacteristics_incomingCallStyle_notColorized() {
+ PendingIntent intent = PendingIntent.getActivity(
+ mContext, 0, new Intent("test1"), PendingIntent.FLAG_IMMUTABLE);
+ Person person = new Person.Builder().setName("Caller").build();
+ Notification n = new Notification.Builder(mContext, "test")
+ .setSmallIcon(android.R.drawable.sym_def_app_icon)
+ .setStyle(Notification.CallStyle.forIncomingCall(person, intent, intent))
+ .setColor(Color.WHITE)
+ .setOngoing(true)
+ .build();
+ assertThat(n.hasPromotableCharacteristics()).isFalse();
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_UI_RICH_ONGOING)
public void testHasPromotableCharacteristics_groupSummary() {
Notification n = new Notification.Builder(mContext, "test")
.setSmallIcon(android.R.drawable.sym_def_app_icon)
diff --git a/core/tests/coretests/src/android/appwidget/AppWidgetEventsTest.kt b/core/tests/coretests/src/android/appwidget/AppWidgetEventsTest.kt
index ea1158c88055..0135378ba681 100644
--- a/core/tests/coretests/src/android/appwidget/AppWidgetEventsTest.kt
+++ b/core/tests/coretests/src/android/appwidget/AppWidgetEventsTest.kt
@@ -16,14 +16,33 @@
package android.appwidget
+import android.app.PendingIntent
+import android.appwidget.AppWidgetHostView.InteractionLogger.MAX_NUM_ITEMS
+import android.content.Intent
import android.graphics.Rect
+import android.view.View
+import android.widget.ListView
+import android.widget.RemoteViews
import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.frameworks.coretests.R
import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class AppWidgetEventsTest {
+ private val context = InstrumentationRegistry.getInstrumentation().targetContext!!
+ private val hostView = AppWidgetHostView(context).apply {
+ setAppWidget(0, AppWidgetManager.getInstance(context).installedProviders.first())
+ }
+ private val pendingIntent = PendingIntent.getActivity(
+ context,
+ 0,
+ Intent(),
+ PendingIntent.FLAG_IMMUTABLE,
+ )
+
@Test
fun createWidgetInteractionEvent() {
val appWidgetId = 1
@@ -48,4 +67,123 @@ class AppWidgetEventsTest {
assertThat(bundle.getIntArray(AppWidgetManager.EXTRA_EVENT_SCROLLED_VIEWS))
.asList().containsExactly(scrolled[0], scrolled[1], scrolled[2])
}
+
+ @Test
+ fun interactionLogger_click() {
+ val itemCount = MAX_NUM_ITEMS + 1
+ // Set a different value for the viewId to test that the logger always uses the
+ // metrics tag if available.
+ fun viewId(i: Int) = i + Int.MIN_VALUE
+ val remoteViews = RemoteViews(context.packageName, R.layout.remote_views_test).apply {
+ for (i in 0 until itemCount) {
+ val metricsTag = i
+ val item =
+ RemoteViews(context.packageName, R.layout.remote_views_text, viewId(i)).apply {
+ setUsageEventTag(viewId(i), metricsTag)
+ setOnClickPendingIntent(viewId(i), pendingIntent)
+ }
+ addView(R.id.layout, item)
+ }
+ }
+ hostView.updateAppWidget(remoteViews)
+ assertThat(hostView.interactionLogger.clickedIds).isEmpty()
+
+
+ for (i in 0 until itemCount.minus(1)) {
+ val item = hostView.findViewById<View>(viewId(i))
+ assertThat(item).isNotNull()
+ assertThat(item.performClick()).isTrue()
+ assertThat(hostView.interactionLogger.clickedIds)
+ .containsExactlyElementsIn(0..i)
+ }
+ assertThat(hostView.interactionLogger.clickedIds).hasSize(MAX_NUM_ITEMS)
+
+ // Last item click should not be recorded because we've reached MAX_VIEW_IDS
+ val lastItem = hostView.findViewById<View>(viewId(itemCount - 1))
+ assertThat(lastItem).isNotNull()
+ assertThat(lastItem.performClick()).isTrue()
+ assertThat(hostView.interactionLogger.clickedIds).hasSize(MAX_NUM_ITEMS)
+ assertThat(hostView.interactionLogger.clickedIds)
+ .containsExactlyElementsIn(0..itemCount.minus(2))
+ }
+
+ @Test
+ fun interactionLogger_click_listItem() {
+ val itemCount = 5
+ val remoteViews = RemoteViews(context.packageName, R.layout.remote_views_list).apply {
+ setPendingIntentTemplate(R.id.list, pendingIntent)
+ setRemoteAdapter(
+ R.id.list,
+ RemoteViews.RemoteCollectionItems.Builder().run {
+ for (i in 0 until itemCount) {
+ val item = RemoteViews(context.packageName, R.layout.remote_views_test)
+ item.setOnClickFillInIntent(R.id.text, Intent())
+ item.setUsageEventTag(R.id.text, i)
+ addItem(i.toLong(), item)
+ }
+ build()
+ }
+ )
+ setUsageEventTag(R.id.list, -1)
+ }
+ hostView.updateAppWidget(remoteViews)
+ assertThat(hostView.interactionLogger.clickedIds).isEmpty()
+
+ val list = hostView.findViewById<ListView>(R.id.list)
+ assertThat(list).isNotNull()
+ list.layout(0, 0, 500, 500)
+ for (i in 0 until itemCount) {
+ val item = list.getChildAt(i).findViewById<View>(R.id.text)
+ assertThat(item.performClick()).isTrue()
+ assertThat(hostView.interactionLogger.clickedIds)
+ .containsExactlyElementsIn(0..i)
+ }
+ }
+
+ @Test
+ fun interactionLogger_scroll() {
+ val itemCount = MAX_NUM_ITEMS + 1
+ // Set a different value for the viewId to test that the logger always uses the
+ // metrics tag if available.
+ fun viewId(i: Int) = i + Int.MIN_VALUE
+ val remoteViews = RemoteViews(context.packageName, R.layout.remote_views_test).apply {
+ for (i in 0 until itemCount) {
+ val metricsTag = i
+ val item =
+ RemoteViews(context.packageName, R.layout.remote_views_list, viewId(i)).apply {
+ setUsageEventTag(viewId(i), metricsTag)
+ setRemoteAdapter(
+ viewId(i),
+ RemoteViews.RemoteCollectionItems.Builder().run {
+ addItem(
+ 0L,
+ RemoteViews(context.packageName, R.layout.remote_views_test)
+ )
+ build()
+ }
+ )
+ }
+ addView(R.id.layout, item)
+ }
+ }
+ hostView.updateAppWidget(remoteViews)
+ assertThat(hostView.interactionLogger.scrolledIds).isEmpty()
+
+ for (i in 0 until itemCount.minus(1)) {
+ val item = hostView.findViewById<ListView>(viewId(i))
+ assertThat(item).isNotNull()
+ item.fling(/* velocityY= */ 100)
+ assertThat(hostView.interactionLogger.scrolledIds)
+ .containsExactlyElementsIn(0..i)
+ }
+ assertThat(hostView.interactionLogger.scrolledIds).hasSize(MAX_NUM_ITEMS)
+
+ // Last item scroll should not be recorded because we've reached MAX_VIEW_IDS
+ val lastItem = hostView.findViewById<ListView>(viewId(itemCount - 1))
+ assertThat(lastItem).isNotNull()
+ lastItem.fling(/* velocityY= */ 100)
+ assertThat(hostView.interactionLogger.scrolledIds).hasSize(MAX_NUM_ITEMS)
+ assertThat(hostView.interactionLogger.scrolledIds)
+ .containsExactlyElementsIn(0..itemCount.minus(2))
+ }
}
diff --git a/core/tests/coretests/src/android/content/pm/UserInfoTest.java b/core/tests/coretests/src/android/content/pm/UserInfoTest.java
index c84c21557ea4..4e8ada5502ac 100644
--- a/core/tests/coretests/src/android/content/pm/UserInfoTest.java
+++ b/core/tests/coretests/src/android/content/pm/UserInfoTest.java
@@ -17,6 +17,7 @@
package android.content.pm;
import static android.content.pm.UserInfo.FLAG_DEMO;
+import static android.content.pm.UserInfo.FLAG_DISABLED;
import static android.content.pm.UserInfo.FLAG_FULL;
import static android.content.pm.UserInfo.FLAG_GUEST;
import static android.content.pm.UserInfo.FLAG_MAIN;
@@ -26,26 +27,23 @@ import static android.os.UserManager.USER_TYPE_FULL_RESTRICTED;
import static android.os.UserManager.USER_TYPE_FULL_SYSTEM;
import static android.os.UserManager.USER_TYPE_SYSTEM_HEADLESS;
-import static com.google.common.truth.Truth.assertThat;
-
+import android.annotation.UserIdInt;
import android.content.pm.UserInfo.UserInfoFlag;
+import android.os.Parcel;
import android.os.UserHandle;
import android.platform.test.annotations.DisableFlags;
import android.platform.test.annotations.EnableFlags;
import android.platform.test.flag.junit.SetFlagsRule;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import com.google.common.truth.Expect;
import org.junit.Rule;
import org.junit.Test;
-import org.junit.runner.RunWith;
-
-@RunWith(AndroidJUnit4.class)
@SmallTest
+@SuppressWarnings("deprecation")
public final class UserInfoTest {
@Rule
@@ -56,39 +54,40 @@ public final class UserInfoTest {
@Test
public void testSimple() throws Exception {
- final UserInfo ui = new UserInfo(10, "Test", UserInfo.FLAG_GUEST);
+ UserInfo ui = createTestUserInfo(FLAG_GUEST);
- assertThat(ui.getUserHandle()).isEqualTo(UserHandle.of(10));
- assertThat(ui.name).isEqualTo("Test");
+ expect.withMessage("getUserHandle()").that(ui.getUserHandle()).isEqualTo(UserHandle.of(10));
+ expect.that(ui.name).isEqualTo("Test");
// Derived based on userType field
- assertThat(ui.isManagedProfile()).isEqualTo(false);
- assertThat(ui.isGuest()).isEqualTo(true);
- assertThat(ui.isRestricted()).isEqualTo(false);
- assertThat(ui.isDemo()).isEqualTo(false);
- assertThat(ui.isCloneProfile()).isEqualTo(false);
- assertThat(ui.isCommunalProfile()).isEqualTo(false);
- assertThat(ui.isPrivateProfile()).isEqualTo(false);
+ expect.withMessage("isManagedProfile()").that(ui.isManagedProfile()).isFalse();
+ expect.withMessage("isGuest()").that(ui.isGuest()).isTrue();
+ expect.withMessage("isRestricted()").that(ui.isRestricted()).isFalse();
+ expect.withMessage("isDemo()").that(ui.isDemo()).isFalse();
+ expect.withMessage("isCloneProfile()").that(ui.isCloneProfile()).isFalse();
+ expect.withMessage("isCommunalProfile()").that(ui.isCommunalProfile()).isFalse();
+ expect.withMessage("isPrivateProfile()").that(ui.isPrivateProfile()).isFalse();
+ expect.withMessage("isSupervisingProfile()").that(ui.isSupervisingProfile()).isFalse();
// Derived based on flags field
- assertThat(ui.isPrimary()).isEqualTo(false);
- assertThat(ui.isAdmin()).isEqualTo(false);
- assertThat(ui.isProfile()).isEqualTo(false);
- assertThat(ui.isEnabled()).isEqualTo(true);
- assertThat(ui.isQuietModeEnabled()).isEqualTo(false);
- assertThat(ui.isEphemeral()).isEqualTo(false);
- assertThat(ui.isForTesting()).isEqualTo(false);
- assertThat(ui.isInitialized()).isEqualTo(false);
- assertThat(ui.isFull()).isEqualTo(false);
- assertThat(ui.isMain()).isEqualTo(false);
+ expect.withMessage("isPrimary()").that(ui.isPrimary()).isFalse();
+ expect.withMessage("isAdmin()").that(ui.isAdmin()).isFalse();
+ expect.withMessage("isProfile()").that(ui.isProfile()).isFalse();
+ expect.withMessage("isEnabled()").that(ui.isEnabled()).isTrue();
+ expect.withMessage("isQuietModeEnabled()").that(ui.isQuietModeEnabled()).isFalse();
+ expect.withMessage("isEphemeral()").that(ui.isEphemeral()).isFalse();
+ expect.withMessage("isForTesting()").that(ui.isForTesting()).isFalse();
+ expect.withMessage("isInitialized()").that(ui.isInitialized()).isFalse();
+ expect.withMessage("isFull()").that(ui.isFull()).isFalse();
+ expect.withMessage("isMain()").that(ui.isMain()).isFalse();
}
@Test
public void testDebug() throws Exception {
- final UserInfo ui = new UserInfo(10, "Test", UserInfo.FLAG_GUEST);
+ UserInfo ui = createTestUserInfo(FLAG_GUEST);
- assertThat(ui.toString()).isNotEmpty();
- assertThat(ui.toFullString()).isNotEmpty();
+ expect.withMessage("toString()").that(ui.toString()).isNotEmpty();
+ expect.withMessage("toFullString()").that(ui.toFullString()).isNotEmpty();
}
@Test
@@ -125,6 +124,70 @@ public final class UserInfoTest {
createTestUserInfo(USER_TYPE_SYSTEM_HEADLESS, FLAG_SYSTEM));
}
+ @Test
+ public void testParcelUnparcelUserInfo() throws Exception {
+ UserInfo info = createUserWithAllFields();
+
+ Parcel out = Parcel.obtain();
+ info.writeToParcel(out, 0);
+ byte[] data = out.marshall();
+ out.recycle();
+
+ Parcel in = Parcel.obtain();
+ try {
+ in.unmarshall(data, 0, data.length);
+ in.setDataPosition(0);
+ UserInfo read = UserInfo.CREATOR.createFromParcel(in);
+ assertUserInfoEquals(info, read, /* parcelCopy= */ true);
+ } finally {
+ in.recycle();
+ }
+ }
+
+ @Test
+ public void testCopyConstructor() throws Exception {
+ UserInfo info = createUserWithAllFields();
+
+ UserInfo copy = new UserInfo(info);
+
+ assertUserInfoEquals(info, copy, /* parcelCopy= */ false);
+ }
+
+ @Test
+ public void testSupportSwitchTo_partial() throws Exception {
+ UserInfo userInfo = createUser(100, FLAG_FULL, /* userType= */ null);
+ userInfo.partial = true;
+ expect.withMessage("Supports switch to a partial user").that(userInfo.supportsSwitchTo())
+ .isFalse();
+ }
+
+ @Test
+ public void testSupportSwitchTo_disabled() throws Exception {
+ UserInfo userInfo = createUser(100, FLAG_DISABLED, /* userType= */ null);
+ expect.withMessage("Supports switch to a DISABLED user").that(userInfo.supportsSwitchTo())
+ .isFalse();
+ }
+
+ @Test
+ public void testSupportSwitchTo_preCreated() throws Exception {
+ UserInfo userInfo = createUser(100, FLAG_FULL, /* userType= */ null);
+ userInfo.preCreated = true;
+ expect.withMessage("Supports switch to a pre-created user")
+ .that(userInfo.supportsSwitchTo())
+ .isFalse();
+
+ userInfo.preCreated = false;
+ expect.withMessage("Supports switch to a full, real user").that(userInfo.supportsSwitchTo())
+ .isTrue();
+ }
+
+ @Test
+ public void testSupportSwitchTo_profile() throws Exception {
+ UserInfo userInfo = createUser(100, FLAG_PROFILE, /* userType= */ null);
+ expect.withMessage("Supports switch to a profile").that(userInfo.supportsSwitchTo())
+ .isFalse();
+ }
+
/**
* Creates a new {@link UserInfo} with id {@code 10}, name {@code Test}, and the given
* {@code flags}.
@@ -141,6 +204,49 @@ public final class UserInfoTest {
return new UserInfo(10, "Test", /* iconPath= */ null, flags, userType);
}
+ /** Creates a UserInfo with the given flags and userType. */
+ private UserInfo createUser(@UserIdInt int userId, @UserInfoFlag int flags, String userType) {
+ return new UserInfo(userId, "A Name", "A path", flags, userType);
+ }
+
+ private UserInfo createUserWithAllFields() {
+ UserInfo user = new UserInfo(/*id= */ 21, "A Name", "A path", /*flags*/ 0x0ff0ff, "A type");
+ user.serialNumber = 5;
+ user.creationTime = 4L << 32;
+ user.lastLoggedInTime = 5L << 32;
+ user.lastLoggedInFingerprint = "afingerprint";
+ user.profileGroupId = 45;
+ user.restrictedProfileParentId = 4;
+ user.profileBadge = 2;
+ user.partial = true;
+ user.guestToRemove = true;
+ user.preCreated = true;
+ user.convertedFromPreCreated = true;
+ return user;
+ }
+
+ private void assertUserInfoEquals(UserInfo one, UserInfo two, boolean parcelCopy) {
+ expect.withMessage("Id").that(two.id).isEqualTo(one.id);
+ expect.withMessage("Name").that(two.name).isEqualTo(one.name);
+ expect.withMessage("Icon path").that(two.iconPath).isEqualTo(one.iconPath);
+ expect.withMessage("Flags").that(two.flags).isEqualTo(one.flags);
+ expect.withMessage("UserType").that(two.userType).isEqualTo(one.userType);
+ expect.withMessage("profile group").that(two.profileGroupId).isEqualTo(one.profileGroupId);
+ expect.withMessage("restricted profile parent").that(two.restrictedProfileParentId)
+ .isEqualTo(one.restrictedProfileParentId);
+ expect.withMessage("profile badge").that(two.profileBadge).isEqualTo(one.profileBadge);
+ expect.withMessage("partial").that(two.partial).isEqualTo(one.partial);
+ expect.withMessage("guestToRemove").that(two.guestToRemove).isEqualTo(one.guestToRemove);
+ expect.withMessage("preCreated").that(two.preCreated).isEqualTo(one.preCreated);
+ if (parcelCopy) {
+ expect.withMessage("convertedFromPreCreated").that(two.convertedFromPreCreated)
+ .isFalse();
+ } else {
+ expect.withMessage("convertedFromPreCreated").that(two.convertedFromPreCreated)
+ .isEqualTo(one.convertedFromPreCreated);
+ }
+ }
+
private void expectCanHaveProfile(String description, UserInfo user) {
expect.withMessage("canHaveProfile() on %s (%s)", description, user)
.that(user.canHaveProfile()).isTrue();
diff --git a/core/tests/coretests/src/android/content/res/XmlResourcesFlaggedTest.kt b/core/tests/coretests/src/android/content/res/XmlResourcesFlaggedTest.kt
new file mode 100644
index 000000000000..820bcfc03724
--- /dev/null
+++ b/core/tests/coretests/src/android/content/res/XmlResourcesFlaggedTest.kt
@@ -0,0 +1,116 @@
+/*
+ * 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.content.res
+
+import android.platform.test.annotations.Presubmit
+import android.platform.test.annotations.RequiresFlagsEnabled
+import android.platform.test.flag.junit.CheckFlagsRule
+import android.platform.test.flag.junit.DeviceFlagsValueProvider
+import android.util.TypedValue
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry
+
+import com.android.frameworks.coretests.R
+import com.android.internal.pm.pkg.parsing.ParsingPackageUtils
+
+import junit.framework.Assert.assertEquals
+import junit.framework.Assert.assertTrue
+
+
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.xmlpull.v1.XmlPullParser
+import org.xmlpull.v1.XmlPullParserException
+
+import java.io.IOException
+
+/**
+* Tests for flag handling within Resources.loadXmlResourceParser() and methods that call it.
+*/
+@Presubmit
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@android.platform.test.annotations.DisabledOnRavenwood(bug = 396458006,
+ reason = "Resource flags don't fully work on Ravenwood yet")
+class XmlResourcesFlaggedTest {
+ @get:Rule
+ val mCheckFlagsRule: CheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule()
+
+ private var mResources: Resources = Resources(null)
+
+ @Before
+ fun setup() {
+ mResources = InstrumentationRegistry.getInstrumentation().getContext().getResources()
+ mResources.getImpl().flushLayoutCache()
+ }
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_LAYOUT_READWRITE_FLAGS)
+ fun flaggedXmlTypedValueMarkedAsSuch() {
+ val tv = TypedValue()
+ mResources.getImpl().getValue(R.xml.flags, tv, false)
+ assertTrue(tv.usesFeatureFlags)
+ }
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_LAYOUT_READWRITE_FLAGS)
+ @Throws(IOException::class, XmlPullParserException::class)
+ fun parsedFlaggedXmlWithTrueOneElement() {
+ ParsingPackageUtils.getAconfigFlags()
+ .addFlagValuesForTesting(mapOf("android.content.res.always_false" to false))
+ val tv = TypedValue()
+ mResources.getImpl().getValue(R.xml.flags, tv, false)
+ val parser = mResources.loadXmlResourceParser(
+ tv.string.toString(),
+ R.xml.flags,
+ tv.assetCookie,
+ "xml",
+ true
+ )
+ assertEquals(XmlPullParser.START_DOCUMENT, parser.next())
+ assertEquals(XmlPullParser.START_TAG, parser.next())
+ assertEquals("first", parser.getName())
+ assertEquals(XmlPullParser.END_TAG, parser.next())
+ assertEquals(XmlPullParser.END_DOCUMENT, parser.next())
+ }
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_LAYOUT_READWRITE_FLAGS)
+ @Throws(IOException::class, XmlPullParserException::class)
+ fun parsedFlaggedXmlWithFalseTwoElements() {
+ val tv = TypedValue()
+ mResources.getImpl().getValue(R.xml.flags, tv, false)
+ val parser = mResources.loadXmlResourceParser(
+ tv.string.toString(),
+ R.xml.flags,
+ tv.assetCookie,
+ "xml",
+ false
+ )
+ assertEquals(XmlPullParser.START_DOCUMENT, parser.next())
+ assertEquals(XmlPullParser.START_TAG, parser.next())
+ assertEquals("first", parser.getName())
+ assertEquals(XmlPullParser.START_TAG, parser.next())
+ assertEquals("second", parser.getName())
+ assertEquals(XmlPullParser.END_TAG, parser.next())
+ assertEquals(XmlPullParser.END_TAG, parser.next())
+ assertEquals(XmlPullParser.END_DOCUMENT, parser.next())
+ }
+}
diff --git a/core/tests/coretests/src/android/text/LayoutTest.java b/core/tests/coretests/src/android/text/LayoutTest.java
index 11ec9f8e1912..7d8afcabad7b 100644
--- a/core/tests/coretests/src/android/text/LayoutTest.java
+++ b/core/tests/coretests/src/android/text/LayoutTest.java
@@ -1029,51 +1029,16 @@ public class LayoutTest {
@Test
@RequiresFlagsEnabled(FLAG_HIGH_CONTRAST_TEXT_SMALL_TEXT_RECT)
- public void highContrastTextEnabled_testWhitespaceText_DrawsBackgroundsWithAdjacentLetters() {
- mTextPaint.setColor(Color.BLACK);
- SpannableString spannedText = new SpannableString("Test\tTap and Space");
-
- // Set the entire text to white initially
- spannedText.setSpan(
- new ForegroundColorSpan(Color.WHITE),
- /* start= */ 0,
- /* end= */ spannedText.length(),
- Spanned.SPAN_INCLUSIVE_EXCLUSIVE
- );
-
- // Find the whitespace character and set its color to black
- for (int i = 0; i < spannedText.length(); i++) {
- if (Character.isWhitespace(spannedText.charAt(i))) {
- spannedText.setSpan(
- new ForegroundColorSpan(Color.BLACK),
- i,
- i + 1,
- Spanned.SPAN_INCLUSIVE_EXCLUSIVE
- );
- }
- }
-
- Layout layout = new StaticLayout(spannedText, mTextPaint, mWidth,
- mAlign, mSpacingMult, mSpacingAdd, /* includePad= */ false);
-
- MockCanvas c = new MockCanvas(/* width= */ 256, /* height= */ 256);
- c.setHighContrastTextEnabled(true);
- layout.draw(
- c,
- /* highlightPaths= */ null,
- /* highlightPaints= */ null,
- /* selectionPath= */ null,
- /* selectionPaint= */ null,
- /* cursorOffsetVertical= */ 0
- );
+ public void highContrastTextEnabled_testWhiteSpaceWithinText_drawsSameBackgroundswithText() {
+ SpannableString spannedText = new SpannableString("Hello\tWorld !");
+ testSpannableStringAppliesAllColorsCorrectly(spannedText);
+ }
- List<MockCanvas.DrawCommand> drawCommands = c.getDrawCommands();
- for (int i = 0; i < drawCommands.size(); i++) {
- MockCanvas.DrawCommand drawCommand = drawCommands.get(i);
- if (drawCommand.rect != null) {
- expect.that(removeAlpha(drawCommand.paint.getColor())).isEqualTo(Color.BLACK);
- }
- }
+ @Test
+ @RequiresFlagsEnabled(FLAG_HIGH_CONTRAST_TEXT_SMALL_TEXT_RECT)
+ public void highContrastTextEnabled_testWhiteSpaceAtStart_drawsCorrectBackgroundsOnText() {
+ SpannableString spannedText = new SpannableString(" HelloWorld!");
+ testSpannableStringAppliesAllColorsCorrectly(spannedText);
}
@Test
@@ -1331,5 +1296,54 @@ public class LayoutTest {
"",
new boolean[]{false});
}
+
+ private void testSpannableStringAppliesAllColorsCorrectly(SpannableString spannedText) {
+ for (int textColor : new int[] {Color.WHITE, Color.BLACK}) {
+ final int contrastingColor = textColor == Color.WHITE ? Color.BLACK : Color.WHITE;
+ // Set the paint color to the contrasting color to verify the high contrast text
+ // background rect color is correct.
+ mTextPaint.setColor(contrastingColor);
+
+ // Set the entire text to test color initially
+ spannedText.setSpan(
+ new ForegroundColorSpan(textColor),
+ /* start= */ 0,
+ /* end= */ spannedText.length(),
+ Spanned.SPAN_INCLUSIVE_EXCLUSIVE
+ );
+
+ Layout layout = new StaticLayout(spannedText, mTextPaint, mWidth,
+ mAlign, mSpacingMult, mSpacingAdd, /* includePad= */ false);
+
+ MockCanvas c = new MockCanvas(/* width= */ 256, /* height= */ 256);
+ c.setHighContrastTextEnabled(true);
+ layout.draw(
+ c,
+ /* highlightPaths= */ null,
+ /* highlightPaints= */ null,
+ /* selectionPath= */ null,
+ /* selectionPaint= */ null,
+ /* cursorOffsetVertical= */ 0
+ );
+
+ int numBackgroundsFound = 0;
+ List<MockCanvas.DrawCommand> drawCommands = c.getDrawCommands();
+ for (int i = 0; i < drawCommands.size(); i++) {
+ MockCanvas.DrawCommand drawCommand = drawCommands.get(i);
+
+ if (drawCommand.rect != null) {
+ numBackgroundsFound++;
+ // Verifies the background color of the high-contrast rectangle drawn behind
+ // the text. In high-contrast mode, the background color should contrast with
+ // the text color. 'contrastingColor' represents the expected background color,
+ // which is the inverse of the text color (e.g., if text is white, background
+ // is black, and vice versa).
+ expect.that(removeAlpha(drawCommand.paint.getColor()))
+ .isEqualTo(contrastingColor);
+ }
+ }
+ expect.that(numBackgroundsFound).isLessThan(spannedText.length());
+ }
+ }
}
diff --git a/core/tests/coretests/src/android/view/ScrollCaptureConnectionTest.java b/core/tests/coretests/src/android/view/ScrollCaptureConnectionTest.java
index bee5dc4bf3c0..81954cb9a1a9 100644
--- a/core/tests/coretests/src/android/view/ScrollCaptureConnectionTest.java
+++ b/core/tests/coretests/src/android/view/ScrollCaptureConnectionTest.java
@@ -16,8 +16,6 @@
package android.view;
-import static androidx.test.InstrumentationRegistry.getTargetContext;
-
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
@@ -32,7 +30,6 @@ import static org.mockito.Mockito.when;
import android.graphics.Point;
import android.graphics.Rect;
import android.os.Binder;
-import android.os.Handler;
import android.os.IBinder;
import android.os.ICancellationSignal;
import android.os.RemoteException;
@@ -54,7 +51,6 @@ import java.util.concurrent.Executor;
/**
* Tests of {@link ScrollCaptureConnection}.
*/
-@SuppressWarnings("UnnecessaryLocalVariable")
@Presubmit
@SmallTest
@RunWith(AndroidJUnit4.class)
@@ -68,9 +64,8 @@ public class ScrollCaptureConnectionTest {
private ScrollCaptureTarget mTarget;
private ScrollCaptureConnection mConnection;
- private IBinder mConnectionBinder = new Binder("ScrollCaptureConnection Test");
+ private final IBinder mConnectionBinder = new Binder("ScrollCaptureConnection Test");
- private Handler mHandler;
@Mock
private Surface mSurface;
@@ -85,7 +80,6 @@ public class ScrollCaptureConnectionTest {
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
- mHandler = new Handler(getTargetContext().getMainLooper());
when(mSurface.isValid()).thenReturn(true);
when(mView.getScrollCaptureHint()).thenReturn(View.SCROLL_CAPTURE_HINT_INCLUDE);
when(mRemote.asBinder()).thenReturn(mConnectionBinder);
@@ -269,8 +263,68 @@ public class ScrollCaptureConnectionTest {
assertFalse(mConnection.isConnected());
}
+ @Test(expected = RemoteException.class)
+ public void testRequestImage_beforeStarted() throws RemoteException {
+ mConnection.requestImage(new Rect(0, 1, 2, 3));
+ }
+
+
+ @Test(expected = RemoteException.class)
+ public void testRequestImage_beforeStartCompleted() throws RemoteException {
+ mFakeUiThread.setImmediate(false);
+ mConnection.startCapture(mSurface, mRemote);
+ mConnection.requestImage(new Rect(0, 1, 2, 3));
+ mFakeUiThread.runAll();
+ }
+
+ @Test
+ public void testCompleteStart_afterClosing() throws RemoteException {
+ mConnection.startCapture(mSurface, mRemote);
+ mConnection.close();
+ mFakeUiThread.setImmediate(false);
+ mCallback.completeStartRequest();
+ mFakeUiThread.runAll();
+ }
+
+ @Test
+ public void testLateCallbacks() throws RemoteException {
+ mConnection.startCapture(mSurface, mRemote);
+ mCallback.completeStartRequest();
+ mConnection.requestImage(new Rect(1, 2, 3, 4));
+ mConnection.endCapture();
+ mFakeUiThread.setImmediate(false);
+ mCallback.completeImageRequest(new Rect(1, 2, 3, 4));
+ mCallback.completeEndRequest();
+ mFakeUiThread.runAll();
+ }
+
+ @Test
+ public void testDelayedClose() throws RemoteException {
+ mConnection.startCapture(mSurface, mRemote);
+ mCallback.completeStartRequest();
+ mFakeUiThread.setImmediate(false);
+ mConnection.endCapture();
+ mFakeUiThread.runAll();
+ mConnection.close();
+ mCallback.completeEndRequest();
+ mFakeUiThread.runAll();
+ }
+
+ @Test
+ public void testRequestImage_delayedCancellation() throws Exception {
+ mConnection.startCapture(mSurface, mRemote);
+ mCallback.completeStartRequest();
+
+ ICancellationSignal signal = mConnection.requestImage(new Rect(1, 2, 3, 4));
+ mFakeUiThread.setImmediate(false);
+
+ signal.cancel();
+ mCallback.completeImageRequest(new Rect(1, 2, 3, 4));
+ }
+
+
static class FakeExecutor implements Executor {
- private Queue<Runnable> mQueue = new ArrayDeque<>();
+ private final Queue<Runnable> mQueue = new ArrayDeque<>();
private boolean mImmediate;
@Override
diff --git a/core/tests/coretests/src/android/view/ViewRootImplTest.java b/core/tests/coretests/src/android/view/ViewRootImplTest.java
index 5774109e1451..1b7805c351db 100644
--- a/core/tests/coretests/src/android/view/ViewRootImplTest.java
+++ b/core/tests/coretests/src/android/view/ViewRootImplTest.java
@@ -16,8 +16,6 @@
package android.view;
-import static android.app.UiModeManager.MODE_NIGHT_NO;
-import static android.app.UiModeManager.MODE_NIGHT_YES;
import static android.util.SequenceUtils.getInitSeq;
import static android.view.HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING;
import static android.view.InputDevice.SOURCE_ROTARY_ENCODER;
@@ -69,10 +67,9 @@ import static org.junit.Assume.assumeTrue;
import android.annotation.NonNull;
import android.app.Instrumentation;
import android.app.UiModeManager;
-import android.app.UiModeManager.ForceInvertType;
import android.content.Context;
+import android.graphics.Color;
import android.graphics.ForceDarkType;
-import android.graphics.ForceDarkType.ForceDarkTypeDef;
import android.graphics.Rect;
import android.hardware.display.DisplayManagerGlobal;
import android.os.Binder;
@@ -101,8 +98,6 @@ import com.android.compatibility.common.util.TestUtils;
import com.android.cts.input.BlockingQueueEventVerifier;
import com.android.window.flags.Flags;
-import com.google.common.truth.Expect;
-
import org.hamcrest.Matcher;
import org.junit.After;
import org.junit.AfterClass;
@@ -131,8 +126,6 @@ public class ViewRootImplTest {
@Rule
public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
- @Rule
- public final Expect mExpect = Expect.create();
private ViewRootImpl mViewRootImpl;
private View mView;
@@ -1516,29 +1509,83 @@ public class ViewRootImplTest {
}
@Test
- @RequiresFlagsEnabled(FLAG_FORCE_INVERT_COLOR)
- public void updateConfiguration_returnsExpectedForceDarkMode() {
+ @EnableFlags(FLAG_FORCE_INVERT_COLOR)
+ public void determineForceDarkType_systemLightMode_returnsNone() throws Exception {
+ waitForSystemNightModeActivated(false);
+
+ TestUtils.waitUntil("Waiting for ForceDarkType to be ready",
+ () -> (mViewRootImpl.determineForceDarkType() == ForceDarkType.NONE));
+
+ }
+
+ @Test
+ @EnableFlags(FLAG_FORCE_INVERT_COLOR)
+ public void determineForceDarkType_systemNightModeAndDisableForceInvertColor_returnsNone()
+ throws Exception {
waitForSystemNightModeActivated(true);
- verifyForceDarkType(/* isAppInNightMode= */ true, /* isForceInvertEnabled= */ true,
- UiModeManager.FORCE_INVERT_TYPE_DARK, ForceDarkType.FORCE_INVERT_COLOR_DARK);
- verifyForceDarkType(/* isAppInNightMode= */ true, /* isForceInvertEnabled= */ false,
- UiModeManager.FORCE_INVERT_TYPE_OFF, ForceDarkType.NONE);
- verifyForceDarkType(/* isAppInNightMode= */ false, /* isForceInvertEnabled= */ true,
- UiModeManager.FORCE_INVERT_TYPE_DARK, ForceDarkType.FORCE_INVERT_COLOR_DARK);
- verifyForceDarkType(/* isAppInNightMode= */ false, /* isForceInvertEnabled= */ false,
- UiModeManager.FORCE_INVERT_TYPE_OFF, ForceDarkType.NONE);
+ enableForceInvertColor(false);
- waitForSystemNightModeActivated(false);
+ TestUtils.waitUntil("Waiting for ForceDarkType to be ready",
+ () -> (mViewRootImpl.determineForceDarkType() == ForceDarkType.NONE));
+ }
+
+ @Test
+ @EnableFlags(FLAG_FORCE_INVERT_COLOR)
+ public void
+ determineForceDarkType_isLightThemeAndIsLightBackground_returnsForceInvertColorDark()
+ throws Exception {
+ // Set up configurations for force invert color
+ waitForSystemNightModeActivated(true);
+ enableForceInvertColor(true);
+
+ setUpViewAttributes(/* isLightTheme= */ true, /* isLightBackground = */ true);
- verifyForceDarkType(/* isAppInNightMode= */ true, /* isForceInvertEnabled= */ true,
- UiModeManager.FORCE_INVERT_TYPE_OFF, ForceDarkType.NONE);
- verifyForceDarkType(/* isAppInNightMode= */ true, /* isForceInvertEnabled= */ false,
- UiModeManager.FORCE_INVERT_TYPE_OFF, ForceDarkType.NONE);
- verifyForceDarkType(/* isAppInNightMode= */ false, /* isForceInvertEnabled= */ true,
- UiModeManager.FORCE_INVERT_TYPE_OFF, ForceDarkType.NONE);
- verifyForceDarkType(/* isAppInNightMode= */ false, /* isForceInvertEnabled= */ false,
- UiModeManager.FORCE_INVERT_TYPE_OFF, ForceDarkType.NONE);
+ TestUtils.waitUntil("Waiting for ForceDarkType to be ready",
+ () -> (mViewRootImpl.determineForceDarkType()
+ == ForceDarkType.FORCE_INVERT_COLOR_DARK));
+ }
+
+ @Test
+ @EnableFlags(FLAG_FORCE_INVERT_COLOR)
+ public void determineForceDarkType_isLightThemeAndNotLightBackground_returnsNone()
+ throws Exception {
+ // Set up configurations for force invert color
+ waitForSystemNightModeActivated(true);
+ enableForceInvertColor(true);
+
+ setUpViewAttributes(/* isLightTheme= */ true, /* isLightBackground = */ false);
+
+ TestUtils.waitUntil("Waiting for ForceDarkType to be ready",
+ () -> (mViewRootImpl.determineForceDarkType() == ForceDarkType.NONE));
+ }
+
+ @Test
+ @EnableFlags(FLAG_FORCE_INVERT_COLOR)
+ public void determineForceDarkType_notLightThemeAndIsLightBackground_returnsNone()
+ throws Exception {
+ // Set up configurations for force invert color
+ waitForSystemNightModeActivated(true);
+ enableForceInvertColor(true);
+
+ setUpViewAttributes(/* isLightTheme= */ false, /* isLightBackground = */ true);
+
+ TestUtils.waitUntil("Waiting for ForceDarkType to be ready",
+ () -> (mViewRootImpl.determineForceDarkType() == ForceDarkType.NONE));
+ }
+
+ @Test
+ @EnableFlags(FLAG_FORCE_INVERT_COLOR)
+ public void determineForceDarkType_notLightThemeAndNotLightBackground_returnsNone()
+ throws Exception {
+ // Set up configurations for force invert color
+ waitForSystemNightModeActivated(true);
+ enableForceInvertColor(true);
+
+ setUpViewAttributes(/* isLightTheme= */ false, /* isLightBackground = */ false);
+
+ TestUtils.waitUntil("Waiting for ForceDarkType to be ready",
+ () -> (mViewRootImpl.determineForceDarkType() == ForceDarkType.NONE));
}
@Test
@@ -1792,29 +1839,35 @@ public class ViewRootImplTest {
sInstrumentation.waitForIdleSync();
}
- private void verifyForceDarkType(boolean isAppInNightMode, boolean isForceInvertEnabled,
- @ForceInvertType int expectedForceInvertType,
- @ForceDarkTypeDef int expectedForceDarkType) {
- var uiModeManager = sContext.getSystemService(UiModeManager.class);
+ private void enableForceInvertColor(boolean enabled) {
ShellIdentityUtils.invokeWithShellPermissions(() -> {
- uiModeManager.setApplicationNightMode(
- isAppInNightMode ? MODE_NIGHT_YES : MODE_NIGHT_NO);
Settings.Secure.putInt(
sContext.getContentResolver(),
Settings.Secure.ACCESSIBILITY_FORCE_INVERT_COLOR_ENABLED,
- isForceInvertEnabled ? 1 : 0);
+ enabled ? 1 : 0
+ );
});
+ }
- sInstrumentation.runOnMainSync(() ->
- mViewRootImpl.updateConfiguration(sContext.getDisplayNoVerify().getDisplayId()));
- try {
- TestUtils.waitUntil("Waiting for force invert state changed",
- () -> (uiModeManager.getForceInvertState() == expectedForceInvertType));
- } catch (Exception e) {
- Log.e(TAG, "Unexpected error trying to apply force invert state. " + e);
- e.printStackTrace();
- }
+ private void setUpViewAttributes(boolean isLightTheme, boolean isLightBackground) {
+ ShellIdentityUtils.invokeWithShellPermissions(() -> {
+ sContext.setTheme(isLightTheme ? android.R.style.Theme_DeviceDefault_Light
+ : android.R.style.Theme_DeviceDefault);
+ });
- mExpect.that(mViewRootImpl.determineForceDarkType()).isEqualTo(expectedForceDarkType);
+ sInstrumentation.runOnMainSync(() -> {
+ View view = new View(sContext);
+ WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams(
+ TYPE_APPLICATION_OVERLAY);
+ layoutParams.token = new Binder();
+ view.setLayoutParams(layoutParams);
+ if (isLightBackground) {
+ view.setBackgroundColor(Color.WHITE);
+ } else {
+ view.setBackgroundColor(Color.BLACK);
+ }
+ mViewRootImpl.setView(view, layoutParams, /* panelParentView= */ null);
+ mViewRootImpl.updateConfiguration(sContext.getDisplayNoVerify().getDisplayId());
+ });
}
}
diff --git a/core/tests/overlaytests/device/Android.bp b/core/tests/overlaytests/device/Android.bp
index 2b22344a4ef2..db1bf9c295b6 100644
--- a/core/tests/overlaytests/device/Android.bp
+++ b/core/tests/overlaytests/device/Android.bp
@@ -28,8 +28,8 @@ android_test {
certificate: "platform",
static_libs: [
"androidx.test.rules",
- "testng",
"compatibility-device-util-axt",
+ "testng",
],
test_suites: ["device-tests"],
data: [
diff --git a/core/tests/overlaytests/device/res/values/config.xml b/core/tests/overlaytests/device/res/values/config.xml
index a30d66f82128..e031b95f5d22 100644
--- a/core/tests/overlaytests/device/res/values/config.xml
+++ b/core/tests/overlaytests/device/res/values/config.xml
@@ -2,7 +2,7 @@
<resources>
<string name="str">none</string>
<string name="str2">none</string>
- <integer name="overlaid">0</integer>
+ <integer name="overlaidInt">0</integer>
<integer name="matrix_100000">100</integer>
<integer name="matrix_100001">100</integer>
<integer name="matrix_100010">100</integer>
@@ -58,6 +58,8 @@
<item>19</item>
</integer-array>
+ <item name="overlaidFloat" format="float" type="dimen">0</item>
+
<attr name="customAttribute" />
<id name="view_1" />
<id name="view_2" />
diff --git a/core/tests/overlaytests/device/src/com/android/overlaytest/FabricatedOverlaysTest.java b/core/tests/overlaytests/device/src/com/android/overlaytest/FabricatedOverlaysTest.java
index 2da9a2ebbdb6..b48e3b7423ff 100644
--- a/core/tests/overlaytests/device/src/com/android/overlaytest/FabricatedOverlaysTest.java
+++ b/core/tests/overlaytests/device/src/com/android/overlaytest/FabricatedOverlaysTest.java
@@ -22,6 +22,7 @@ import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertNull;
import static org.testng.Assert.assertThrows;
+import android.annotation.NonNull;
import android.content.Context;
import android.content.om.FabricatedOverlay;
import android.content.om.OverlayIdentifier;
@@ -44,14 +45,17 @@ import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import java.util.Collections;
+import java.util.Objects;
import java.util.concurrent.TimeoutException;
+import java.util.function.Function;
@RunWith(JUnit4.class)
@MediumTest
public class FabricatedOverlaysTest {
private static final String TAG = "FabricatedOverlaysTest";
- private final String TEST_RESOURCE = "integer/overlaid";
- private final String TEST_OVERLAY_NAME = "Test";
+ private static final String TEST_INT_RESOURCE = "integer/overlaidInt";
+ private static final String TEST_FLOAT_RESOURCE = "dimen/overlaidFloat";
+ private static final String TEST_OVERLAY_NAME = "Test";
private Context mContext;
private Resources mResources;
@@ -84,10 +88,10 @@ public class FabricatedOverlaysTest {
public void testFabricatedOverlay() throws Exception {
final FabricatedOverlay overlay = new FabricatedOverlay.Builder(
mContext.getPackageName(), TEST_OVERLAY_NAME, mContext.getPackageName())
- .setResourceValue(TEST_RESOURCE, TypedValue.TYPE_INT_DEC, 1)
+ .setResourceValue(TEST_INT_RESOURCE, TypedValue.TYPE_INT_DEC, 1)
.build();
- waitForResourceValue(0);
+ waitForIntResourceValue(0);
mOverlayManager.commit(new OverlayManagerTransaction.Builder()
.registerFabricatedOverlay(overlay)
.build());
@@ -104,63 +108,63 @@ public class FabricatedOverlaysTest {
assertNotNull(info);
assertTrue(info.isEnabled());
- waitForResourceValue(1);
+ waitForIntResourceValue(1);
mOverlayManager.commit(new OverlayManagerTransaction.Builder()
.unregisterFabricatedOverlay(overlay.getIdentifier())
.build());
- waitForResourceValue(0);
+ waitForIntResourceValue(0);
}
@Test
public void testRegisterEnableAtomic() throws Exception {
final FabricatedOverlay overlay = new FabricatedOverlay.Builder(
mContext.getPackageName(), TEST_OVERLAY_NAME, mContext.getPackageName())
- .setResourceValue(TEST_RESOURCE, TypedValue.TYPE_INT_DEC, 1)
+ .setResourceValue(TEST_INT_RESOURCE, TypedValue.TYPE_INT_DEC, 1)
.build();
- waitForResourceValue(0);
+ waitForIntResourceValue(0);
mOverlayManager.commit(new OverlayManagerTransaction.Builder()
.registerFabricatedOverlay(overlay)
.setEnabled(overlay.getIdentifier(), true, mUserId)
.build());
- waitForResourceValue(1);
+ waitForIntResourceValue(1);
}
@Test
public void testRegisterTwice() throws Exception {
FabricatedOverlay overlay = new FabricatedOverlay.Builder(
mContext.getPackageName(), TEST_OVERLAY_NAME, mContext.getPackageName())
- .setResourceValue(TEST_RESOURCE, TypedValue.TYPE_INT_DEC, 1)
+ .setResourceValue(TEST_INT_RESOURCE, TypedValue.TYPE_INT_DEC, 1)
.build();
- waitForResourceValue(0);
+ waitForIntResourceValue(0);
mOverlayManager.commit(new OverlayManagerTransaction.Builder()
.registerFabricatedOverlay(overlay)
.setEnabled(overlay.getIdentifier(), true, mUserId)
.build());
- waitForResourceValue(1);
+ waitForIntResourceValue(1);
overlay = new FabricatedOverlay.Builder(
mContext.getPackageName(), TEST_OVERLAY_NAME, mContext.getPackageName())
- .setResourceValue(TEST_RESOURCE, TypedValue.TYPE_INT_DEC, 2)
+ .setResourceValue(TEST_INT_RESOURCE, TypedValue.TYPE_INT_DEC, 2)
.build();
mOverlayManager.commit(new OverlayManagerTransaction.Builder()
.registerFabricatedOverlay(overlay)
.build());
- waitForResourceValue(2);
+ waitForIntResourceValue(2);
}
@Test
public void testInvalidOwningPackageName() throws Exception {
final FabricatedOverlay overlay = new FabricatedOverlay.Builder(
"android", TEST_OVERLAY_NAME, mContext.getPackageName())
- .setResourceValue(TEST_RESOURCE, TypedValue.TYPE_INT_DEC, 1)
+ .setResourceValue(TEST_INT_RESOURCE, TypedValue.TYPE_INT_DEC, 1)
.build();
- waitForResourceValue(0);
+ waitForIntResourceValue(0);
assertThrows(SecurityException.class, () ->
mOverlayManager.commit(new OverlayManagerTransaction.Builder()
.registerFabricatedOverlay(overlay)
@@ -174,10 +178,10 @@ public class FabricatedOverlaysTest {
public void testInvalidOverlayName() throws Exception {
final FabricatedOverlay overlay = new FabricatedOverlay.Builder(
mContext.getPackageName(), "invalid@name", mContext.getPackageName())
- .setResourceValue(TEST_RESOURCE, TypedValue.TYPE_INT_DEC, 1)
+ .setResourceValue(TEST_INT_RESOURCE, TypedValue.TYPE_INT_DEC, 1)
.build();
- waitForResourceValue(0);
+ waitForIntResourceValue(0);
assertThrows(SecurityException.class, () ->
mOverlayManager.commit(new OverlayManagerTransaction.Builder()
.registerFabricatedOverlay(overlay)
@@ -195,7 +199,7 @@ public class FabricatedOverlaysTest {
{
FabricatedOverlay overlay = new FabricatedOverlay.Builder(mContext.getPackageName(),
longestName, mContext.getPackageName())
- .setResourceValue(TEST_RESOURCE, TypedValue.TYPE_INT_DEC, 1)
+ .setResourceValue(TEST_INT_RESOURCE, TypedValue.TYPE_INT_DEC, 1)
.build();
mOverlayManager.commit(new OverlayManagerTransaction.Builder()
@@ -206,7 +210,7 @@ public class FabricatedOverlaysTest {
{
FabricatedOverlay overlay = new FabricatedOverlay.Builder(mContext.getPackageName(),
longestName + "a", mContext.getPackageName())
- .setResourceValue(TEST_RESOURCE, TypedValue.TYPE_INT_DEC, 1)
+ .setResourceValue(TEST_INT_RESOURCE, TypedValue.TYPE_INT_DEC, 1)
.build();
assertThrows(SecurityException.class, () ->
@@ -267,11 +271,11 @@ public class FabricatedOverlaysTest {
public void testInvalidResourceValues() throws Exception {
final FabricatedOverlay overlay = new FabricatedOverlay.Builder(
"android", TEST_OVERLAY_NAME, mContext.getPackageName())
- .setResourceValue(TEST_RESOURCE, TypedValue.TYPE_INT_DEC, 1)
+ .setResourceValue(TEST_INT_RESOURCE, TypedValue.TYPE_INT_DEC, 1)
.setResourceValue("color/something", TypedValue.TYPE_INT_DEC, 1)
.build();
- waitForResourceValue(0);
+ waitForIntResourceValue(0);
assertThrows(SecurityException.class, () ->
mOverlayManager.commit(new OverlayManagerTransaction.Builder()
.registerFabricatedOverlay(overlay)
@@ -285,10 +289,10 @@ public class FabricatedOverlaysTest {
public void testTransactionFailRollback() throws Exception {
final FabricatedOverlay overlay = new FabricatedOverlay.Builder(
mContext.getPackageName(), TEST_OVERLAY_NAME, mContext.getPackageName())
- .setResourceValue(TEST_RESOURCE, TypedValue.TYPE_INT_DEC, 1)
+ .setResourceValue(TEST_INT_RESOURCE, TypedValue.TYPE_INT_DEC, 1)
.build();
- waitForResourceValue(0);
+ waitForIntResourceValue(0);
assertThrows(SecurityException.class, () ->
mOverlayManager.commit(new OverlayManagerTransaction.Builder()
.registerFabricatedOverlay(overlay)
@@ -299,16 +303,40 @@ public class FabricatedOverlaysTest {
assertNull(mOverlayManager.getOverlayInfo(overlay.getIdentifier(), mUserHandle));
}
- void waitForResourceValue(final int expectedValue) throws TimeoutException {
+ @Test
+ public void setResourceValue_forFloatType_succeeds() throws Exception {
+ final float overlaidValue = 5.7f;
+ final FabricatedOverlay overlay = new FabricatedOverlay.Builder(
+ mContext.getPackageName(), TEST_OVERLAY_NAME, mContext.getPackageName()).build();
+ overlay.setResourceValue(TEST_FLOAT_RESOURCE, overlaidValue, null /* configuration */);
+
+ waitForFloatResourceValue(0);
+ mOverlayManager.commit(new OverlayManagerTransaction.Builder()
+ .registerFabricatedOverlay(overlay)
+ .setEnabled(overlay.getIdentifier(), true, mUserId)
+ .build());
+
+ waitForFloatResourceValue(overlaidValue);
+ }
+
+ private void waitForIntResourceValue(final int expectedValue) throws TimeoutException {
+ waitForResourceValue(expectedValue, TEST_INT_RESOURCE, id -> mResources.getInteger(id));
+ }
+
+ private void waitForFloatResourceValue(final float expectedValue) throws TimeoutException {
+ waitForResourceValue(expectedValue, TEST_FLOAT_RESOURCE, id -> mResources.getFloat(id));
+ }
+
+ private <T> void waitForResourceValue(final T expectedValue, final String resourceName,
+ @NonNull Function<Integer, T> resourceValueEmitter) throws TimeoutException {
final long timeOutDuration = 10000;
final long endTime = System.currentTimeMillis() + timeOutDuration;
- final String resourceName = TEST_RESOURCE;
final int resourceId = mResources.getIdentifier(resourceName, "",
mContext.getPackageName());
- int resourceValue = 0;
+ T resourceValue = null;
while (System.currentTimeMillis() < endTime) {
- resourceValue = mResources.getInteger(resourceId);
- if (resourceValue == expectedValue) {
+ resourceValue = resourceValueEmitter.apply(resourceId);
+ if (Objects.equals(expectedValue, resourceValue)) {
return;
}
}
diff --git a/data/etc/platform.xml b/data/etc/platform.xml
index ca20aebf95d8..ea1ce48fe001 100644
--- a/data/etc/platform.xml
+++ b/data/etc/platform.xml
@@ -62,6 +62,12 @@
<permission name="android.permission.READ_LOGS" >
<group gid="log" />
+ <group gid="update_engine_log" />
+ </permission>
+
+ <permission name="android.permission.READ_UPDATE_ENGINE_LOGS"
+ featureFlag="com.android.update_engine.minor_changes_2025q4" >
+ <group gid="update_engine_log" />
</permission>
<permission name="android.permission.ACCESS_MTP" >
diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml
index 1d8eed2749d5..62e14d368a1c 100644
--- a/data/etc/privapp-permissions-platform.xml
+++ b/data/etc/privapp-permissions-platform.xml
@@ -621,6 +621,8 @@ applications that come with the platform
<permission name="android.permission.READ_COLOR_ZONES"/>
<!-- Permission required for CTS test - CtsTextClassifierTestCases -->
<permission name="android.permission.ACCESS_TEXT_CLASSIFIER_BY_TYPE"/>
+ <!-- Permission required for CTS test - CtsSecurityTestCases -->
+ <permission name="android.permission.MANAGE_DEVICE_POLICY_MTE"/>
</privapp-permissions>
<privapp-permissions package="com.android.soundpicker">
diff --git a/data/keyboards/Android.bp b/data/keyboards/Android.bp
index 69b29bd5c7d3..423b55bd85db 100644
--- a/data/keyboards/Android.bp
+++ b/data/keyboards/Android.bp
@@ -12,16 +12,12 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package {
- default_applicable_licenses: ["Android-Apache-2.0"],
-}
-
genrule {
name: "validate_framework_keymaps",
srcs: [
- "*.idc",
- "*.kcm",
"*.kl",
+ "*.kcm",
+ "*.idc",
],
tools: ["validatekeymaps"],
out: ["stamp"],
@@ -37,6 +33,7 @@ prebuilt_usr_keylayout {
srcs: [
"*.kl",
],
+ no_full_install: true,
}
prebuilt_usr_keychars {
@@ -44,6 +41,7 @@ prebuilt_usr_keychars {
srcs: [
"*.kcm",
],
+ no_full_install: true,
}
prebuilt_usr_idc {
@@ -51,4 +49,5 @@ prebuilt_usr_idc {
srcs: [
"*.idc",
],
+ no_full_install: true,
}
diff --git a/data/keyboards/keyboards.mk b/data/keyboards/keyboards.mk
index 47bc63268754..c7ce8cd6693a 100644
--- a/data/keyboards/keyboards.mk
+++ b/data/keyboards/keyboards.mk
@@ -14,7 +14,9 @@
# Warning: this is actually a product definition, to be inherited from
-PRODUCT_PACKAGES += \
- keylayout_data \
- keychars_data \
- idc_data
+PRODUCT_COPY_FILES := \
+ $(call find-copy-subdir-files,*.kl,$(LOCAL_PATH),system/usr/keylayout) \
+ $(call find-copy-subdir-files,*.kcm,$(LOCAL_PATH),system/usr/keychars) \
+ $(call find-copy-subdir-files,*.idc,$(LOCAL_PATH),system/usr/idc)
+
+
diff --git a/libs/WindowManager/Shell/res/values/styles.xml b/libs/WindowManager/Shell/res/values/styles.xml
index 08cda7b94a78..086c8a5651c3 100644
--- a/libs/WindowManager/Shell/res/values/styles.xml
+++ b/libs/WindowManager/Shell/res/values/styles.xml
@@ -51,7 +51,6 @@
<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/res/values/dimen.xml b/libs/WindowManager/Shell/shared/res/values/dimen.xml
index 74b6023bde36..c3987caa87e5 100644
--- a/libs/WindowManager/Shell/shared/res/values/dimen.xml
+++ b/libs/WindowManager/Shell/shared/res/values/dimen.xml
@@ -45,7 +45,7 @@
<dimen name="drop_target_full_screen_padding">20dp</dimen>
<dimen name="drop_target_desktop_window_padding_small">100dp</dimen>
<dimen name="drop_target_desktop_window_padding_large">130dp</dimen>
- <dimen name="drop_target_expanded_view_width">364</dimen>
+ <dimen name="drop_target_expanded_view_width">330</dimen>
<dimen name="drop_target_expanded_view_height">578</dimen>
<dimen name="drop_target_expanded_view_padding_bottom">108</dimen>
<dimen name="drop_target_expanded_view_padding_horizontal">24</dimen>
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/GroupedTaskInfo.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/GroupedTaskInfo.java
index f68afea92850..006dc1439d6d 100644
--- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/GroupedTaskInfo.java
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/GroupedTaskInfo.java
@@ -124,7 +124,7 @@ public class GroupedTaskInfo implements Parcelable {
* Create new for a pair of tasks in split screen
*/
public static GroupedTaskInfo forSplitTasks(@NonNull TaskInfo task1,
- @NonNull TaskInfo task2, @NonNull SplitBounds splitBounds) {
+ @NonNull TaskInfo task2, @Nullable SplitBounds splitBounds) {
return new GroupedTaskInfo(/* deskId = */ -1, /* displayId = */ INVALID_DISPLAY,
List.of(task1, task2),
splitBounds, TYPE_SPLIT, /* minimizedFreeformTaskIds = */ null);
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DragZoneFactory.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DragZoneFactory.kt
index afeaf70c9d62..ffd1f5f3a39e 100644
--- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DragZoneFactory.kt
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DragZoneFactory.kt
@@ -133,7 +133,7 @@ class DragZoneFactory(
fullScreenDropTargetPadding = 20.dpToPx()
desktopWindowDropTargetPaddingSmall = 100.dpToPx()
desktopWindowDropTargetPaddingLarge = 130.dpToPx()
- expandedViewDropTargetWidth = 364.dpToPx()
+ expandedViewDropTargetWidth = 330.dpToPx()
expandedViewDropTargetHeight = 578.dpToPx()
expandedViewDropTargetPaddingBottom = 108.dpToPx()
expandedViewDropTargetPaddingHorizontal = 24.dpToPx()
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 746632f67725..f91154c7a362 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
@@ -1050,7 +1050,8 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont
() -> mShellExecutor.execute(this::onBackAnimationFinished));
if (mApps.length >= 1) {
- BackMotionEvent startEvent = mCurrentTracker.createStartEvent(mApps[0]);
+ BackMotionEvent startEvent = mCurrentTracker.createStartEvent(
+ Flags.removeDepartTargetFromMotion() ? null : mApps[0]);
dispatchOnBackStarted(mActiveCallback, startEvent);
if (startEvent.getSwipeEdge() == EDGE_NONE) {
// TODO(b/373544911): onBackStarted is dispatched here so that
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitState.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitState.java
index ad0e7fc187e9..394c445787b4 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitState.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitState.java
@@ -17,6 +17,10 @@
package com.android.wm.shell.common.split;
import static com.android.wm.shell.shared.split.SplitScreenConstants.NOT_IN_SPLIT;
+import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_10_90;
+import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_90_10;
+import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_3_10_45_45;
+import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_3_45_45_10;
import static com.android.wm.shell.shared.split.SplitScreenConstants.SplitScreenState;
import android.graphics.Rect;
@@ -62,4 +66,12 @@ public class SplitState {
public List<RectF> getCurrentLayout() {
return getLayout(mState);
}
+
+ /** @return {@code true} if at least one app is partially offscreen in the current layout. */
+ public boolean currentStateSupportsOffscreenApps() {
+ return mState == SNAP_TO_2_10_90
+ || mState == SNAP_TO_2_90_10
+ || mState == SNAP_TO_3_10_45_45
+ || mState == SNAP_TO_3_45_45_10;
+ }
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
index b3c25d495002..ad509bcc1ceb 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
@@ -766,6 +766,7 @@ public abstract class WMShellBaseModule {
@ShellMainThread ShellExecutor mainExecutor,
@ShellMainThread Handler mainHandler,
@ShellAnimationThread ShellExecutor animExecutor,
+ @ShellAnimationThread Handler animHandler,
RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer,
HomeTransitionObserver homeTransitionObserver,
FocusTransitionObserver focusTransitionObserver) {
@@ -775,7 +776,7 @@ public abstract class WMShellBaseModule {
}
return new Transitions(context, shellInit, shellCommandHandler, shellController, organizer,
pool, displayController, displayInsetsController, mainExecutor, mainHandler,
- animExecutor, rootTaskDisplayAreaOrganizer, homeTransitionObserver,
+ animExecutor, animHandler, rootTaskDisplayAreaOrganizer, homeTransitionObserver,
focusTransitionObserver);
}
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 25737c4950d6..80c6f2e5ff33 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
@@ -49,9 +49,6 @@ class DesktopDisplayEventHandler(
private val desktopDisplayModeController: DesktopDisplayModeController,
) : OnDisplaysChangedListener, OnDeskRemovedListener {
- private val desktopRepository: DesktopRepository
- get() = desktopUserRepositories.current
-
init {
shellInit.addInitCallback({ onInit() }, this)
}
@@ -66,7 +63,7 @@ class DesktopDisplayEventHandler(
object : UserChangeListener {
override fun onUserChanged(newUserId: Int, userContext: Context) {
val displayIds = rootTaskDisplayAreaOrganizer.displayIds
- createDefaultDesksIfNeeded(displayIds.toSet())
+ createDefaultDesksIfNeeded(displayIds.toSet(), newUserId)
}
}
)
@@ -75,15 +72,18 @@ class DesktopDisplayEventHandler(
override fun onDisplayAdded(displayId: Int) {
if (displayId != DEFAULT_DISPLAY) {
- desktopDisplayModeController.refreshDisplayWindowingMode()
+ desktopDisplayModeController.updateExternalDisplayWindowingMode(displayId)
+ // The default display's windowing mode depends on the availability of the external
+ // display. So updating the default display's windowing mode here.
+ desktopDisplayModeController.updateDefaultDisplayWindowingMode()
}
- createDefaultDesksIfNeeded(displayIds = setOf(displayId))
+ createDefaultDesksIfNeeded(displayIds = setOf(displayId), userId = null)
}
override fun onDisplayRemoved(displayId: Int) {
if (displayId != DEFAULT_DISPLAY) {
- desktopDisplayModeController.refreshDisplayWindowingMode()
+ desktopDisplayModeController.updateDefaultDisplayWindowingMode()
}
// TODO: b/362720497 - move desks in closing display to the remaining desk.
@@ -94,28 +94,30 @@ class DesktopDisplayEventHandler(
DesktopExperienceFlags.ENABLE_DISPLAY_CONTENT_MODE_MANAGEMENT.isTrue &&
displayId != DEFAULT_DISPLAY
) {
- desktopDisplayModeController.refreshDisplayWindowingMode()
+ desktopDisplayModeController.updateExternalDisplayWindowingMode(displayId)
+ // The default display's windowing mode depends on the desktop eligibility of the
+ // external display. So updating the default display's windowing mode here.
+ desktopDisplayModeController.updateDefaultDisplayWindowingMode()
}
}
override fun onDeskRemoved(lastDisplayId: Int, deskId: Int) {
- val remainingDesks = desktopRepository.getNumberOfDesks(lastDisplayId)
- if (remainingDesks == 0) {
- logV("All desks removed from display#$lastDisplayId")
- createDefaultDesksIfNeeded(setOf(lastDisplayId))
- }
+ createDefaultDesksIfNeeded(setOf(lastDisplayId), userId = null)
}
- private fun createDefaultDesksIfNeeded(displayIds: Set<Int>) {
+ private fun createDefaultDesksIfNeeded(displayIds: Set<Int>, userId: Int?) {
if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) return
logV("createDefaultDesksIfNeeded displays=%s", displayIds)
mainScope.launch {
desktopRepositoryInitializer.isInitialized.collect { initialized ->
if (!initialized) return@collect
+ val repository =
+ userId?.let { desktopUserRepositories.getProfile(userId) }
+ ?: desktopUserRepositories.current
displayIds
.filter { displayId -> displayId != Display.INVALID_DISPLAY }
.filter { displayId -> supportsDesks(displayId) }
- .filter { displayId -> desktopRepository.getNumberOfDesks(displayId) == 0 }
+ .filter { displayId -> repository.getNumberOfDesks(displayId) == 0 }
.also { displaysNeedingDesk ->
logV(
"createDefaultDesksIfNeeded creating default desks in displays=%s",
@@ -125,7 +127,7 @@ class DesktopDisplayEventHandler(
.forEach { displayId ->
// TODO: b/393978539 - consider activating the desk on creation when
// applicable, such as for connected displays.
- desktopTasksController.createDesk(displayId)
+ desktopTasksController.createDesk(displayId, repository.userId)
}
cancel()
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayModeController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayModeController.kt
index ea2fdc0ee8ed..dec489e8fc63 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayModeController.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayModeController.kt
@@ -56,44 +56,51 @@ class DesktopDisplayModeController(
@ShellMainThread private val mainHandler: Handler,
) {
- private val onTabletModeChangedListener =
- object : InputManager.OnTabletModeChangedListener {
- override fun onTabletModeChanged(whenNanos: Long, inTabletMode: Boolean) {
- refreshDisplayWindowingMode()
- }
- }
-
private val inputDeviceListener =
object : InputManager.InputDeviceListener {
override fun onInputDeviceAdded(deviceId: Int) {
- refreshDisplayWindowingMode()
+ updateDefaultDisplayWindowingMode()
}
override fun onInputDeviceChanged(deviceId: Int) {
- refreshDisplayWindowingMode()
+ updateDefaultDisplayWindowingMode()
}
override fun onInputDeviceRemoved(deviceId: Int) {
- refreshDisplayWindowingMode()
+ updateDefaultDisplayWindowingMode()
}
}
init {
if (DesktopExperienceFlags.FORM_FACTOR_BASED_DESKTOP_FIRST_SWITCH.isTrue) {
- inputManager.registerOnTabletModeChangedListener(
- onTabletModeChangedListener,
- mainHandler,
- )
inputManager.registerInputDeviceListener(inputDeviceListener, mainHandler)
}
}
- fun refreshDisplayWindowingMode() {
+ fun updateExternalDisplayWindowingMode(displayId: Int) {
+ if (!DesktopExperienceFlags.ENABLE_DISPLAY_CONTENT_MODE_MANAGEMENT.isTrue) return
+
+ val desktopModeSupported =
+ displayController.getDisplay(displayId)?.let { display ->
+ DesktopModeStatus.isDesktopModeSupportedOnDisplay(context, display)
+ } ?: false
+ if (!desktopModeSupported) return
+
+ // An external display should always be a freeform display when desktop mode is enabled.
+ updateDisplayWindowingMode(displayId, WINDOWING_MODE_FREEFORM)
+ }
+
+ fun updateDefaultDisplayWindowingMode() {
if (!DesktopExperienceFlags.ENABLE_DISPLAY_WINDOWING_MODE_SWITCHING.isTrue) return
- val targetDisplayWindowingMode = getTargetWindowingModeForDefaultDisplay()
- val tdaInfo = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)
- requireNotNull(tdaInfo) { "DisplayAreaInfo of DEFAULT_DISPLAY must be non-null." }
+ updateDisplayWindowingMode(DEFAULT_DISPLAY, getTargetWindowingModeForDefaultDisplay())
+ }
+
+ private fun updateDisplayWindowingMode(displayId: Int, targetDisplayWindowingMode: Int) {
+ val tdaInfo =
+ requireNotNull(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(displayId)) {
+ "DisplayAreaInfo of display#$displayId must be non-null."
+ }
val currentDisplayWindowingMode = tdaInfo.configuration.windowConfiguration.windowingMode
if (currentDisplayWindowingMode == targetDisplayWindowingMode) {
// Already in the target mode.
@@ -101,15 +108,16 @@ class DesktopDisplayModeController(
}
logV(
- "As an external display is connected, changing default display's windowing mode from" +
- " ${windowingModeToString(currentDisplayWindowingMode)}" +
- " to ${windowingModeToString(targetDisplayWindowingMode)}"
+ "Changing display#%d's windowing mode from %s to %s",
+ displayId,
+ windowingModeToString(currentDisplayWindowingMode),
+ windowingModeToString(targetDisplayWindowingMode),
)
val wct = WindowContainerTransaction()
wct.setWindowingMode(tdaInfo.token, targetDisplayWindowingMode)
shellTaskOrganizer
- .getRunningTasks(DEFAULT_DISPLAY)
+ .getRunningTasks(displayId)
.filter { it.activityType == ACTIVITY_TYPE_STANDARD }
.forEach {
// TODO: b/391965153 - Reconsider the logic under multi-desk window hierarchy
@@ -125,7 +133,7 @@ class DesktopDisplayModeController(
// The override windowing mode of DesktopWallpaper can be UNDEFINED on fullscreen-display
// right after the first launch while its resolved windowing mode is FULLSCREEN. We here
// it has the FULLSCREEN override windowing mode.
- desktopWallpaperActivityTokenProvider.getToken(DEFAULT_DISPLAY)?.let { token ->
+ desktopWallpaperActivityTokenProvider.getToken(displayId)?.let { token ->
wct.setWindowingMode(token, WINDOWING_MODE_FULLSCREEN)
}
transitions.startTransition(TRANSIT_CHANGE, wct, /* handler= */ null)
@@ -139,7 +147,7 @@ class DesktopDisplayModeController(
return true
}
if (DesktopExperienceFlags.FORM_FACTOR_BASED_DESKTOP_FIRST_SWITCH.isTrue) {
- if (isInClamshellMode() || hasAnyMouseDevice()) {
+ if (hasAnyTouchpadDevice() && hasAnyPhysicalKeyboardDevice()) {
return true
}
}
@@ -186,17 +194,25 @@ class DesktopDisplayModeController(
private fun hasExternalDisplay() =
rootTaskDisplayAreaOrganizer.getDisplayIds().any { it != DEFAULT_DISPLAY }
- private fun hasAnyMouseDevice() =
- inputManager.inputDeviceIds.any {
- inputManager.getInputDevice(it)?.supportsSource(InputDevice.SOURCE_MOUSE) == true
+ private fun hasAnyTouchpadDevice() =
+ inputManager.inputDeviceIds.any { deviceId ->
+ inputManager.getInputDevice(deviceId)?.let { device ->
+ device.supportsSource(InputDevice.SOURCE_TOUCHPAD) && device.isEnabled()
+ } ?: false
}
- private fun isInClamshellMode() = inputManager.isInTabletMode() == InputManager.SWITCH_STATE_OFF
+ private fun hasAnyPhysicalKeyboardDevice() =
+ inputManager.inputDeviceIds.any { deviceId ->
+ inputManager.getInputDevice(deviceId)?.let { device ->
+ !device.isVirtual() && device.isFullKeyboard() && device.isEnabled()
+ } ?: false
+ }
private fun isDefaultDisplayDesktopEligible(): Boolean {
- val display = requireNotNull(displayController.getDisplay(DEFAULT_DISPLAY)) {
- "Display object of DEFAULT_DISPLAY must be non-null."
- }
+ val display =
+ requireNotNull(displayController.getDisplay(DEFAULT_DISPLAY)) {
+ "Display object of DEFAULT_DISPLAY must be non-null."
+ }
return DesktopModeStatus.isDesktopModeSupportedOnDisplay(context, display)
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java
index 1c5138f486e4..8bbe36dd6644 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java
@@ -57,7 +57,7 @@ import com.android.wm.shell.shared.bubbles.BubbleAnythingFlagHelper;
import com.android.wm.shell.shared.bubbles.BubbleDropTargetBoundsProvider;
import com.android.wm.shell.windowdecor.tiling.SnapEventHandler;
-import java.util.Arrays;
+import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@@ -307,7 +307,8 @@ public class DesktopModeVisualIndicator {
if (splitRightRegion.contains(x, y)) {
result = IndicatorType.TO_SPLIT_RIGHT_INDICATOR;
}
- if (BubbleAnythingFlagHelper.enableBubbleToFullscreen()) {
+ if (BubbleAnythingFlagHelper.enableBubbleToFullscreen()
+ && mDragStartState == DragStartState.FROM_FULLSCREEN) {
if (calculateBubbleLeftRegion(layout).contains(x, y)) {
result = IndicatorType.TO_BUBBLE_LEFT_INDICATOR;
} else if (calculateBubbleRightRegion(layout).contains(x, y)) {
@@ -415,30 +416,59 @@ public class DesktopModeVisualIndicator {
private List<Pair<Rect, IndicatorType>> initSmallTabletRegions(DisplayLayout layout,
boolean isLeftRightSplit) {
- boolean dragFromFullscreen = mDragStartState == DragStartState.FROM_FULLSCREEN;
- boolean dragFromSplit = mDragStartState == DragStartState.FROM_SPLIT;
- if (isLeftRightSplit && (dragFromFullscreen || dragFromSplit)) {
+ return switch (mDragStartState) {
+ case DragStartState.FROM_FULLSCREEN -> initSmallTabletRegionsFromFullscreen(layout,
+ isLeftRightSplit);
+ case DragStartState.FROM_SPLIT -> initSmallTabletRegionsFromSplit(layout,
+ isLeftRightSplit);
+ default -> Collections.emptyList();
+ };
+ }
+
+ private List<Pair<Rect, IndicatorType>> initSmallTabletRegionsFromFullscreen(
+ DisplayLayout layout, boolean isLeftRightSplit) {
+
+ List<Pair<Rect, IndicatorType>> result = new ArrayList<>();
+ if (BubbleAnythingFlagHelper.enableBubbleToFullscreen()) {
+ result.add(new Pair<>(calculateBubbleLeftRegion(layout), TO_BUBBLE_LEFT_INDICATOR));
+ result.add(new Pair<>(calculateBubbleRightRegion(layout), TO_BUBBLE_RIGHT_INDICATOR));
+ }
+
+ if (isLeftRightSplit) {
int splitRegionWidth = mContext.getResources().getDimensionPixelSize(
com.android.wm.shell.shared.R.dimen.drag_zone_h_split_from_app_width_fold);
- return Arrays.asList(
- new Pair<>(calculateBubbleLeftRegion(layout), TO_BUBBLE_LEFT_INDICATOR),
- new Pair<>(calculateBubbleRightRegion(layout), TO_BUBBLE_RIGHT_INDICATOR),
- new Pair<>(calculateSplitLeftRegion(layout, splitRegionWidth,
- /* captionHeight= */ 0), TO_SPLIT_LEFT_INDICATOR),
- new Pair<>(calculateSplitRightRegion(layout, splitRegionWidth,
- /* captionHeight= */ 0), TO_SPLIT_RIGHT_INDICATOR),
- new Pair<>(new Rect(), TO_FULLSCREEN_INDICATOR) // default to fullscreen
- );
+ result.add(new Pair<>(calculateSplitLeftRegion(layout, splitRegionWidth,
+ /* captionHeight= */ 0), TO_SPLIT_LEFT_INDICATOR));
+ result.add(new Pair<>(calculateSplitRightRegion(layout, splitRegionWidth,
+ /* captionHeight= */ 0), TO_SPLIT_RIGHT_INDICATOR));
}
- if (dragFromFullscreen) {
- // If left/right split is not available, we can only drag fullscreen tasks
- // TODO(b/401352409): add support for top/bottom split zones
- return Arrays.asList(
- new Pair<>(calculateBubbleLeftRegion(layout), TO_BUBBLE_LEFT_INDICATOR),
- new Pair<>(calculateBubbleRightRegion(layout), TO_BUBBLE_RIGHT_INDICATOR),
- new Pair<>(new Rect(), TO_FULLSCREEN_INDICATOR) // default to fullscreen
- );
+ // TODO(b/401352409): add support for top/bottom split zones
+ // default to fullscreen
+ result.add(new Pair<>(new Rect(), TO_FULLSCREEN_INDICATOR));
+ return result;
+ }
+
+ private List<Pair<Rect, IndicatorType>> initSmallTabletRegionsFromSplit(DisplayLayout layout,
+ boolean isLeftRightSplit) {
+ if (!isLeftRightSplit) {
+ // Dragging a top/bottom split is not supported on small tablets
+ return Collections.emptyList();
}
- return Collections.emptyList();
+
+ List<Pair<Rect, IndicatorType>> result = new ArrayList<>();
+ if (BubbleAnythingFlagHelper.enableBubbleAnything()) {
+ result.add(new Pair<>(calculateBubbleLeftRegion(layout), TO_BUBBLE_LEFT_INDICATOR));
+ result.add(new Pair<>(calculateBubbleRightRegion(layout), TO_BUBBLE_RIGHT_INDICATOR));
+ }
+
+ int splitRegionWidth = mContext.getResources().getDimensionPixelSize(
+ com.android.wm.shell.shared.R.dimen.drag_zone_h_split_from_app_width_fold);
+ result.add(new Pair<>(calculateSplitLeftRegion(layout, splitRegionWidth,
+ /* captionHeight= */ 0), TO_SPLIT_LEFT_INDICATOR));
+ result.add(new Pair<>(calculateSplitRightRegion(layout, splitRegionWidth,
+ /* captionHeight= */ 0), TO_SPLIT_RIGHT_INDICATOR));
+ // default to fullscreen
+ result.add(new Pair<>(new Rect(), TO_FULLSCREEN_INDICATOR));
+ return result;
}
}
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 6214f329e0fd..8b1d3fa65ac6 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
@@ -42,6 +42,7 @@ import android.os.Handler
import android.os.IBinder
import android.os.SystemProperties
import android.os.UserHandle
+import android.os.UserManager
import android.util.Slog
import android.view.Display
import android.view.Display.DEFAULT_DISPLAY
@@ -115,7 +116,6 @@ import com.android.wm.shell.desktopmode.multidesks.DeskTransition
import com.android.wm.shell.desktopmode.multidesks.DesksOrganizer
import com.android.wm.shell.desktopmode.multidesks.DesksTransitionObserver
import com.android.wm.shell.desktopmode.multidesks.OnDeskRemovedListener
-import com.android.wm.shell.desktopmode.multidesks.createDesk
import com.android.wm.shell.desktopmode.persistence.DesktopRepositoryInitializer
import com.android.wm.shell.desktopmode.persistence.DesktopRepositoryInitializer.DeskRecreationFactory
import com.android.wm.shell.draganddrop.DragAndDropController
@@ -163,6 +163,7 @@ import java.util.Optional
import java.util.concurrent.Executor
import java.util.concurrent.TimeUnit
import java.util.function.Consumer
+import kotlin.coroutines.suspendCoroutine
import kotlin.jvm.optionals.getOrNull
/**
@@ -283,14 +284,8 @@ class DesktopTasksController(
if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
desktopRepositoryInitializer.deskRecreationFactory =
- DeskRecreationFactory { deskUserId, destinationDisplayId, deskId ->
- if (deskUserId != userId) {
- // TODO: b/400984250 - add multi-user support for multi-desk restoration.
- logW("Tried to re-create desk of another user.")
- null
- } else {
- desksOrganizer.createDesk(destinationDisplayId)
- }
+ DeskRecreationFactory { deskUserId, destinationDisplayId, _ ->
+ createDeskSuspending(displayId = destinationDisplayId, userId = deskUserId)
}
}
}
@@ -493,22 +488,55 @@ class DesktopTasksController(
runOnTransitStart?.invoke(transition)
}
- /** Creates a new desk in the given display. */
- fun createDesk(displayId: Int) {
+ /** Adds a new desk to the given display for the given user. */
+ fun createDesk(displayId: Int, userId: Int = this.userId) {
+ logV("addDesk displayId=%d, userId=%d", displayId, userId)
+ val repository = userRepositories.getProfile(userId)
+ createDesk(displayId, userId) { deskId ->
+ if (deskId == null) {
+ logW("Failed to add desk in displayId=%d for userId=%d", displayId, userId)
+ } else {
+ repository.addDesk(displayId = displayId, deskId = deskId)
+ }
+ }
+ }
+
+ private fun createDesk(displayId: Int, userId: Int = this.userId, onResult: (Int?) -> Unit) {
if (displayId == Display.INVALID_DISPLAY) {
logW("createDesk attempt with invalid displayId", displayId)
+ onResult(null)
return
}
- if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
- desksOrganizer.createDesk(displayId) { deskId ->
- taskRepository.addDesk(displayId = displayId, deskId = deskId)
- }
- } else {
+ if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
// In single-desk, the desk reuses the display id.
- taskRepository.addDesk(displayId = displayId, deskId = displayId)
+ logD("createDesk reusing displayId=%d for single-desk", displayId)
+ onResult(displayId)
+ return
+ }
+ if (
+ DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_HSUM.isTrue &&
+ UserManager.isHeadlessSystemUserMode() &&
+ UserHandle.USER_SYSTEM == userId
+ ) {
+ logW("createDesk ignoring attempt for system user")
+ return
+ }
+ desksOrganizer.createDesk(displayId, userId) { deskId ->
+ logD(
+ "createDesk obtained deskId=%d for displayId=%d and userId=%d",
+ deskId,
+ displayId,
+ userId,
+ )
+ onResult(deskId)
}
}
+ private suspend fun createDeskSuspending(displayId: Int, userId: Int = this.userId): Int? =
+ suspendCoroutine { cont ->
+ createDesk(displayId, userId) { deskId -> cont.resumeWith(Result.success(deskId)) }
+ }
+
/** Moves task to desktop mode if task is running, else launches it in desktop mode. */
@JvmOverloads
fun moveTaskToDefaultDeskAndActivate(
@@ -3024,18 +3052,17 @@ class DesktopTasksController(
}
val wct = WindowContainerTransaction()
- if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
- tasksToRemove.forEach {
- val task = shellTaskOrganizer.getRunningTaskInfo(it)
- if (task != null) {
- wct.removeTask(task.token)
- } else {
- recentTasksController?.removeBackgroundTask(it)
- }
+ tasksToRemove.forEach {
+ // TODO: b/404595635 - consider moving this block into [DesksOrganizer].
+ val task = shellTaskOrganizer.getRunningTaskInfo(it)
+ if (task != null) {
+ wct.removeTask(task.token)
+ } else {
+ recentTasksController?.removeBackgroundTask(it)
}
- } else {
- // TODO: 362720497 - double check background tasks are also removed.
- desksOrganizer.removeDesk(wct, deskId)
+ }
+ if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
+ desksOrganizer.removeDesk(wct, deskId, userId)
}
if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue && wct.isEmpty) return
val transition = transitions.startTransition(TRANSIT_CLOSE, wct, /* handler= */ null)
@@ -3601,6 +3628,7 @@ class DesktopTasksController(
pw.println("${prefix}DesktopTasksController")
DesktopModeStatus.dump(pw, innerPrefix, context)
userRepositories.dump(pw, innerPrefix)
+ focusTransitionObserver.dump(pw, innerPrefix)
}
/** The interface for calls from outside the shell, within the host process. */
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksOrganizer.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksOrganizer.kt
index 1effcdb20505..605465b15468 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksOrganizer.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksOrganizer.kt
@@ -18,13 +18,11 @@ package com.android.wm.shell.desktopmode.multidesks
import android.app.ActivityManager
import android.window.TransitionInfo
import android.window.WindowContainerTransaction
-import com.android.wm.shell.desktopmode.multidesks.DesksOrganizer.OnCreateCallback
-import kotlin.coroutines.suspendCoroutine
/** An organizer of desk containers in which to host child desktop windows. */
interface DesksOrganizer {
- /** Creates a new desk container in the given display. */
- fun createDesk(displayId: Int, callback: OnCreateCallback)
+ /** Creates a new desk container to use in the given display for the given user. */
+ fun createDesk(displayId: Int, userId: Int, callback: OnCreateCallback)
/** Activates the given desk, making it visible in its display. */
fun activateDesk(wct: WindowContainerTransaction, deskId: Int)
@@ -32,8 +30,8 @@ interface DesksOrganizer {
/** Deactivates the given desk, removing it as the default launch container for new tasks. */
fun deactivateDesk(wct: WindowContainerTransaction, deskId: Int)
- /** Removes the given desk and its desktop windows. */
- fun removeDesk(wct: WindowContainerTransaction, deskId: Int)
+ /** Removes the given desk of the given user. */
+ fun removeDesk(wct: WindowContainerTransaction, deskId: Int, userId: Int)
/** Moves the given task to the given desk. */
fun moveTaskToDesk(
@@ -87,9 +85,3 @@ interface DesksOrganizer {
fun onCreated(deskId: Int)
}
}
-
-/** Creates a new desk container in the given display. */
-suspend fun DesksOrganizer.createDesk(displayId: Int): Int = suspendCoroutine { cont ->
- val onCreateCallback = OnCreateCallback { deskId -> cont.resumeWith(Result.success(deskId)) }
- createDesk(displayId, onCreateCallback)
-}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizer.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizer.kt
index c30987ac7640..e4edeb95be6d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizer.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizer.kt
@@ -30,6 +30,7 @@ import android.window.TransitionInfo
import android.window.WindowContainerToken
import android.window.WindowContainerTransaction
import androidx.core.util.forEach
+import androidx.core.util.valueIterator
import com.android.internal.annotations.VisibleForTesting
import com.android.internal.protolog.ProtoLog
import com.android.wm.shell.ShellTaskOrganizer
@@ -40,7 +41,13 @@ import com.android.wm.shell.sysui.ShellCommandHandler
import com.android.wm.shell.sysui.ShellInit
import java.io.PrintWriter
-/** A [DesksOrganizer] that uses root tasks as the container of each desk. */
+/**
+ * A [DesksOrganizer] that uses root tasks as the container of each desk.
+ *
+ * Note that root tasks are reusable between multiple users at the same time, and may also be
+ * pre-created to have one ready for the first entry to the default desk, so root-task existence
+ * does not imply a formal desk exists to the user.
+ */
class RootTaskDesksOrganizer(
shellInit: ShellInit,
shellCommandHandler: ShellCommandHandler,
@@ -65,9 +72,26 @@ class RootTaskDesksOrganizer(
}
}
- override fun createDesk(displayId: Int, callback: OnCreateCallback) {
- logV("createDesk in display: %d", displayId)
- createDeskRootRequests += CreateDeskRequest(displayId, callback)
+ override fun createDesk(displayId: Int, userId: Int, callback: OnCreateCallback) {
+ logV("createDesk in displayId=%d userId=%s", displayId, userId)
+ // Find an existing desk that is not yet used by this user.
+ val unassignedDesk =
+ deskRootsByDeskId
+ .valueIterator()
+ .asSequence()
+ .filterNot { desk -> userId in desk.users }
+ .firstOrNull()
+ if (unassignedDesk != null) {
+ unassignedDesk.users.add(userId)
+ callback.onCreated(unassignedDesk.deskId)
+ return
+ }
+ createDeskRoot(displayId, userId, callback)
+ }
+
+ private fun createDeskRoot(displayId: Int, userId: Int, callback: OnCreateCallback) {
+ logV("createDeskRoot in display: %d for user: %d", displayId, userId)
+ createDeskRootRequests += CreateDeskRequest(displayId, userId, callback)
shellTaskOrganizer.createRootTask(
displayId,
WINDOWING_MODE_FREEFORM,
@@ -76,31 +100,52 @@ class RootTaskDesksOrganizer(
)
}
- override fun removeDesk(wct: WindowContainerTransaction, deskId: Int) {
- logV("removeDesk %d", deskId)
- deskRootsByDeskId[deskId]?.let { root -> wct.removeRootTask(root.token) }
- deskMinimizationRootsByDeskId[deskId]?.let { root -> wct.removeRootTask(root.token) }
+ override fun removeDesk(wct: WindowContainerTransaction, deskId: Int, userId: Int) {
+ logV("removeDesk %d for userId=%d", deskId, userId)
+ val deskRoot = deskRootsByDeskId[deskId]
+ if (deskRoot == null) {
+ logW("removeDesk attempted to remove non-existent desk=%d", deskId)
+ return
+ }
+ updateLaunchRoot(wct, deskId, enabled = false)
+ deskRoot.users.remove(userId)
+ if (deskRoot.users.isEmpty()) {
+ // No longer in use by any users, remove it completely.
+ logD("removeDesk %d is no longer used by any users, removing it completely", deskId)
+ wct.removeRootTask(deskRoot.token)
+ deskMinimizationRootsByDeskId[deskId]?.let { root -> wct.removeRootTask(root.token) }
+ }
}
override fun activateDesk(wct: WindowContainerTransaction, deskId: Int) {
logV("activateDesk %d", deskId)
val root = checkNotNull(deskRootsByDeskId[deskId]) { "Root not found for desk: $deskId" }
wct.reorder(root.token, /* onTop= */ true)
- wct.setLaunchRoot(
- /* container= */ root.taskInfo.token,
- /* windowingModes= */ intArrayOf(WINDOWING_MODE_FREEFORM, WINDOWING_MODE_UNDEFINED),
- /* activityTypes= */ intArrayOf(ACTIVITY_TYPE_UNDEFINED, ACTIVITY_TYPE_STANDARD),
- )
+ updateLaunchRoot(wct, deskId, enabled = true)
}
override fun deactivateDesk(wct: WindowContainerTransaction, deskId: Int) {
logV("deactivateDesk %d", deskId)
+ updateLaunchRoot(wct, deskId, enabled = false)
+ }
+
+ private fun updateLaunchRoot(wct: WindowContainerTransaction, deskId: Int, enabled: Boolean) {
val root = checkNotNull(deskRootsByDeskId[deskId]) { "Root not found for desk: $deskId" }
- wct.setLaunchRoot(
- /* container= */ root.taskInfo.token,
- /* windowingModes= */ null,
- /* activityTypes= */ null,
- )
+ root.isLaunchRootRequested = enabled
+ logD("updateLaunchRoot deskId=%d enabled=%b", deskId, enabled)
+ if (enabled) {
+ wct.setLaunchRoot(
+ /* container= */ root.taskInfo.token,
+ /* windowingModes= */ intArrayOf(WINDOWING_MODE_FREEFORM, WINDOWING_MODE_UNDEFINED),
+ /* activityTypes= */ intArrayOf(ACTIVITY_TYPE_UNDEFINED, ACTIVITY_TYPE_STANDARD),
+ )
+ } else {
+ wct.setLaunchRoot(
+ /* container= */ root.taskInfo.token,
+ /* windowingModes= */ null,
+ /* activityTypes= */ null,
+ )
+ }
}
override fun moveTaskToDesk(
@@ -275,7 +320,13 @@ class RootTaskDesksOrganizer(
// Appearing root matches desk request.
val deskId = taskInfo.taskId
logV("Desk #$deskId appeared")
- deskRootsByDeskId[deskId] = DeskRoot(deskId, taskInfo, leash)
+ deskRootsByDeskId[deskId] =
+ DeskRoot(
+ deskId = deskId,
+ taskInfo = taskInfo,
+ leash = leash,
+ users = mutableSetOf(deskRequest.userId),
+ )
createDeskRootRequests.remove(deskRequest)
deskRequest.onCreateCallback.onCreated(deskId)
createDeskMinimizationRoot(displayId = appearingInDisplayId, deskId = deskId)
@@ -430,6 +481,8 @@ class RootTaskDesksOrganizer(
val taskInfo: RunningTaskInfo,
val leash: SurfaceControl,
val children: MutableSet<Int> = mutableSetOf(),
+ val users: MutableSet<Int> = mutableSetOf(),
+ var isLaunchRootRequested: Boolean = false,
) {
val token: WindowContainerToken = taskInfo.token
}
@@ -449,15 +502,24 @@ class RootTaskDesksOrganizer(
private data class CreateDeskRequest(
val displayId: Int,
+ val userId: Int,
val onCreateCallback: OnCreateCallback,
)
private data class CreateDeskMinimizationRootRequest(val displayId: Int, val deskId: Int)
+ private fun logD(msg: String, vararg arguments: Any?) {
+ ProtoLog.d(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments)
+ }
+
private fun logV(msg: String, vararg arguments: Any?) {
ProtoLog.v(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 fun logE(msg: String, vararg arguments: Any?) {
ProtoLog.e(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments)
}
@@ -473,7 +535,9 @@ class RootTaskDesksOrganizer(
val minimizationRoot = deskMinimizationRootsByDeskId[deskId]
pw.println("$innerPrefix #$deskId visible=${root.taskInfo.isVisible}")
pw.println("$innerPrefix displayId=${root.taskInfo.displayId}")
+ pw.println("$innerPrefix isLaunchRootRequested=${root.isLaunchRootRequested}")
pw.println("$innerPrefix children=${root.children}")
+ pw.println("$innerPrefix users=${root.users}")
pw.println("$innerPrefix minimization root:")
pw.println("$innerPrefix rootId=${minimizationRoot?.rootId}")
if (minimizationRoot != null) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java
index 51ef0ec60c3a..9ec1c7d65a6e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java
@@ -50,7 +50,9 @@ import android.graphics.Point;
import android.graphics.PointF;
import android.graphics.Rect;
import android.os.Bundle;
+import android.os.Debug;
import android.os.IBinder;
+import android.util.Log;
import android.view.SurfaceControl;
import android.view.WindowManager;
import android.window.TransitionInfo;
@@ -315,6 +317,20 @@ public class PipTransition extends PipTransitionController implements
return startAlphaTypeEnterAnimation(info, startTransaction, finishTransaction,
finishCallback);
}
+
+ TransitionInfo.Change pipActivityChange = PipTransitionUtils
+ .getDeferConfigActivityChange(info, pipChange.getTaskInfo().getToken());
+ if (pipActivityChange == null) {
+ // Legacy-enter and swipe-pip-to-home filters did not resolve a scheduled PiP entry.
+ // Bounds-type enter animation is the last resort, and it requires a config-at-end
+ // activity amongst the list of changes. If no such change, something went wrong.
+ Log.wtf(TAG, String.format("""
+ PipTransition.startAnimation didn't handle a scheduled PiP entry
+ transitionInfo=%s,
+ callers=%s""", info, Debug.getCallers(4)));
+ return false;
+ }
+
return startBoundsTypeEnterAnimation(info, startTransaction, finishTransaction,
finishCallback);
} else if (transition == mExitViaExpandTransition) {
@@ -839,26 +855,15 @@ public class PipTransition extends PipTransitionController implements
return true;
}
- // Sometimes root PiP task can have TF children. These child containers can be collected
- // even if they can promote to their parents: e.g. if they are marked as "organized".
- // So we count the chain of containers under PiP task as one "real" changing target;
- // iterate through changes bottom-to-top to properly identify parents.
- int expectedTargetCount = 1;
- WindowContainerToken lastPipChildToken = pipChange.getContainer();
- for (int i = info.getChanges().size() - 1; i >= 0; --i) {
- TransitionInfo.Change change = info.getChanges().get(i);
- if (change == pipChange || change.getContainer() == null) continue;
- if (change.getParent() != null && change.getParent().equals(lastPipChildToken)) {
- // Allow an extra change since our pinned root task has a child.
- ++expectedTargetCount;
- lastPipChildToken = change.getContainer();
- }
- }
-
- // If the only root task change in the changes list is a opening type PiP task,
- // then this is legacy-enter PiP.
- return info.getChanges().size() == expectedTargetCount
- && TransitionUtil.isOpeningMode(pipChange.getMode());
+ // #getEnterPipTransaction() always attempts to mark PiP activity as config-at-end one.
+ // However, the activity will only actually be marked config-at-end by Core if it is
+ // both isVisible and isVisibleRequested, which is when we can't run bounds animation.
+ //
+ // So we can use the absence of a config-at-end activity as a signal that we should run
+ // a legacy-enter PiP animation instead.
+ return TransitionUtil.isOpeningMode(pipChange.getMode())
+ && PipTransitionUtils.getDeferConfigActivityChange(
+ info, pipChange.getContainer()) == null;
}
return false;
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
index 10db5ca03637..d240aca522bb 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
@@ -3618,9 +3618,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
finishEnterSplitScreen(finishT);
addDividerBarToTransition(info, true /* show */);
- if (Flags.enableFlexibleTwoAppSplit()) {
- addAllDimLayersToTransition(info, true /* show */);
- }
+ addAllDimLayersToTransition(info, true /* show */);
return true;
}
@@ -3871,9 +3869,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
}
addDividerBarToTransition(info, false /* show */);
- if (Flags.enableFlexibleTwoAppSplit()) {
- addAllDimLayersToTransition(info, false /* show */);
- }
+ addAllDimLayersToTransition(info, false /* show */);
}
/** Call this when the recents animation canceled during split-screen. */
@@ -3999,8 +3995,15 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
info.addChange(barChange);
}
- /** Add dim layers to the transition, so that they can be hidden/shown when animation starts. */
+ /**
+ * Add dim layers to the transition, so that they can be hidden/shown when animation starts.
+ * They're only added if there is at least one offscreen app.
+ */
private void addAllDimLayersToTransition(@NonNull TransitionInfo info, boolean show) {
+ if (!mSplitState.currentStateSupportsOffscreenApps()) {
+ return;
+ }
+
if (Flags.enableFlexibleSplit()) {
List<StageTaskListener> stages = mStageOrderOperator.getActiveStages();
for (int i = 0; i < stages.size(); i++) {
@@ -4008,7 +4011,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
mSplitState.getCurrentLayout().get(i).roundOut(mTempRect1);
addDimLayerToTransition(info, show, stage, mTempRect1);
}
- } else {
+ } else if (enableFlexibleTwoAppSplit()) {
addDimLayerToTransition(info, show, mMainStage, getMainStageBounds());
addDimLayerToTransition(info, show, mSideStage, getSideStageBounds());
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultSurfaceAnimator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultSurfaceAnimator.java
index bff08ba6d88f..3240cbb779c6 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultSurfaceAnimator.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultSurfaceAnimator.java
@@ -169,7 +169,7 @@ public class DefaultSurfaceAnimator {
needCrop = true;
}
if (needCrop) {
- t.setCrop(leash, mAnimClipRect);
+ t.setWindowCrop(leash, mAnimClipRect);
}
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
index e9200834c5dd..5b6993863c5d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
@@ -133,6 +133,7 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler {
private final DisplayController mDisplayController;
private final Context mContext;
private final Handler mMainHandler;
+ private final Handler mAnimHandler;
private final ShellExecutor mMainExecutor;
private final ShellExecutor mAnimExecutor;
private final TransitionAnimation mTransitionAnimation;
@@ -171,6 +172,7 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler {
@NonNull TransactionPool transactionPool,
@NonNull ShellExecutor mainExecutor, @NonNull Handler mainHandler,
@NonNull ShellExecutor animExecutor,
+ @NonNull Handler animHandler,
@NonNull RootTaskDisplayAreaOrganizer rootTDAOrganizer,
@NonNull InteractionJankMonitor interactionJankMonitor) {
mDisplayController = displayController;
@@ -179,6 +181,7 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler {
mMainHandler = mainHandler;
mMainExecutor = mainExecutor;
mAnimExecutor = animExecutor;
+ mAnimHandler = animHandler;
mTransitionAnimation = new TransitionAnimation(context, false /* debug */, Transitions.TAG);
mCurrentUserId = UserHandle.myUserId();
mDevicePolicyManager = mContext.getSystemService(DevicePolicyManager.class);
@@ -349,10 +352,6 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler {
mAnimations.put(transition, animations);
final boolean isTaskTransition = isTaskTransition(info);
- if (isTaskTransition) {
- mInteractionJankMonitor.begin(info.getRoot(0).getLeash(), mContext,
- mMainHandler, CUJ_DEFAULT_TASK_TO_TASK_ANIMATION);
- }
final Runnable onAnimFinish = () -> {
if (!animations.isEmpty()) return;
@@ -642,6 +641,10 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler {
// now start animations. they are started on another thread, so we have to post them
// *after* applying the startTransaction
mAnimExecutor.execute(() -> {
+ if (isTaskTransition) {
+ mInteractionJankMonitor.begin(info.getRoot(0).getLeash(), mContext,
+ mAnimHandler, CUJ_DEFAULT_TASK_TO_TASK_ANIMATION);
+ }
for (int i = 0; i < animations.size(); ++i) {
animations.get(i).start();
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/FocusTransitionObserver.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/FocusTransitionObserver.java
index f0f1ad05008b..b91fb048dc09 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/FocusTransitionObserver.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/FocusTransitionObserver.java
@@ -31,6 +31,7 @@ import android.annotation.NonNull;
import android.app.ActivityManager.RunningTaskInfo;
import android.os.RemoteException;
import android.util.ArraySet;
+import android.util.IndentingPrintWriter;
import android.util.Slog;
import android.util.SparseArray;
import android.window.TransitionInfo;
@@ -38,6 +39,7 @@ import android.window.TransitionInfo;
import com.android.wm.shell.shared.FocusTransitionListener;
import com.android.wm.shell.shared.IFocusTransitionListener;
+import java.io.PrintWriter;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -237,4 +239,21 @@ public class FocusTransitionObserver {
}
return task.displayId == mFocusedDisplayId && isFocusedOnDisplay(task);
}
+
+ /** Dumps focused display and tasks. */
+ public void dump(PrintWriter originalWriter, String prefix) {
+ final IndentingPrintWriter writer =
+ new IndentingPrintWriter(originalWriter, " ", prefix);
+ writer.println("FocusTransitionObserver:");
+ writer.increaseIndent();
+ writer.printf("currentFocusedDisplayId=%d\n", mFocusedDisplayId);
+ writer.println("currentFocusedTaskOnDisplay:");
+ writer.increaseIndent();
+ for (int i = 0; i < mFocusedTaskOnDisplay.size(); i++) {
+ writer.printf("Display #%d: taskId=%d topActivity=%s\n",
+ mFocusedTaskOnDisplay.keyAt(i),
+ mFocusedTaskOnDisplay.valueAt(i).taskId,
+ mFocusedTaskOnDisplay.valueAt(i).topActivity);
+ }
+ }
}
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 3dc8733c879d..84724268cfc2 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
@@ -215,6 +215,7 @@ public class Transitions implements RemoteCallable<Transitions>,
private final Context mContext;
private final ShellExecutor mMainExecutor;
private final ShellExecutor mAnimExecutor;
+ private final Handler mAnimHandler;
private final TransitionPlayerImpl mPlayerImpl;
private final DefaultTransitionHandler mDefaultTransitionHandler;
private final RemoteTransitionHandler mRemoteTransitionHandler;
@@ -319,11 +320,12 @@ public class Transitions implements RemoteCallable<Transitions>,
@NonNull ShellExecutor mainExecutor,
@NonNull Handler mainHandler,
@NonNull ShellExecutor animExecutor,
+ @NonNull Handler animHandler,
@NonNull HomeTransitionObserver homeTransitionObserver,
@NonNull FocusTransitionObserver focusTransitionObserver) {
this(context, shellInit, new ShellCommandHandler(), shellController, organizer, pool,
displayController, displayInsetsController, mainExecutor, mainHandler, animExecutor,
- new RootTaskDisplayAreaOrganizer(mainExecutor, context, shellInit),
+ animHandler, new RootTaskDisplayAreaOrganizer(mainExecutor, context, shellInit),
homeTransitionObserver, focusTransitionObserver);
}
@@ -338,6 +340,7 @@ public class Transitions implements RemoteCallable<Transitions>,
@NonNull ShellExecutor mainExecutor,
@NonNull Handler mainHandler,
@NonNull ShellExecutor animExecutor,
+ @NonNull Handler animHandler,
@NonNull RootTaskDisplayAreaOrganizer rootTDAOrganizer,
@NonNull HomeTransitionObserver homeTransitionObserver,
@NonNull FocusTransitionObserver focusTransitionObserver) {
@@ -345,11 +348,12 @@ public class Transitions implements RemoteCallable<Transitions>,
mContext = context;
mMainExecutor = mainExecutor;
mAnimExecutor = animExecutor;
+ mAnimHandler = animHandler;
mDisplayController = displayController;
mPlayerImpl = new TransitionPlayerImpl();
mDefaultTransitionHandler = new DefaultTransitionHandler(context, shellInit,
displayController, displayInsetsController, pool, mainExecutor, mainHandler,
- animExecutor, rootTDAOrganizer, InteractionJankMonitor.getInstance());
+ animExecutor, mAnimHandler, rootTDAOrganizer, InteractionJankMonitor.getInstance());
mRemoteTransitionHandler = new RemoteTransitionHandler(mMainExecutor);
mShellCommandHandler = shellCommandHandler;
mShellController = shellController;
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 cdadce57d610..71bb153e4b1e 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
@@ -508,6 +508,9 @@ class HandleMenu(
private val iconButtonRippleRadius = context.resources.getDimensionPixelSize(
R.dimen.desktop_mode_handle_menu_icon_button_ripple_radius
)
+ private val handleMenuCornerRadius = context.resources.getDimensionPixelSize(
+ R.dimen.desktop_mode_handle_menu_corner_radius
+ )
private val iconButtonDrawableInsetsBase = DrawableInsets(
t = iconButtondrawableBaseInset,
b = iconButtondrawableBaseInset, l = iconButtondrawableBaseInset,
@@ -866,14 +869,21 @@ class HandleMenu(
private fun bindMoreActionsPill(style: MenuStyle) {
moreActionsPill.background.setTint(style.backgroundColor)
-
- arrayOf(
+ val buttons = arrayOf(
screenshotBtn to SHOULD_SHOW_SCREENSHOT_BUTTON,
newWindowBtn to shouldShowNewWindowButton,
manageWindowBtn to shouldShowManageWindowsButton,
changeAspectRatioBtn to shouldShowChangeAspectRatioButton,
restartBtn to shouldShowRestartButton,
- ).forEach { (button, shouldShow) ->
+ )
+ val firstVisible = buttons.find { it.second }?.first
+ val lastVisible = buttons.findLast { it.second }?.first
+
+ buttons.forEach { (button, shouldShow) ->
+ val topRadius =
+ if (button == firstVisible) handleMenuCornerRadius.toFloat() else 0f
+ val bottomRadius =
+ if (button == lastVisible) handleMenuCornerRadius.toFloat() else 0f
button.apply {
isGone = !shouldShow
textView.apply {
@@ -881,6 +891,13 @@ class HandleMenu(
startMarquee()
}
iconView.imageTintList = ColorStateList.valueOf(style.textColor)
+ background = createBackgroundDrawable(
+ color = style.textColor,
+ cornerRadius = floatArrayOf(
+ topRadius, topRadius, topRadius, topRadius,
+ bottomRadius, bottomRadius, bottomRadius, bottomRadius
+ ),
+ drawableInsets = DrawableInsets())
}
}
}
@@ -899,6 +916,10 @@ class HandleMenu(
openInAppOrBrowserBtn.apply {
contentDescription = btnText
+ background = createBackgroundDrawable(
+ color = style.textColor,
+ cornerRadius = handleMenuCornerRadius,
+ drawableInsets = DrawableInsets())
textView.apply {
text = btnText
setTextColor(style.textColor)
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/AppHandleAndHeaderVisibilityHelper.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/AppHandleAndHeaderVisibilityHelper.kt
index 39ccf5bd03a7..950eeccf6a4a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/AppHandleAndHeaderVisibilityHelper.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/AppHandleAndHeaderVisibilityHelper.kt
@@ -23,6 +23,7 @@ import android.view.WindowManager
import android.window.DesktopExperienceFlags.ENABLE_BUG_FIXES_FOR_SECONDARY_DISPLAY
import com.android.wm.shell.common.DisplayController
import com.android.wm.shell.desktopmode.DesktopWallpaperActivity.Companion.isWallpaperTask
+import com.android.wm.shell.shared.bubbles.BubbleAnythingFlagHelper
import com.android.wm.shell.shared.desktopmode.DesktopModeCompatPolicy
import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
import com.android.wm.shell.splitscreen.SplitScreenController
@@ -52,7 +53,8 @@ class AppHandleAndHeaderVisibilityHelper (
private fun allowedForTask(taskInfo: ActivityManager.RunningTaskInfo): Boolean {
// TODO (b/382023296): Remove once we no longer rely on
// Flags.enableBugFixesForSecondaryDisplay as it is taken care of in #allowedForDisplay
- if (displayController.getDisplay(taskInfo.displayId) == null) {
+ val display = displayController.getDisplay(taskInfo.displayId)
+ if (display == null) {
// If DisplayController doesn't have it tracked, it could be a private/managed display.
return false
}
@@ -68,8 +70,7 @@ class AppHandleAndHeaderVisibilityHelper (
// TODO (b/382023296): Remove once we no longer rely on
// Flags.enableBugFixesForSecondaryDisplay as it is taken care of in #allowedForDisplay
val isOnLargeScreen =
- displayController.getDisplay(taskInfo.displayId).minSizeDimensionDp >=
- WindowManager.LARGE_SCREEN_SMALLEST_SCREEN_WIDTH_DP
+ display.minSizeDimensionDp >= WindowManager.LARGE_SCREEN_SMALLEST_SCREEN_WIDTH_DP
if (!DesktopModeStatus.canEnterDesktopMode(context)
&& DesktopModeStatus.overridesShowAppHandle(context)
&& !isOnLargeScreen
@@ -78,6 +79,14 @@ class AppHandleAndHeaderVisibilityHelper (
// small screens
return false
}
+ if (BubbleAnythingFlagHelper.enableBubbleToFullscreen()
+ && !DesktopModeStatus.isDesktopModeSupportedOnDisplay(context, display)
+ ) {
+ // TODO(b/388853233): enable handles for split tasks once drag to bubble is enabled
+ if (taskInfo.windowingMode != WindowConfiguration.WINDOWING_MODE_FULLSCREEN) {
+ return false
+ }
+ }
return DesktopModeStatus.canEnterDesktopModeOrShowAppHandle(context)
&& !isWallpaperTask(taskInfo)
&& taskInfo.windowingMode != WindowConfiguration.WINDOWING_MODE_PINNED
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/ButtonBackgroundDrawableUtils.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/ButtonBackgroundDrawableUtils.kt
index f08cfa987cc7..33e743016d0d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/ButtonBackgroundDrawableUtils.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/ButtonBackgroundDrawableUtils.kt
@@ -51,10 +51,20 @@ fun replaceColorAlpha(@ColorInt color: Int, alpha: Int): Int {
*/
fun createBackgroundDrawable(
@ColorInt color: Int, cornerRadius: Int, drawableInsets: DrawableInsets
+): Drawable = createBackgroundDrawable(
+ color,
+ FloatArray(8) { cornerRadius.toFloat() },
+ drawableInsets)
+
+/**
+ * Creates a background drawable with specified color, corner radius, and insets.
+ */
+fun createBackgroundDrawable(
+ @ColorInt color: Int, cornerRadius: FloatArray, drawableInsets: DrawableInsets
): Drawable = LayerDrawable(arrayOf(
ShapeDrawable().apply {
shape = RoundRectShape(
- FloatArray(8) { cornerRadius.toFloat() },
+ cornerRadius,
/* inset= */ null,
/* innerRadii= */ null
)
diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/Android.bp b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/Android.bp
index 7585c977809e..50581f7e01f3 100644
--- a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/Android.bp
+++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/Android.bp
@@ -33,7 +33,6 @@ android_test {
"WMShellFlickerTestsBase",
"WMShellScenariosDesktopMode",
"WMShellTestUtils",
- "ui-trace-collector",
],
data: ["trace_config/*"],
}
diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/UnminimizeAppFromTaskbar.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/UnminimizeAppFromTaskbar.kt
index 7d9f2bf8fdf6..05ddb4043b82 100644
--- a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/UnminimizeAppFromTaskbar.kt
+++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/UnminimizeAppFromTaskbar.kt
@@ -19,7 +19,7 @@ package com.android.wm.shell.scenarios
import android.app.Instrumentation
import android.tools.NavBar
import android.tools.Rotation
-import android.tools.device.apphelpers.GmailAppHelper
+import android.tools.device.apphelpers.CalculatorAppHelper
import android.tools.flicker.rules.ChangeDisplayOrientationRule
import android.tools.traces.parsers.WindowManagerStateHelper
import androidx.test.platform.app.InstrumentationRegistry
@@ -44,8 +44,9 @@ abstract class UnminimizeAppFromTaskbar(val rotation: Rotation = Rotation.ROTATI
private val wmHelper = WindowManagerStateHelper(instrumentation)
private val device = UiDevice.getInstance(instrumentation)
private val testApp = DesktopModeAppHelper(SimpleAppHelper(instrumentation))
- private val gmailHelper = GmailAppHelper(instrumentation)
- private val gmailApp = DesktopModeAppHelper(gmailHelper)
+ private val calculatorHelper = CalculatorAppHelper(instrumentation)
+ private val calculatorApp = DesktopModeAppHelper(calculatorHelper)
+
@Rule
@JvmField val testSetupRule = Utils.testSetupRule(NavBar.MODE_GESTURAL, rotation)
@@ -59,20 +60,20 @@ abstract class UnminimizeAppFromTaskbar(val rotation: Rotation = Rotation.ROTATI
ChangeDisplayOrientationRule.setRotation(rotation)
testApp.enterDesktopMode(wmHelper, device)
tapl.showTaskbarIfHidden()
- gmailApp.launchViaIntent(wmHelper)
- gmailApp.minimizeDesktopApp(wmHelper, device)
+ calculatorApp.launchViaIntent(wmHelper)
+ calculatorApp.minimizeDesktopApp(wmHelper, device)
}
@Test
open fun unminimizeApp() {
tapl.launchedAppState.taskbar
- .getAppIcon(gmailHelper.appName)
- .launch(gmailHelper.packageName)
+ .getAppIcon(calculatorHelper.appName)
+ .launch(calculatorApp.packageName)
}
@After
fun teardown() {
testApp.exit(wmHelper)
- gmailApp.exit(wmHelper)
+ calculatorApp.exit(wmHelper)
}
-} \ No newline at end of file
+}
diff --git a/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/AndroidTestTemplate.xml b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/AndroidTestTemplate.xml
index 7659ec903480..8cbec687d8d8 100644
--- a/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/AndroidTestTemplate.xml
+++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/AndroidTestTemplate.xml
@@ -74,10 +74,6 @@
<option name="shell-timeout" value="6600s"/>
<option name="test-timeout" value="6000s"/>
<option name="hidden-api-checks" value="false"/>
- <option name="device-listeners" value="android.tools.collectors.DefaultUITraceListener"/>
- <!-- DefaultUITraceListener args -->
- <option name="instrumentation-arg" key="per_run" value="true"/>
- <option name="instrumentation-arg" key="perfetto_persist_pid_track" value="true"/>
</test>
<!-- Needed for pulling the collected trace config on to the host -->
<metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector">
diff --git a/libs/WindowManager/Shell/tests/e2e/utils/src/com/android/wm/shell/SimulatedConnectedDisplayTestRule.kt b/libs/WindowManager/Shell/tests/e2e/utils/src/com/android/wm/shell/SimulatedConnectedDisplayTestRule.kt
deleted file mode 100644
index f9b69d3f5f7e..000000000000
--- a/libs/WindowManager/Shell/tests/e2e/utils/src/com/android/wm/shell/SimulatedConnectedDisplayTestRule.kt
+++ /dev/null
@@ -1,172 +0,0 @@
-/*
- * Copyright 2025 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell
-
-import android.graphics.Point
-import android.hardware.display.DisplayManager
-import android.hardware.display.DisplayManager.DisplayListener
-import android.os.Handler
-import android.os.Looper
-import android.provider.Settings
-import android.util.Log
-import androidx.test.platform.app.InstrumentationRegistry
-import kotlin.time.Duration.Companion.seconds
-import kotlinx.coroutines.channels.awaitClose
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.callbackFlow
-import kotlinx.coroutines.flow.take
-import kotlinx.coroutines.runBlocking
-import kotlinx.coroutines.withTimeoutOrNull
-import org.junit.rules.TestRule
-import org.junit.runner.Description
-import org.junit.runners.model.Statement
-
-/**
- * A TestRule to manage multiple simulated connected overlay displays.
- */
-class SimulatedConnectedDisplayTestRule : TestRule {
-
- private val context = InstrumentationRegistry.getInstrumentation().targetContext
- private val displayManager = context.getSystemService(DisplayManager::class.java)
- private val addedDisplays = mutableListOf<Int>()
-
- override fun apply(base: Statement, description: Description): Statement =
- object : Statement() {
- override fun evaluate() {
- try {
- base.evaluate()
- } finally {
- teardown()
- }
- }
- }
-
- private fun teardown() {
- cleanupTestDisplays()
- }
-
- /**
- * Adds multiple overlay displays with specified dimensions. Any existing overlay displays
- * will be removed before adding the new ones.
- *
- * @param displays A list of [Point] objects, where each [Point] represents the
- * width and height of a simulated display.
- * @return List of displayIds of added displays.
- */
- fun setupTestDisplays(displays: List<Point>): List<Int> = runBlocking {
- // Cleanup any existing overlay displays.
- cleanupTestDisplays()
-
- if (displays.isEmpty()) {
- Log.w(TAG, "setupTestDisplays called with an empty list. No displays created.")
- return@runBlocking emptyList()
- }
-
- val displayAddedFlow: Flow<Int> = callbackFlow {
- val listener = object : DisplayListener {
- override fun onDisplayAdded(displayId: Int) {
- trySend(displayId)
- }
-
- override fun onDisplayRemoved(displayId: Int) {}
- override fun onDisplayChanged(displayId: Int) {}
- }
-
- val handler = Handler(Looper.getMainLooper())
- displayManager.registerDisplayListener(listener, handler)
-
- awaitClose {
- displayManager.unregisterDisplayListener(listener)
- }
- }
-
- val displaySettings = displays.joinToString(separator = ";") { size ->
- "${size.x}x${size.y}/$DEFAULT_DENSITY"
- }
-
- // Add the overlay displays
- Settings.Global.putString(
- InstrumentationRegistry.getInstrumentation().context.contentResolver,
- Settings.Global.OVERLAY_DISPLAY_DEVICES,
- displaySettings
- )
- withTimeoutOrNull(TIMEOUT) {
- displayAddedFlow.take(displays.size).collect { displayId ->
- addedDisplays.add(displayId)
- }
- } ?: error("Timed out waiting for displays to be added.")
- addedDisplays
- }
-
- /**
- * Adds multiple overlay displays with default dimensions. Any existing overlay displays
- * will be removed before adding the new ones.
- *
- * @param count number of displays to add.
- * @return List of displayIds of added displays.
- */
- fun setupTestDisplays(count: Int): List<Int> {
- val displays = List(count) { Point(DEFAULT_WIDTH, DEFAULT_HEIGHT) }
- return setupTestDisplays(displays)
- }
-
- private fun cleanupTestDisplays() = runBlocking {
- val displayRemovedFlow: Flow<Int> = callbackFlow {
- val listener = object : DisplayListener {
- override fun onDisplayAdded(displayId: Int) {}
- override fun onDisplayRemoved(displayId: Int) {
- trySend(displayId)
- }
-
- override fun onDisplayChanged(displayId: Int) {}
- }
- val handler = Handler(Looper.getMainLooper())
- displayManager.registerDisplayListener(listener, handler)
-
- awaitClose {
- displayManager.unregisterDisplayListener(listener)
- }
- }
-
- // Remove overlay displays. We'll execute this regardless of addedDisplays just to
- // ensure all overlay displays are removed before and after the test.
- // Note: If we want to restore the original overlay display added before this test (and its
- // topology), it will be complicated as re-adding overlay display would lead to different
- // displayId and topology could not be restored easily.
- Settings.Global.putString(
- InstrumentationRegistry.getInstrumentation().context.contentResolver,
- Settings.Global.OVERLAY_DISPLAY_DEVICES,
- null
- )
-
- if (!addedDisplays.isEmpty()) {
- withTimeoutOrNull(TIMEOUT) {
- displayRemovedFlow.take(addedDisplays.size).collect { displayId ->
- addedDisplays.remove(displayId)
- }
- } ?: error("Timed out waiting for displays to be removed: $addedDisplays")
- }
- }
-
- private companion object {
- const val DEFAULT_WIDTH = 1280
- const val DEFAULT_HEIGHT = 720
- const val DEFAULT_DENSITY = 160
- const val TAG = "SimulatedConnectedDisplayTestRule"
- val TIMEOUT = 10.seconds
- }
-}
diff --git a/libs/WindowManager/Shell/tests/flicker/Android.bp b/libs/WindowManager/Shell/tests/flicker/Android.bp
index 7b6cfe3f9f8a..98b0bd0b589d 100644
--- a/libs/WindowManager/Shell/tests/flicker/Android.bp
+++ b/libs/WindowManager/Shell/tests/flicker/Android.bp
@@ -38,15 +38,15 @@ java_library {
],
static_libs: [
"androidx.test.ext.junit",
- "flickertestapplib",
+ "com_android_wm_shell_flags_lib",
"flickerlib",
"flickerlib-helpers",
+ "flickertestapplib",
+ "launcher-aosp-tapl",
+ "launcher-helper-lib",
"platform-test-annotations",
"wm-flicker-common-app-helpers",
"wm-flicker-common-assertions",
- "launcher-helper-lib",
- "launcher-aosp-tapl",
- "com_android_wm_shell_flags_lib",
],
}
@@ -60,18 +60,18 @@ java_defaults {
test_suites: ["device-tests"],
libs: ["android.test.runner.stubs.system"],
static_libs: [
- "wm-shell-flicker-utils",
"androidx.test.ext.junit",
- "flickertestapplib",
"flickerlib",
"flickerlib-helpers",
"flickerlib-trace_processor_shell",
+ "flickertestapplib",
+ "launcher-aosp-tapl",
+ "launcher-helper-lib",
"platform-test-annotations",
"platform-test-rules",
"wm-flicker-common-app-helpers",
"wm-flicker-common-assertions",
- "launcher-helper-lib",
- "launcher-aosp-tapl",
+ "wm-shell-flicker-utils",
],
data: [
":FlickerTestApp",
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 85a431be8e8b..42dcaf4b4f33 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
@@ -51,6 +51,7 @@ import org.mockito.Mockito.times
import org.mockito.Mockito.verify
import org.mockito.kotlin.argumentCaptor
import org.mockito.kotlin.clearInvocations
+import org.mockito.kotlin.mock
import org.mockito.kotlin.whenever
import org.mockito.quality.Strictness
@@ -213,12 +214,15 @@ class DesktopDisplayEventHandlerTest : ShellTestCase() {
@EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
fun testUserChanged_createsDeskWhenNeeded() =
testScope.runTest {
+ val userId = 11
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)
+ val mockRepository = mock<DesktopRepository>()
+ whenever(mockDesktopUserRepositories.getProfile(userId)).thenReturn(mockRepository)
+ whenever(mockRepository.getNumberOfDesks(displayId = 2)).thenReturn(0)
+ whenever(mockRepository.getNumberOfDesks(displayId = 3)).thenReturn(0)
+ whenever(mockRepository.getNumberOfDesks(displayId = 4)).thenReturn(1)
whenever(mockRootTaskDisplayAreaOrganizer.displayIds).thenReturn(intArrayOf(2, 3, 4))
desktopRepositoryInitializer.initialize(mockDesktopUserRepositories)
handler.onDisplayAdded(displayId = 2)
@@ -227,7 +231,7 @@ class DesktopDisplayEventHandlerTest : ShellTestCase() {
runCurrent()
clearInvocations(mockDesktopTasksController)
- userChangeListenerCaptor.lastValue.onUserChanged(1, context)
+ userChangeListenerCaptor.lastValue.onUserChanged(userId, context)
runCurrent()
verify(mockDesktopTasksController).createDesk(displayId = 2)
@@ -238,20 +242,22 @@ class DesktopDisplayEventHandlerTest : ShellTestCase() {
@Test
fun testConnectExternalDisplay() {
onDisplaysChangedListenerCaptor.lastValue.onDisplayAdded(externalDisplayId)
- verify(desktopDisplayModeController).refreshDisplayWindowingMode()
+ verify(desktopDisplayModeController).updateExternalDisplayWindowingMode(externalDisplayId)
+ verify(desktopDisplayModeController).updateDefaultDisplayWindowingMode()
}
@Test
fun testDisconnectExternalDisplay() {
onDisplaysChangedListenerCaptor.lastValue.onDisplayRemoved(externalDisplayId)
- verify(desktopDisplayModeController).refreshDisplayWindowingMode()
+ verify(desktopDisplayModeController).updateDefaultDisplayWindowingMode()
}
@Test
@EnableFlags(DisplayFlags.FLAG_ENABLE_DISPLAY_CONTENT_MODE_MANAGEMENT)
fun testDesktopModeEligibleChanged() {
onDisplaysChangedListenerCaptor.lastValue.onDesktopModeEligibleChanged(externalDisplayId)
- verify(desktopDisplayModeController).refreshDisplayWindowingMode()
+ verify(desktopDisplayModeController).updateExternalDisplayWindowingMode(externalDisplayId)
+ verify(desktopDisplayModeController).updateDefaultDisplayWindowingMode()
}
private class FakeDesktopRepositoryInitializer : DesktopRepositoryInitializer {
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayModeControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayModeControllerTest.kt
index 96b826f93aae..7e9ee34c8f68 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayModeControllerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayModeControllerTest.kt
@@ -101,10 +101,13 @@ class DesktopDisplayModeControllerTest(
private val fullscreenTask =
TestRunningTaskInfoBuilder().setWindowingMode(WINDOWING_MODE_FULLSCREEN).build()
private val defaultTDA = DisplayAreaInfo(MockToken().token(), DEFAULT_DISPLAY, 0)
+ private val externalTDA = DisplayAreaInfo(MockToken().token(), EXTERNAL_DISPLAY_ID, 0)
private val wallpaperToken = MockToken().token()
private val defaultDisplay = mock<Display>()
private val externalDisplay = mock<Display>()
- private val mouseDevice = mock<InputDevice>()
+ private val touchpadDevice = mock<InputDevice>()
+ private val keyboardDevice = mock<InputDevice>()
+ private val connectedDeviceIds = mutableListOf<Int>()
private lateinit var extendedDisplaySettingsRestoreSession:
ExtendedDisplaySettingsRestoreSession
@@ -127,6 +130,8 @@ class DesktopDisplayModeControllerTest(
whenever(transitions.startTransition(anyInt(), any(), isNull())).thenReturn(Binder())
whenever(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY))
.thenReturn(defaultTDA)
+ whenever(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(EXTERNAL_DISPLAY_ID))
+ .thenReturn(externalTDA)
controller =
DesktopDisplayModeController(
context,
@@ -145,16 +150,18 @@ class DesktopDisplayModeControllerTest(
whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(wallpaperToken)
whenever(displayController.getDisplay(DEFAULT_DISPLAY)).thenReturn(defaultDisplay)
whenever(displayController.getDisplay(EXTERNAL_DISPLAY_ID)).thenReturn(externalDisplay)
- setTabletModeStatus(SwitchState.UNKNOWN)
- whenever(
- DesktopModeStatus.isDesktopModeSupportedOnDisplay(
- context,
- defaultDisplay
- )
- ).thenReturn(true)
- whenever(mouseDevice.supportsSource(InputDevice.SOURCE_MOUSE)).thenReturn(true)
- whenever(inputManager.getInputDevice(EXTERNAL_DEVICE_ID)).thenReturn(mouseDevice)
- setMouseConnected(false)
+ whenever(DesktopModeStatus.isDesktopModeSupportedOnDisplay(context, defaultDisplay))
+ .thenReturn(true)
+ whenever(touchpadDevice.supportsSource(InputDevice.SOURCE_TOUCHPAD)).thenReturn(true)
+ whenever(touchpadDevice.isEnabled()).thenReturn(true)
+ whenever(inputManager.getInputDevice(TOUCHPAD_DEVICE_ID)).thenReturn(touchpadDevice)
+ whenever(keyboardDevice.isFullKeyboard()).thenReturn(true)
+ whenever(keyboardDevice.isVirtual()).thenReturn(false)
+ whenever(keyboardDevice.isEnabled()).thenReturn(true)
+ whenever(inputManager.getInputDevice(KEYBOARD_DEVICE_ID)).thenReturn(keyboardDevice)
+ whenever(inputManager.inputDeviceIds).thenAnswer { connectedDeviceIds.toIntArray() }
+ setTouchpadConnected(false)
+ setKeyboardConnected(false)
}
@After
@@ -211,8 +218,8 @@ class DesktopDisplayModeControllerTest(
@DisableFlags(Flags.FLAG_FORM_FACTOR_BASED_DESKTOP_FIRST_SWITCH)
fun testTargetWindowingMode_formfactorDisabled(
@TestParameter param: ExternalDisplayBasedTargetModeTestCase,
- @TestParameter tabletModeStatus: SwitchState,
- @TestParameter hasAnyMouseDevice: Boolean,
+ @TestParameter hasAnyTouchpadDevice: Boolean,
+ @TestParameter hasAnyKeyboardDevice: Boolean,
) {
whenever(mockWindowManager.getWindowingMode(anyInt()))
.thenReturn(param.defaultWindowingMode)
@@ -221,15 +228,11 @@ class DesktopDisplayModeControllerTest(
} else {
disconnectExternalDisplay()
}
- setTabletModeStatus(tabletModeStatus)
- setMouseConnected(hasAnyMouseDevice)
+ setTouchpadConnected(hasAnyTouchpadDevice)
+ setKeyboardConnected(hasAnyKeyboardDevice)
setExtendedMode(param.extendedDisplayEnabled)
- whenever(
- DesktopModeStatus.isDesktopModeSupportedOnDisplay(
- context,
- defaultDisplay
- )
- ).thenReturn(param.isDefaultDisplayDesktopEligible)
+ whenever(DesktopModeStatus.isDesktopModeSupportedOnDisplay(context, defaultDisplay))
+ .thenReturn(param.isDefaultDisplayDesktopEligible)
assertThat(controller.getTargetWindowingModeForDefaultDisplay())
.isEqualTo(param.expectedWindowingMode)
@@ -246,15 +249,11 @@ class DesktopDisplayModeControllerTest(
} else {
disconnectExternalDisplay()
}
- setTabletModeStatus(param.tabletModeStatus)
setExtendedMode(param.extendedDisplayEnabled)
- whenever(
- DesktopModeStatus.isDesktopModeSupportedOnDisplay(
- context,
- defaultDisplay
- )
- ).thenReturn(param.isDefaultDisplayDesktopEligible)
- setMouseConnected(param.hasAnyMouseDevice)
+ whenever(DesktopModeStatus.isDesktopModeSupportedOnDisplay(context, defaultDisplay))
+ .thenReturn(param.isDefaultDisplayDesktopEligible)
+ setTouchpadConnected(param.hasAnyTouchpadDevice)
+ setKeyboardConnected(param.hasAnyKeyboardDevice)
assertThat(controller.getTargetWindowingModeForDefaultDisplay())
.isEqualTo(param.expectedWindowingMode)
@@ -296,30 +295,36 @@ class DesktopDisplayModeControllerTest(
.isEqualTo(WINDOWING_MODE_UNDEFINED)
}
+ @Test
+ @EnableFlags(DisplayFlags.FLAG_ENABLE_DISPLAY_CONTENT_MODE_MANAGEMENT)
+ fun externalDisplayWindowingMode() {
+ externalTDA.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN
+ setExtendedMode(true)
+
+ controller.updateExternalDisplayWindowingMode(EXTERNAL_DISPLAY_ID)
+
+ val arg = argumentCaptor<WindowContainerTransaction>()
+ verify(transitions, times(1)).startTransition(eq(TRANSIT_CHANGE), arg.capture(), isNull())
+ assertThat(arg.firstValue.changes[externalTDA.token.asBinder()]?.windowingMode)
+ .isEqualTo(WINDOWING_MODE_FREEFORM)
+ }
+
private fun connectExternalDisplay() {
whenever(rootTaskDisplayAreaOrganizer.getDisplayIds())
.thenReturn(intArrayOf(DEFAULT_DISPLAY, EXTERNAL_DISPLAY_ID))
- controller.refreshDisplayWindowingMode()
+ controller.updateDefaultDisplayWindowingMode()
}
private fun disconnectExternalDisplay() {
whenever(rootTaskDisplayAreaOrganizer.getDisplayIds())
.thenReturn(intArrayOf(DEFAULT_DISPLAY))
- controller.refreshDisplayWindowingMode()
- }
-
- private fun setTabletModeStatus(status: SwitchState) {
- whenever(inputManager.isInTabletMode()).thenReturn(status.value)
+ controller.updateDefaultDisplayWindowingMode()
}
private fun setExtendedMode(enabled: Boolean) {
if (DisplayFlags.enableDisplayContentModeManagement()) {
- whenever(
- DesktopModeStatus.isDesktopModeSupportedOnDisplay(
- context,
- externalDisplay
- )
- ).thenReturn(enabled)
+ whenever(DesktopModeStatus.isDesktopModeSupportedOnDisplay(context, externalDisplay))
+ .thenReturn(enabled)
} else {
Settings.Global.putInt(
context.contentResolver,
@@ -329,9 +334,20 @@ class DesktopDisplayModeControllerTest(
}
}
- private fun setMouseConnected(connected: Boolean) {
- whenever(inputManager.inputDeviceIds)
- .thenReturn(if (connected) intArrayOf(EXTERNAL_DEVICE_ID) else intArrayOf())
+ private fun setTouchpadConnected(connected: Boolean) {
+ if (connected) {
+ connectedDeviceIds.add(TOUCHPAD_DEVICE_ID)
+ } else {
+ connectedDeviceIds.remove(TOUCHPAD_DEVICE_ID)
+ }
+ }
+
+ private fun setKeyboardConnected(connected: Boolean) {
+ if (connected) {
+ connectedDeviceIds.add(KEYBOARD_DEVICE_ID)
+ } else {
+ connectedDeviceIds.remove(KEYBOARD_DEVICE_ID)
+ }
}
private class ExtendedDisplaySettingsRestoreSession(
@@ -358,13 +374,8 @@ class DesktopDisplayModeControllerTest(
companion object {
const val EXTERNAL_DISPLAY_ID = 100
- const val EXTERNAL_DEVICE_ID = 10
-
- enum class SwitchState(val value: Int) {
- UNKNOWN(InputManager.SWITCH_STATE_UNKNOWN),
- ON(InputManager.SWITCH_STATE_ON),
- OFF(InputManager.SWITCH_STATE_OFF),
- }
+ const val TOUCHPAD_DEVICE_ID = 10
+ const val KEYBOARD_DEVICE_ID = 11
enum class ExternalDisplayBasedTargetModeTestCase(
val defaultWindowingMode: Int,
@@ -490,393 +501,265 @@ class DesktopDisplayModeControllerTest(
enum class FormFactorBasedTargetModeTestCase(
val hasExternalDisplay: Boolean,
val extendedDisplayEnabled: Boolean,
- val tabletModeStatus: SwitchState,
val isDefaultDisplayDesktopEligible: Boolean,
- val hasAnyMouseDevice: Boolean,
+ val hasAnyTouchpadDevice: Boolean,
+ val hasAnyKeyboardDevice: Boolean,
val expectedWindowingMode: Int,
) {
- EXTERNAL_EXTENDED_TABLET_NO_PROJECTED_NO_MOUSE(
- hasExternalDisplay = true,
- extendedDisplayEnabled = true,
- tabletModeStatus = SwitchState.ON,
- isDefaultDisplayDesktopEligible = true,
- hasAnyMouseDevice = false,
- expectedWindowingMode = WINDOWING_MODE_FREEFORM,
- ),
- NO_EXTERNAL_EXTENDED_TABLET_NO_PROJECTED_NO_MOUSE(
- hasExternalDisplay = false,
- extendedDisplayEnabled = true,
- tabletModeStatus = SwitchState.ON,
- isDefaultDisplayDesktopEligible = true,
- hasAnyMouseDevice = false,
- expectedWindowingMode = WINDOWING_MODE_FULLSCREEN,
- ),
- EXTERNAL_MIRROR_TABLET_NO_PROJECTED_NO_MOUSE(
- hasExternalDisplay = true,
- extendedDisplayEnabled = false,
- tabletModeStatus = SwitchState.ON,
- isDefaultDisplayDesktopEligible = true,
- hasAnyMouseDevice = false,
- expectedWindowingMode = WINDOWING_MODE_FULLSCREEN,
- ),
- NO_EXTERNAL_MIRROR_TABLET_NO_PROJECTED_NO_MOUSE(
- hasExternalDisplay = false,
- extendedDisplayEnabled = false,
- tabletModeStatus = SwitchState.ON,
- isDefaultDisplayDesktopEligible = true,
- hasAnyMouseDevice = false,
- expectedWindowingMode = WINDOWING_MODE_FULLSCREEN,
- ),
- EXTERNAL_EXTENDED_CLAMSHELL_NO_PROJECTED_NO_MOUSE(
+ EXTERNAL_EXTENDED_NO_PROJECTED_TOUCHPAD_KEYBOARD(
hasExternalDisplay = true,
extendedDisplayEnabled = true,
- tabletModeStatus = SwitchState.OFF,
isDefaultDisplayDesktopEligible = true,
- hasAnyMouseDevice = false,
+ hasAnyTouchpadDevice = true,
+ hasAnyKeyboardDevice = true,
expectedWindowingMode = WINDOWING_MODE_FREEFORM,
),
- NO_EXTERNAL_EXTENDED_CLAMSHELL_NO_PROJECTED_NO_MOUSE(
+ NO_EXTERNAL_EXTENDED_NO_PROJECTED_TOUCHPAD_KEYBOARD(
hasExternalDisplay = false,
extendedDisplayEnabled = true,
- tabletModeStatus = SwitchState.OFF,
isDefaultDisplayDesktopEligible = true,
- hasAnyMouseDevice = false,
+ hasAnyTouchpadDevice = true,
+ hasAnyKeyboardDevice = true,
expectedWindowingMode = WINDOWING_MODE_FREEFORM,
),
- EXTERNAL_MIRROR_CLAMSHELL_NO_PROJECTED_NO_MOUSE(
+ EXTERNAL_MIRROR_NO_PROJECTED_TOUCHPAD_KEYBOARD(
hasExternalDisplay = true,
extendedDisplayEnabled = false,
- tabletModeStatus = SwitchState.OFF,
isDefaultDisplayDesktopEligible = true,
- hasAnyMouseDevice = false,
+ hasAnyTouchpadDevice = true,
+ hasAnyKeyboardDevice = true,
expectedWindowingMode = WINDOWING_MODE_FREEFORM,
),
- NO_EXTERNAL_MIRROR_CLAMSHELL_NO_PROJECTED_NO_MOUSE(
+ NO_EXTERNAL_MIRROR_NO_PROJECTED_TOUCHPAD_KEYBOARD(
hasExternalDisplay = false,
extendedDisplayEnabled = false,
- tabletModeStatus = SwitchState.OFF,
isDefaultDisplayDesktopEligible = true,
- hasAnyMouseDevice = false,
+ hasAnyTouchpadDevice = true,
+ hasAnyKeyboardDevice = true,
expectedWindowingMode = WINDOWING_MODE_FREEFORM,
),
- EXTERNAL_EXTENDED_UNKNOWN_NO_PROJECTED_NO_MOUSE(
+ EXTERNAL_EXTENDED_PROJECTED_TOUCHPAD_KEYBOARD(
hasExternalDisplay = true,
extendedDisplayEnabled = true,
- tabletModeStatus = SwitchState.UNKNOWN,
- isDefaultDisplayDesktopEligible = true,
- hasAnyMouseDevice = false,
- expectedWindowingMode = WINDOWING_MODE_FREEFORM,
- ),
- NO_EXTERNAL_EXTENDED_UNKNOWN_NO_PROJECTED_NO_MOUSE(
- hasExternalDisplay = false,
- extendedDisplayEnabled = true,
- tabletModeStatus = SwitchState.UNKNOWN,
- isDefaultDisplayDesktopEligible = true,
- hasAnyMouseDevice = false,
- expectedWindowingMode = WINDOWING_MODE_FULLSCREEN,
- ),
- EXTERNAL_MIRROR_UNKNOWN_NO_PROJECTED_NO_MOUSE(
- hasExternalDisplay = true,
- extendedDisplayEnabled = false,
- tabletModeStatus = SwitchState.UNKNOWN,
- isDefaultDisplayDesktopEligible = true,
- hasAnyMouseDevice = false,
- expectedWindowingMode = WINDOWING_MODE_FULLSCREEN,
- ),
- NO_EXTERNAL_MIRROR_UNKNOWN_NO_PROJECTED_NO_MOUSE(
- hasExternalDisplay = false,
- extendedDisplayEnabled = false,
- tabletModeStatus = SwitchState.UNKNOWN,
- isDefaultDisplayDesktopEligible = true,
- hasAnyMouseDevice = false,
- expectedWindowingMode = WINDOWING_MODE_FULLSCREEN,
- ),
- EXTERNAL_EXTENDED_TABLET_PROJECTED_NO_MOUSE(
- hasExternalDisplay = true,
- extendedDisplayEnabled = true,
- tabletModeStatus = SwitchState.ON,
isDefaultDisplayDesktopEligible = false,
- hasAnyMouseDevice = false,
+ hasAnyTouchpadDevice = true,
+ hasAnyKeyboardDevice = true,
expectedWindowingMode = WINDOWING_MODE_FULLSCREEN,
),
- NO_EXTERNAL_EXTENDED_TABLET_PROJECTED_NO_MOUSE(
+ NO_EXTERNAL_EXTENDED_PROJECTED_TOUCHPAD_KEYBOARD(
hasExternalDisplay = false,
extendedDisplayEnabled = true,
- tabletModeStatus = SwitchState.ON,
isDefaultDisplayDesktopEligible = false,
- hasAnyMouseDevice = false,
+ hasAnyTouchpadDevice = true,
+ hasAnyKeyboardDevice = true,
expectedWindowingMode = WINDOWING_MODE_FULLSCREEN,
),
- EXTERNAL_MIRROR_TABLET_PROJECTED_NO_MOUSE(
+ EXTERNAL_MIRROR_PROJECTED_TOUCHPAD_KEYBOARD(
hasExternalDisplay = true,
extendedDisplayEnabled = false,
- tabletModeStatus = SwitchState.ON,
isDefaultDisplayDesktopEligible = false,
- hasAnyMouseDevice = false,
+ hasAnyTouchpadDevice = true,
+ hasAnyKeyboardDevice = true,
expectedWindowingMode = WINDOWING_MODE_FULLSCREEN,
),
- NO_EXTERNAL_MIRROR_TABLET_PROJECTED_NO_MOUSE(
+ NO_EXTERNAL_MIRROR_PROJECTED_TOUCHPAD_KEYBOARD(
hasExternalDisplay = false,
extendedDisplayEnabled = false,
- tabletModeStatus = SwitchState.ON,
isDefaultDisplayDesktopEligible = false,
- hasAnyMouseDevice = false,
+ hasAnyTouchpadDevice = true,
+ hasAnyKeyboardDevice = true,
expectedWindowingMode = WINDOWING_MODE_FULLSCREEN,
),
- EXTERNAL_EXTENDED_CLAMSHELL_PROJECTED_NO_MOUSE(
+ EXTERNAL_EXTENDED_NO_PROJECTED_NO_TOUCHPAD_KEYBOARD(
hasExternalDisplay = true,
extendedDisplayEnabled = true,
- tabletModeStatus = SwitchState.OFF,
- isDefaultDisplayDesktopEligible = false,
- hasAnyMouseDevice = false,
- expectedWindowingMode = WINDOWING_MODE_FULLSCREEN,
+ isDefaultDisplayDesktopEligible = true,
+ hasAnyTouchpadDevice = false,
+ hasAnyKeyboardDevice = true,
+ expectedWindowingMode = WINDOWING_MODE_FREEFORM,
),
- NO_EXTERNAL_EXTENDED_CLAMSHELL_PROJECTED_NO_MOUSE(
+ NO_EXTERNAL_EXTENDED_NO_PROJECTED_NO_TOUCHPAD_KEYBOARD(
hasExternalDisplay = false,
extendedDisplayEnabled = true,
- tabletModeStatus = SwitchState.OFF,
- isDefaultDisplayDesktopEligible = false,
- hasAnyMouseDevice = false,
+ isDefaultDisplayDesktopEligible = true,
+ hasAnyTouchpadDevice = false,
+ hasAnyKeyboardDevice = true,
expectedWindowingMode = WINDOWING_MODE_FULLSCREEN,
),
- EXTERNAL_MIRROR_CLAMSHELL_PROJECTED_NO_MOUSE(
+ EXTERNAL_MIRROR_NO_PROJECTED_NO_TOUCHPAD_KEYBOARD(
hasExternalDisplay = true,
extendedDisplayEnabled = false,
- tabletModeStatus = SwitchState.OFF,
- isDefaultDisplayDesktopEligible = false,
- hasAnyMouseDevice = false,
+ isDefaultDisplayDesktopEligible = true,
+ hasAnyTouchpadDevice = false,
+ hasAnyKeyboardDevice = true,
expectedWindowingMode = WINDOWING_MODE_FULLSCREEN,
),
- NO_EXTERNAL_MIRROR_CLAMSHELL_PROJECTED_NO_MOUSE(
+ NO_EXTERNAL_MIRROR_NO_PROJECTED_NO_TOUCHPAD_KEYBOARD(
hasExternalDisplay = false,
extendedDisplayEnabled = false,
- tabletModeStatus = SwitchState.OFF,
- isDefaultDisplayDesktopEligible = false,
- hasAnyMouseDevice = false,
+ isDefaultDisplayDesktopEligible = true,
+ hasAnyTouchpadDevice = false,
+ hasAnyKeyboardDevice = true,
expectedWindowingMode = WINDOWING_MODE_FULLSCREEN,
),
- EXTERNAL_EXTENDED_UNKNOWN_PROJECTED_NO_MOUSE(
+ EXTERNAL_EXTENDED_PROJECTED_NO_TOUCHPAD_KEYBOARD(
hasExternalDisplay = true,
extendedDisplayEnabled = true,
- tabletModeStatus = SwitchState.UNKNOWN,
isDefaultDisplayDesktopEligible = false,
- hasAnyMouseDevice = false,
+ hasAnyTouchpadDevice = false,
+ hasAnyKeyboardDevice = true,
expectedWindowingMode = WINDOWING_MODE_FULLSCREEN,
),
- NO_EXTERNAL_EXTENDED_UNKNOWN_PROJECTED_NO_MOUSE(
+ NO_EXTERNAL_EXTENDED_PROJECTED_NO_TOUCHPAD_KEYBOARD(
hasExternalDisplay = false,
extendedDisplayEnabled = true,
- tabletModeStatus = SwitchState.UNKNOWN,
isDefaultDisplayDesktopEligible = false,
- hasAnyMouseDevice = false,
+ hasAnyTouchpadDevice = false,
+ hasAnyKeyboardDevice = true,
expectedWindowingMode = WINDOWING_MODE_FULLSCREEN,
),
- EXTERNAL_MIRROR_UNKNOWN_PROJECTED_NO_MOUSE(
+ EXTERNAL_MIRROR_PROJECTED_NO_TOUCHPAD_KEYBOARD(
hasExternalDisplay = true,
extendedDisplayEnabled = false,
- tabletModeStatus = SwitchState.UNKNOWN,
isDefaultDisplayDesktopEligible = false,
- hasAnyMouseDevice = false,
+ hasAnyTouchpadDevice = false,
+ hasAnyKeyboardDevice = true,
expectedWindowingMode = WINDOWING_MODE_FULLSCREEN,
),
- NO_EXTERNAL_MIRROR_UNKNOWN_PROJECTED_NO_MOUSE(
+ NO_EXTERNAL_MIRROR_PROJECTED_NO_TOUCHPAD_KEYBOARD(
hasExternalDisplay = false,
extendedDisplayEnabled = false,
- tabletModeStatus = SwitchState.UNKNOWN,
isDefaultDisplayDesktopEligible = false,
- hasAnyMouseDevice = false,
+ hasAnyTouchpadDevice = false,
+ hasAnyKeyboardDevice = true,
expectedWindowingMode = WINDOWING_MODE_FULLSCREEN,
),
- EXTERNAL_EXTENDED_TABLET_NO_PROJECTED_MOUSE(
+ EXTERNAL_EXTENDED_NO_PROJECTED_TOUCHPAD_NO_KEYBOARD(
hasExternalDisplay = true,
extendedDisplayEnabled = true,
- tabletModeStatus = SwitchState.ON,
isDefaultDisplayDesktopEligible = true,
- hasAnyMouseDevice = true,
+ hasAnyTouchpadDevice = true,
+ hasAnyKeyboardDevice = false,
expectedWindowingMode = WINDOWING_MODE_FREEFORM,
),
- NO_EXTERNAL_EXTENDED_TABLET_NO_PROJECTED_MOUSE(
+ NO_EXTERNAL_EXTENDED_NO_PROJECTED_TOUCHPAD_NO_KEYBOARD(
hasExternalDisplay = false,
extendedDisplayEnabled = true,
- tabletModeStatus = SwitchState.ON,
isDefaultDisplayDesktopEligible = true,
- hasAnyMouseDevice = true,
- expectedWindowingMode = WINDOWING_MODE_FREEFORM,
- ),
- EXTERNAL_MIRROR_TABLET_NO_PROJECTED_MOUSE(
- hasExternalDisplay = true,
- extendedDisplayEnabled = false,
- tabletModeStatus = SwitchState.ON,
- isDefaultDisplayDesktopEligible = true,
- hasAnyMouseDevice = true,
- expectedWindowingMode = WINDOWING_MODE_FREEFORM,
- ),
- NO_EXTERNAL_MIRROR_TABLET_NO_PROJECTED_MOUSE(
- hasExternalDisplay = false,
- extendedDisplayEnabled = false,
- tabletModeStatus = SwitchState.ON,
- isDefaultDisplayDesktopEligible = true,
- hasAnyMouseDevice = true,
- expectedWindowingMode = WINDOWING_MODE_FREEFORM,
- ),
- EXTERNAL_EXTENDED_CLAMSHELL_NO_PROJECTED_MOUSE(
- hasExternalDisplay = true,
- extendedDisplayEnabled = true,
- tabletModeStatus = SwitchState.OFF,
- isDefaultDisplayDesktopEligible = true,
- hasAnyMouseDevice = true,
- expectedWindowingMode = WINDOWING_MODE_FREEFORM,
- ),
- NO_EXTERNAL_EXTENDED_CLAMSHELL_NO_PROJECTED_MOUSE(
- hasExternalDisplay = false,
- extendedDisplayEnabled = true,
- tabletModeStatus = SwitchState.OFF,
- isDefaultDisplayDesktopEligible = true,
- hasAnyMouseDevice = true,
- expectedWindowingMode = WINDOWING_MODE_FREEFORM,
- ),
- EXTERNAL_MIRROR_CLAMSHELL_NO_PROJECTED_MOUSE(
- hasExternalDisplay = true,
- extendedDisplayEnabled = false,
- tabletModeStatus = SwitchState.OFF,
- isDefaultDisplayDesktopEligible = true,
- hasAnyMouseDevice = true,
- expectedWindowingMode = WINDOWING_MODE_FREEFORM,
- ),
- NO_EXTERNAL_MIRROR_CLAMSHELL_NO_PROJECTED_MOUSE(
- hasExternalDisplay = false,
- extendedDisplayEnabled = false,
- tabletModeStatus = SwitchState.OFF,
- isDefaultDisplayDesktopEligible = true,
- hasAnyMouseDevice = true,
- expectedWindowingMode = WINDOWING_MODE_FREEFORM,
- ),
- EXTERNAL_EXTENDED_UNKNOWN_NO_PROJECTED_MOUSE(
- hasExternalDisplay = true,
- extendedDisplayEnabled = true,
- tabletModeStatus = SwitchState.UNKNOWN,
- isDefaultDisplayDesktopEligible = true,
- hasAnyMouseDevice = true,
- expectedWindowingMode = WINDOWING_MODE_FREEFORM,
- ),
- NO_EXTERNAL_EXTENDED_UNKNOWN_NO_PROJECTED_MOUSE(
- hasExternalDisplay = false,
- extendedDisplayEnabled = true,
- tabletModeStatus = SwitchState.UNKNOWN,
- isDefaultDisplayDesktopEligible = true,
- hasAnyMouseDevice = true,
- expectedWindowingMode = WINDOWING_MODE_FREEFORM,
+ hasAnyTouchpadDevice = true,
+ hasAnyKeyboardDevice = false,
+ expectedWindowingMode = WINDOWING_MODE_FULLSCREEN,
),
- EXTERNAL_MIRROR_UNKNOWN_NO_PROJECTED_MOUSE(
+ EXTERNAL_MIRROR_NO_PROJECTED_TOUCHPAD_NO_KEYBOARD(
hasExternalDisplay = true,
extendedDisplayEnabled = false,
- tabletModeStatus = SwitchState.UNKNOWN,
isDefaultDisplayDesktopEligible = true,
- hasAnyMouseDevice = true,
- expectedWindowingMode = WINDOWING_MODE_FREEFORM,
+ hasAnyTouchpadDevice = true,
+ hasAnyKeyboardDevice = false,
+ expectedWindowingMode = WINDOWING_MODE_FULLSCREEN,
),
- NO_EXTERNAL_MIRROR_UNKNOWN_NO_PROJECTED_MOUSE(
+ NO_EXTERNAL_MIRROR_NO_PROJECTED_TOUCHPAD_NO_KEYBOARD(
hasExternalDisplay = false,
extendedDisplayEnabled = false,
- tabletModeStatus = SwitchState.UNKNOWN,
isDefaultDisplayDesktopEligible = true,
- hasAnyMouseDevice = true,
- expectedWindowingMode = WINDOWING_MODE_FREEFORM,
+ hasAnyTouchpadDevice = true,
+ hasAnyKeyboardDevice = false,
+ expectedWindowingMode = WINDOWING_MODE_FULLSCREEN,
),
- EXTERNAL_EXTENDED_TABLET_PROJECTED_MOUSE(
+ EXTERNAL_EXTENDED_PROJECTED_TOUCHPAD_NO_KEYBOARD(
hasExternalDisplay = true,
extendedDisplayEnabled = true,
- tabletModeStatus = SwitchState.ON,
isDefaultDisplayDesktopEligible = false,
- hasAnyMouseDevice = true,
+ hasAnyTouchpadDevice = true,
+ hasAnyKeyboardDevice = false,
expectedWindowingMode = WINDOWING_MODE_FULLSCREEN,
),
- NO_EXTERNAL_EXTENDED_TABLET_PROJECTED_MOUSE(
+ NO_EXTERNAL_EXTENDED_PROJECTED_TOUCHPAD_NO_KEYBOARD(
hasExternalDisplay = false,
extendedDisplayEnabled = true,
- tabletModeStatus = SwitchState.ON,
isDefaultDisplayDesktopEligible = false,
- hasAnyMouseDevice = true,
+ hasAnyTouchpadDevice = true,
+ hasAnyKeyboardDevice = false,
expectedWindowingMode = WINDOWING_MODE_FULLSCREEN,
),
- EXTERNAL_MIRROR_TABLET_PROJECTED_MOUSE(
+ EXTERNAL_MIRROR_PROJECTED_TOUCHPAD_NO_KEYBOARD(
hasExternalDisplay = true,
extendedDisplayEnabled = false,
- tabletModeStatus = SwitchState.ON,
isDefaultDisplayDesktopEligible = false,
- hasAnyMouseDevice = true,
+ hasAnyTouchpadDevice = true,
+ hasAnyKeyboardDevice = false,
expectedWindowingMode = WINDOWING_MODE_FULLSCREEN,
),
- NO_EXTERNAL_MIRROR_TABLET_PROJECTED_MOUSE(
+ NO_EXTERNAL_MIRROR_PROJECTED_TOUCHPAD_NO_KEYBOARD(
hasExternalDisplay = false,
extendedDisplayEnabled = false,
- tabletModeStatus = SwitchState.ON,
isDefaultDisplayDesktopEligible = false,
- hasAnyMouseDevice = true,
+ hasAnyTouchpadDevice = true,
+ hasAnyKeyboardDevice = false,
expectedWindowingMode = WINDOWING_MODE_FULLSCREEN,
),
- EXTERNAL_EXTENDED_CLAMSHELL_PROJECTED_MOUSE(
+ EXTERNAL_EXTENDED_NO_PROJECTED_NO_TOUCHPAD_NO_KEYBOARD(
hasExternalDisplay = true,
extendedDisplayEnabled = true,
- tabletModeStatus = SwitchState.OFF,
- isDefaultDisplayDesktopEligible = false,
- hasAnyMouseDevice = true,
- expectedWindowingMode = WINDOWING_MODE_FULLSCREEN,
+ isDefaultDisplayDesktopEligible = true,
+ hasAnyTouchpadDevice = false,
+ hasAnyKeyboardDevice = false,
+ expectedWindowingMode = WINDOWING_MODE_FREEFORM,
),
- NO_EXTERNAL_EXTENDED_CLAMSHELL_PROJECTED_MOUSE(
+ NO_EXTERNAL_EXTENDED_NO_PROJECTED_NO_TOUCHPAD_NO_KEYBOARD(
hasExternalDisplay = false,
extendedDisplayEnabled = true,
- tabletModeStatus = SwitchState.OFF,
- isDefaultDisplayDesktopEligible = false,
- hasAnyMouseDevice = true,
+ isDefaultDisplayDesktopEligible = true,
+ hasAnyTouchpadDevice = false,
+ hasAnyKeyboardDevice = false,
expectedWindowingMode = WINDOWING_MODE_FULLSCREEN,
),
- EXTERNAL_MIRROR_CLAMSHELL_PROJECTED_MOUSE(
+ EXTERNAL_MIRROR_NO_PROJECTED_NO_TOUCHPAD_NO_KEYBOARD(
hasExternalDisplay = true,
extendedDisplayEnabled = false,
- tabletModeStatus = SwitchState.OFF,
- isDefaultDisplayDesktopEligible = false,
- hasAnyMouseDevice = true,
+ isDefaultDisplayDesktopEligible = true,
+ hasAnyTouchpadDevice = false,
+ hasAnyKeyboardDevice = false,
expectedWindowingMode = WINDOWING_MODE_FULLSCREEN,
),
- NO_EXTERNAL_MIRROR_CLAMSHELL_PROJECTED_MOUSE(
+ NO_EXTERNAL_MIRROR_NO_PROJECTED_NO_TOUCHPAD_NO_KEYBOARD(
hasExternalDisplay = false,
extendedDisplayEnabled = false,
- tabletModeStatus = SwitchState.OFF,
- isDefaultDisplayDesktopEligible = false,
- hasAnyMouseDevice = true,
+ isDefaultDisplayDesktopEligible = true,
+ hasAnyTouchpadDevice = false,
+ hasAnyKeyboardDevice = false,
expectedWindowingMode = WINDOWING_MODE_FULLSCREEN,
),
- EXTERNAL_EXTENDED_UNKNOWN_PROJECTED_MOUSE(
+ EXTERNAL_EXTENDED_PROJECTED_NO_TOUCHPAD_NO_KEYBOARD(
hasExternalDisplay = true,
extendedDisplayEnabled = true,
- tabletModeStatus = SwitchState.UNKNOWN,
isDefaultDisplayDesktopEligible = false,
- hasAnyMouseDevice = true,
+ hasAnyTouchpadDevice = false,
+ hasAnyKeyboardDevice = false,
expectedWindowingMode = WINDOWING_MODE_FULLSCREEN,
),
- NO_EXTERNAL_EXTENDED_UNKNOWN_PROJECTED_MOUSE(
+ NO_EXTERNAL_EXTENDED_PROJECTED_NO_TOUCHPAD_NO_KEYBOARD(
hasExternalDisplay = false,
extendedDisplayEnabled = true,
- tabletModeStatus = SwitchState.UNKNOWN,
isDefaultDisplayDesktopEligible = false,
- hasAnyMouseDevice = true,
+ hasAnyTouchpadDevice = false,
+ hasAnyKeyboardDevice = false,
expectedWindowingMode = WINDOWING_MODE_FULLSCREEN,
),
- EXTERNAL_MIRROR_UNKNOWN_PROJECTED_MOUSE(
+ EXTERNAL_MIRROR_PROJECTED_NO_TOUCHPAD_NO_KEYBOARD(
hasExternalDisplay = true,
extendedDisplayEnabled = false,
- tabletModeStatus = SwitchState.UNKNOWN,
isDefaultDisplayDesktopEligible = false,
- hasAnyMouseDevice = true,
+ hasAnyTouchpadDevice = false,
+ hasAnyKeyboardDevice = false,
expectedWindowingMode = WINDOWING_MODE_FULLSCREEN,
),
- NO_EXTERNAL_MIRROR_UNKNOWN_PROJECTED_MOUSE(
+ NO_EXTERNAL_MIRROR_PROJECTED_NO_TOUCHPAD_NO_KEYBOARD(
hasExternalDisplay = false,
extendedDisplayEnabled = false,
- tabletModeStatus = SwitchState.UNKNOWN,
isDefaultDisplayDesktopEligible = false,
- hasAnyMouseDevice = true,
+ hasAnyTouchpadDevice = false,
+ hasAnyKeyboardDevice = false,
expectedWindowingMode = WINDOWING_MODE_FULLSCREEN,
),
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicatorTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicatorTest.kt
index 652fae01c1b2..a4052890f08a 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicatorTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicatorTest.kt
@@ -283,14 +283,32 @@ class DesktopModeVisualIndicatorTest : ShellTestCase() {
com.android.wm.shell.Flags.FLAG_ENABLE_BUBBLE_TO_FULLSCREEN,
com.android.wm.shell.Flags.FLAG_ENABLE_CREATE_ANY_BUBBLE,
)
- fun testDefaultIndicators_bubblesEnabled() {
+ fun testDefaultIndicators_enableBubbleToFullscreen() {
createVisualIndicator(DesktopModeVisualIndicator.DragStartState.FROM_FULLSCREEN)
var result = visualIndicator.updateIndicatorType(PointF(10f, 1500f))
assertThat(result)
.isEqualTo(DesktopModeVisualIndicator.IndicatorType.TO_BUBBLE_LEFT_INDICATOR)
- result = visualIndicator.updateIndicatorType(PointF(2300f, 1500f))
+ result = visualIndicator.updateIndicatorType(PointF(2390f, 1500f))
assertThat(result)
.isEqualTo(DesktopModeVisualIndicator.IndicatorType.TO_BUBBLE_RIGHT_INDICATOR)
+
+ // Check that bubble zones are not available from split
+ createVisualIndicator(DesktopModeVisualIndicator.DragStartState.FROM_SPLIT)
+ result = visualIndicator.updateIndicatorType(PointF(10f, 1500f))
+ assertThat(result)
+ .isEqualTo(DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_LEFT_INDICATOR)
+ result = visualIndicator.updateIndicatorType(PointF(2390f, 1500f))
+ assertThat(result)
+ .isEqualTo(DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_RIGHT_INDICATOR)
+
+ // Check that bubble zones are not available from desktop
+ createVisualIndicator(DesktopModeVisualIndicator.DragStartState.FROM_FREEFORM)
+ result = visualIndicator.updateIndicatorType(PointF(10f, 1500f))
+ assertThat(result)
+ .isEqualTo(DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_LEFT_INDICATOR)
+ result = visualIndicator.updateIndicatorType(PointF(2390f, 1500f))
+ assertThat(result)
+ .isEqualTo(DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_RIGHT_INDICATOR)
}
@Test
@@ -298,7 +316,7 @@ class DesktopModeVisualIndicatorTest : ShellTestCase() {
com.android.wm.shell.Flags.FLAG_ENABLE_BUBBLE_TO_FULLSCREEN,
com.android.wm.shell.Flags.FLAG_ENABLE_CREATE_ANY_BUBBLE,
)
- fun testDefaultIndicators_foldable_leftRightSplit() {
+ fun testDefaultIndicators_foldable_enableBubbleToFullscreen_dragFromFullscreen() {
setUpFoldable()
createVisualIndicator(
@@ -325,13 +343,47 @@ class DesktopModeVisualIndicatorTest : ShellTestCase() {
result = visualIndicator.updateIndicatorType(foldRightBottom())
assertThat(result)
.isEqualTo(DesktopModeVisualIndicator.IndicatorType.TO_BUBBLE_RIGHT_INDICATOR)
+ }
+
+ @Test
+ @EnableFlags(
+ com.android.wm.shell.Flags.FLAG_ENABLE_BUBBLE_TO_FULLSCREEN,
+ com.android.wm.shell.Flags.FLAG_ENABLE_CREATE_ANY_BUBBLE,
+ )
+ @DisableFlags(com.android.wm.shell.Flags.FLAG_ENABLE_BUBBLE_ANYTHING)
+ fun testDefaultIndicators_foldable_enableBubbleToFullscreen_dragFromSplit() {
+ setUpFoldable()
createVisualIndicator(
DesktopModeVisualIndicator.DragStartState.FROM_SPLIT,
isSmallTablet = true,
isLeftRightSplit = true,
)
- result = visualIndicator.updateIndicatorType(foldCenter())
+ var result = visualIndicator.updateIndicatorType(foldCenter())
+ assertThat(result)
+ .isEqualTo(DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR)
+
+ // Check that bubbles are not available from split
+ result = visualIndicator.updateIndicatorType(foldLeftBottom())
+ assertThat(result)
+ .isEqualTo(DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_LEFT_INDICATOR)
+
+ result = visualIndicator.updateIndicatorType(foldRightBottom())
+ assertThat(result)
+ .isEqualTo(DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_RIGHT_INDICATOR)
+ }
+
+ @Test
+ @EnableFlags(com.android.wm.shell.Flags.FLAG_ENABLE_BUBBLE_ANYTHING)
+ fun testDefaultIndicators_foldable_enableBubbleAnything_dragFromSplit() {
+ setUpFoldable()
+
+ createVisualIndicator(
+ DesktopModeVisualIndicator.DragStartState.FROM_SPLIT,
+ isSmallTablet = true,
+ isLeftRightSplit = true,
+ )
+ var result = visualIndicator.updateIndicatorType(foldCenter())
assertThat(result)
.isEqualTo(DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR)
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 bcdff11363ab..d81786b5e6a5 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
@@ -50,6 +50,7 @@ import android.os.Binder
import android.os.Bundle
import android.os.Handler
import android.os.IBinder
+import android.os.UserHandle
import android.os.UserManager
import android.platform.test.annotations.DisableFlags
import android.platform.test.annotations.EnableFlags
@@ -2380,9 +2381,10 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER)
@DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
fun moveToFullscreen_tdaFreeform_windowingModeFullscreen_removesWallpaperActivity() {
+ whenever(DesktopModeStatus.enterDesktopByDefaultOnFreeformDisplay(context))
+ .thenReturn(false)
val homeTask = setUpHomeTask()
val task = setUpFreeformTask()
-
assertNotNull(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY))
.configuration
.windowConfiguration
@@ -2434,9 +2436,10 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND,
)
fun moveToFullscreen_tdaFreeform_windowingModeFullscreen_homeBehindFullscreen_multiDesksEnabled() {
+ whenever(DesktopModeStatus.enterDesktopByDefaultOnFreeformDisplay(context))
+ .thenReturn(false)
val homeTask = setUpHomeTask()
val task = setUpFreeformTask()
-
assertNotNull(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY))
.configuration
.windowConfiguration
@@ -5535,7 +5538,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
controller.removeDesk(deskId = 2)
- verify(desksOrganizer).removeDesk(any(), eq(2))
+ verify(desksOrganizer).removeDesk(any(), eq(2), any())
verify(desksTransitionsObserver)
.addPendingTransition(
argThat {
@@ -5551,6 +5554,49 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION,
Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND,
)
+ fun removeDesk_multipleDesks_removesRunningTasks() {
+ val transition = Binder()
+ whenever(transitions.startTransition(eq(TRANSIT_CLOSE), any(), anyOrNull()))
+ .thenReturn(transition)
+ taskRepository.addDesk(DEFAULT_DISPLAY, deskId = 2)
+ val task1 = setUpFreeformTask(deskId = 2)
+ val task2 = setUpFreeformTask(deskId = 2)
+ val task3 = setUpFreeformTask(deskId = 2)
+
+ controller.removeDesk(deskId = 2)
+
+ val wct = getLatestWct(TRANSIT_CLOSE)
+ wct.assertRemove(task1.token)
+ wct.assertRemove(task2.token)
+ wct.assertRemove(task3.token)
+ }
+
+ @Test
+ @EnableFlags(
+ Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION,
+ Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND,
+ )
+ fun removeDesk_multipleDesks_removesRecentTasks() {
+ val transition = Binder()
+ whenever(transitions.startTransition(eq(TRANSIT_CLOSE), any(), anyOrNull()))
+ .thenReturn(transition)
+ taskRepository.addDesk(DEFAULT_DISPLAY, deskId = 2)
+ val task1 = setUpFreeformTask(deskId = 2, background = true)
+ val task2 = setUpFreeformTask(deskId = 2, background = true)
+ val task3 = setUpFreeformTask(deskId = 2, background = true)
+
+ controller.removeDesk(deskId = 2)
+
+ verify(recentTasksController).removeBackgroundTask(task1.taskId)
+ verify(recentTasksController).removeBackgroundTask(task2.taskId)
+ verify(recentTasksController).removeBackgroundTask(task3.taskId)
+ }
+
+ @Test
+ @EnableFlags(
+ Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION,
+ Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND,
+ )
fun activateDesk_multipleDesks_addsPendingTransition() {
val deskId = 0
val transition = Binder()
@@ -7507,11 +7553,12 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
@EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
fun testCreateDesk() {
val currentDeskCount = taskRepository.getNumberOfDesks(DEFAULT_DISPLAY)
- whenever(desksOrganizer.createDesk(eq(DEFAULT_DISPLAY), any())).thenAnswer { invocation ->
- (invocation.arguments[1] as DesksOrganizer.OnCreateCallback).onCreated(deskId = 5)
+ whenever(desksOrganizer.createDesk(eq(DEFAULT_DISPLAY), any(), any())).thenAnswer {
+ invocation ->
+ (invocation.arguments[2] as DesksOrganizer.OnCreateCallback).onCreated(deskId = 5)
}
- controller.createDesk(DEFAULT_DISPLAY)
+ controller.createDesk(DEFAULT_DISPLAY, taskRepository.userId)
assertThat(taskRepository.getNumberOfDesks(DEFAULT_DISPLAY)).isEqualTo(currentDeskCount + 1)
}
@@ -7521,7 +7568,17 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
fun testCreateDesk_invalidDisplay_dropsRequest() {
controller.createDesk(INVALID_DISPLAY)
- verify(desksOrganizer, never()).createDesk(any(), any())
+ verify(desksOrganizer, never()).createDesk(any(), any(), any())
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun testCreateDesk_systemUser_dropsRequest() {
+ assumeTrue(UserManager.isHeadlessSystemUserMode())
+
+ controller.createDesk(DEFAULT_DISPLAY, UserHandle.USER_SYSTEM)
+
+ verify(desksOrganizer, never()).createDesk(any(), any(), any())
}
@Test
@@ -8254,6 +8311,15 @@ private fun WindowContainerTransaction.assertReorderSequenceInRange(
.inOrder()
}
+private fun WindowContainerTransaction.assertRemove(token: WindowContainerToken) {
+ assertThat(
+ hierarchyOps.any { hop ->
+ hop.container == token.asBinder() && hop.type == HIERARCHY_OP_TYPE_REMOVE_TASK
+ }
+ )
+ .isTrue()
+}
+
private fun WindowContainerTransaction.assertRemoveAt(index: Int, token: WindowContainerToken) {
assertIndexInBounds(index)
val op = hierarchyOps[index]
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizerTest.kt
index e57fc38e3607..34f832b4fba4 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizerTest.kt
@@ -19,7 +19,7 @@ import android.app.ActivityManager
import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED
import android.testing.AndroidTestingRunner
-import android.view.Display
+import android.view.Display.DEFAULT_DISPLAY
import android.view.SurfaceControl
import android.view.WindowManager.TRANSIT_TO_FRONT
import android.window.TransitionInfo
@@ -41,9 +41,9 @@ import com.android.wm.shell.desktopmode.multidesks.RootTaskDesksOrganizer.DeskRo
import com.android.wm.shell.sysui.ShellCommandHandler
import com.android.wm.shell.sysui.ShellInit
import com.google.common.truth.Truth.assertThat
+import kotlin.coroutines.suspendCoroutine
import kotlin.test.assertNotNull
import kotlinx.coroutines.test.runTest
-import org.junit.Assert.assertEquals
import org.junit.Assert.assertThrows
import org.junit.Before
import org.junit.Test
@@ -86,11 +86,21 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
organizer.setOnDesktopTaskInfoChangedListener(taskInfoChangedListener)
}
- @Test fun testCreateDesk_createsDeskAndMinimizationRoots() = runTest { createDesk() }
+ @Test fun testCreateDesk_createsDeskAndMinimizationRoots() = runTest { createDeskSuspending() }
+
+ @Test
+ fun testCreateDesk_rootExistsForOtherUser_reusesRoot() = runTest {
+ val desk = createDeskSuspending(userId = PRIMARY_USER_ID)
+
+ val deskId =
+ organizer.createDeskSuspending(displayId = DEFAULT_DISPLAY, userId = SECONDARY_USER_ID)
+
+ assertThat(deskId).isEqualTo(desk.deskRoot.deskId)
+ }
@Test
fun testCreateMinimizationRoot_marksHidden() = runTest {
- val desk = createDesk()
+ val desk = createDeskSuspending()
verify(mockShellTaskOrganizer)
.applyTransaction(
@@ -115,7 +125,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
@Test
fun testOnTaskAppeared_duplicateRoot_throws() = runTest {
- val desk = createDesk()
+ val desk = createDeskSuspending()
assertThrows(Exception::class.java) {
organizer.onTaskAppeared(desk.deskRoot.taskInfo, SurfaceControl())
@@ -124,7 +134,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
@Test
fun testOnTaskAppeared_duplicateMinimizedRoot_throws() = runTest {
- val desk = createDesk()
+ val desk = createDeskSuspending()
assertThrows(Exception::class.java) {
organizer.onTaskAppeared(desk.minimizationRoot.taskInfo, SurfaceControl())
@@ -133,7 +143,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
@Test
fun testOnTaskVanished_removesRoot() = runTest {
- val desk = createDesk()
+ val desk = createDeskSuspending()
organizer.onTaskVanished(desk.deskRoot.taskInfo)
@@ -142,7 +152,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
@Test
fun testOnTaskVanished_removesMinimizedRoot() = runTest {
- val desk = createDesk()
+ val desk = createDeskSuspending()
organizer.onTaskVanished(desk.deskRoot.taskInfo)
organizer.onTaskVanished(desk.minimizationRoot.taskInfo)
@@ -152,7 +162,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
@Test
fun testDesktopWindowAppearsInDesk() = runTest {
- val desk = createDesk()
+ val desk = createDeskSuspending()
val child = createFreeformTask().apply { parentTaskId = desk.deskRoot.deskId }
organizer.onTaskAppeared(child, SurfaceControl())
@@ -162,7 +172,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
@Test
fun testDesktopWindowAppearsInDeskMinimizationRoot() = runTest {
- val desk = createDesk()
+ val desk = createDeskSuspending()
val child = createFreeformTask().apply { parentTaskId = desk.minimizationRoot.rootId }
organizer.onTaskAppeared(child, SurfaceControl())
@@ -172,7 +182,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
@Test
fun testDesktopWindowMovesToMinimizationRoot() = runTest {
- val desk = createDesk()
+ val desk = createDeskSuspending()
val child = createFreeformTask().apply { parentTaskId = desk.deskRoot.deskId }
organizer.onTaskAppeared(child, SurfaceControl())
@@ -185,7 +195,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
@Test
fun testDesktopWindowDisappearsFromDesk() = runTest {
- val desk = createDesk()
+ val desk = createDeskSuspending()
val child = createFreeformTask().apply { parentTaskId = desk.deskRoot.deskId }
organizer.onTaskAppeared(child, SurfaceControl())
@@ -196,7 +206,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
@Test
fun testDesktopWindowDisappearsFromDeskMinimizationRoot() = runTest {
- val desk = createDesk()
+ val desk = createDeskSuspending()
val child = createFreeformTask().apply { parentTaskId = desk.minimizationRoot.rootId }
organizer.onTaskAppeared(child, SurfaceControl())
@@ -206,11 +216,23 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
}
@Test
+ fun testRemoveDesk_disablesAsLaunchRoot() = runTest {
+ val desk = createDeskSuspending(userId = PRIMARY_USER_ID)
+ val wct = WindowContainerTransaction()
+ organizer.activateDesk(wct, desk.deskRoot.deskId)
+ assertThat(desk.deskRoot.isLaunchRootRequested).isTrue()
+
+ organizer.removeDesk(wct, desk.deskRoot.deskId, userId = PRIMARY_USER_ID)
+
+ assertThat(desk.deskRoot.isLaunchRootRequested).isFalse()
+ }
+
+ @Test
fun testRemoveDesk_removesDeskRoot() = runTest {
- val desk = createDesk()
+ val desk = createDeskSuspending(userId = PRIMARY_USER_ID)
val wct = WindowContainerTransaction()
- organizer.removeDesk(wct, desk.deskRoot.deskId)
+ organizer.removeDesk(wct, desk.deskRoot.deskId, userId = PRIMARY_USER_ID)
assertThat(
wct.hierarchyOps.any { hop ->
@@ -223,10 +245,10 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
@Test
fun testRemoveDesk_removesMinimizationRoot() = runTest {
- val desk = createDesk()
+ val desk = createDeskSuspending(userId = PRIMARY_USER_ID)
val wct = WindowContainerTransaction()
- organizer.removeDesk(wct, desk.deskRoot.deskId)
+ organizer.removeDesk(wct, desk.deskRoot.deskId, userId = PRIMARY_USER_ID)
assertThat(
wct.hierarchyOps.any { hop ->
@@ -238,8 +260,27 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
}
@Test
+ fun testRemoveDesk_rootUsedByOtherUser_keepsDeskRoot() = runTest {
+ val primaryUserDesk = createDeskSuspending(userId = PRIMARY_USER_ID)
+ val secondaryUserDesk = createDeskSuspending(userId = SECONDARY_USER_ID)
+ assertThat(primaryUserDesk).isEqualTo(secondaryUserDesk)
+
+ val wct = WindowContainerTransaction()
+ organizer.removeDesk(wct, primaryUserDesk.deskRoot.deskId, userId = PRIMARY_USER_ID)
+
+ assertThat(
+ wct.hierarchyOps.any { hop ->
+ hop.type == HierarchyOp.HIERARCHY_OP_TYPE_REMOVE_ROOT_TASK &&
+ hop.container == primaryUserDesk.deskRoot.token.asBinder()
+ }
+ )
+ .isFalse()
+ assertThat(primaryUserDesk.deskRoot.users).containsExactly(SECONDARY_USER_ID)
+ }
+
+ @Test
fun testActivateDesk() = runTest {
- val desk = createDesk()
+ val desk = createDeskSuspending()
val wct = WindowContainerTransaction()
organizer.activateDesk(wct, desk.deskRoot.deskId)
@@ -271,7 +312,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
@Test
fun testMoveTaskToDesk() = runTest {
- val desk = createDesk()
+ val desk = createDeskSuspending()
val desktopTask = createFreeformTask().apply { parentTaskId = -1 }
val wct = WindowContainerTransaction()
@@ -308,7 +349,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
@Test
fun testGetDeskAtEnd() = runTest {
- val desk = createDesk()
+ val desk = createDeskSuspending()
val task = createFreeformTask().apply { parentTaskId = desk.deskRoot.deskId }
val endDesk =
@@ -321,7 +362,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
@Test
fun testGetDeskAtEnd_inMinimizationRoot() = runTest {
- val desk = createDesk()
+ val desk = createDeskSuspending()
val task = createFreeformTask().apply { parentTaskId = desk.minimizationRoot.rootId }
val endDesk =
@@ -334,7 +375,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
@Test
fun testIsDeskActiveAtEnd() = runTest {
- val desk = createDesk()
+ val desk = createDeskSuspending()
val isActive =
organizer.isDeskActiveAtEnd(
@@ -352,7 +393,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
@Test
fun deactivateDesk_clearsLaunchRoot() = runTest {
val wct = WindowContainerTransaction()
- val desk = createDesk()
+ val desk = createDeskSuspending()
organizer.activateDesk(wct, desk.deskRoot.deskId)
organizer.deactivateDesk(wct, desk.deskRoot.deskId)
@@ -370,7 +411,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
@Test
fun isDeskChange_forDeskId() = runTest {
- val desk = createDesk()
+ val desk = createDeskSuspending()
assertThat(
organizer.isDeskChange(
@@ -385,7 +426,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
@Test
fun isDeskChange_forDeskId_inMinimizationRoot() = runTest {
- val desk = createDesk()
+ val desk = createDeskSuspending()
assertThat(
organizer.isDeskChange(
@@ -403,7 +444,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
@Test
fun isDeskChange_anyDesk() = runTest {
- val desk = createDesk()
+ val desk = createDeskSuspending()
assertThat(
organizer.isDeskChange(
@@ -417,7 +458,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
@Test
fun isDeskChange_anyDesk_inMinimizationRoot() = runTest {
- val desk = createDesk()
+ val desk = createDeskSuspending()
assertThat(
organizer.isDeskChange(
@@ -434,7 +475,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
@Test
fun minimizeTask() = runTest {
- val desk = createDesk()
+ val desk = createDeskSuspending()
val task = createFreeformTask().apply { parentTaskId = desk.deskRoot.deskId }
val wct = WindowContainerTransaction()
organizer.moveTaskToDesk(wct, desk.deskRoot.deskId, task)
@@ -447,7 +488,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
@Test
fun minimizeTask_alreadyMinimized_noOp() = runTest {
- val desk = createDesk()
+ val desk = createDeskSuspending()
val task = createFreeformTask().apply { parentTaskId = desk.minimizationRoot.rootId }
val wct = WindowContainerTransaction()
organizer.onTaskAppeared(task, SurfaceControl())
@@ -459,8 +500,8 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
@Test
fun minimizeTask_inDifferentDesk_noOp() = runTest {
- val desk = createDesk()
- val otherDesk = createDesk()
+ val desk = createDeskSuspending()
+ val otherDesk = createDeskSuspending()
val task = createFreeformTask().apply { parentTaskId = otherDesk.deskRoot.deskId }
val wct = WindowContainerTransaction()
organizer.onTaskAppeared(task, SurfaceControl())
@@ -472,7 +513,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
@Test
fun unminimizeTask() = runTest {
- val desk = createDesk()
+ val desk = createDeskSuspending()
val task = createFreeformTask().apply { parentTaskId = desk.deskRoot.deskId }
val wct = WindowContainerTransaction()
organizer.moveTaskToDesk(wct, desk.deskRoot.deskId, task)
@@ -489,7 +530,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
@Test
fun unminimizeTask_alreadyUnminimized_noOp() = runTest {
- val desk = createDesk()
+ val desk = createDeskSuspending()
val task = createFreeformTask().apply { parentTaskId = desk.deskRoot.deskId }
val wct = WindowContainerTransaction()
organizer.moveTaskToDesk(wct, desk.deskRoot.deskId, task)
@@ -503,7 +544,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
@Test
fun unminimizeTask_notInDesk_noOp() = runTest {
- val desk = createDesk()
+ val desk = createDeskSuspending()
val task = createFreeformTask()
val wct = WindowContainerTransaction()
@@ -514,7 +555,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
@Test
fun reorderTaskToFront() = runTest {
- val desk = createDesk()
+ val desk = createDeskSuspending()
val task = createFreeformTask().apply { parentTaskId = desk.deskRoot.deskId }
val wct = WindowContainerTransaction()
organizer.onTaskAppeared(task, SurfaceControl())
@@ -534,7 +575,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
@Test
fun reorderTaskToFront_notInDesk_noOp() = runTest {
- val desk = createDesk()
+ val desk = createDeskSuspending()
val task = createFreeformTask()
val wct = WindowContainerTransaction()
@@ -553,7 +594,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
@Test
fun reorderTaskToFront_minimized_unminimizesAndReorders() = runTest {
- val desk = createDesk()
+ val desk = createDeskSuspending()
val task = createFreeformTask().apply { parentTaskId = desk.deskRoot.deskId }
val wct = WindowContainerTransaction()
organizer.onTaskAppeared(task, SurfaceControl())
@@ -578,7 +619,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
fun onTaskAppeared_visibleDesk_onlyDesk_disablesLaunchAdjacent() = runTest {
launchAdjacentController.launchAdjacentEnabled = true
- createDesk(visible = true)
+ createDeskSuspending(visible = true)
assertThat(launchAdjacentController.launchAdjacentEnabled).isFalse()
}
@@ -587,7 +628,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
fun onTaskAppeared_invisibleDesk_onlyDesk_enablesLaunchAdjacent() = runTest {
launchAdjacentController.launchAdjacentEnabled = false
- createDesk(visible = false)
+ createDeskSuspending(visible = false)
assertThat(launchAdjacentController.launchAdjacentEnabled).isTrue()
}
@@ -596,8 +637,8 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
fun onTaskAppeared_invisibleDesk_otherVisibleDesk_disablesLaunchAdjacent() = runTest {
launchAdjacentController.launchAdjacentEnabled = true
- createDesk(visible = true)
- createDesk(visible = false)
+ createDeskSuspending(visible = true)
+ createDeskSuspending(visible = false)
assertThat(launchAdjacentController.launchAdjacentEnabled).isFalse()
}
@@ -606,7 +647,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
fun onTaskInfoChanged_deskBecomesVisible_onlyDesk_disablesLaunchAdjacent() = runTest {
launchAdjacentController.launchAdjacentEnabled = true
- val desk = createDesk(visible = false)
+ val desk = createDeskSuspending(visible = false)
desk.deskRoot.taskInfo.isVisible = true
organizer.onTaskInfoChanged(desk.deskRoot.taskInfo)
@@ -617,7 +658,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
fun onTaskInfoChanged_deskBecomesInvisible_onlyDesk_enablesLaunchAdjacent() = runTest {
launchAdjacentController.launchAdjacentEnabled = false
- val desk = createDesk(visible = true)
+ val desk = createDeskSuspending(visible = true)
desk.deskRoot.taskInfo.isVisible = false
organizer.onTaskInfoChanged(desk.deskRoot.taskInfo)
@@ -628,8 +669,8 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
fun onTaskInfoChanged_deskBecomesInvisible_otherVisibleDesk_disablesLaunchAdjacent() = runTest {
launchAdjacentController.launchAdjacentEnabled = true
- createDesk(visible = true)
- val desk = createDesk(visible = true)
+ createDeskSuspending(visible = true)
+ val desk = createDeskSuspending(visible = true)
desk.deskRoot.taskInfo.isVisible = false
organizer.onTaskInfoChanged(desk.deskRoot.taskInfo)
@@ -640,7 +681,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
fun onTaskVanished_visibleDeskDisappears_onlyDesk_enablesLaunchAdjacent() = runTest {
launchAdjacentController.launchAdjacentEnabled = false
- val desk = createDesk(visible = true)
+ val desk = createDeskSuspending(visible = true)
organizer.onTaskVanished(desk.deskRoot.taskInfo)
assertThat(launchAdjacentController.launchAdjacentEnabled).isTrue()
@@ -650,8 +691,8 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
fun onTaskVanished_visibleDeskDisappears_otherDeskVisible_disablesLaunchAdjacent() = runTest {
launchAdjacentController.launchAdjacentEnabled = true
- createDesk(visible = true)
- val desk = createDesk(visible = true)
+ createDeskSuspending(visible = true)
+ val desk = createDeskSuspending(visible = true)
organizer.onTaskVanished(desk.deskRoot.taskInfo)
assertThat(launchAdjacentController.launchAdjacentEnabled).isFalse()
@@ -659,7 +700,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
@Test
fun onTaskInfoChanged_taskNotRoot_invokesListener() = runTest {
- createDesk()
+ createDeskSuspending()
val task = createFreeformTask().apply { taskId = TEST_CHILD_TASK_ID }
organizer.onTaskInfoChanged(task)
@@ -669,7 +710,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
@Test
fun onTaskInfoChanged_isDeskRoot_doesNotInvokeListener() = runTest {
- val deskRoot = createDesk().deskRoot
+ val deskRoot = createDeskSuspending().deskRoot
organizer.onTaskInfoChanged(deskRoot.taskInfo)
@@ -678,7 +719,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
@Test
fun onTaskInfoChanged_isMinimizationRoot_doesNotInvokeListener() = runTest {
- val minimizationRoot = createDesk().minimizationRoot
+ val minimizationRoot = createDeskSuspending().minimizationRoot
organizer.onTaskInfoChanged(minimizationRoot.taskInfo)
@@ -690,7 +731,10 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
val minimizationRoot: DeskMinimizationRoot,
)
- private suspend fun createDesk(visible: Boolean = true): DeskRoots {
+ private suspend fun createDeskSuspending(
+ visible: Boolean = true,
+ userId: Int = PRIMARY_USER_ID,
+ ): DeskRoots {
val freeformRootTask =
createFreeformTask().apply {
parentTaskId = -1
@@ -701,7 +745,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
Mockito.reset(mockShellTaskOrganizer)
whenever(
mockShellTaskOrganizer.createRootTask(
- Display.DEFAULT_DISPLAY,
+ DEFAULT_DISPLAY,
WINDOWING_MODE_FREEFORM,
organizer,
true,
@@ -715,13 +759,9 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
val listener = (invocation.arguments[2] as TaskListener)
listener.onTaskAppeared(minimizationRootTask, SurfaceControl())
}
- val deskId = organizer.createDesk(Display.DEFAULT_DISPLAY)
- assertEquals(freeformRootTask.taskId, deskId)
- val deskRoot = assertNotNull(organizer.deskRootsByDeskId.get(freeformRootTask.taskId))
- val minimizationRoot =
- assertNotNull(organizer.deskMinimizationRootsByDeskId[freeformRootTask.taskId])
- assertThat(minimizationRoot.deskId).isEqualTo(freeformRootTask.taskId)
- assertThat(minimizationRoot.rootId).isEqualTo(minimizationRootTask.taskId)
+ val deskId = organizer.createDeskSuspending(DEFAULT_DISPLAY, userId)
+ val deskRoot = assertNotNull(organizer.deskRootsByDeskId.get(deskId))
+ val minimizationRoot = assertNotNull(organizer.deskMinimizationRootsByDeskId[deskId])
return DeskRoots(deskRoot, minimizationRoot)
}
@@ -746,7 +786,14 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
hop.toTop
}
+ private suspend fun DesksOrganizer.createDeskSuspending(displayId: Int, userId: Int): Int =
+ suspendCoroutine { cont ->
+ createDesk(displayId, userId) { deskId -> cont.resumeWith(Result.success(deskId)) }
+ }
+
companion object {
+ private const val PRIMARY_USER_ID = 10
+ private const val SECONDARY_USER_ID = 11
private const val TEST_CHILD_TASK_ID = 100
}
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java
index 82373ff1bc41..64bd86134d92 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java
@@ -167,6 +167,7 @@ public class StageCoordinatorTests extends ShellTestCase {
private final TestShellExecutor mMainExecutor = new TestShellExecutor();
private final ShellExecutor mAnimExecutor = new TestShellExecutor();
private final Handler mMainHandler = new Handler(Looper.getMainLooper());
+ private final Handler mAnimHandler = mock(Handler.class);
private final DisplayAreaInfo mDisplayAreaInfo = new DisplayAreaInfo(new MockToken().token(),
DEFAULT_DISPLAY, 0);
private final ActivityManager.RunningTaskInfo mMainChildTaskInfo =
@@ -629,7 +630,7 @@ public class StageCoordinatorTests extends ShellTestCase {
ShellInit shellInit = new ShellInit(mMainExecutor);
final Transitions t = new Transitions(mContext, shellInit, mock(ShellController.class),
mTaskOrganizer, mTransactionPool, mock(DisplayController.class),
- mDisplayInsetsController, mMainExecutor, mMainHandler, mAnimExecutor,
+ mDisplayInsetsController, mMainExecutor, mMainHandler, mAnimExecutor, mAnimHandler,
mock(HomeTransitionObserver.class), mock(FocusTransitionObserver.class));
shellInit.init();
return t;
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/DefaultTransitionHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/DefaultTransitionHandlerTest.java
index 6996d44af034..2dab39184247 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/DefaultTransitionHandlerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/DefaultTransitionHandlerTest.java
@@ -100,7 +100,8 @@ public class DefaultTransitionHandlerTest extends ShellTestCase {
mTransitionHandler = new DefaultTransitionHandler(
mContext, mShellInit, mDisplayController, mDisplayInsetsController,
mTransactionPool, mMainExecutor, mMainHandler, mAnimExecutor,
- mRootTaskDisplayAreaOrganizer, mock(InteractionJankMonitor.class));
+ mock(Handler.class), mRootTaskDisplayAreaOrganizer,
+ mock(InteractionJankMonitor.class));
mShellInit.init();
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/HomeTransitionObserverTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/HomeTransitionObserverTest.java
index 52634c08dafd..5d77766dc0db 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/HomeTransitionObserverTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/HomeTransitionObserverTest.java
@@ -88,6 +88,7 @@ public class HomeTransitionObserverTest extends ShellTestCase {
private final ShellExecutor mAnimExecutor = new TestShellExecutor();
private final TestShellExecutor mMainExecutor = new TestShellExecutor();
private final Handler mMainHandler = new Handler(Looper.getMainLooper());
+ private final Handler mAnimHandler = mock(Handler.class);
private final DisplayController mDisplayController = mock(DisplayController.class);
private final DisplayInsetsController mDisplayInsetsController =
mock(DisplayInsetsController.class);
@@ -105,7 +106,7 @@ public class HomeTransitionObserverTest extends ShellTestCase {
mDisplayInsetsController, mock(ShellInit.class));
mTransition = new Transitions(mContext, mock(ShellInit.class), mock(ShellController.class),
mOrganizer, mTransactionPool, mDisplayController, mDisplayInsetsController,
- mMainExecutor, mMainHandler, mAnimExecutor, mHomeTransitionObserver,
+ mMainExecutor, mMainHandler, mAnimExecutor, mAnimHandler, mHomeTransitionObserver,
mock(FocusTransitionObserver.class));
mHomeTransitionObserver.setHomeTransitionListener(mTransition, mListener);
}
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 44bb2154f170..4dd9cab1d340 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
@@ -146,6 +146,7 @@ public class ShellTransitionTests extends ShellTestCase {
private final ShellExecutor mAnimExecutor = new TestShellExecutor();
private final TestTransitionHandler mDefaultHandler = new TestTransitionHandler();
private final Handler mMainHandler = new Handler(Looper.getMainLooper());
+ private final Handler mAnimHandler = mock(Handler.class);
private final DisplayInsetsController mDisplayInsets =
mock(DisplayInsetsController.class);
@@ -160,7 +161,7 @@ public class ShellTransitionTests extends ShellTestCase {
ShellInit shellInit = mock(ShellInit.class);
final Transitions t = new Transitions(mContext, shellInit, mock(ShellController.class),
mOrganizer, mTransactionPool, createTestDisplayController(), mDisplayInsets,
- mMainExecutor, mMainHandler, mAnimExecutor,
+ mMainExecutor, mMainHandler, mAnimExecutor, mAnimHandler,
mock(HomeTransitionObserver.class), mock(FocusTransitionObserver.class));
// One from Transitions, one from RootTaskDisplayAreaOrganizer
verify(shellInit).addInitCallback(any(), eq(t));
@@ -173,7 +174,7 @@ public class ShellTransitionTests extends ShellTestCase {
ShellController shellController = mock(ShellController.class);
final Transitions t = new Transitions(mContext, shellInit, shellController,
mOrganizer, mTransactionPool, createTestDisplayController(), mDisplayInsets,
- mMainExecutor, mMainHandler, mAnimExecutor,
+ mMainExecutor, mMainHandler, mAnimExecutor, mAnimHandler,
mock(HomeTransitionObserver.class), mock(FocusTransitionObserver.class));
shellInit.init();
verify(shellController, times(1)).addExternalInterface(
@@ -1318,7 +1319,7 @@ public class ShellTransitionTests extends ShellTestCase {
final Transitions transitions =
new Transitions(mContext, shellInit, mock(ShellController.class), mOrganizer,
mTransactionPool, createTestDisplayController(), mDisplayInsets,
- mMainExecutor, mMainHandler, mAnimExecutor,
+ mMainExecutor, mMainHandler, mAnimExecutor, mAnimHandler,
mock(HomeTransitionObserver.class),
mock(FocusTransitionObserver.class));
final RecentTasksController mockRecentsTaskController = mock(RecentTasksController.class);
@@ -1914,7 +1915,8 @@ public class ShellTransitionTests extends ShellTestCase {
ShellInit shellInit = new ShellInit(mMainExecutor);
final Transitions t = new Transitions(mContext, shellInit, mock(ShellController.class),
mOrganizer, mTransactionPool, createTestDisplayController(), mDisplayInsets,
- mMainExecutor, mMainHandler, mAnimExecutor, mock(HomeTransitionObserver.class),
+ mMainExecutor, mMainHandler, mAnimExecutor, mAnimHandler,
+ mock(HomeTransitionObserver.class),
mock(FocusTransitionObserver.class));
shellInit.init();
return t;
diff --git a/libs/androidfw/AssetManager2.cpp b/libs/androidfw/AssetManager2.cpp
index e09ab5fd1643..508750720690 100644
--- a/libs/androidfw/AssetManager2.cpp
+++ b/libs/androidfw/AssetManager2.cpp
@@ -82,6 +82,9 @@ struct FindEntryResult {
// The bitmask of configuration axis with which the resource value varies.
uint32_t type_flags;
+ // The bitmask of ResTable_entry flags
+ uint16_t entry_flags;
+
// The dynamic package ID map for the package from which this resource came from.
const DynamicRefTable* dynamic_ref_table;
@@ -600,12 +603,12 @@ base::expected<std::set<ResTable_config>, IOError> AssetManager2::GetResourceCon
return configurations;
}
-std::set<std::string> AssetManager2::GetResourceLocales(bool exclude_system,
- bool merge_equivalent_languages) const {
+LoadedPackage::Locales AssetManager2::GetResourceLocales(
+ bool exclude_system, bool merge_equivalent_languages) const {
ATRACE_NAME("AssetManager::GetResourceLocales");
auto op = StartOperation();
- std::set<std::string> locales;
+ LoadedPackage::Locales locales;
const auto non_system_overlays =
exclude_system ? GetNonSystemOverlays() : std::set<ApkAssetsPtr>();
@@ -619,8 +622,7 @@ std::set<std::string> AssetManager2::GetResourceLocales(bool exclude_system,
if (!non_system_overlays.empty()) {
// Exclude overlays that target only system resources.
const auto& apk_assets = GetApkAssets(package_group.cookies_[i]);
- if (apk_assets && apk_assets->IsOverlay() &&
- non_system_overlays.find(apk_assets) == non_system_overlays.end()) {
+ if (apk_assets && apk_assets->IsOverlay() && !non_system_overlays.contains(apk_assets)) {
continue;
}
}
@@ -1031,6 +1033,7 @@ base::expected<FindEntryResult, NullOrIOError> AssetManager2::FindEntryInternal(
.entry = *entry,
.config = *best_config,
.type_flags = type_flags,
+ .entry_flags = (*best_entry_verified)->flags(),
.dynamic_ref_table = package_group.dynamic_ref_table.get(),
.package_name = &best_package->GetPackageName(),
.type_string_ref = StringPoolRef(best_package->GetTypeStringPool(), best_type->id - 1),
@@ -1185,16 +1188,16 @@ base::expected<AssetManager2::SelectedValue, NullOrIOError> AssetManager2::GetRe
}
// Create a reference since we can't represent this complex type as a Res_value.
- return SelectedValue(Res_value::TYPE_REFERENCE, resid, result->cookie, result->type_flags,
- resid, result->config);
+ return SelectedValue(Res_value::TYPE_REFERENCE, resid, result->cookie, result->entry_flags,
+ result->type_flags, resid, result->config);
}
// Convert the package ID to the runtime assigned package ID.
Res_value value = std::get<Res_value>(result->entry);
result->dynamic_ref_table->lookupResourceValue(&value);
- return SelectedValue(value.dataType, value.data, result->cookie, result->type_flags,
- resid, result->config);
+ return SelectedValue(value.dataType, value.data, result->cookie, result->entry_flags,
+ result->type_flags, resid, result->config);
}
base::expected<std::monostate, NullOrIOError> AssetManager2::ResolveReference(
@@ -1847,8 +1850,8 @@ std::optional<AssetManager2::SelectedValue> Theme::GetAttribute(uint32_t resid)
}
return AssetManager2::SelectedValue(entry_it->value.dataType, entry_it->value.data,
- entry_it->cookie, type_spec_flags, 0U /* resid */,
- {} /* config */);
+ entry_it->cookie, 0U /* entry flags*/, type_spec_flags,
+ 0U /* resid */, {} /* config */);
}
return std::nullopt;
}
diff --git a/libs/androidfw/LoadedArsc.cpp b/libs/androidfw/LoadedArsc.cpp
index d9166a16cdea..7cebb6d79502 100644
--- a/libs/androidfw/LoadedArsc.cpp
+++ b/libs/androidfw/LoadedArsc.cpp
@@ -361,14 +361,18 @@ base::expected<std::monostate, IOError> LoadedPackage::CollectConfigurations(
return {};
}
-void LoadedPackage::CollectLocales(bool canonicalize, std::set<std::string>* out_locales) const {
- char temp_locale[RESTABLE_MAX_LOCALE_LEN];
+void LoadedPackage::CollectLocales(bool canonicalize,
+ Locales* out_locales) const {
for (const auto& type_spec : type_specs_) {
for (const auto& type_entry : type_spec.second.type_entries) {
if (type_entry.config.locale != 0) {
+ char temp_locale[RESTABLE_MAX_LOCALE_LEN];
type_entry.config.getBcp47Locale(temp_locale, canonicalize);
- std::string locale(temp_locale);
- out_locales->insert(std::move(locale));
+ auto locale_sv = std::string_view(temp_locale);
+ if (auto it = out_locales->lower_bound(locale_sv);
+ it == out_locales->end() || *it != locale_sv) {
+ out_locales->emplace_hint(it, locale_sv);
+ }
}
}
}
diff --git a/libs/androidfw/include/androidfw/AssetManager2.h b/libs/androidfw/include/androidfw/AssetManager2.h
index b0179524f6cd..e312042cf7da 100644
--- a/libs/androidfw/include/androidfw/AssetManager2.h
+++ b/libs/androidfw/include/androidfw/AssetManager2.h
@@ -189,8 +189,8 @@ class AssetManager2 {
// ('android' package, other libraries) will be excluded from the list.
// If `merge_equivalent_languages` is set to true, resource locales will be canonicalized
// and de-duped in the resulting list.
- std::set<std::string> GetResourceLocales(bool exclude_system = false,
- bool merge_equivalent_languages = false) const;
+ LoadedPackage::Locales GetResourceLocales(
+ bool exclude_system = false, bool merge_equivalent_languages = false) const;
// Searches the set of APKs loaded by this AssetManager and opens the first one found located
// in the assets/ directory.
@@ -257,6 +257,7 @@ class AssetManager2 {
: cookie(entry.cookie),
data(entry.value.data),
type(entry.value.dataType),
+ entry_flags(0U),
flags(bag->type_spec_flags),
resid(0U),
config() {
@@ -271,6 +272,9 @@ class AssetManager2 {
// Type of the data value.
uint8_t type;
+ // The bitmask of ResTable_entry flags
+ uint16_t entry_flags;
+
// The bitmask of configuration axis that this resource varies with.
// See ResTable_config::CONFIG_*.
uint32_t flags;
@@ -283,9 +287,10 @@ class AssetManager2 {
private:
SelectedValue(uint8_t value_type, Res_value::data_type value_data, ApkAssetsCookie cookie,
- uint32_t type_flags, uint32_t resid, ResTable_config config) :
- cookie(cookie), data(value_data), type(value_type), flags(type_flags),
- resid(resid), config(std::move(config)) {}
+ uint16_t entry_flags, uint32_t type_flags, uint32_t resid, ResTable_config config)
+ :
+ cookie(cookie), data(value_data), type(value_type), entry_flags(entry_flags),
+ flags(type_flags), resid(resid), config(std::move(config)) {}
};
// Retrieves the best matching resource value with ID `resid`.
diff --git a/libs/androidfw/include/androidfw/LoadedArsc.h b/libs/androidfw/include/androidfw/LoadedArsc.h
index 413b27829474..eb99dc7b8eff 100644
--- a/libs/androidfw/include/androidfw/LoadedArsc.h
+++ b/libs/androidfw/include/androidfw/LoadedArsc.h
@@ -238,7 +238,8 @@ class LoadedPackage {
// Populates a set of strings representing locales.
// If `canonicalize` is set to true, each locale is transformed into its canonical format
// before being inserted into the set. This may cause some equivalent locales to de-dupe.
- void CollectLocales(bool canonicalize, std::set<std::string>* out_locales) const;
+ using Locales = std::set<std::string, std::less<>>;
+ void CollectLocales(bool canonicalize, Locales* out_locales) const;
// type_idx is TT - 1 from 0xPPTTEEEE.
inline const TypeSpec* GetTypeSpecByTypeIndex(uint8_t type_index) const {
diff --git a/libs/androidfw/tests/AssetManager2_bench.cpp b/libs/androidfw/tests/AssetManager2_bench.cpp
index 136f5ea639a1..c469817d3595 100644
--- a/libs/androidfw/tests/AssetManager2_bench.cpp
+++ b/libs/androidfw/tests/AssetManager2_bench.cpp
@@ -191,7 +191,7 @@ static void BM_AssetManagerGetResourceLocales(benchmark::State& state) {
assets.SetApkAssets({apk});
for (auto&& _ : state) {
- std::set<std::string> locales =
+ auto locales =
assets.GetResourceLocales(false /*exclude_system*/, true /*merge_equivalent_languages*/);
benchmark::DoNotOptimize(locales);
}
diff --git a/libs/androidfw/tests/AssetManager2_test.cpp b/libs/androidfw/tests/AssetManager2_test.cpp
index 3f228841f6ba..a691c5dabfcb 100644
--- a/libs/androidfw/tests/AssetManager2_test.cpp
+++ b/libs/androidfw/tests/AssetManager2_test.cpp
@@ -23,6 +23,7 @@
#include "androidfw/ResourceUtils.h"
#include "data/appaslib/R.h"
#include "data/basic/R.h"
+#include "data/flagged/R.h"
#include "data/lib_one/R.h"
#include "data/lib_two/R.h"
#include "data/libclient/R.h"
@@ -32,6 +33,7 @@
namespace app = com::android::app;
namespace appaslib = com::android::appaslib::app;
namespace basic = com::android::basic;
+namespace flagged = com::android::flagged;
namespace lib_one = com::android::lib_one;
namespace lib_two = com::android::lib_two;
namespace libclient = com::android::libclient;
@@ -87,6 +89,10 @@ class AssetManager2Test : public ::testing::Test {
overlayable_assets_ = ApkAssets::Load("overlayable/overlayable.apk");
ASSERT_THAT(overlayable_assets_, NotNull());
+
+ flagged_assets_ = ApkAssets::Load("flagged/flagged.apk");
+ ASSERT_THAT(app_assets_, NotNull());
+
chdir(original_path.c_str());
}
@@ -104,6 +110,7 @@ class AssetManager2Test : public ::testing::Test {
AssetManager2::ApkAssetsPtr app_assets_;
AssetManager2::ApkAssetsPtr overlay_assets_;
AssetManager2::ApkAssetsPtr overlayable_assets_;
+ AssetManager2::ApkAssetsPtr flagged_assets_;
};
TEST_F(AssetManager2Test, FindsResourceFromSingleApkAssets) {
@@ -621,7 +628,7 @@ TEST_F(AssetManager2Test, GetResourceLocales) {
AssetManager2 assetmanager;
assetmanager.SetApkAssets({system_assets_, basic_de_fr_assets_});
- std::set<std::string> locales = assetmanager.GetResourceLocales();
+ auto locales = assetmanager.GetResourceLocales();
// We expect the locale sv from the system assets, and de and fr from basic_de_fr assets.
EXPECT_EQ(3u, locales.size());
@@ -856,4 +863,12 @@ TEST_F(AssetManager2Test, GetApkAssets) {
EXPECT_EQ(1, lib_one_assets_->getStrongCount());
}
+TEST_F(AssetManager2Test, GetFlaggedAssets) {
+ AssetManager2 assetmanager;
+ assetmanager.SetApkAssets({flagged_assets_});
+ auto value = assetmanager.GetResource(flagged::R::xml::flagged, false, 0);
+ ASSERT_TRUE(value.has_value());
+ EXPECT_TRUE(value->entry_flags & ResTable_entry::FLAG_USES_FEATURE_FLAGS);
+}
+
} // namespace android
diff --git a/libs/androidfw/tests/data/flagged/AndroidManifest.xml b/libs/androidfw/tests/data/flagged/AndroidManifest.xml
new file mode 100644
index 000000000000..cc1394328797
--- /dev/null
+++ b/libs/androidfw/tests/data/flagged/AndroidManifest.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2025 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.basic">
+ <application />
+</manifest>
diff --git a/libs/androidfw/tests/data/flagged/R.h b/libs/androidfw/tests/data/flagged/R.h
new file mode 100644
index 000000000000..33ccab28cdd3
--- /dev/null
+++ b/libs/androidfw/tests/data/flagged/R.h
@@ -0,0 +1,35 @@
+/*
+* Copyright (C) 2025 The Android Open Source Project
+*
+* Licensed under the Apache License, Version 2.0 (the "License");
+* you may not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.
+*/
+
+#pragma once
+
+#include <cstdint>
+
+namespace com {
+namespace android {
+namespace flagged {
+
+struct R {
+ struct xml {
+ enum : uint32_t {
+ flagged = 0x7f010000,
+ };
+ };
+};
+
+} // namespace flagged
+} // namespace android
+} // namespace com \ No newline at end of file
diff --git a/libs/androidfw/tests/data/flagged/build b/libs/androidfw/tests/data/flagged/build
new file mode 100755
index 000000000000..9e5d21ba1833
--- /dev/null
+++ b/libs/androidfw/tests/data/flagged/build
@@ -0,0 +1,28 @@
+#!/bin/bash
+#
+# 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.
+#
+
+set -e
+
+PATH_TO_FRAMEWORK_RES=${ANDROID_BUILD_TOP}/prebuilts/sdk/current/public/android.jar
+
+aapt2 compile --dir res -o compiled.flata
+aapt2 link -o flagged.apk \
+ --manifest AndroidManifest.xml \
+ -I $PATH_TO_FRAMEWORK_RES \
+ -I ../basic/basic.apk \
+ compiled.flata
+rm compiled.flata
diff --git a/libs/androidfw/tests/data/flagged/flagged.apk b/libs/androidfw/tests/data/flagged/flagged.apk
new file mode 100644
index 000000000000..94b8f4d9fcf0
--- /dev/null
+++ b/libs/androidfw/tests/data/flagged/flagged.apk
Binary files differ
diff --git a/libs/androidfw/tests/data/flagged/res/xml/flagged.xml b/libs/androidfw/tests/data/flagged/res/xml/flagged.xml
new file mode 100644
index 000000000000..5fe8d1b3ac27
--- /dev/null
+++ b/libs/androidfw/tests/data/flagged/res/xml/flagged.xml
@@ -0,0 +1,18 @@
+<?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.
+-->
+<first xmlns:android="http://schemas.android.com/apk/res/android">
+ <second android:featureFlag="android.content.res.always_false"/>
+</first> \ No newline at end of file
diff --git a/libs/hwui/Android.bp b/libs/hwui/Android.bp
index ab1be7e6128d..1bde5ff43aa8 100644
--- a/libs/hwui/Android.bp
+++ b/libs/hwui/Android.bp
@@ -168,6 +168,14 @@ cc_defaults {
"libutils",
],
},
+ host_linux: {
+ shared_libs: [
+ "libaconfig_storage_read_api_cc",
+ ],
+ whole_static_libs: [
+ "hwui_flags_cc_lib",
+ ],
+ },
},
}
diff --git a/libs/hwui/jni/Bitmap.cpp b/libs/hwui/jni/Bitmap.cpp
index b6a2ad7064a9..1a258e022dd0 100644
--- a/libs/hwui/jni/Bitmap.cpp
+++ b/libs/hwui/jni/Bitmap.cpp
@@ -2,6 +2,9 @@
#include "Bitmap.h"
#include <android-base/unique_fd.h>
+#ifdef __linux__
+#include <com_android_graphics_hwui_flags.h>
+#endif
#include <hwui/Bitmap.h>
#include <hwui/Paint.h>
#include <inttypes.h>
@@ -33,15 +36,6 @@
#endif
#include "android_nio_utils.h"
-#ifdef __ANDROID__
-#include <com_android_graphics_hwui_flags.h>
-namespace hwui_flags = com::android::graphics::hwui::flags;
-#else
-namespace hwui_flags {
-constexpr bool bitmap_parcel_ashmem_as_immutable() { return false; }
-}
-#endif
-
#define DEBUG_PARCEL 0
static jclass gBitmap_class;
@@ -861,7 +855,7 @@ static bool shouldParcelAsMutable(SkBitmap& bitmap, AParcel* parcel) {
return false;
}
- if (!hwui_flags::bitmap_parcel_ashmem_as_immutable()) {
+ if (!com::android::graphics::hwui::flags::bitmap_parcel_ashmem_as_immutable()) {
return true;
}
diff --git a/location/api/system-current.txt b/location/api/system-current.txt
index 47984745fafc..8026d4662cb9 100644
--- a/location/api/system-current.txt
+++ b/location/api/system-current.txt
@@ -32,7 +32,7 @@ package android.location {
@FlaggedApi("android.location.flags.gnss_assistance_interface") public final class BeidouAssistance implements android.os.Parcelable {
method public int describeContents();
method @Nullable public android.location.GnssAlmanac getAlmanac();
- method @Nullable public android.location.AuxiliaryInformation getAuxiliaryInformation();
+ method @NonNull public java.util.List<android.location.AuxiliaryInformation> getAuxiliaryInformation();
method @Nullable public android.location.KlobucharIonosphericModel getIonosphericModel();
method @Nullable public android.location.LeapSecondsModel getLeapSecondsModel();
method @NonNull public java.util.List<android.location.RealTimeIntegrityModel> getRealTimeIntegrityModels();
@@ -48,7 +48,7 @@ package android.location {
ctor public BeidouAssistance.Builder();
method @NonNull public android.location.BeidouAssistance build();
method @NonNull public android.location.BeidouAssistance.Builder setAlmanac(@Nullable android.location.GnssAlmanac);
- method @NonNull public android.location.BeidouAssistance.Builder setAuxiliaryInformation(@Nullable android.location.AuxiliaryInformation);
+ method @NonNull public android.location.BeidouAssistance.Builder setAuxiliaryInformation(@NonNull java.util.List<android.location.AuxiliaryInformation>);
method @NonNull public android.location.BeidouAssistance.Builder setIonosphericModel(@Nullable android.location.KlobucharIonosphericModel);
method @NonNull public android.location.BeidouAssistance.Builder setLeapSecondsModel(@Nullable android.location.LeapSecondsModel);
method @NonNull public android.location.BeidouAssistance.Builder setRealTimeIntegrityModels(@NonNull java.util.List<android.location.RealTimeIntegrityModel>);
@@ -176,7 +176,7 @@ package android.location {
@FlaggedApi("android.location.flags.gnss_assistance_interface") public final class GalileoAssistance implements android.os.Parcelable {
method public int describeContents();
method @Nullable public android.location.GnssAlmanac getAlmanac();
- method @Nullable public android.location.AuxiliaryInformation getAuxiliaryInformation();
+ method @NonNull public java.util.List<android.location.AuxiliaryInformation> getAuxiliaryInformation();
method @Nullable public android.location.GalileoIonosphericModel getIonosphericModel();
method @Nullable public android.location.LeapSecondsModel getLeapSecondsModel();
method @NonNull public java.util.List<android.location.RealTimeIntegrityModel> getRealTimeIntegrityModels();
@@ -192,7 +192,7 @@ package android.location {
ctor public GalileoAssistance.Builder();
method @NonNull public android.location.GalileoAssistance build();
method @NonNull public android.location.GalileoAssistance.Builder setAlmanac(@Nullable android.location.GnssAlmanac);
- method @NonNull public android.location.GalileoAssistance.Builder setAuxiliaryInformation(@Nullable android.location.AuxiliaryInformation);
+ method @NonNull public android.location.GalileoAssistance.Builder setAuxiliaryInformation(@NonNull java.util.List<android.location.AuxiliaryInformation>);
method @NonNull public android.location.GalileoAssistance.Builder setIonosphericModel(@Nullable android.location.GalileoIonosphericModel);
method @NonNull public android.location.GalileoAssistance.Builder setLeapSecondsModel(@Nullable android.location.LeapSecondsModel);
method @NonNull public android.location.GalileoAssistance.Builder setRealTimeIntegrityModels(@NonNull java.util.List<android.location.RealTimeIntegrityModel>);
@@ -346,7 +346,8 @@ package android.location {
@FlaggedApi("android.location.flags.gnss_assistance_interface") public final class GlonassAssistance implements android.os.Parcelable {
method public int describeContents();
method @Nullable public android.location.GlonassAlmanac getAlmanac();
- method @Nullable public android.location.AuxiliaryInformation getAuxiliaryInformation();
+ method @NonNull public java.util.List<android.location.AuxiliaryInformation> getAuxiliaryInformation();
+ method @NonNull public java.util.List<android.location.RealTimeIntegrityModel> getRealTimeIntegrityModels();
method @NonNull public java.util.List<android.location.GnssAssistance.GnssSatelliteCorrections> getSatelliteCorrections();
method @NonNull public java.util.List<android.location.GlonassSatelliteEphemeris> getSatelliteEphemeris();
method @NonNull public java.util.List<android.location.TimeModel> getTimeModels();
@@ -359,7 +360,8 @@ package android.location {
ctor public GlonassAssistance.Builder();
method @NonNull public android.location.GlonassAssistance build();
method @NonNull public android.location.GlonassAssistance.Builder setAlmanac(@Nullable android.location.GlonassAlmanac);
- method @NonNull public android.location.GlonassAssistance.Builder setAuxiliaryInformation(@Nullable android.location.AuxiliaryInformation);
+ method @NonNull public android.location.GlonassAssistance.Builder setAuxiliaryInformation(@NonNull java.util.List<android.location.AuxiliaryInformation>);
+ method @NonNull public android.location.GlonassAssistance.Builder setRealTimeIntegrityModels(@NonNull java.util.List<android.location.RealTimeIntegrityModel>);
method @NonNull public android.location.GlonassAssistance.Builder setSatelliteCorrections(@NonNull java.util.List<android.location.GnssAssistance.GnssSatelliteCorrections>);
method @NonNull public android.location.GlonassAssistance.Builder setSatelliteEphemeris(@NonNull java.util.List<android.location.GlonassSatelliteEphemeris>);
method @NonNull public android.location.GlonassAssistance.Builder setTimeModels(@NonNull java.util.List<android.location.TimeModel>);
@@ -717,7 +719,7 @@ package android.location {
@FlaggedApi("android.location.flags.gnss_assistance_interface") public final class GpsAssistance implements android.os.Parcelable {
method public int describeContents();
method @Nullable public android.location.GnssAlmanac getAlmanac();
- method @Nullable public android.location.AuxiliaryInformation getAuxiliaryInformation();
+ method @NonNull public java.util.List<android.location.AuxiliaryInformation> getAuxiliaryInformation();
method @Nullable public android.location.KlobucharIonosphericModel getIonosphericModel();
method @Nullable public android.location.LeapSecondsModel getLeapSecondsModel();
method @NonNull public java.util.List<android.location.RealTimeIntegrityModel> getRealTimeIntegrityModels();
@@ -733,7 +735,7 @@ package android.location {
ctor public GpsAssistance.Builder();
method @NonNull public android.location.GpsAssistance build();
method @NonNull public android.location.GpsAssistance.Builder setAlmanac(@Nullable android.location.GnssAlmanac);
- method @NonNull public android.location.GpsAssistance.Builder setAuxiliaryInformation(@Nullable android.location.AuxiliaryInformation);
+ method @NonNull public android.location.GpsAssistance.Builder setAuxiliaryInformation(@NonNull java.util.List<android.location.AuxiliaryInformation>);
method @NonNull public android.location.GpsAssistance.Builder setIonosphericModel(@Nullable android.location.KlobucharIonosphericModel);
method @NonNull public android.location.GpsAssistance.Builder setLeapSecondsModel(@Nullable android.location.LeapSecondsModel);
method @NonNull public android.location.GpsAssistance.Builder setRealTimeIntegrityModels(@NonNull java.util.List<android.location.RealTimeIntegrityModel>);
@@ -1253,7 +1255,7 @@ package android.location {
@FlaggedApi("android.location.flags.gnss_assistance_interface") public final class QzssAssistance implements android.os.Parcelable {
method public int describeContents();
method @Nullable public android.location.GnssAlmanac getAlmanac();
- method @Nullable public android.location.AuxiliaryInformation getAuxiliaryInformation();
+ method @NonNull public java.util.List<android.location.AuxiliaryInformation> getAuxiliaryInformation();
method @Nullable public android.location.KlobucharIonosphericModel getIonosphericModel();
method @Nullable public android.location.LeapSecondsModel getLeapSecondsModel();
method @NonNull public java.util.List<android.location.RealTimeIntegrityModel> getRealTimeIntegrityModels();
@@ -1269,7 +1271,7 @@ package android.location {
ctor public QzssAssistance.Builder();
method @NonNull public android.location.QzssAssistance build();
method @NonNull public android.location.QzssAssistance.Builder setAlmanac(@Nullable android.location.GnssAlmanac);
- method @NonNull public android.location.QzssAssistance.Builder setAuxiliaryInformation(@Nullable android.location.AuxiliaryInformation);
+ method @NonNull public android.location.QzssAssistance.Builder setAuxiliaryInformation(@NonNull java.util.List<android.location.AuxiliaryInformation>);
method @NonNull public android.location.QzssAssistance.Builder setIonosphericModel(@Nullable android.location.KlobucharIonosphericModel);
method @NonNull public android.location.QzssAssistance.Builder setLeapSecondsModel(@Nullable android.location.LeapSecondsModel);
method @NonNull public android.location.QzssAssistance.Builder setRealTimeIntegrityModels(@NonNull java.util.List<android.location.RealTimeIntegrityModel>);
diff --git a/location/java/android/location/BeidouAssistance.java b/location/java/android/location/BeidouAssistance.java
index e35493ed1007..274332dab9a8 100644
--- a/location/java/android/location/BeidouAssistance.java
+++ b/location/java/android/location/BeidouAssistance.java
@@ -50,8 +50,8 @@ public final class BeidouAssistance implements Parcelable {
/** The leap seconds model. */
@Nullable private final LeapSecondsModel mLeapSecondsModel;
- /** The auxiliary information. */
- @Nullable private final AuxiliaryInformation mAuxiliaryInformation;
+ /** The list of auxiliary informations. */
+ @NonNull private final List<AuxiliaryInformation> mAuxiliaryInformation;
/** The list of time models. */
@NonNull private final List<TimeModel> mTimeModels;
@@ -70,7 +70,12 @@ public final class BeidouAssistance implements Parcelable {
mIonosphericModel = builder.mIonosphericModel;
mUtcModel = builder.mUtcModel;
mLeapSecondsModel = builder.mLeapSecondsModel;
- mAuxiliaryInformation = builder.mAuxiliaryInformation;
+ if (builder.mAuxiliaryInformation != null) {
+ mAuxiliaryInformation =
+ Collections.unmodifiableList(new ArrayList<>(builder.mAuxiliaryInformation));
+ } else {
+ mAuxiliaryInformation = new ArrayList<>();
+ }
if (builder.mTimeModels != null) {
mTimeModels = Collections.unmodifiableList(new ArrayList<>(builder.mTimeModels));
} else {
@@ -120,9 +125,9 @@ public final class BeidouAssistance implements Parcelable {
return mLeapSecondsModel;
}
- /** Returns the auxiliary information. */
- @Nullable
- public AuxiliaryInformation getAuxiliaryInformation() {
+ /** Returns the list of auxiliary informations. */
+ @NonNull
+ public List<AuxiliaryInformation> getAuxiliaryInformation() {
return mAuxiliaryInformation;
}
@@ -178,7 +183,7 @@ public final class BeidouAssistance implements Parcelable {
dest.writeTypedObject(mIonosphericModel, flags);
dest.writeTypedObject(mUtcModel, flags);
dest.writeTypedObject(mLeapSecondsModel, flags);
- dest.writeTypedObject(mAuxiliaryInformation, flags);
+ dest.writeTypedList(mAuxiliaryInformation);
dest.writeTypedList(mTimeModels);
dest.writeTypedList(mSatelliteEphemeris);
dest.writeTypedList(mRealTimeIntegrityModels);
@@ -196,7 +201,7 @@ public final class BeidouAssistance implements Parcelable {
.setUtcModel(in.readTypedObject(UtcModel.CREATOR))
.setLeapSecondsModel(in.readTypedObject(LeapSecondsModel.CREATOR))
.setAuxiliaryInformation(
- in.readTypedObject(AuxiliaryInformation.CREATOR))
+ in.createTypedArrayList(AuxiliaryInformation.CREATOR))
.setTimeModels(in.createTypedArrayList(TimeModel.CREATOR))
.setSatelliteEphemeris(
in.createTypedArrayList(BeidouSatelliteEphemeris.CREATOR))
@@ -219,7 +224,7 @@ public final class BeidouAssistance implements Parcelable {
private KlobucharIonosphericModel mIonosphericModel;
private UtcModel mUtcModel;
private LeapSecondsModel mLeapSecondsModel;
- private AuxiliaryInformation mAuxiliaryInformation;
+ private List<AuxiliaryInformation> mAuxiliaryInformation;
private List<TimeModel> mTimeModels;
private List<BeidouSatelliteEphemeris> mSatelliteEphemeris;
private List<RealTimeIntegrityModel> mRealTimeIntegrityModels;
@@ -253,10 +258,10 @@ public final class BeidouAssistance implements Parcelable {
return this;
}
- /** Sets the auxiliary information. */
+ /** Sets the list of auxiliary informations. */
@NonNull
public Builder setAuxiliaryInformation(
- @Nullable AuxiliaryInformation auxiliaryInformation) {
+ @NonNull List<AuxiliaryInformation> auxiliaryInformation) {
mAuxiliaryInformation = auxiliaryInformation;
return this;
}
diff --git a/location/java/android/location/GalileoAssistance.java b/location/java/android/location/GalileoAssistance.java
index 7f81ccdf346f..f73ce400dd9d 100644
--- a/location/java/android/location/GalileoAssistance.java
+++ b/location/java/android/location/GalileoAssistance.java
@@ -50,8 +50,8 @@ public final class GalileoAssistance implements Parcelable {
/** The leap seconds model. */
@Nullable private final LeapSecondsModel mLeapSecondsModel;
- /** The auxiliary information. */
- @Nullable private final AuxiliaryInformation mAuxiliaryInformation;
+ /** The list of auxiliary informations. */
+ @NonNull private final List<AuxiliaryInformation> mAuxiliaryInformation;
/** The list of time models. */
@NonNull private final List<TimeModel> mTimeModels;
@@ -70,7 +70,12 @@ public final class GalileoAssistance implements Parcelable {
mIonosphericModel = builder.mIonosphericModel;
mUtcModel = builder.mUtcModel;
mLeapSecondsModel = builder.mLeapSecondsModel;
- mAuxiliaryInformation = builder.mAuxiliaryInformation;
+ if (builder.mAuxiliaryInformation != null) {
+ mAuxiliaryInformation =
+ Collections.unmodifiableList(new ArrayList<>(builder.mAuxiliaryInformation));
+ } else {
+ mAuxiliaryInformation = new ArrayList<>();
+ }
if (builder.mTimeModels != null) {
mTimeModels = Collections.unmodifiableList(new ArrayList<>(builder.mTimeModels));
} else {
@@ -120,9 +125,9 @@ public final class GalileoAssistance implements Parcelable {
return mLeapSecondsModel;
}
- /** Returns the auxiliary information. */
- @Nullable
- public AuxiliaryInformation getAuxiliaryInformation() {
+ /** Returns the list of auxiliary informations. */
+ @NonNull
+ public List<AuxiliaryInformation> getAuxiliaryInformation() {
return mAuxiliaryInformation;
}
@@ -161,7 +166,7 @@ public final class GalileoAssistance implements Parcelable {
dest.writeTypedObject(mIonosphericModel, flags);
dest.writeTypedObject(mUtcModel, flags);
dest.writeTypedObject(mLeapSecondsModel, flags);
- dest.writeTypedObject(mAuxiliaryInformation, flags);
+ dest.writeTypedList(mAuxiliaryInformation);
dest.writeTypedList(mTimeModels);
dest.writeTypedList(mSatelliteEphemeris);
dest.writeTypedList(mRealTimeIntegrityModels);
@@ -196,7 +201,7 @@ public final class GalileoAssistance implements Parcelable {
.setUtcModel(in.readTypedObject(UtcModel.CREATOR))
.setLeapSecondsModel(in.readTypedObject(LeapSecondsModel.CREATOR))
.setAuxiliaryInformation(
- in.readTypedObject(AuxiliaryInformation.CREATOR))
+ in.createTypedArrayList(AuxiliaryInformation.CREATOR))
.setTimeModels(in.createTypedArrayList(TimeModel.CREATOR))
.setSatelliteEphemeris(
in.createTypedArrayList(GalileoSatelliteEphemeris.CREATOR))
@@ -219,7 +224,7 @@ public final class GalileoAssistance implements Parcelable {
private GalileoIonosphericModel mIonosphericModel;
private UtcModel mUtcModel;
private LeapSecondsModel mLeapSecondsModel;
- private AuxiliaryInformation mAuxiliaryInformation;
+ private List<AuxiliaryInformation> mAuxiliaryInformation;
private List<TimeModel> mTimeModels;
private List<GalileoSatelliteEphemeris> mSatelliteEphemeris;
private List<RealTimeIntegrityModel> mRealTimeIntegrityModels;
@@ -253,10 +258,10 @@ public final class GalileoAssistance implements Parcelable {
return this;
}
- /** Sets the auxiliary information. */
+ /** Sets the list of auxiliary informations. */
@NonNull
public Builder setAuxiliaryInformation(
- @Nullable AuxiliaryInformation auxiliaryInformation) {
+ @NonNull List<AuxiliaryInformation> auxiliaryInformation) {
mAuxiliaryInformation = auxiliaryInformation;
return this;
}
diff --git a/location/java/android/location/GlonassAssistance.java b/location/java/android/location/GlonassAssistance.java
index c7ed1c52b403..8c5ddbb10a07 100644
--- a/location/java/android/location/GlonassAssistance.java
+++ b/location/java/android/location/GlonassAssistance.java
@@ -44,8 +44,8 @@ public final class GlonassAssistance implements Parcelable {
/** The UTC model. */
@Nullable private final UtcModel mUtcModel;
- /** The auxiliary information. */
- @Nullable private final AuxiliaryInformation mAuxiliaryInformation;
+ /** The list of auxiliary informations. */
+ @NonNull private final List<AuxiliaryInformation> mAuxiliaryInformation;
/** The list of time models. */
@NonNull private final List<TimeModel> mTimeModels;
@@ -56,10 +56,18 @@ public final class GlonassAssistance implements Parcelable {
/** The list of Glonass satellite corrections. */
@NonNull private final List<GnssSatelliteCorrections> mSatelliteCorrections;
+ /** The list of real time integrity models. */
+ @NonNull private final List<RealTimeIntegrityModel> mRealTimeIntegrityModels;
+
private GlonassAssistance(Builder builder) {
mAlmanac = builder.mAlmanac;
mUtcModel = builder.mUtcModel;
- mAuxiliaryInformation = builder.mAuxiliaryInformation;
+ if (builder.mAuxiliaryInformation != null) {
+ mAuxiliaryInformation =
+ Collections.unmodifiableList(new ArrayList<>(builder.mAuxiliaryInformation));
+ } else {
+ mAuxiliaryInformation = new ArrayList<>();
+ }
if (builder.mTimeModels != null) {
mTimeModels = Collections.unmodifiableList(new ArrayList<>(builder.mTimeModels));
} else {
@@ -77,6 +85,12 @@ public final class GlonassAssistance implements Parcelable {
} else {
mSatelliteCorrections = new ArrayList<>();
}
+ if (builder.mRealTimeIntegrityModels != null) {
+ mRealTimeIntegrityModels =
+ Collections.unmodifiableList(new ArrayList<>(builder.mRealTimeIntegrityModels));
+ } else {
+ mRealTimeIntegrityModels = new ArrayList<>();
+ }
}
/** Returns the Glonass almanac. */
@@ -109,9 +123,15 @@ public final class GlonassAssistance implements Parcelable {
return mSatelliteCorrections;
}
- /** Returns the auxiliary information. */
- @Nullable
- public AuxiliaryInformation getAuxiliaryInformation() {
+ /** Returns the list of real time integrity models. */
+ @NonNull
+ public List<RealTimeIntegrityModel> getRealTimeIntegrityModels() {
+ return mRealTimeIntegrityModels;
+ }
+
+ /** Returns the list of auxiliary informations. */
+ @NonNull
+ public List<AuxiliaryInformation> getAuxiliaryInformation() {
return mAuxiliaryInformation;
}
@@ -124,10 +144,11 @@ public final class GlonassAssistance implements Parcelable {
public void writeToParcel(@NonNull Parcel dest, int flags) {
dest.writeTypedObject(mAlmanac, flags);
dest.writeTypedObject(mUtcModel, flags);
- dest.writeTypedObject(mAuxiliaryInformation, flags);
+ dest.writeTypedList(mAuxiliaryInformation);
dest.writeTypedList(mTimeModels);
dest.writeTypedList(mSatelliteEphemeris);
dest.writeTypedList(mSatelliteCorrections);
+ dest.writeTypedList(mRealTimeIntegrityModels);
}
@Override
@@ -140,6 +161,7 @@ public final class GlonassAssistance implements Parcelable {
builder.append(", timeModels = ").append(mTimeModels);
builder.append(", satelliteEphemeris = ").append(mSatelliteEphemeris);
builder.append(", satelliteCorrections = ").append(mSatelliteCorrections);
+ builder.append(", realTimeIntegrityModels = ").append(mRealTimeIntegrityModels);
builder.append("]");
return builder.toString();
}
@@ -152,12 +174,14 @@ public final class GlonassAssistance implements Parcelable {
.setAlmanac(in.readTypedObject(GlonassAlmanac.CREATOR))
.setUtcModel(in.readTypedObject(UtcModel.CREATOR))
.setAuxiliaryInformation(
- in.readTypedObject(AuxiliaryInformation.CREATOR))
+ in.createTypedArrayList(AuxiliaryInformation.CREATOR))
.setTimeModels(in.createTypedArrayList(TimeModel.CREATOR))
.setSatelliteEphemeris(
in.createTypedArrayList(GlonassSatelliteEphemeris.CREATOR))
.setSatelliteCorrections(
in.createTypedArrayList(GnssSatelliteCorrections.CREATOR))
+ .setRealTimeIntegrityModels(
+ in.createTypedArrayList(RealTimeIntegrityModel.CREATOR))
.build();
}
@@ -171,10 +195,11 @@ public final class GlonassAssistance implements Parcelable {
public static final class Builder {
private GlonassAlmanac mAlmanac;
private UtcModel mUtcModel;
- private AuxiliaryInformation mAuxiliaryInformation;
+ private List<AuxiliaryInformation> mAuxiliaryInformation;
private List<TimeModel> mTimeModels;
private List<GlonassSatelliteEphemeris> mSatelliteEphemeris;
private List<GnssSatelliteCorrections> mSatelliteCorrections;
+ private List<RealTimeIntegrityModel> mRealTimeIntegrityModels;
/** Sets the Glonass almanac. */
@NonNull
@@ -190,10 +215,10 @@ public final class GlonassAssistance implements Parcelable {
return this;
}
- /** Sets the auxiliary information. */
+ /** Sets the list of auxiliary informations. */
@NonNull
public Builder setAuxiliaryInformation(
- @Nullable AuxiliaryInformation auxiliaryInformation) {
+ @NonNull List<AuxiliaryInformation> auxiliaryInformation) {
mAuxiliaryInformation = auxiliaryInformation;
return this;
}
@@ -221,6 +246,14 @@ public final class GlonassAssistance implements Parcelable {
return this;
}
+ /** Sets the list of real time integrity models. */
+ @NonNull
+ public Builder setRealTimeIntegrityModels(
+ @NonNull List<RealTimeIntegrityModel> realTimeIntegrityModels) {
+ mRealTimeIntegrityModels = realTimeIntegrityModels;
+ return this;
+ }
+
/** Builds the {@link GlonassAssistance}. */
@NonNull
public GlonassAssistance build() {
diff --git a/location/java/android/location/GpsAssistance.java b/location/java/android/location/GpsAssistance.java
index 5a8802f057e2..45b13b2f97f6 100644
--- a/location/java/android/location/GpsAssistance.java
+++ b/location/java/android/location/GpsAssistance.java
@@ -51,8 +51,8 @@ public final class GpsAssistance implements Parcelable {
/** The leap seconds model. */
@Nullable private final LeapSecondsModel mLeapSecondsModel;
- /** The auxiliary information. */
- @Nullable private final AuxiliaryInformation mAuxiliaryInformation;
+ /** The list of auxiliary informations. */
+ @NonNull private final List<AuxiliaryInformation> mAuxiliaryInformation;
/** The list of time models. */
@NonNull private final List<TimeModel> mTimeModels;
@@ -71,7 +71,12 @@ public final class GpsAssistance implements Parcelable {
mIonosphericModel = builder.mIonosphericModel;
mUtcModel = builder.mUtcModel;
mLeapSecondsModel = builder.mLeapSecondsModel;
- mAuxiliaryInformation = builder.mAuxiliaryInformation;
+ if (builder.mAuxiliaryInformation != null) {
+ mAuxiliaryInformation =
+ Collections.unmodifiableList(new ArrayList<>(builder.mAuxiliaryInformation));
+ } else {
+ mAuxiliaryInformation = new ArrayList<>();
+ }
if (builder.mTimeModels != null) {
mTimeModels = Collections.unmodifiableList(new ArrayList<>(builder.mTimeModels));
} else {
@@ -121,9 +126,9 @@ public final class GpsAssistance implements Parcelable {
return mLeapSecondsModel;
}
- /** Returns the auxiliary information. */
- @Nullable
- public AuxiliaryInformation getAuxiliaryInformation() {
+ /** Returns the list of auxiliary informations. */
+ @NonNull
+ public List<AuxiliaryInformation> getAuxiliaryInformation() {
return mAuxiliaryInformation;
}
@@ -163,7 +168,7 @@ public final class GpsAssistance implements Parcelable {
.setUtcModel(in.readTypedObject(UtcModel.CREATOR))
.setLeapSecondsModel(in.readTypedObject(LeapSecondsModel.CREATOR))
.setAuxiliaryInformation(
- in.readTypedObject(AuxiliaryInformation.CREATOR))
+ in.createTypedArrayList(AuxiliaryInformation.CREATOR))
.setTimeModels(in.createTypedArrayList(TimeModel.CREATOR))
.setSatelliteEphemeris(
in.createTypedArrayList(GpsSatelliteEphemeris.CREATOR))
@@ -191,7 +196,7 @@ public final class GpsAssistance implements Parcelable {
dest.writeTypedObject(mIonosphericModel, flags);
dest.writeTypedObject(mUtcModel, flags);
dest.writeTypedObject(mLeapSecondsModel, flags);
- dest.writeTypedObject(mAuxiliaryInformation, flags);
+ dest.writeTypedList(mAuxiliaryInformation);
dest.writeTypedList(mTimeModels);
dest.writeTypedList(mSatelliteEphemeris);
dest.writeTypedList(mRealTimeIntegrityModels);
@@ -221,7 +226,7 @@ public final class GpsAssistance implements Parcelable {
private KlobucharIonosphericModel mIonosphericModel;
private UtcModel mUtcModel;
private LeapSecondsModel mLeapSecondsModel;
- private AuxiliaryInformation mAuxiliaryInformation;
+ private List<AuxiliaryInformation> mAuxiliaryInformation;
private List<TimeModel> mTimeModels;
private List<GpsSatelliteEphemeris> mSatelliteEphemeris;
private List<RealTimeIntegrityModel> mRealTimeIntegrityModels;
@@ -256,10 +261,10 @@ public final class GpsAssistance implements Parcelable {
return this;
}
- /** Sets the auxiliary information. */
+ /** Sets the list of auxiliary informations. */
@NonNull
public Builder setAuxiliaryInformation(
- @Nullable AuxiliaryInformation auxiliaryInformation) {
+ @NonNull List<AuxiliaryInformation> auxiliaryInformation) {
mAuxiliaryInformation = auxiliaryInformation;
return this;
}
diff --git a/location/java/android/location/QzssAssistance.java b/location/java/android/location/QzssAssistance.java
index 27c34370316e..75a267f2dd2a 100644
--- a/location/java/android/location/QzssAssistance.java
+++ b/location/java/android/location/QzssAssistance.java
@@ -50,8 +50,8 @@ public final class QzssAssistance implements Parcelable {
/** The leap seconds model. */
@Nullable private final LeapSecondsModel mLeapSecondsModel;
- /** The auxiliary information. */
- @Nullable private final AuxiliaryInformation mAuxiliaryInformation;
+ /** The list of auxiliary informations. */
+ @NonNull private final List<AuxiliaryInformation> mAuxiliaryInformation;
/** The list of time models. */
@NonNull private final List<TimeModel> mTimeModels;
@@ -70,7 +70,12 @@ public final class QzssAssistance implements Parcelable {
mIonosphericModel = builder.mIonosphericModel;
mUtcModel = builder.mUtcModel;
mLeapSecondsModel = builder.mLeapSecondsModel;
- mAuxiliaryInformation = builder.mAuxiliaryInformation;
+ if (builder.mAuxiliaryInformation != null) {
+ mAuxiliaryInformation =
+ Collections.unmodifiableList(new ArrayList<>(builder.mAuxiliaryInformation));
+ } else {
+ mAuxiliaryInformation = new ArrayList<>();
+ }
if (builder.mTimeModels != null) {
mTimeModels = Collections.unmodifiableList(new ArrayList<>(builder.mTimeModels));
} else {
@@ -120,9 +125,9 @@ public final class QzssAssistance implements Parcelable {
return mLeapSecondsModel;
}
- /** Returns the auxiliary information. */
- @Nullable
- public AuxiliaryInformation getAuxiliaryInformation() {
+ /** Returns the list of auxiliary informations. */
+ @NonNull
+ public List<AuxiliaryInformation> getAuxiliaryInformation() {
return mAuxiliaryInformation;
}
@@ -162,7 +167,7 @@ public final class QzssAssistance implements Parcelable {
.setUtcModel(in.readTypedObject(UtcModel.CREATOR))
.setLeapSecondsModel(in.readTypedObject(LeapSecondsModel.CREATOR))
.setAuxiliaryInformation(
- in.readTypedObject(AuxiliaryInformation.CREATOR))
+ in.createTypedArrayList(AuxiliaryInformation.CREATOR))
.setTimeModels(in.createTypedArrayList(TimeModel.CREATOR))
.setSatelliteEphemeris(
in.createTypedArrayList(QzssSatelliteEphemeris.CREATOR))
@@ -190,7 +195,7 @@ public final class QzssAssistance implements Parcelable {
dest.writeTypedObject(mIonosphericModel, flags);
dest.writeTypedObject(mUtcModel, flags);
dest.writeTypedObject(mLeapSecondsModel, flags);
- dest.writeTypedObject(mAuxiliaryInformation, flags);
+ dest.writeTypedList(mAuxiliaryInformation);
dest.writeTypedList(mTimeModels);
dest.writeTypedList(mSatelliteEphemeris);
dest.writeTypedList(mRealTimeIntegrityModels);
@@ -220,7 +225,7 @@ public final class QzssAssistance implements Parcelable {
private KlobucharIonosphericModel mIonosphericModel;
private UtcModel mUtcModel;
private LeapSecondsModel mLeapSecondsModel;
- private AuxiliaryInformation mAuxiliaryInformation;
+ private List<AuxiliaryInformation> mAuxiliaryInformation;
private List<TimeModel> mTimeModels;
private List<QzssSatelliteEphemeris> mSatelliteEphemeris;
private List<RealTimeIntegrityModel> mRealTimeIntegrityModels;
@@ -254,10 +259,10 @@ public final class QzssAssistance implements Parcelable {
return this;
}
- /** Sets the auxiliary information. */
+ /** Sets the list of auxiliary informations. */
@NonNull
public Builder setAuxiliaryInformation(
- @Nullable AuxiliaryInformation auxiliaryInformation) {
+ @NonNull List<AuxiliaryInformation> auxiliaryInformation) {
mAuxiliaryInformation = auxiliaryInformation;
return this;
}
diff --git a/location/java/android/location/flags/location.aconfig b/location/java/android/location/flags/location.aconfig
index 9cc58ae35692..4b460c6ab039 100644
--- a/location/java/android/location/flags/location.aconfig
+++ b/location/java/android/location/flags/location.aconfig
@@ -199,6 +199,16 @@ flag {
}
flag {
+ name: "fix_is_in_emergency_anr"
+ namespace: "location"
+ description: "Avoid calling IPC with a lock to avoid deadlock"
+ bug: "355384257"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+flag {
name: "gnss_assistance_interface_jni"
namespace: "location"
description: "Flag for GNSS assistance interface JNI"
diff --git a/media/java/android/media/MediaCodec.java b/media/java/android/media/MediaCodec.java
index 15c832392a22..d0676e693b95 100644
--- a/media/java/android/media/MediaCodec.java
+++ b/media/java/android/media/MediaCodec.java
@@ -50,6 +50,7 @@ import android.os.Message;
import android.os.PersistableBundle;
import android.os.Trace;
import android.view.Surface;
+import android.util.Log;
import java.io.IOException;
import java.lang.annotation.Retention;
@@ -1656,6 +1657,7 @@ import java.util.function.Supplier;
</table>
*/
final public class MediaCodec {
+ private static final String TAG = "MediaCodec";
/**
* Per buffer metadata includes an offset and size specifying
@@ -2496,6 +2498,49 @@ final public class MediaCodec {
}
keys[i] = "audio-hw-sync";
values[i] = AudioSystem.getAudioHwSyncForSession(sessionId);
+ } else if (applyPictureProfiles() && mediaQualityFw()
+ && entry.getKey().equals(MediaFormat.KEY_PICTURE_PROFILE_INSTANCE)) {
+ PictureProfile pictureProfile = null;
+ try {
+ pictureProfile = (PictureProfile) entry.getValue();
+ } catch (ClassCastException e) {
+ throw new IllegalArgumentException(
+ "Cannot cast the instance parameter to PictureProfile!");
+ } catch (Exception e) {
+ Log.e(TAG, Log.getStackTraceString(e));
+ throw new IllegalArgumentException("Unexpected exception when casting the "
+ + "instance parameter to PictureProfile!");
+ }
+ if (pictureProfile == null) {
+ throw new IllegalArgumentException(
+ "Picture profile instance parameter is null!");
+ }
+ PictureProfileHandle handle = pictureProfile.getHandle();
+ if (handle != PictureProfileHandle.NONE) {
+ keys[i] = PARAMETER_KEY_PICTURE_PROFILE_HANDLE;
+ values[i] = Long.valueOf(handle.getId());
+ }
+ } else if (applyPictureProfiles() && mediaQualityFw()
+ && entry.getKey().equals(MediaFormat.KEY_PICTURE_PROFILE_ID)) {
+ String pictureProfileId = null;
+ try {
+ pictureProfileId = (String) entry.getValue();
+ } catch (ClassCastException e) {
+ throw new IllegalArgumentException(
+ "Cannot cast the KEY_PICTURE_PROFILE_ID parameter to String!");
+ } catch (Exception e) {
+ Log.e(TAG, Log.getStackTraceString(e));
+ throw new IllegalArgumentException("Unexpected exception when casting the "
+ + "KEY_PICTURE_PROFILE_ID parameter!");
+ }
+ if (pictureProfileId == null) {
+ throw new IllegalArgumentException(
+ "KEY_PICTURE_PROFILE_ID parameter is null!");
+ }
+ if (!pictureProfileId.isEmpty()) {
+ keys[i] = MediaFormat.KEY_PICTURE_PROFILE_ID;
+ values[i] = pictureProfileId;
+ }
} else {
keys[i] = entry.getKey();
values[i] = entry.getValue();
@@ -5424,7 +5469,7 @@ final public class MediaCodec {
throw new IllegalArgumentException(
"Cannot cast the instance parameter to PictureProfile!");
} catch (Exception e) {
- android.util.Log.getStackTraceString(e);
+ Log.e(TAG, Log.getStackTraceString(e));
throw new IllegalArgumentException("Unexpected exception when casting the "
+ "instance parameter to PictureProfile!");
}
@@ -5437,6 +5482,26 @@ final public class MediaCodec {
keys[i] = PARAMETER_KEY_PICTURE_PROFILE_HANDLE;
values[i] = Long.valueOf(handle.getId());
}
+ } else if (applyPictureProfiles() && mediaQualityFw()
+ && key.equals(MediaFormat.KEY_PICTURE_PROFILE_ID)) {
+ String pictureProfileId = null;
+ try {
+ pictureProfileId = (String) params.get(key);
+ } catch (ClassCastException e) {
+ throw new IllegalArgumentException(
+ "Cannot cast the KEY_PICTURE_PROFILE_ID parameter to String!");
+ } catch (Exception e) {
+ Log.e(TAG, Log.getStackTraceString(e));
+ throw new IllegalArgumentException("Unexpected exception when casting the "
+ + "KEY_PICTURE_PROFILE_ID parameter!");
+ }
+ if (pictureProfileId == null) {
+ throw new IllegalArgumentException("KEY_PICTURE_PROFILE_ID parameter is null!");
+ }
+ if (!pictureProfileId.isEmpty()) {
+ keys[i] = MediaFormat.KEY_PICTURE_PROFILE_ID;
+ values[i] = pictureProfileId;
+ }
} else {
keys[i] = key;
Object value = params.get(key);
@@ -5455,10 +5520,9 @@ final public class MediaCodec {
}
private void logAndRun(String message, Runnable r) {
- final String TAG = "MediaCodec";
- android.util.Log.d(TAG, "enter: " + message);
+ Log.d(TAG, "enter: " + message);
r.run();
- android.util.Log.d(TAG, "exit : " + message);
+ Log.d(TAG, "exit : " + message);
}
/**
diff --git a/media/java/android/media/flags/media_better_together.aconfig b/media/java/android/media/flags/media_better_together.aconfig
index 7221f1ddeb7f..15e87f80ef64 100644
--- a/media/java/android/media/flags/media_better_together.aconfig
+++ b/media/java/android/media/flags/media_better_together.aconfig
@@ -62,6 +62,16 @@ flag {
}
flag {
+ name: "enable_fix_for_empty_system_routes_crash"
+ namespace: "media_better_together"
+ description: "Fixes a bug causing SystemUI to crash due to an empty system routes list in the routing framework."
+ bug: "357468728"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+flag {
name: "enable_suggested_device_api"
is_exported: true
namespace: "media_better_together"
diff --git a/media/java/android/media/projection/MediaProjectionAppContent.aidl b/media/java/android/media/projection/MediaProjectionAppContent.aidl
new file mode 100644
index 000000000000..6ead69b9fdc6
--- /dev/null
+++ b/media/java/android/media/projection/MediaProjectionAppContent.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media.projection;
+
+parcelable MediaProjectionAppContent; \ No newline at end of file
diff --git a/media/java/android/media/projection/MediaProjectionAppContent.java b/media/java/android/media/projection/MediaProjectionAppContent.java
new file mode 100644
index 000000000000..da0bdc191c0c
--- /dev/null
+++ b/media/java/android/media/projection/MediaProjectionAppContent.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media.projection;
+
+import android.annotation.FlaggedApi;
+import android.graphics.Bitmap;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import androidx.annotation.NonNull;
+
+import java.util.Objects;
+
+/**
+ * Holds information about content an app can share via the MediaProjection APIs.
+ * <p>
+ * An application requesting a {@link MediaProjection session} can add its own content in the
+ * list of available content along with the whole screen or a single application.
+ * <p>
+ * Each instance of {@link MediaProjectionAppContent} contains an id that is used to identify the
+ * content chosen by the user back to the advertising application, thus the meaning of the id is
+ * only relevant to that application.
+ */
+@FlaggedApi(com.android.media.projection.flags.Flags.FLAG_APP_CONTENT_SHARING)
+public final class MediaProjectionAppContent implements Parcelable {
+
+ private final Bitmap mThumbnail;
+ private final CharSequence mTitle;
+ private final int mId;
+
+ /**
+ * Constructor to pass a thumbnail, title and id.
+ *
+ * @param thumbnail The thumbnail representing this content to be shown to the user.
+ * @param title A user visible string representing the title of this content.
+ * @param id An arbitrary int defined by the advertising application to be fed back once
+ * the user made their choice.
+ */
+ public MediaProjectionAppContent(@NonNull Bitmap thumbnail, @NonNull CharSequence title,
+ int id) {
+ mThumbnail = Objects.requireNonNull(thumbnail, "thumbnail can't be null").asShared();
+ mTitle = Objects.requireNonNull(title, "title can't be null");
+ mId = id;
+ }
+
+ /**
+ * Returns thumbnail representing this content to be shown to the user.
+ *
+ * @hide
+ */
+ @NonNull
+ public Bitmap getThumbnail() {
+ return mThumbnail;
+ }
+
+ /**
+ * Returns user visible string representing the title of this content.
+ *
+ * @hide
+ */
+ @NonNull
+ public CharSequence getTitle() {
+ return mTitle;
+ }
+
+ /**
+ * Returns the arbitrary int defined by the advertising application to be fed back once
+ * the user made their choice.
+ *
+ * @hide
+ */
+ public int getId() {
+ return mId;
+ }
+
+ private MediaProjectionAppContent(Parcel in) {
+ mThumbnail = in.readParcelable(this.getClass().getClassLoader(), Bitmap.class);
+ mTitle = in.readCharSequence();
+ mId = in.readInt();
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeParcelable(mThumbnail, flags);
+ dest.writeCharSequence(mTitle);
+ dest.writeInt(mId);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @NonNull
+ public static final Creator<MediaProjectionAppContent> CREATOR =
+ new Creator<>() {
+ @NonNull
+ @Override
+ public MediaProjectionAppContent createFromParcel(@NonNull Parcel in) {
+ return new MediaProjectionAppContent(in);
+ }
+
+ @NonNull
+ @Override
+ public MediaProjectionAppContent[] newArray(int size) {
+ return new MediaProjectionAppContent[size];
+ }
+ };
+}
diff --git a/media/java/android/media/projection/MediaProjectionConfig.java b/media/java/android/media/projection/MediaProjectionConfig.java
index 598b534e81ca..cd674e9f2ad1 100644
--- a/media/java/android/media/projection/MediaProjectionConfig.java
+++ b/media/java/android/media/projection/MediaProjectionConfig.java
@@ -20,23 +20,56 @@ import static android.view.Display.DEFAULT_DISPLAY;
import static java.lang.annotation.RetentionPolicy.SOURCE;
+import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.IntRange;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.annotation.SuppressLint;
import android.os.Parcelable;
-import com.android.internal.util.AnnotationValidations;
+import com.android.media.projection.flags.Flags;
import java.lang.annotation.Retention;
+import java.util.Arrays;
+import java.util.Objects;
/**
* Configure the {@link MediaProjection} session requested from
* {@link MediaProjectionManager#createScreenCaptureIntent(MediaProjectionConfig)}.
+ * <p>
+ * This configuration should be used to provide the user with options for choosing the content to
+ * be shared with the requesting application.
*/
public final class MediaProjectionConfig implements Parcelable {
/**
+ * Bitmask for setting whether this configuration is for projecting the whole display.
+ */
+ @FlaggedApi(Flags.FLAG_APP_CONTENT_SHARING)
+ public static final int PROJECTION_SOURCE_DISPLAY = 1 << 1;
+
+ /**
+ * Bitmask for setting whether this configuration is for projecting the a custom region display.
+ *
+ * @hide
+ */
+ public static final int PROJECTION_SOURCE_DISPLAY_REGION = 1 << 2;
+
+ /**
+ * Bitmask for setting whether this configuration is for projecting the a single application.
+ */
+ @FlaggedApi(Flags.FLAG_APP_CONTENT_SHARING)
+ public static final int PROJECTION_SOURCE_APP = 1 << 3;
+
+ /**
+ * Bitmask for setting whether this configuration is for projecting the content provided by an
+ * application.
+ */
+ @FlaggedApi(com.android.media.projection.flags.Flags.FLAG_APP_CONTENT_SHARING)
+ public static final int PROJECTION_SOURCE_APP_CONTENT = 1 << 4;
+
+ /**
* The user, rather than the host app, determines which region of the display to capture.
*
* @hide
@@ -44,39 +77,109 @@ public final class MediaProjectionConfig implements Parcelable {
public static final int CAPTURE_REGION_USER_CHOICE = 0;
/**
+ * @hide
+ */
+ public static final int DEFAULT_PROJECTION_SOURCES =
+ PROJECTION_SOURCE_DISPLAY | PROJECTION_SOURCE_APP;
+
+ /**
* The host app specifies a particular display to capture.
*
* @hide
*/
public static final int CAPTURE_REGION_FIXED_DISPLAY = 1;
+ private static final int[] PROJECTION_SOURCES =
+ new int[]{PROJECTION_SOURCE_DISPLAY, PROJECTION_SOURCE_DISPLAY_REGION,
+ PROJECTION_SOURCE_APP,
+ PROJECTION_SOURCE_APP_CONTENT};
+
+ private static final String[] PROJECTION_SOURCES_STRING =
+ new String[]{"PROJECTION_SOURCE_DISPLAY", "PROJECTION_SOURCE_DISPLAY_REGION",
+ "PROJECTION_SOURCE_APP", "PROJECTION_SOURCE_APP_CONTENT"};
+
+ private static final int VALID_PROJECTION_SOURCES = createValidSourcesMask();
+
+ private final int mInitialSelection;
+
/** @hide */
@IntDef(prefix = "CAPTURE_REGION_", value = {CAPTURE_REGION_USER_CHOICE,
CAPTURE_REGION_FIXED_DISPLAY})
@Retention(SOURCE)
+ @Deprecated // Remove when FLAG_APP_CONTENT_SHARING is removed
public @interface CaptureRegion {
}
+ /** @hide */
+ @IntDef(flag = true, prefix = "PROJECTION_SOURCE_", value = {PROJECTION_SOURCE_DISPLAY,
+ PROJECTION_SOURCE_DISPLAY_REGION, PROJECTION_SOURCE_APP, PROJECTION_SOURCE_APP_CONTENT})
+ @Retention(SOURCE)
+ public @interface MediaProjectionSource {
+ }
+
/**
- * The particular display to capture. Only used when {@link #getRegionToCapture()} is
- * {@link #CAPTURE_REGION_FIXED_DISPLAY}; ignored otherwise.
+ * The particular display to capture. Only used when {@link #PROJECTION_SOURCE_DISPLAY} is set,
+ * ignored otherwise.
* <p>
* Only supports values of {@link android.view.Display#DEFAULT_DISPLAY}.
*/
@IntRange(from = DEFAULT_DISPLAY, to = DEFAULT_DISPLAY)
- private int mDisplayToCapture;
+ private final int mDisplayToCapture;
/**
* The region to capture. Defaults to the user's choice.
*/
@CaptureRegion
+ @Deprecated // Remove when FLAG_APP_CONTENT_SHARING is removed
private int mRegionToCapture;
/**
+ * The region to capture. Defaults to the user's choice.
+ */
+ @MediaProjectionSource
+ private final int mProjectionSources;
+
+ /**
+ * @see #getRequesterHint()
+ */
+ @Nullable
+ private final String mRequesterHint;
+
+ /**
* Customized instance, with region set to the provided value.
+ * @deprecated To be removed FLAG_APP_CONTENT_SHARING is removed
*/
+ @Deprecated // Remove when FLAG_APP_CONTENT_SHARING is removed
private MediaProjectionConfig(@CaptureRegion int captureRegion) {
+ if (Flags.appContentSharing()) {
+ throw new UnsupportedOperationException(
+ "Flag FLAG_APP_CONTENT_SHARING enabled. This method must not be called.");
+ }
mRegionToCapture = captureRegion;
+ mDisplayToCapture = DEFAULT_DISPLAY;
+
+ mRequesterHint = null;
+ mInitialSelection = -1;
+ mProjectionSources = -1;
+ }
+
+ /**
+ * Customized instance, with region set to the provided value.
+ */
+ private MediaProjectionConfig(@MediaProjectionSource int projectionSource,
+ @Nullable String requesterHint, int displayId, int initialSelection) {
+ if (!Flags.appContentSharing()) {
+ throw new UnsupportedOperationException(
+ "Flag FLAG_APP_CONTENT_SHARING disabled. This method must not be called");
+ }
+ if (projectionSource == 0) {
+ mProjectionSources = DEFAULT_PROJECTION_SOURCES;
+ } else {
+ mProjectionSources = projectionSource;
+ }
+ mRequesterHint = requesterHint;
+ mDisplayToCapture = displayId;
+ mInitialSelection = initialSelection;
}
/**
@@ -84,16 +187,17 @@ public final class MediaProjectionConfig implements Parcelable {
*/
@NonNull
public static MediaProjectionConfig createConfigForDefaultDisplay() {
- MediaProjectionConfig config = new MediaProjectionConfig(CAPTURE_REGION_FIXED_DISPLAY);
- config.mDisplayToCapture = DEFAULT_DISPLAY;
- return config;
+ if (Flags.appContentSharing()) {
+ return new Builder().setSourceEnabled(PROJECTION_SOURCE_DISPLAY, true).build();
+ } else {
+ return new MediaProjectionConfig(CAPTURE_REGION_FIXED_DISPLAY);
+ }
}
/**
* Returns an instance which allows the user to decide which region is captured. The consent
* dialog presents the user with all possible options. If the user selects display capture,
* then only the {@link android.view.Display#DEFAULT_DISPLAY} is supported.
- *
* <p>
* When passed in to
* {@link MediaProjectionManager#createScreenCaptureIntent(MediaProjectionConfig)}, the consent
@@ -103,13 +207,18 @@ public final class MediaProjectionConfig implements Parcelable {
*/
@NonNull
public static MediaProjectionConfig createConfigForUserChoice() {
- return new MediaProjectionConfig(CAPTURE_REGION_USER_CHOICE);
+ if (Flags.appContentSharing()) {
+ return new MediaProjectionConfig.Builder().build();
+ } else {
+ return new MediaProjectionConfig(CAPTURE_REGION_USER_CHOICE);
+ }
}
/**
* Returns string representation of the captured region.
*/
@NonNull
+ @Deprecated // Remove when FLAG_APP_CONTENT_SHARING is removed
private static String captureRegionToString(int value) {
return switch (value) {
case CAPTURE_REGION_USER_CHOICE -> "CAPTURE_REGION_USERS_CHOICE";
@@ -118,16 +227,42 @@ public final class MediaProjectionConfig implements Parcelable {
};
}
+ /**
+ * Returns string representation of the captured region.
+ */
+ @NonNull
+ private static String projectionSourceToString(int value) {
+ StringBuilder stringBuilder = new StringBuilder();
+ for (int i = 0; i < PROJECTION_SOURCES.length; i++) {
+ if ((value & PROJECTION_SOURCES[i]) > 0) {
+ stringBuilder.append(PROJECTION_SOURCES_STRING[i]);
+ stringBuilder.append(" ");
+ value &= ~PROJECTION_SOURCES[i];
+ }
+ }
+ if (value > 0) {
+ stringBuilder.append("Unknown projection sources: ");
+ stringBuilder.append(Integer.toHexString(value));
+ }
+ return stringBuilder.toString();
+ }
+
@Override
public String toString() {
- return "MediaProjectionConfig { " + "displayToCapture = " + mDisplayToCapture + ", "
- + "regionToCapture = " + captureRegionToString(mRegionToCapture) + " }";
+ if (Flags.appContentSharing()) {
+ return ("MediaProjectionConfig{mInitialSelection=%d, mDisplayToCapture=%d, "
+ + "mProjectionSource=%s, mRequesterHint='%s'}").formatted(mInitialSelection,
+ mDisplayToCapture, projectionSourceToString(mProjectionSources),
+ mRequesterHint);
+ } else {
+ return "MediaProjectionConfig { " + "displayToCapture = " + mDisplayToCapture + ", "
+ + "regionToCapture = " + captureRegionToString(mRegionToCapture) + " }";
+ }
}
-
/**
- * The particular display to capture. Only used when {@link #getRegionToCapture()} is
- * {@link #CAPTURE_REGION_FIXED_DISPLAY}; ignored otherwise.
+ * The particular display to capture. Only used when {@link #PROJECTION_SOURCE_DISPLAY} is
+ * set; ignored otherwise.
* <p>
* Only supports values of {@link android.view.Display#DEFAULT_DISPLAY}.
*
@@ -146,27 +281,57 @@ public final class MediaProjectionConfig implements Parcelable {
return mRegionToCapture;
}
+ /**
+ * A bitmask representing of requested projection sources.
+ * <p>
+ * The system supports different kind of media projection session. Although the user is
+ * picking the target content, the requesting application can configure the choices displayed
+ * to the user.
+ */
+ @FlaggedApi(Flags.FLAG_APP_CONTENT_SHARING)
+ public @MediaProjectionSource int getProjectionSources() {
+ return mProjectionSources;
+ }
+
@Override
public boolean equals(@Nullable Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
MediaProjectionConfig that = (MediaProjectionConfig) o;
- return mDisplayToCapture == that.mDisplayToCapture
- && mRegionToCapture == that.mRegionToCapture;
+ if (Flags.appContentSharing()) {
+ return mDisplayToCapture == that.mDisplayToCapture
+ && mProjectionSources == that.mProjectionSources
+ && mInitialSelection == that.mInitialSelection
+ && Objects.equals(mRequesterHint, that.mRequesterHint);
+ } else {
+ return mDisplayToCapture == that.mDisplayToCapture
+ && mRegionToCapture == that.mRegionToCapture;
+ }
}
@Override
public int hashCode() {
int _hash = 1;
- _hash = 31 * _hash + mDisplayToCapture;
- _hash = 31 * _hash + mRegionToCapture;
+ if (Flags.appContentSharing()) {
+ return Objects.hash(mDisplayToCapture, mProjectionSources, mInitialSelection,
+ mRequesterHint);
+ } else {
+ _hash = 31 * _hash + mDisplayToCapture;
+ _hash = 31 * _hash + mRegionToCapture;
+ }
return _hash;
}
@Override
public void writeToParcel(@NonNull android.os.Parcel dest, int flags) {
dest.writeInt(mDisplayToCapture);
- dest.writeInt(mRegionToCapture);
+ if (Flags.appContentSharing()) {
+ dest.writeInt(mProjectionSources);
+ dest.writeString(mRequesterHint);
+ dest.writeInt(mInitialSelection);
+ } else {
+ dest.writeInt(mRegionToCapture);
+ }
}
@Override
@@ -176,12 +341,17 @@ public final class MediaProjectionConfig implements Parcelable {
/** @hide */
/* package-private */ MediaProjectionConfig(@NonNull android.os.Parcel in) {
- int displayToCapture = in.readInt();
- int regionToCapture = in.readInt();
-
- mDisplayToCapture = displayToCapture;
- mRegionToCapture = regionToCapture;
- AnnotationValidations.validate(CaptureRegion.class, null, mRegionToCapture);
+ mDisplayToCapture = in.readInt();
+ if (Flags.appContentSharing()) {
+ mProjectionSources = in.readInt();
+ mRequesterHint = in.readString();
+ mInitialSelection = in.readInt();
+ } else {
+ mRegionToCapture = in.readInt();
+ mProjectionSources = -1;
+ mRequesterHint = null;
+ mInitialSelection = -1;
+ }
}
public static final @NonNull Parcelable.Creator<MediaProjectionConfig> CREATOR =
@@ -196,4 +366,138 @@ public final class MediaProjectionConfig implements Parcelable {
return new MediaProjectionConfig(in);
}
};
+
+ /**
+ * Returns true if the provided source should be enabled.
+ *
+ * @param projectionSource projection source integer to check for. The parameter can also be a
+ * bitmask of multiple sources.
+ */
+ @FlaggedApi(Flags.FLAG_APP_CONTENT_SHARING)
+ public boolean isSourceEnabled(@MediaProjectionSource int projectionSource) {
+ return (mProjectionSources & projectionSource) > 0;
+ }
+
+ /**
+ * Returns a bit mask of one, and only one, of the projection type flag.
+ */
+ @FlaggedApi(Flags.FLAG_APP_CONTENT_SHARING)
+ @MediaProjectionSource
+ public int getInitiallySelectedSource() {
+ return mInitialSelection;
+ }
+
+ /**
+ * A hint set by the requesting app indicating who the requester of this {@link MediaProjection}
+ * session is.
+ * <p>
+ * The UI component prompting the user for the permission to start the session can use
+ * this hint to provide more information about the origin of the request (e.g. a browser
+ * tab title, a meeting id if sharing to a video conferencing app, a player name if
+ * sharing the screen within a game).
+ *
+ * @return the hint to be displayed if set, null otherwise.
+ */
+ @FlaggedApi(Flags.FLAG_APP_CONTENT_SHARING)
+ @Nullable
+ public CharSequence getRequesterHint() {
+ return mRequesterHint;
+ }
+
+ private static int createValidSourcesMask() {
+ int validSources = 0;
+ for (int projectionSource : PROJECTION_SOURCES) {
+ validSources |= projectionSource;
+ }
+ return validSources;
+ }
+
+ @FlaggedApi(Flags.FLAG_APP_CONTENT_SHARING)
+ public static final class Builder {
+ private int mOptions = 0;
+ private String mRequesterHint = null;
+
+ @MediaProjectionSource
+ private int mInitialSelection;
+
+ public Builder() {
+ if (!Flags.appContentSharing()) {
+ throw new UnsupportedOperationException("Flag FLAG_APP_CONTENT_SHARING disabled");
+ }
+ }
+
+ /**
+ * Indicates which projection source the UI component should display to the user
+ * first. Calling this method without enabling the respective choice will have no effect.
+ *
+ * @return instance of this {@link Builder}.
+ * @see #setSourceEnabled(int, boolean)
+ */
+ @NonNull
+ public Builder setInitiallySelectedSource(@MediaProjectionSource int projectionSource) {
+ for (int source : PROJECTION_SOURCES) {
+ if (projectionSource == source) {
+ mInitialSelection = projectionSource;
+ return this;
+ }
+ }
+ throw new IllegalArgumentException(
+ ("projectionSource is no a valid projection source. projectionSource must be "
+ + "one of %s but was %s")
+ .formatted(Arrays.toString(PROJECTION_SOURCES_STRING),
+ projectionSourceToString(projectionSource)));
+ }
+
+ /**
+ * Let the requesting app indicate who the requester of this {@link MediaProjection}
+ * session is..
+ * <p>
+ * The UI component prompting the user for the permission to start the session can use
+ * this hint to provide more information about the origin of the request (e.g. a browser
+ * tab title, a meeting id if sharing to a video conferencing app, a player name if
+ * sharing the screen within a game).
+ * <p>
+ * Note that setting this won't hide or change the name of the application
+ * requesting the session.
+ *
+ * @return instance of this {@link Builder}.
+ */
+ @NonNull
+ public Builder setRequesterHint(@Nullable String requesterHint) {
+ mRequesterHint = requesterHint;
+ return this;
+ }
+
+ /**
+ * Set whether the UI component requesting the user permission to share their screen
+ * should display an option to share the specified source
+ *
+ * @param source the projection source to enable or disable
+ * @param enabled true to enable the source, false otherwise
+ * @return this instance for chaining.
+ * @throws IllegalArgumentException if the source is not one of the valid sources.
+ */
+ @NonNull
+ @SuppressLint("MissingGetterMatchingBuilder") // isSourceEnabled is defined
+ public Builder setSourceEnabled(@MediaProjectionSource int source, boolean enabled) {
+ if ((source & VALID_PROJECTION_SOURCES) == 0) {
+ throw new IllegalArgumentException(
+ ("source is no a valid projection source. source must be "
+ + "any of %s but was %s")
+ .formatted(Arrays.toString(PROJECTION_SOURCES_STRING),
+ projectionSourceToString(source)));
+ }
+ mOptions = enabled ? mOptions | source : mOptions & ~source;
+ return this;
+ }
+
+ /**
+ * Builds a new immutable instance of {@link MediaProjectionConfig}
+ */
+ @NonNull
+ public MediaProjectionConfig build() {
+ return new MediaProjectionConfig(mOptions, mRequesterHint, DEFAULT_DISPLAY,
+ mInitialSelection);
+ }
+ }
}
diff --git a/media/java/android/media/projection/MediaProjectionManager.java b/media/java/android/media/projection/MediaProjectionManager.java
index 9036bf385d96..4a5392d3c0c3 100644
--- a/media/java/android/media/projection/MediaProjectionManager.java
+++ b/media/java/android/media/projection/MediaProjectionManager.java
@@ -29,6 +29,7 @@ import android.compat.annotation.Overridable;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
+import android.hardware.display.VirtualDisplay;
import android.os.Handler;
import android.os.IBinder;
import android.os.RemoteException;
@@ -78,9 +79,12 @@ public final class MediaProjectionManager {
private static final String TAG = "MediaProjectionManager";
/**
- * This change id ensures that users are presented with a choice of capturing a single app
- * or the entire screen when initiating a MediaProjection session, overriding the usage of
- * MediaProjectionConfig#createConfigForDefaultDisplay.
+ * If enabled, this change id ensures that users are presented with a choice of capturing a
+ * single app and the entire screen when initiating a MediaProjection session, overriding the
+ * usage of MediaProjectionConfig#createConfigForDefaultDisplay.
+ * <p>
+ *
+ * <a href=" https://developer.android.com/guide/practices/device-compatibility-mode#override_disable_media_projection_single_app_option">More info</a>
*
* @hide
*/
diff --git a/media/java/android/media/projection/TEST_MAPPING b/media/java/android/media/projection/TEST_MAPPING
index ea62287b7411..62e776b822d2 100644
--- a/media/java/android/media/projection/TEST_MAPPING
+++ b/media/java/android/media/projection/TEST_MAPPING
@@ -4,4 +4,4 @@
"path": "frameworks/base/services/core/java/com/android/server/media/projection"
}
]
-} \ No newline at end of file
+}
diff --git a/media/java/android/media/quality/ActiveProcessingPicture.java b/media/java/android/media/quality/ActiveProcessingPicture.java
index e16ad62e23f2..15c2e47fe820 100644
--- a/media/java/android/media/quality/ActiveProcessingPicture.java
+++ b/media/java/android/media/quality/ActiveProcessingPicture.java
@@ -31,16 +31,26 @@ import androidx.annotation.NonNull;
public final class ActiveProcessingPicture implements Parcelable {
private final int mId;
private final String mProfileId;
+ private final boolean mForGlobal;
public ActiveProcessingPicture(int id, @NonNull String profileId) {
mId = id;
mProfileId = profileId;
+ mForGlobal = true;
+ }
+
+ /** @hide */
+ public ActiveProcessingPicture(int id, @NonNull String profileId, boolean forGlobal) {
+ mId = id;
+ mProfileId = profileId;
+ mForGlobal = forGlobal;
}
/** @hide */
ActiveProcessingPicture(Parcel in) {
mId = in.readInt();
mProfileId = in.readString();
+ mForGlobal = in.readBoolean();
}
@NonNull
@@ -73,6 +83,14 @@ public final class ActiveProcessingPicture implements Parcelable {
return mProfileId;
}
+ /**
+ * @hide
+ */
+ @NonNull
+ public boolean isForGlobal() {
+ return mForGlobal;
+ }
+
@Override
public int describeContents() {
return 0;
@@ -82,5 +100,6 @@ public final class ActiveProcessingPicture implements Parcelable {
public void writeToParcel(@NonNull Parcel dest, int flags) {
dest.writeInt(mId);
dest.writeString(mProfileId);
+ dest.writeBoolean(mForGlobal);
}
}
diff --git a/media/java/android/media/quality/MediaQualityManager.java b/media/java/android/media/quality/MediaQualityManager.java
index bfd01380a2ee..3f0ba3100191 100644
--- a/media/java/android/media/quality/MediaQualityManager.java
+++ b/media/java/android/media/quality/MediaQualityManager.java
@@ -214,11 +214,31 @@ public final class MediaQualityManager {
}
};
+ IActiveProcessingPictureListener apListener = new IActiveProcessingPictureListener.Stub() {
+ @Override
+ public void onActiveProcessingPicturesChanged(List<ActiveProcessingPicture> aps) {
+ List<ActiveProcessingPicture> nonGlobal = new ArrayList<>();
+ for (ActiveProcessingPicture ap : aps) {
+ if (!ap.isForGlobal()) {
+ nonGlobal.add(ap);
+ }
+ }
+ for (ActiveProcessingPictureListenerRecord record : mApListenerRecords) {
+ if (record.mIsGlobal) {
+ record.postActiveProcessingPicturesChanged(aps);
+ } else {
+ record.postActiveProcessingPicturesChanged(nonGlobal);
+ }
+ }
+ }
+ };
+
try {
if (mService != null) {
mService.registerPictureProfileCallback(ppCallback);
mService.registerSoundProfileCallback(spCallback);
mService.registerAmbientBacklightCallback(abCallback);
+ mService.registerActiveProcessingPictureListener(apListener);
}
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
@@ -378,6 +398,18 @@ public final class MediaQualityManager {
}
/**
+ * Gets picture profile handle for TV input.
+ * @hide
+ */
+ public long getPictureProfileForTvInput(String inputId) {
+ try {
+ return mService.getPictureProfileForTvInput(inputId, mUserHandle.getIdentifier());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
* Gets sound profile handle by profile ID.
* @hide
*/
@@ -1213,6 +1245,15 @@ public final class MediaQualityManager {
public Consumer<List<ActiveProcessingPicture>> getListener() {
return mListener;
}
+
+ public void postActiveProcessingPicturesChanged(List<ActiveProcessingPicture> aps) {
+ mExecutor.execute(new Runnable() {
+ @Override
+ public void run() {
+ mListener.accept(aps);
+ }
+ });
+ }
}
/**
diff --git a/media/java/android/media/quality/PictureProfile.java b/media/java/android/media/quality/PictureProfile.java
index 8a585efe032c..0121193a7c86 100644
--- a/media/java/android/media/quality/PictureProfile.java
+++ b/media/java/android/media/quality/PictureProfile.java
@@ -18,6 +18,7 @@ package android.media.quality;
import android.annotation.FlaggedApi;
import android.annotation.IntDef;
+import android.annotation.StringDef;
import android.annotation.SystemApi;
import android.media.tv.TvInputInfo;
import android.media.tv.flags.Flags;
@@ -72,6 +73,19 @@ public final class PictureProfile implements Parcelable {
*/
public static final int TYPE_APPLICATION = 2;
+ /**
+ * Default profile name
+ * @hide
+ */
+ public static final String NAME_DEFAULT = "default";
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @StringDef(prefix = "NAME_", value = {
+ NAME_DEFAULT
+ })
+ public @interface ProfileName {}
+
/** @hide */
@Retention(RetentionPolicy.SOURCE)
@IntDef(flag = false, prefix = "ERROR_", value = {
@@ -114,6 +128,32 @@ public final class PictureProfile implements Parcelable {
*/
public static final int ERROR_NOT_ALLOWLISTED = 4;
+ /**
+ * SDR status.
+ * @hide
+ */
+ public static final String STATUS_SDR = "SDR";
+
+ /**
+ * HDR status.
+ * @hide
+ */
+ public static final String STATUS_HDR = "HDR";
+
+ /** @hide */
+ public static final String NAME_STANDARD = "standard";
+ /** @hide */
+ public static final String NAME_VIVID = "vivid";
+ /** @hide */
+ public static final String NAME_SPORTS = "sports";
+ /** @hide */
+ public static final String NAME_GAME = "game";
+ /** @hide */
+ public static final String NAME_MOVIE = "movie";
+ /** @hide */
+ public static final String NAME_ENERGY_SAVING = "energy_saving";
+ /** @hide */
+ public static final String NAME_USER = "user";
private PictureProfile(@NonNull Parcel in) {
mId = in.readString();
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/shared/model/SmartspaceMediaLoadingModel.kt b/media/java/android/media/quality/aidl/android/media/quality/IActiveProcessingPictureListener.aidl
index 90ddadf09d1d..f7d19baac7a1 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/shared/model/SmartspaceMediaLoadingModel.kt
+++ b/media/java/android/media/quality/aidl/android/media/quality/IActiveProcessingPictureListener.aidl
@@ -14,22 +14,15 @@
* limitations under the License.
*/
-package com.android.systemui.media.controls.shared.model
-/** Models smartspace media loading state. */
-sealed class SmartspaceMediaLoadingModel {
+package android.media.quality;
- abstract val key: String
+import android.media.quality.ActiveProcessingPicture;
- /** Smartspace media has been loaded. */
- data class Loaded(
- override val key: String,
- val isPrioritized: Boolean = false,
- ) : SmartspaceMediaLoadingModel()
-
- /** Smartspace media has been removed. */
- data class Removed(
- override val key: String,
- val immediatelyUpdateUi: Boolean = true,
- ) : SmartspaceMediaLoadingModel()
+/**
+ * Interface to receive event from media quality service.
+ * @hide
+ */
+oneway interface IActiveProcessingPictureListener {
+ void onActiveProcessingPicturesChanged(in List<ActiveProcessingPicture> ap);
}
diff --git a/media/java/android/media/quality/aidl/android/media/quality/IMediaQualityManager.aidl b/media/java/android/media/quality/aidl/android/media/quality/IMediaQualityManager.aidl
index b5afa6afa5e0..ff1bf0228474 100644
--- a/media/java/android/media/quality/aidl/android/media/quality/IMediaQualityManager.aidl
+++ b/media/java/android/media/quality/aidl/android/media/quality/IMediaQualityManager.aidl
@@ -17,6 +17,7 @@
package android.media.quality;
import android.media.quality.AmbientBacklightSettings;
+import android.media.quality.IActiveProcessingPictureListener;
import android.media.quality.IAmbientBacklightCallback;
import android.media.quality.IPictureProfileCallback;
import android.media.quality.ISoundProfileCallback;
@@ -47,7 +48,13 @@ interface IMediaQualityManager {
void setPictureProfileAllowList(in List<String> packages, int userId);
List<PictureProfileHandle> getPictureProfileHandle(in String[] id, int userId);
- SoundProfile createSoundProfile(in SoundProfile pp, int userId);
+ long getPictureProfileHandleValue(in String id, int userId);
+ long getDefaultPictureProfileHandleValue(int userId);
+ void notifyPictureProfileHandleSelection(in long handle, int userId);
+
+ long getPictureProfileForTvInput(in String inputId, int userId);
+
+ void createSoundProfile(in SoundProfile pp, int userId);
void updateSoundProfile(in String id, in SoundProfile pp, int userId);
void removeSoundProfile(in String id, int userId);
boolean setDefaultSoundProfile(in String id, int userId);
@@ -64,6 +71,7 @@ interface IMediaQualityManager {
void registerPictureProfileCallback(in IPictureProfileCallback cb);
void registerSoundProfileCallback(in ISoundProfileCallback cb);
void registerAmbientBacklightCallback(in IAmbientBacklightCallback cb);
+ void registerActiveProcessingPictureListener(in IActiveProcessingPictureListener l);
List<ParameterCapability> getParameterCapabilities(in List<String> names, int userId);
diff --git a/media/java/android/media/tv/extension/scan/IScanInterface.aidl b/media/java/android/media/tv/extension/scan/IScanInterface.aidl
index b44d1d243150..ea6e8a1d4104 100644
--- a/media/java/android/media/tv/extension/scan/IScanInterface.aidl
+++ b/media/java/android/media/tv/extension/scan/IScanInterface.aidl
@@ -24,7 +24,7 @@ import android.os.Bundle;
*/
interface IScanInterface {
IBinder createSession(int broadcastType, String countryCode, String operator,
- in IScanListener listener);
+ in IScanListener listener, in Bundle optionalParams);
Bundle getParameters(int broadcastType, String countryCode, String operator,
in Bundle params);
}
diff --git a/media/tests/projection/Android.bp b/media/tests/projection/Android.bp
index 0b02d3cb4250..0b4b7dbbca1f 100644
--- a/media/tests/projection/Android.bp
+++ b/media/tests/projection/Android.bp
@@ -26,6 +26,7 @@ android_test {
"androidx.test.runner",
"androidx.test.rules",
"androidx.test.ext.junit",
+ "flag-junit",
"frameworks-base-testutils",
"mockito-target-extended-minus-junit4",
"platform-test-annotations",
diff --git a/media/tests/projection/src/android/media/projection/MediaProjectionAppContentTest.java b/media/tests/projection/src/android/media/projection/MediaProjectionAppContentTest.java
new file mode 100644
index 000000000000..7e167c63a2a2
--- /dev/null
+++ b/media/tests/projection/src/android/media/projection/MediaProjectionAppContentTest.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media.projection;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.graphics.Bitmap;
+import android.os.Parcel;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class MediaProjectionAppContentTest {
+
+ @Test
+ public void testConstructorAndGetters() {
+ // Create a mock Bitmap
+ Bitmap mockBitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888);
+
+ // Create a MediaProjectionAppContent object
+ MediaProjectionAppContent content = new MediaProjectionAppContent(mockBitmap, "Test Title",
+ 123);
+
+ // Verify the values using getters
+ assertThat(content.getTitle()).isEqualTo("Test Title");
+ assertThat(content.getId()).isEqualTo(123);
+ // Compare bitmap configurations and dimensions
+ assertThat(content.getThumbnail().getConfig()).isEqualTo(mockBitmap.getConfig());
+ assertThat(content.getThumbnail().getWidth()).isEqualTo(mockBitmap.getWidth());
+ assertThat(content.getThumbnail().getHeight()).isEqualTo(mockBitmap.getHeight());
+ }
+
+ @Test
+ public void testParcelable() {
+ // Create a mock Bitmap
+ Bitmap mockBitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888);
+
+ // Create a MediaProjectionAppContent object
+ MediaProjectionAppContent content = new MediaProjectionAppContent(mockBitmap, "Test Title",
+ 123);
+
+ // Parcel and unparcel the object
+ Parcel parcel = Parcel.obtain();
+ content.writeToParcel(parcel, 0);
+ parcel.setDataPosition(0);
+ MediaProjectionAppContent unparceledContent =
+ MediaProjectionAppContent.CREATOR.createFromParcel(parcel);
+
+ // Verify the values of the unparceled object
+ assertThat(unparceledContent.getTitle()).isEqualTo("Test Title");
+ assertThat(unparceledContent.getId()).isEqualTo(123);
+ // Compare bitmap configurations and dimensions
+ assertThat(unparceledContent.getThumbnail().getConfig()).isEqualTo(mockBitmap.getConfig());
+ assertThat(unparceledContent.getThumbnail().getWidth()).isEqualTo(mockBitmap.getWidth());
+ assertThat(unparceledContent.getThumbnail().getHeight()).isEqualTo(mockBitmap.getHeight());
+
+ parcel.recycle();
+ }
+
+ @Test
+ public void testCreatorNewArray() {
+ // Create a new array using the CREATOR
+ MediaProjectionAppContent[] contentArray = MediaProjectionAppContent.CREATOR.newArray(5);
+
+ // Verify that the array is not null and has the correct size
+ assertThat(contentArray).isNotNull();
+ assertThat(contentArray).hasLength(5);
+ }
+}
diff --git a/media/tests/projection/src/android/media/projection/MediaProjectionConfigTest.java b/media/tests/projection/src/android/media/projection/MediaProjectionConfigTest.java
index 2820606958b7..bc0eae1a3ec7 100644
--- a/media/tests/projection/src/android/media/projection/MediaProjectionConfigTest.java
+++ b/media/tests/projection/src/android/media/projection/MediaProjectionConfigTest.java
@@ -18,22 +18,31 @@ package android.media.projection;
import static android.media.projection.MediaProjectionConfig.CAPTURE_REGION_FIXED_DISPLAY;
import static android.media.projection.MediaProjectionConfig.CAPTURE_REGION_USER_CHOICE;
+import static android.media.projection.MediaProjectionConfig.PROJECTION_SOURCE_DISPLAY;
+import static android.media.projection.MediaProjectionConfig.DEFAULT_PROJECTION_SOURCES;
import static android.view.Display.DEFAULT_DISPLAY;
import static com.google.common.truth.Truth.assertThat;
import android.os.Parcel;
import android.platform.test.annotations.Presubmit;
+import android.platform.test.annotations.RequiresFlagsDisabled;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
+import com.android.media.projection.flags.Flags;
+
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
/**
* Tests for the {@link MediaProjectionConfig} class.
- *
+ * <p>
* Build/Install/Run:
* atest MediaProjectionTests:MediaProjectionConfigTest
*/
@@ -41,6 +50,11 @@ import org.junit.runner.RunWith;
@Presubmit
@RunWith(AndroidJUnit4.class)
public class MediaProjectionConfigTest {
+
+ @Rule
+ public final CheckFlagsRule mCheckFlagsRule =
+ DeviceFlagsValueProvider.createCheckFlagsRule();
+
private static final MediaProjectionConfig DISPLAY_CONFIG =
MediaProjectionConfig.createConfigForDefaultDisplay();
private static final MediaProjectionConfig USERS_CHOICE_CONFIG =
@@ -57,17 +71,33 @@ public class MediaProjectionConfigTest {
}
@Test
+ @RequiresFlagsDisabled(Flags.FLAG_APP_CONTENT_SHARING)
public void testCreateDisplayConfig() {
assertThat(DISPLAY_CONFIG.getRegionToCapture()).isEqualTo(CAPTURE_REGION_FIXED_DISPLAY);
assertThat(DISPLAY_CONFIG.getDisplayToCapture()).isEqualTo(DEFAULT_DISPLAY);
}
@Test
+ @RequiresFlagsDisabled(Flags.FLAG_APP_CONTENT_SHARING)
public void testCreateUsersChoiceConfig() {
assertThat(USERS_CHOICE_CONFIG.getRegionToCapture()).isEqualTo(CAPTURE_REGION_USER_CHOICE);
}
@Test
+ @RequiresFlagsEnabled(Flags.FLAG_APP_CONTENT_SHARING)
+ public void testDefaultProjectionSources() {
+ assertThat(USERS_CHOICE_CONFIG.getProjectionSources())
+ .isEqualTo(DEFAULT_PROJECTION_SOURCES);
+ }
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_APP_CONTENT_SHARING)
+ public void testCreateDisplayConfigProjectionSource() {
+ assertThat(DISPLAY_CONFIG.getProjectionSources()).isEqualTo(PROJECTION_SOURCE_DISPLAY);
+ assertThat(DISPLAY_CONFIG.getDisplayToCapture()).isEqualTo(DEFAULT_DISPLAY);
+ }
+
+ @Test
public void testEquals() {
assertThat(MediaProjectionConfig.createConfigForUserChoice()).isEqualTo(
USERS_CHOICE_CONFIG);
diff --git a/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java b/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java
index defbc1142adb..28b891ebc3c9 100644
--- a/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java
+++ b/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java
@@ -596,7 +596,10 @@ public class ExternalStorageProvider extends FileSystemProvider {
}
@Override
- protected void onDocIdDeleted(String docId) {
+ protected void onDocIdDeleted(String docId, boolean shouldRevokeUriPermission) {
+ if (!shouldRevokeUriPermission) {
+ return;
+ }
Uri uri = DocumentsContract.buildDocumentUri(AUTHORITY, docId);
getContext().revokeUriPermission(uri, ~0);
}
diff --git a/packages/SettingsLib/BannerMessagePreference/res/color/settingslib_banner_button_background_normal.xml b/packages/SettingsLib/BannerMessagePreference/res/color/settingslib_banner_button_background_normal.xml
index 8037a8bb75be..8a234fa6ca9e 100644
--- a/packages/SettingsLib/BannerMessagePreference/res/color/settingslib_banner_button_background_normal.xml
+++ b/packages/SettingsLib/BannerMessagePreference/res/color/settingslib_banner_button_background_normal.xml
@@ -17,8 +17,8 @@
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_enabled="false"
- android:alpha="@dimen/material_emphasis_disabled_background" android:color="?attr/colorOnSurface"/>
+ android:alpha="@dimen/material_emphasis_disabled_background" android:color="@color/settingslib_materialColorPrimary"/>
<item android:state_checked="true" android:color="?attr/colorContainerChecked"/>
<item android:state_checkable="true" android:color="?attr/colorContainerUnchecked"/>
- <item android:color="?attr/colorContainer" />
+ <item android:color="@color/settingslib_materialColorPrimary" />
</selector> \ No newline at end of file
diff --git a/packages/SettingsLib/BannerMessagePreference/res/color/settingslib_banner_filled_button_content_high.xml b/packages/SettingsLib/BannerMessagePreference/res/color/settingslib_banner_filled_button_content_high.xml
new file mode 100644
index 000000000000..43b236938956
--- /dev/null
+++ b/packages/SettingsLib/BannerMessagePreference/res/color/settingslib_banner_filled_button_content_high.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2025 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_enabled="false"
+ android:alpha="@dimen/material_emphasis_disabled" android:color="@color/settingslib_colorContentLevel_high"/>
+ <item android:color="@color/settingslib_colorContentLevel_high" />
+</selector> \ No newline at end of file
diff --git a/packages/SettingsLib/BannerMessagePreference/res/color/settingslib_banner_filled_button_content_low.xml b/packages/SettingsLib/BannerMessagePreference/res/color/settingslib_banner_filled_button_content_low.xml
new file mode 100644
index 000000000000..b7a9d7c5175b
--- /dev/null
+++ b/packages/SettingsLib/BannerMessagePreference/res/color/settingslib_banner_filled_button_content_low.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2025 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_enabled="false"
+ android:alpha="@dimen/material_emphasis_disabled" android:color="@color/settingslib_colorContentLevel_low"/>
+ <item android:color="@color/settingslib_colorContentLevel_low" />
+</selector> \ No newline at end of file
diff --git a/packages/SettingsLib/BannerMessagePreference/res/color/settingslib_banner_filled_button_content_medium.xml b/packages/SettingsLib/BannerMessagePreference/res/color/settingslib_banner_filled_button_content_medium.xml
new file mode 100644
index 000000000000..8e41cb03f4d1
--- /dev/null
+++ b/packages/SettingsLib/BannerMessagePreference/res/color/settingslib_banner_filled_button_content_medium.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2025 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_enabled="false"
+ android:alpha="@dimen/material_emphasis_disabled" android:color="@color/settingslib_colorContentLevel_medium"/>
+ <item android:color="@color/settingslib_colorContentLevel_medium" />
+</selector> \ No newline at end of file
diff --git a/packages/SettingsLib/BannerMessagePreference/res/color/settingslib_banner_filled_button_content_normal.xml b/packages/SettingsLib/BannerMessagePreference/res/color/settingslib_banner_filled_button_content_normal.xml
new file mode 100644
index 000000000000..1dd5cdecfffc
--- /dev/null
+++ b/packages/SettingsLib/BannerMessagePreference/res/color/settingslib_banner_filled_button_content_normal.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2025 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_enabled="false"
+ android:alpha="@dimen/material_emphasis_disabled" android:color="@color/settingslib_materialColorOnPrimary"/>
+ <item android:color="@color/settingslib_materialColorOnPrimary" />
+</selector> \ No newline at end of file
diff --git a/packages/SettingsLib/BannerMessagePreference/res/color/settingslib_banner_outline_button_content.xml b/packages/SettingsLib/BannerMessagePreference/res/color/settingslib_banner_outline_button_content.xml
new file mode 100644
index 000000000000..3a06fb38d5d8
--- /dev/null
+++ b/packages/SettingsLib/BannerMessagePreference/res/color/settingslib_banner_outline_button_content.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2025 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_enabled="false"
+ android:alpha="@dimen/material_emphasis_disabled" android:color="@color/settingslib_materialColorOnSurface"/>
+ <item android:color="@color/settingslib_materialColorOnSurface" />
+</selector> \ No newline at end of file
diff --git a/packages/SettingsLib/BannerMessagePreference/res/color/settingslib_banner_outline_button_stroke_normal.xml b/packages/SettingsLib/BannerMessagePreference/res/color/settingslib_banner_outline_button_stroke_normal.xml
new file mode 100644
index 000000000000..8d0b65712d35
--- /dev/null
+++ b/packages/SettingsLib/BannerMessagePreference/res/color/settingslib_banner_outline_button_stroke_normal.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2025 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_enabled="false"
+ android:alpha="@dimen/material_emphasis_disabled_background" android:color="@color/settingslib_materialColorOutline"/>
+ <item android:color="@color/settingslib_materialColorOutline" />
+</selector> \ No newline at end of file
diff --git a/packages/SettingsLib/BannerMessagePreference/res/values-v36/styles_expressive.xml b/packages/SettingsLib/BannerMessagePreference/res/values-v36/styles_expressive.xml
index 09e07ccef683..cd9faecc49c4 100644
--- a/packages/SettingsLib/BannerMessagePreference/res/values-v36/styles_expressive.xml
+++ b/packages/SettingsLib/BannerMessagePreference/res/values-v36/styles_expressive.xml
@@ -64,7 +64,6 @@
<style name="Banner.PositiveButton.SettingsLib.Expressive"
parent="@style/SettingsLibButtonStyle.Expressive.Filled.Extra">
- <item name="android:textColor">?android:attr/textColorPrimaryInverse</item>
<item name="materialSizeOverlay">@style/SizeOverlay.Material3Expressive.Button.Small</item>
</style>
diff --git a/packages/SettingsLib/BannerMessagePreference/src/com/android/settingslib/widget/BannerMessagePreference.java b/packages/SettingsLib/BannerMessagePreference/src/com/android/settingslib/widget/BannerMessagePreference.java
index c90a76a39510..dbd0f6424ff8 100644
--- a/packages/SettingsLib/BannerMessagePreference/src/com/android/settingslib/widget/BannerMessagePreference.java
+++ b/packages/SettingsLib/BannerMessagePreference/src/com/android/settingslib/widget/BannerMessagePreference.java
@@ -58,35 +58,42 @@ public class BannerMessagePreference extends Preference implements GroupSectionD
HIGH(0,
R.color.banner_background_attention_high,
R.color.banner_accent_attention_high,
- R.color.settingslib_banner_button_background_high),
+ R.color.settingslib_banner_button_background_high,
+ R.color.settingslib_banner_filled_button_content_high),
MEDIUM(1,
R.color.banner_background_attention_medium,
R.color.banner_accent_attention_medium,
- R.color.settingslib_banner_button_background_medium),
+ R.color.settingslib_banner_button_background_medium,
+ R.color.settingslib_banner_filled_button_content_medium),
LOW(2,
R.color.banner_background_attention_low,
R.color.banner_accent_attention_low,
- R.color.settingslib_banner_button_background_low),
+ R.color.settingslib_banner_button_background_low,
+ R.color.settingslib_banner_filled_button_content_low),
NORMAL(3,
R.color.banner_background_attention_normal,
R.color.banner_accent_attention_normal,
- R.color.settingslib_banner_button_background_normal);
+ R.color.settingslib_banner_button_background_normal,
+ R.color.settingslib_banner_filled_button_content_normal);
// Corresponds to the enum value of R.attr.attentionLevel
private final int mAttrValue;
@ColorRes private final int mBackgroundColorResId;
@ColorRes private final int mAccentColorResId;
@ColorRes private final int mButtonBackgroundColorResId;
+ @ColorRes private final int mButtonContentColorResId;
AttentionLevel(
int attrValue,
@ColorRes int backgroundColorResId,
@ColorRes int accentColorResId,
- @ColorRes int buttonBackgroundColorResId) {
+ @ColorRes int buttonBackgroundColorResId,
+ @ColorRes int buttonContentColorResId) {
mAttrValue = attrValue;
mBackgroundColorResId = backgroundColorResId;
mAccentColorResId = accentColorResId;
mButtonBackgroundColorResId = buttonBackgroundColorResId;
+ mButtonContentColorResId = buttonContentColorResId;
}
static AttentionLevel fromAttr(int attrValue) {
@@ -109,6 +116,10 @@ public class BannerMessagePreference extends Preference implements GroupSectionD
public @ColorRes int getButtonBackgroundColorResId() {
return mButtonBackgroundColorResId;
}
+
+ public @ColorRes int getButtonContentColorResId() {
+ return mButtonContentColorResId;
+ }
}
private static final String TAG = "BannerPreference";
@@ -181,6 +192,7 @@ public class BannerMessagePreference extends Preference implements GroupSectionD
public void onBindViewHolder(@NonNull PreferenceViewHolder holder) {
super.onBindViewHolder(holder);
final Context context = getContext();
+ final Resources resources = context.getResources();
final TextView titleView = (TextView) holder.findViewById(R.id.banner_title);
CharSequence title = getTitle();
@@ -200,7 +212,7 @@ public class BannerMessagePreference extends Preference implements GroupSectionD
final Resources.Theme theme = context.getTheme();
@ColorInt final int accentColor =
- context.getResources().getColor(mAttentionLevel.getAccentColorResId(), theme);
+ resources.getColor(mAttentionLevel.getAccentColorResId(), theme);
final ImageView iconView = (ImageView) holder.findViewById(R.id.banner_icon);
if (iconView != null) {
@@ -211,9 +223,7 @@ public class BannerMessagePreference extends Preference implements GroupSectionD
} else {
iconView.setVisibility(View.VISIBLE);
iconView.setImageDrawable(
- icon == null
- ? getContext().getDrawable(R.drawable.ic_warning)
- : icon);
+ icon == null ? context.getDrawable(R.drawable.ic_warning) : icon);
if (mAttentionLevel != AttentionLevel.NORMAL
&& !SettingsThemeHelper.isExpressiveTheme(context)) {
iconView.setColorFilter(
@@ -224,14 +234,24 @@ public class BannerMessagePreference extends Preference implements GroupSectionD
if (IS_AT_LEAST_S) {
@ColorInt final int backgroundColor =
- context.getResources().getColor(
- mAttentionLevel.getBackgroundColorResId(), theme);
-
- @ColorInt final int btnBackgroundColor =
- context.getResources().getColor(mAttentionLevel.getButtonBackgroundColorResId(),
- theme);
- ColorStateList strokeColor = context.getResources().getColorStateList(
- mAttentionLevel.getButtonBackgroundColorResId(), theme);
+ resources.getColor(mAttentionLevel.getBackgroundColorResId(), theme);
+
+ ColorStateList btnBackgroundColor =
+ resources.getColorStateList(
+ mAttentionLevel.getButtonBackgroundColorResId(), theme);
+ ColorStateList btnStrokeColor =
+ mAttentionLevel == AttentionLevel.NORMAL
+ ? resources.getColorStateList(
+ R.color.settingslib_banner_outline_button_stroke_normal, theme)
+ : btnBackgroundColor;
+ ColorStateList filledBtnTextColor =
+ resources.getColorStateList(
+ mAttentionLevel.getButtonContentColorResId(), theme);
+ ColorStateList outlineBtnTextColor =
+ mAttentionLevel == AttentionLevel.NORMAL
+ ? btnBackgroundColor
+ : resources.getColorStateList(
+ R.color.settingslib_banner_outline_button_content, theme);
holder.setDividerAllowedAbove(false);
holder.setDividerAllowedBelow(false);
@@ -242,10 +262,10 @@ public class BannerMessagePreference extends Preference implements GroupSectionD
mPositiveButtonInfo.mColor = accentColor;
mNegativeButtonInfo.mColor = accentColor;
- if (mAttentionLevel != AttentionLevel.NORMAL) {
- mPositiveButtonInfo.mBackgroundColor = btnBackgroundColor;
- mNegativeButtonInfo.mStrokeColor = strokeColor;
- }
+ mPositiveButtonInfo.mBackgroundColor = btnBackgroundColor;
+ mPositiveButtonInfo.mTextColor = filledBtnTextColor;
+ mNegativeButtonInfo.mStrokeColor = btnStrokeColor;
+ mNegativeButtonInfo.mTextColor = outlineBtnTextColor;
mDismissButtonInfo.mButton = (ImageButton) holder.findViewById(R.id.banner_dismiss_btn);
mDismissButtonInfo.setUpButton();
@@ -261,8 +281,6 @@ public class BannerMessagePreference extends Preference implements GroupSectionD
headerView.setText(mHeader);
headerView.setVisibility(TextUtils.isEmpty(mHeader) ? View.GONE : View.VISIBLE);
}
-
-
} else {
holder.setDividerAllowedAbove(true);
holder.setDividerAllowedBelow(true);
@@ -567,8 +585,9 @@ public class BannerMessagePreference extends Preference implements GroupSectionD
private boolean mIsVisible = true;
private boolean mIsEnabled = true;
@ColorInt private int mColor;
- @ColorInt private int mBackgroundColor;
+ @Nullable private ColorStateList mBackgroundColor;
@Nullable private ColorStateList mStrokeColor;
+ @Nullable private ColorStateList mTextColor;
void setUpButton() {
if (mButton == null) {
@@ -586,12 +605,15 @@ public class BannerMessagePreference extends Preference implements GroupSectionD
if (IS_AT_LEAST_S) {
if (btn != null && SettingsThemeHelper.isExpressiveTheme(btn.getContext())) {
- if (mBackgroundColor != 0) {
- btn.setBackgroundColor(mBackgroundColor);
+ if (mBackgroundColor != null) {
+ btn.setBackgroundTintList(mBackgroundColor);
}
if (mStrokeColor != null) {
btn.setStrokeColor(mStrokeColor);
}
+ if (mTextColor != null) {
+ btn.setTextColor(mTextColor);
+ }
} else {
mButton.setTextColor(mColor);
}
diff --git a/packages/SettingsLib/Metadata/Android.bp b/packages/SettingsLib/Metadata/Android.bp
index 564c3985264d..8701d3d8daae 100644
--- a/packages/SettingsLib/Metadata/Android.bp
+++ b/packages/SettingsLib/Metadata/Android.bp
@@ -19,4 +19,7 @@ android_library {
"androidx.fragment_fragment",
],
kotlincflags: ["-Xjvm-default=all"],
+ optimize: {
+ proguard_flags_files: ["proguard.pgcfg"],
+ },
}
diff --git a/packages/SettingsLib/Metadata/proguard.pgcfg b/packages/SettingsLib/Metadata/proguard.pgcfg
new file mode 100644
index 000000000000..3a137732a229
--- /dev/null
+++ b/packages/SettingsLib/Metadata/proguard.pgcfg
@@ -0,0 +1,8 @@
+# Preserve names for IPC codec to support unmarshalling Parcelable
+-keepnames class com.android.settingslib.metadata.PreferenceCoordinate {
+ public static final ** CREATOR;
+}
+
+-keepnames class com.android.settingslib.metadata.PreferenceScreenCoordinate {
+ public static final ** CREATOR;
+}
diff --git a/packages/SettingsLib/SelectorWithWidgetPreference/res/layout-v36/settingslib_expressive_preference_selector_with_widget.xml b/packages/SettingsLib/SelectorWithWidgetPreference/res/layout-v36/settingslib_preference_selector_with_widget.xml
index a79d69dbff8c..adaec8524241 100644
--- a/packages/SettingsLib/SelectorWithWidgetPreference/res/layout-v36/settingslib_expressive_preference_selector_with_widget.xml
+++ b/packages/SettingsLib/SelectorWithWidgetPreference/res/layout-v36/settingslib_preference_selector_with_widget.xml
@@ -23,34 +23,20 @@
android:background="?android:attr/selectableItemBackground"
android:gravity="center_vertical"
android:minHeight="?android:attr/listPreferredItemHeightSmall"
+ android:paddingStart="?android:attr/listPreferredItemPaddingStart"
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd">
<LinearLayout
android:id="@android:id/widget_frame"
android:layout_width="wrap_content"
- android:layout_height="match_parent"
- android:paddingEnd="@dimen/settingslib_expressive_space_extrasmall6"
+ android:layout_height="wrap_content"
android:gravity="center"
- android:minWidth="32dp"
+ android:minWidth="@dimen/settingslib_expressive_space_medium3"
+ android:minHeight="@dimen/settingslib_expressive_space_medium3"
+ android:layout_marginEnd="@dimen/settingslib_expressive_space_extrasmall6"
android:orientation="vertical"/>
- <LinearLayout
- android:id="@+id/icon_frame"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:gravity="center_vertical"
- android:minWidth="32dp"
- android:orientation="horizontal"
- android:layout_marginEnd="@dimen/settingslib_expressive_space_small1"
- android:paddingTop="@dimen/settingslib_expressive_space_extrasmall2"
- android:paddingBottom="@dimen/settingslib_expressive_space_extrasmall2">
- <androidx.preference.internal.PreferenceImageView
- android:id="@android:id/icon"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- settings:maxWidth="@dimen/secondary_app_icon_size"
- settings:maxHeight="@dimen/secondary_app_icon_size"/>
- </LinearLayout>
+ <include layout="@layout/settingslib_expressive_preference_icon_frame"/>
<LinearLayout
android:layout_width="0dp"
diff --git a/packages/SettingsLib/SelectorWithWidgetPreference/src/com/android/settingslib/widget/SelectorWithWidgetPreference.java b/packages/SettingsLib/SelectorWithWidgetPreference/src/com/android/settingslib/widget/SelectorWithWidgetPreference.java
index 465b6ccf4d9c..cde8b332f2e7 100644
--- a/packages/SettingsLib/SelectorWithWidgetPreference/src/com/android/settingslib/widget/SelectorWithWidgetPreference.java
+++ b/packages/SettingsLib/SelectorWithWidgetPreference/src/com/android/settingslib/widget/SelectorWithWidgetPreference.java
@@ -238,10 +238,7 @@ public class SelectorWithWidgetPreference extends CheckBoxPreference {
} else {
setWidgetLayoutResource(R.layout.settingslib_preference_widget_radiobutton);
}
- int resID = SettingsThemeHelper.isExpressiveTheme(context)
- ? R.layout.settingslib_expressive_preference_selector_with_widget
- : R.layout.preference_selector_with_widget;
- setLayoutResource(resID);
+ setLayoutResource(R.layout.preference_selector_with_widget);
setIconSpaceReserved(false);
final TypedArray a =
diff --git a/packages/SettingsLib/SettingsSpinner/res/drawable-v36/settings_expressive_spinner_dropdown_background.xml b/packages/SettingsLib/SettingsSpinner/res/drawable-v36/settings_expressive_spinner_dropdown_background.xml
new file mode 100644
index 000000000000..f29f3ae79fa6
--- /dev/null
+++ b/packages/SettingsLib/SettingsSpinner/res/drawable-v36/settings_expressive_spinner_dropdown_background.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2025 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <!-- Highlight the selected item -->
+ <item android:state_activated="true" android:drawable="@drawable/settings_expressive_spinner_dropdown_item_selected"/>
+</selector>
diff --git a/packages/SettingsLib/SettingsSpinner/res/drawable-v36/settings_expressive_spinner_dropdown_item_selected.xml b/packages/SettingsLib/SettingsSpinner/res/drawable-v36/settings_expressive_spinner_dropdown_item_selected.xml
new file mode 100644
index 000000000000..5da3f7172582
--- /dev/null
+++ b/packages/SettingsLib/SettingsSpinner/res/drawable-v36/settings_expressive_spinner_dropdown_item_selected.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.
+ -->
+
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+ android:color="?android:colorControlHighlight">
+ <item>
+ <shape android:shape="rectangle">
+ <solid
+ android:color="@color/settingslib_materialColorPrimaryContainer" />
+ <corners
+ android:radius="@dimen/settingslib_expressive_radius_large2" />
+ </shape>
+ </item>
+</ripple>
diff --git a/packages/SettingsLib/SettingsSpinner/res/layout-v33/settings_spinner_view.xml b/packages/SettingsLib/SettingsSpinner/res/layout-v33/settings_spinner_view.xml
index 1d0c9b941881..3c379bf0162d 100644
--- a/packages/SettingsLib/SettingsSpinner/res/layout-v33/settings_spinner_view.xml
+++ b/packages/SettingsLib/SettingsSpinner/res/layout-v33/settings_spinner_view.xml
@@ -18,6 +18,8 @@
<TextView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@android:id/text1"
+ android:layout_centerVertical="true"
+ android:gravity="center_vertical"
style="@style/SettingsSpinnerTitleBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
diff --git a/packages/SettingsLib/SettingsSpinner/res/layout-v36/settings_expressive_spinner_preference_full.xml b/packages/SettingsLib/SettingsSpinner/res/layout-v36/settings_expressive_spinner_preference_full.xml
new file mode 100644
index 000000000000..6d1057c8780b
--- /dev/null
+++ b/packages/SettingsLib/SettingsSpinner/res/layout-v36/settings_expressive_spinner_preference_full.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.
+-->
+<RelativeLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:minHeight="?android:attr/listPreferredItemHeight"
+ android:paddingStart="?android:attr/listPreferredItemPaddingStart"
+ android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+ android:filterTouchesWhenObscured="true">
+ <Spinner
+ android:id="@+id/spinner"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_centerVertical="true"/>
+</RelativeLayout>
diff --git a/packages/SettingsLib/SettingsSpinner/res/layout-v36/settings_expressive_spinner_preference_full_outlined.xml b/packages/SettingsLib/SettingsSpinner/res/layout-v36/settings_expressive_spinner_preference_full_outlined.xml
new file mode 100644
index 000000000000..217d1431cd18
--- /dev/null
+++ b/packages/SettingsLib/SettingsSpinner/res/layout-v36/settings_expressive_spinner_preference_full_outlined.xml
@@ -0,0 +1,33 @@
+<?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.
+-->
+
+<RelativeLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:minHeight="?android:attr/listPreferredItemHeight"
+ android:paddingStart="?android:attr/listPreferredItemPaddingStart"
+ android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+ android:filterTouchesWhenObscured="true">
+
+ <Spinner
+ android:id="@+id/spinner"
+ style="@style/SettingslibSpinnerStyle.Expressive.Outlined"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_centerVertical="true"/>
+</RelativeLayout>
diff --git a/packages/SettingsLib/SettingsSpinner/res/layout-v36/settings_expressive_spinner_preference_outlined.xml b/packages/SettingsLib/SettingsSpinner/res/layout-v36/settings_expressive_spinner_preference_outlined.xml
new file mode 100644
index 000000000000..3aefb887cedb
--- /dev/null
+++ b/packages/SettingsLib/SettingsSpinner/res/layout-v36/settings_expressive_spinner_preference_outlined.xml
@@ -0,0 +1,33 @@
+<?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.
+-->
+
+<RelativeLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:minHeight="?android:attr/listPreferredItemHeight"
+ android:paddingStart="?android:attr/listPreferredItemPaddingStart"
+ android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+ android:filterTouchesWhenObscured="true">
+
+ <Spinner
+ android:id="@+id/spinner"
+ style="@style/SettingslibSpinnerStyle.Expressive.Outlined"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_centerVertical="true"/>
+</RelativeLayout>
diff --git a/packages/SettingsLib/SettingsSpinner/res/layout-v36/settings_expressive_spinner_view_full.xml b/packages/SettingsLib/SettingsSpinner/res/layout-v36/settings_expressive_spinner_view_full.xml
new file mode 100644
index 000000000000..d3832f786ccb
--- /dev/null
+++ b/packages/SettingsLib/SettingsSpinner/res/layout-v36/settings_expressive_spinner_view_full.xml
@@ -0,0 +1,22 @@
+<?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.
+-->
+<TextView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@android:id/text1"
+ android:layout_centerVertical="true"
+ android:gravity="center_vertical"
+ style="@style/SettingsSpinnerTitleBar.Expressive.Large"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:filterTouchesWhenObscured="true"/>
diff --git a/packages/SettingsLib/SettingsSpinner/res/layout-v36/settings_expressive_spinner_view_full_outlined.xml b/packages/SettingsLib/SettingsSpinner/res/layout-v36/settings_expressive_spinner_view_full_outlined.xml
new file mode 100644
index 000000000000..2c172e955a09
--- /dev/null
+++ b/packages/SettingsLib/SettingsSpinner/res/layout-v36/settings_expressive_spinner_view_full_outlined.xml
@@ -0,0 +1,26 @@
+<?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.
+-->
+
+<TextView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@android:id/text1"
+ android:layout_centerVertical="true"
+ android:gravity="center_vertical"
+ style="@style/SettingsSpinnerTitleBar.Expressive.Large.Outlined"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:filterTouchesWhenObscured="true"/>
diff --git a/packages/SettingsLib/SettingsSpinner/res/layout-v36/settings_expressive_spinner_view_large.xml b/packages/SettingsLib/SettingsSpinner/res/layout-v36/settings_expressive_spinner_view_large.xml
new file mode 100644
index 000000000000..3e7f0fa7ca4f
--- /dev/null
+++ b/packages/SettingsLib/SettingsSpinner/res/layout-v36/settings_expressive_spinner_view_large.xml
@@ -0,0 +1,22 @@
+<?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.
+-->
+<TextView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@android:id/text1"
+ android:layout_centerVertical="true"
+ android:gravity="center_vertical"
+ style="@style/SettingsSpinnerTitleBar.Expressive.Large"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:filterTouchesWhenObscured="true"/>
diff --git a/packages/SettingsLib/SettingsSpinner/res/layout-v36/settings_expressive_spinner_view_large_outlined.xml b/packages/SettingsLib/SettingsSpinner/res/layout-v36/settings_expressive_spinner_view_large_outlined.xml
new file mode 100644
index 000000000000..6601c8cd97a5
--- /dev/null
+++ b/packages/SettingsLib/SettingsSpinner/res/layout-v36/settings_expressive_spinner_view_large_outlined.xml
@@ -0,0 +1,26 @@
+<?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.
+-->
+
+<TextView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@android:id/text1"
+ android:layout_centerVertical="true"
+ android:gravity="center_vertical"
+ style="@style/SettingsSpinnerTitleBar.Expressive.Large.Outlined"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:filterTouchesWhenObscured="true"/>
diff --git a/packages/SettingsLib/SettingsSpinner/res/layout-v36/settings_expressvie_spinner_dropdown_view.xml b/packages/SettingsLib/SettingsSpinner/res/layout-v36/settings_expressvie_spinner_dropdown_view.xml
new file mode 100644
index 000000000000..acf2a0dd5858
--- /dev/null
+++ b/packages/SettingsLib/SettingsSpinner/res/layout-v36/settings_expressvie_spinner_dropdown_view.xml
@@ -0,0 +1,36 @@
+<?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.
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ style="@style/SettingsSpinnerDropdown.Expressive">
+ <ImageView
+ android:id="@android:id/icon"
+ android:layout_width="@dimen/settingslib_expressive_space_small3"
+ android:layout_height="@dimen/settingslib_expressive_space_small3"
+ android:importantForAccessibility="no"
+ android:src="@drawable/settingslib_expressive_icon_check"
+ android:tint="@color/settingslib_spinner_dropdown_color"
+ android:layout_gravity="center_vertical"
+ android:layout_marginEnd="@dimen/settingslib_expressive_space_extrasmall4"
+ android:scaleType="centerInside"/>
+
+ <TextView
+ android:id="@android:id/text1"
+ style="@style/SettingsSpinnerDropdownText"
+ android:gravity="center_vertical"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:filterTouchesWhenObscured="true"/>
+</LinearLayout>
diff --git a/packages/SettingsLib/SettingsSpinner/res/layout-v36/settings_expressvie_spinner_view.xml b/packages/SettingsLib/SettingsSpinner/res/layout-v36/settings_expressvie_spinner_view.xml
new file mode 100644
index 000000000000..e300099ee298
--- /dev/null
+++ b/packages/SettingsLib/SettingsSpinner/res/layout-v36/settings_expressvie_spinner_view.xml
@@ -0,0 +1,22 @@
+<?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.
+-->
+<TextView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@android:id/text1"
+ android:layout_centerVertical="true"
+ android:gravity="center_vertical"
+ style="@style/SettingsSpinnerTitleBar.Expressive"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:filterTouchesWhenObscured="true"/>
diff --git a/packages/SettingsLib/SettingsSpinner/res/layout-v36/settings_expressvie_spinner_view_outlined.xml b/packages/SettingsLib/SettingsSpinner/res/layout-v36/settings_expressvie_spinner_view_outlined.xml
new file mode 100644
index 000000000000..73e254e9bc15
--- /dev/null
+++ b/packages/SettingsLib/SettingsSpinner/res/layout-v36/settings_expressvie_spinner_view_outlined.xml
@@ -0,0 +1,26 @@
+<?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.
+-->
+
+<TextView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@android:id/text1"
+ android:layout_centerVertical="true"
+ android:gravity="center_vertical"
+ style="@style/SettingsSpinnerTitleBar.Expressive.Outlined"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:filterTouchesWhenObscured="true"/>
diff --git a/packages/SettingsLib/SettingsSpinner/res/values-v33/styles.xml b/packages/SettingsLib/SettingsSpinner/res/values-v33/styles.xml
index 6e26ae180685..2d720d210def 100644
--- a/packages/SettingsLib/SettingsSpinner/res/values-v33/styles.xml
+++ b/packages/SettingsLib/SettingsSpinner/res/values-v33/styles.xml
@@ -27,6 +27,7 @@
<item name="android:paddingEnd">36dp</item>
<item name="android:paddingTop">@dimen/spinner_padding_top_or_bottom</item>
<item name="android:paddingBottom">@dimen/spinner_padding_top_or_bottom</item>
+ <item name="android:filterTouchesWhenObscured">true</item>
</style>
<style name="SettingsSpinnerDropdown">
@@ -40,5 +41,6 @@
<item name="android:paddingEnd">36dp</item>
<item name="android:paddingTop">@dimen/spinner_padding_top_or_bottom</item>
<item name="android:paddingBottom">@dimen/spinner_padding_top_or_bottom</item>
+ <item name="android:filterTouchesWhenObscured">true</item>
</style>
</resources>
diff --git a/packages/SettingsLib/SettingsSpinner/res/values-v36/attr.xml b/packages/SettingsLib/SettingsSpinner/res/values-v36/attr.xml
new file mode 100644
index 000000000000..154149acf26d
--- /dev/null
+++ b/packages/SettingsLib/SettingsSpinner/res/values-v36/attr.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2025 The Android Open Source Project
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+ http://www.apache.org/licenses/LICENSE-2.0
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<resources>
+ <attr name="SettingsSpinnerPreferenceStyle" format="reference"/>
+ <declare-styleable name="SettingsSpinnerPreference">
+ <attr name="style" format="enum">
+ <enum name="normal" value="0"/>
+ <enum name="large" value="1"/>
+ <enum name="full" value="2"/>
+ <enum name="outlined" value="3"/>
+ <enum name="large_outlined" value="4"/>
+ <enum name="full_outlined" value="5"/>
+ </attr>
+ </declare-styleable>
+</resources>
diff --git a/packages/SettingsLib/SettingsSpinner/res/values-v36/styles.xml b/packages/SettingsLib/SettingsSpinner/res/values-v36/styles.xml
new file mode 100644
index 000000000000..2cb4518af287
--- /dev/null
+++ b/packages/SettingsLib/SettingsSpinner/res/values-v36/styles.xml
@@ -0,0 +1,59 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2025 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources>
+ <style name="SettingsSpinnerTitleBar.Expressive">
+ <item name="android:textAppearance">@style/TextAppearance.SettingsLib.LabelLarge</item>
+ <item name="android:textColor">@color/settingslib_materialColorOnSecondaryContainer</item>
+ <item name="android:maxLines">1</item>
+ <item name="android:ellipsize">marquee</item>
+ <item name="android:minHeight">@dimen/settingslib_expressive_space_medium3</item>
+ <item name="android:paddingHorizontal">@dimen/settingslib_expressive_space_small1</item>
+ <item name="android:paddingVertical">@dimen/settingslib_expressive_space_extrasmall5</item>
+ </style>
+
+ <style name="SettingsSpinnerTitleBar.Expressive.Large">
+ <item name="android:textAppearance">@style/TextAppearance.SettingsLib.TitleMedium</item>
+ <item name="android:minHeight">@dimen/settingslib_expressive_space_medium5</item>
+ <item name="android:paddingHorizontal">@dimen/settingslib_expressive_space_small4</item>
+ <item name="android:paddingVertical">@dimen/settingslib_expressive_space_small1</item>
+ </style>
+
+ <style name="SettingsSpinnerTitleBar.Expressive.Outlined">
+ <item name="android:textColor">@color/settingslib_materialColorPrimary</item>
+ </style>
+
+ <style name="SettingsSpinnerTitleBar.Expressive.Large.Outlined">
+ <item name="android:textColor">@color/settingslib_materialColorPrimary</item>
+ </style>
+
+ <style name="SettingsSpinnerDropdown.Expressive">
+ <item name="android:background">@drawable/settings_expressive_spinner_dropdown_background</item>
+ <item name="android:minHeight">@dimen/spinner_dropdown_height</item>
+ <item name="android:paddingStart">@dimen/settingslib_expressive_space_extrasmall4</item>
+ <item name="android:paddingEnd">@dimen/settingslib_expressive_space_extrasmall4</item>
+ <item name="android:paddingTop">@dimen/settingslib_expressive_space_extrasmall7</item>
+ <item name="android:paddingBottom">@dimen/settingslib_expressive_space_extrasmall7</item>
+ </style>
+
+ <style name="SettingsSpinnerDropdownText">
+ <item name="android:textAppearance">@style/TextAppearance.SettingsLib.LabelLarge</item>
+ <item name="android:textColor">@color/settingslib_spinner_dropdown_color</item>
+ <item name="android:maxLines">1</item>
+ <item name="android:ellipsize">marquee</item>
+ </style>
+</resources>
diff --git a/packages/SettingsLib/SettingsSpinner/src/com/android/settingslib/widget/SettingsSpinnerAdapter.java b/packages/SettingsLib/SettingsSpinner/src/com/android/settingslib/widget/SettingsSpinnerAdapter.java
index f33cacd36c6d..2f9f7038f6f7 100644
--- a/packages/SettingsLib/SettingsSpinner/src/com/android/settingslib/widget/SettingsSpinnerAdapter.java
+++ b/packages/SettingsLib/SettingsSpinner/src/com/android/settingslib/widget/SettingsSpinnerAdapter.java
@@ -22,7 +22,13 @@ import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
+import android.widget.ImageView;
+import android.widget.TextView;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import com.android.settingslib.widget.SettingsSpinnerPreference.Style;
import com.android.settingslib.widget.spinner.R;
+
/**
* An ArrayAdapter which was used by Spinner with settings style.
* @param <T> the data type to be loaded.
@@ -30,8 +36,13 @@ import com.android.settingslib.widget.spinner.R;
public class SettingsSpinnerAdapter<T> extends ArrayAdapter<T> {
private static final int DEFAULT_RESOURCE = R.layout.settings_spinner_view;
- private static final int DFAULT_DROPDOWN_RESOURCE = R.layout.settings_spinner_dropdown_view;
+ private static final int DEFAULT_DROPDOWN_RESOURCE = R.layout.settings_spinner_dropdown_view;
+ private static final int DEFAULT_EXPRESSIVE_RESOURCE =
+ R.layout.settings_expressvie_spinner_view;
+ private static final int DEFAULT_EXPRESSIVE_DROPDOWN_RESOURCE =
+ R.layout.settings_expressvie_spinner_dropdown_view;
private final LayoutInflater mDefaultInflater;
+ private int mSelectedPosition = -1;
/**
* Constructs a new SettingsSpinnerAdapter with the given context.
@@ -41,17 +52,74 @@ public class SettingsSpinnerAdapter<T> extends ArrayAdapter<T> {
* access the current theme, resources, etc.
*/
public SettingsSpinnerAdapter(Context context) {
- super(context, getDefaultResource());
+ super(context, getDefaultResource(context, Style.NORMAL));
+
+ setDropDownViewResource(getDropdownResource(context));
+ mDefaultInflater = LayoutInflater.from(context);
+ }
+
+ @Override
+ public View getDropDownView(
+ int position, @Nullable View convertView, @NonNull ViewGroup parent) {
+ View view;
+ if (convertView == null) {
+ view =
+ mDefaultInflater.inflate(
+ getDropdownResource(getContext()), parent, false /* attachToRoot */);
+ } else {
+ view = convertView;
+ }
+ TextView textView = view.findViewById(android.R.id.text1);
+ ImageView iconView = view.findViewById(android.R.id.icon);
+ iconView.setVisibility((position == mSelectedPosition) ? View.VISIBLE : View.GONE);
+ String item = (String) getItem(position);
+ textView.setText(item);
+ return view;
+ }
- setDropDownViewResource(getDropdownResource());
+ public void setSelectedPosition(int pos) {
+ mSelectedPosition = pos;
+ }
+
+ public SettingsSpinnerAdapter(Context context, SettingsSpinnerPreference.Style style) {
+ super(context, getDefaultResource(context, style));
+
+ setDropDownViewResource(getDropdownResource(context));
mDefaultInflater = LayoutInflater.from(context);
}
+ private static int getDefaultResourceWithStyle(Style style) {
+ switch (style) {
+ case NORMAL -> {
+ return DEFAULT_EXPRESSIVE_RESOURCE;
+ }
+ case LARGE -> {
+ return R.layout.settings_expressive_spinner_view_large;
+ }
+ case FULL_WIDTH -> {
+ return R.layout.settings_expressive_spinner_view_full;
+ }
+ case OUTLINED -> {
+ return R.layout.settings_expressvie_spinner_view_outlined;
+ }
+ case LARGE_OUTLINED -> {
+ return R.layout.settings_expressive_spinner_view_large_outlined;
+ }
+ case FULL_OUTLINED -> {
+ return R.layout.settings_expressive_spinner_view_full_outlined;
+ }
+ default -> {
+ return DEFAULT_RESOURCE;
+ }
+ }
+ }
+
/**
* In overridded {@link #getView(int, View, ViewGroup)}, use this method to get default view.
*/
public View getDefaultView(int position, View convertView, ViewGroup parent) {
- return mDefaultInflater.inflate(getDefaultResource(), parent, false /* attachToRoot */);
+ return mDefaultInflater.inflate(
+ getDefaultResource(getContext(), Style.NORMAL), parent, false /* attachToRoot */);
}
/**
@@ -59,15 +127,21 @@ public class SettingsSpinnerAdapter<T> extends ArrayAdapter<T> {
* drop down view.
*/
public View getDefaultDropDownView(int position, View convertView, ViewGroup parent) {
- return mDefaultInflater.inflate(getDropdownResource(), parent, false /* attachToRoot */);
+ return mDefaultInflater.inflate(
+ getDropdownResource(getContext()), parent, false /* attachToRoot */);
}
- private static int getDefaultResource() {
+ private static int getDefaultResource(Context context, Style style) {
+ int resId = SettingsThemeHelper.isExpressiveTheme(context)
+ ? getDefaultResourceWithStyle(style) : DEFAULT_DROPDOWN_RESOURCE;
return (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
- ? DEFAULT_RESOURCE : android.R.layout.simple_spinner_dropdown_item;
+ ? resId : android.R.layout.simple_spinner_dropdown_item;
}
- private static int getDropdownResource() {
+
+ private static int getDropdownResource(Context context) {
+ int resId = SettingsThemeHelper.isExpressiveTheme(context)
+ ? DEFAULT_EXPRESSIVE_DROPDOWN_RESOURCE : DEFAULT_DROPDOWN_RESOURCE;
return (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
- ? DFAULT_DROPDOWN_RESOURCE : android.R.layout.simple_spinner_dropdown_item;
+ ? resId : android.R.layout.simple_spinner_dropdown_item;
}
}
diff --git a/packages/SettingsLib/SettingsSpinner/src/com/android/settingslib/widget/SettingsSpinnerPreference.java b/packages/SettingsLib/SettingsSpinner/src/com/android/settingslib/widget/SettingsSpinnerPreference.java
index 1170f1e7c695..b357369155b6 100644
--- a/packages/SettingsLib/SettingsSpinner/src/com/android/settingslib/widget/SettingsSpinnerPreference.java
+++ b/packages/SettingsLib/SettingsSpinner/src/com/android/settingslib/widget/SettingsSpinnerPreference.java
@@ -17,12 +17,15 @@
package com.android.settingslib.widget;
import android.content.Context;
+import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.view.View;
import android.view.accessibility.AccessibilityEvent;
import android.widget.AdapterView;
import android.widget.Spinner;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import androidx.preference.Preference;
import androidx.preference.Preference.OnPreferenceClickListener;
import androidx.preference.PreferenceViewHolder;
@@ -44,29 +47,28 @@ public class SettingsSpinnerPreference extends Preference
/**
* Perform inflation from XML and apply a class-specific base style.
*
- * @param context The {@link Context} this is associated with, through which it can
- * access the current theme, resources, {@link SharedPreferences}, etc.
- * @param attrs The attributes of the XML tag that is inflating the preference
+ * @param context The {@link Context} this is associated with, through which it can access the
+ * current theme, resources, {@link SharedPreferences}, etc.
+ * @param attrs The attributes of the XML tag that is inflating the preference
* @param defStyle An attribute in the current theme that contains a reference to a style
- * resource that supplies default values for the view. Can be 0 to not
- * look for defaults.
+ * resource that supplies default values for the view. Can be 0 to not look for defaults.
*/
public SettingsSpinnerPreference(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
- setLayoutResource(R.layout.settings_spinner_preference);
+ initAttributes(context, attrs, defStyle);
setOnPreferenceClickListener(this);
}
/**
* Perform inflation from XML and apply a class-specific base style.
*
- * @param context The {@link Context} this is associated with, through which it can
- * access the current theme, resources, {@link SharedPreferences}, etc.
- * @param attrs The attributes of the XML tag that is inflating the preference
+ * @param context The {@link Context} this is associated with, through which it can access the
+ * current theme, resources, {@link SharedPreferences}, etc.
+ * @param attrs The attributes of the XML tag that is inflating the preference
*/
- public SettingsSpinnerPreference(Context context, AttributeSet attrs) {
+ public SettingsSpinnerPreference(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
- setLayoutResource(R.layout.settings_spinner_preference);
+ initAttributes(context, attrs, 0);
setOnPreferenceClickListener(this);
}
@@ -75,8 +77,36 @@ public class SettingsSpinnerPreference extends Preference
*
* @param context The Context this is associated with.
*/
- public SettingsSpinnerPreference(Context context) {
+ public SettingsSpinnerPreference(@NonNull Context context) {
this(context, null);
+ initAttributes(context, null, 0);
+ }
+
+ public enum Style {
+ NORMAL,
+ LARGE,
+ FULL_WIDTH,
+ OUTLINED,
+ LARGE_OUTLINED,
+ FULL_OUTLINED,
+ }
+
+ private void initAttributes(
+ @NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+ int layoutRes = R.layout.settings_spinner_preference;
+ try (TypedArray a =
+ context.obtainStyledAttributes(
+ attrs, R.styleable.SettingsSpinnerPreference, defStyleAttr, 0)) {
+ int style = a.getInteger(R.styleable.SettingsSpinnerPreference_style, 0);
+ switch (style) {
+ case 2 -> layoutRes = R.layout.settings_expressive_spinner_preference_full;
+ case 3 -> layoutRes = R.layout.settings_expressive_spinner_preference_outlined;
+ case 4 -> layoutRes = R.layout.settings_expressive_spinner_preference_outlined;
+ case 5 -> layoutRes = R.layout.settings_expressive_spinner_preference_full_outlined;
+ default -> layoutRes = R.layout.settings_spinner_preference;
+ }
+ }
+ setLayoutResource(layoutRes);
}
@Override
@@ -115,6 +145,12 @@ public class SettingsSpinnerPreference extends Preference
public void onBindViewHolder(PreferenceViewHolder holder) {
super.onBindViewHolder(holder);
final Spinner spinner = (Spinner) holder.findViewById(R.id.spinner);
+ if (spinner == null) {
+ return;
+ }
+ if (mAdapter != null) {
+ mAdapter.setSelectedPosition(mPosition);
+ }
spinner.setAdapter(mAdapter);
spinner.setSelection(mPosition);
spinner.setOnItemSelectedListener(mOnSelectedListener);
@@ -140,20 +176,22 @@ public class SettingsSpinnerPreference extends Preference
private final AdapterView.OnItemSelectedListener mOnSelectedListener =
new AdapterView.OnItemSelectedListener() {
- @Override
- public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
- if (mPosition == position) return;
- mPosition = position;
- if (mListener != null) {
- mListener.onItemSelected(parent, view, position, id);
- }
- }
+ @Override
+ public void onItemSelected(
+ AdapterView<?> parent, View view, int position, long id) {
+ if (mPosition == position) return;
+ mPosition = position;
+ mAdapter.setSelectedPosition(mPosition);
+ if (mListener != null) {
+ mListener.onItemSelected(parent, view, position, id);
+ }
+ }
- @Override
- public void onNothingSelected(AdapterView<?> parent) {
- if (mListener != null) {
- mListener.onNothingSelected(parent);
- }
- }
- };
+ @Override
+ public void onNothingSelected(AdapterView<?> parent) {
+ if (mListener != null) {
+ mListener.onNothingSelected(parent);
+ }
+ }
+ };
}
diff --git a/packages/SettingsLib/SettingsTheme/res/drawable-v36/settingslib_expressive_spinner_background.xml b/packages/SettingsLib/SettingsTheme/res/drawable-v36/settingslib_expressive_spinner_background.xml
new file mode 100644
index 000000000000..139418b38e03
--- /dev/null
+++ b/packages/SettingsLib/SettingsTheme/res/drawable-v36/settingslib_expressive_spinner_background.xml
@@ -0,0 +1,53 @@
+<?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.
+ -->
+
+<ripple
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:color="?android:attr/colorControlHighlight">
+
+ <item android:id="@android:id/background">
+ <layer-list
+ android:paddingMode="stack"
+ android:paddingStart="0dp"
+ android:paddingEnd="@dimen/settingslib_expressive_space_small4">
+ <item>
+ <shape>
+ <corners android:radius="@dimen/settingslib_expressive_radius_full"/>
+ <solid android:color="@color/settingslib_materialColorSecondaryContainer"/>
+ <size android:height="@dimen/settingslib_expressive_space_medium3"/>
+ </shape>
+ </item>
+
+ <item
+ android:gravity="center|end"
+ android:width="@dimen/settingslib_expressive_space_small4"
+ android:height="@dimen/settingslib_expressive_space_small4"
+ android:end="@dimen/settingslib_expressive_space_small1">
+ <vector
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24"
+ android:tint="@color/settingslib_materialColorOnSecondaryContainer">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M16.59,8.59L12,13.17 7.41,8.59 6,10l6,6 6,-6 -1.41,-1.41z"/>
+ </vector>
+ </item>
+ </layer-list>
+ </item>
+</ripple>
diff --git a/packages/SettingsLib/SettingsTheme/res/drawable-v36/settingslib_expressive_spinner_background_outlined.xml b/packages/SettingsLib/SettingsTheme/res/drawable-v36/settingslib_expressive_spinner_background_outlined.xml
new file mode 100644
index 000000000000..f32e13e7f83a
--- /dev/null
+++ b/packages/SettingsLib/SettingsTheme/res/drawable-v36/settingslib_expressive_spinner_background_outlined.xml
@@ -0,0 +1,55 @@
+<?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.
+ -->
+
+<ripple
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:color="?android:attr/colorControlHighlight">
+
+ <item android:id="@android:id/background">
+ <layer-list
+ android:paddingMode="stack"
+ android:paddingStart="0dp"
+ android:paddingEnd="@dimen/settingslib_expressive_space_small4">
+ <item>
+ <shape>
+ <corners android:radius="@dimen/settingslib_expressive_radius_full"/>
+ <stroke
+ android:color="@color/settingslib_materialColorOutlineVariant"
+ android:width="1dp"/>
+ <size android:height="@dimen/settingslib_expressive_space_medium3"/>
+ </shape>
+ </item>
+
+ <item
+ android:gravity="center|end"
+ android:width="@dimen/settingslib_expressive_space_small4"
+ android:height="@dimen/settingslib_expressive_space_small4"
+ android:end="@dimen/settingslib_expressive_space_small1">
+ <vector
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24"
+ android:tint="@color/settingslib_materialColorPrimary">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M16.59,8.59L12,13.17 7.41,8.59 6,10l6,6 6,-6 -1.41,-1.41z"/>
+ </vector>
+ </item>
+ </layer-list>
+ </item>
+</ripple>
diff --git a/packages/SettingsLib/SettingsTheme/res/drawable-v36/settingslib_expressive_spinner_dropdown_background.xml b/packages/SettingsLib/SettingsTheme/res/drawable-v36/settingslib_expressive_spinner_dropdown_background.xml
new file mode 100644
index 000000000000..ac38c3e9223b
--- /dev/null
+++ b/packages/SettingsLib/SettingsTheme/res/drawable-v36/settingslib_expressive_spinner_dropdown_background.xml
@@ -0,0 +1,38 @@
+<?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.
+ -->
+
+<ripple
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:color="?android:attr/colorControlHighlight">
+
+ <item android:id="@android:id/background">
+ <layer-list
+ android:paddingMode="stack"
+ android:paddingStart="@dimen/settingslib_expressive_space_extrasmall4"
+ android:paddingEnd="@dimen/settingslib_expressive_space_extrasmall4"
+ android:paddingTop="@dimen/settingslib_expressive_space_extrasmall4"
+ android:paddingBottom="@dimen/settingslib_expressive_space_extrasmall4">
+
+ <item>
+ <shape>
+ <corners android:radius="@dimen/settingslib_expressive_radius_large3"/>
+ <solid android:color="@color/settingslib_materialColorSurfaceContainerLow"/>
+ </shape>
+ </item>
+ </layer-list>
+ </item>
+</ripple>
diff --git a/packages/SettingsLib/SettingsTheme/res/values-v36/styles_expressive.xml b/packages/SettingsLib/SettingsTheme/res/values-v36/styles_expressive.xml
index de48f99215fb..9cdbce4a4c78 100644
--- a/packages/SettingsLib/SettingsTheme/res/values-v36/styles_expressive.xml
+++ b/packages/SettingsLib/SettingsTheme/res/values-v36/styles_expressive.xml
@@ -31,6 +31,17 @@
<item name="trackTint">@color/settingslib_expressive_color_main_switch_track</item>
</style>
+ <style name="SettingslibSpinnerStyle.Expressive"
+ parent="android:style/Widget.Material.Spinner">
+ <item name="android:background">@drawable/settingslib_expressive_spinner_background</item>
+ <item name="android:popupBackground">@drawable/settingslib_expressive_spinner_dropdown_background</item>
+ <item name="android:dropDownVerticalOffset">@dimen/settingslib_expressive_space_large3</item>
+ </style>
+
+ <style name="SettingslibSpinnerStyle.Expressive.Outlined">
+ <item name="android:background">@drawable/settingslib_expressive_spinner_background_outlined</item>
+ </style>
+
<style name="EntityHeader">
<item name="android:paddingTop">@dimen/settingslib_expressive_space_small4</item>
<item name="android:paddingBottom">@dimen/settingslib_expressive_space_small1</item>
@@ -125,4 +136,4 @@
<item name="android:layout_marginEnd">@dimen/settingslib_expressive_space_extrasmall4</item>
<item name="android:background">@drawable/settingslib_expressive_button_background_outline</item>
</style>
-</resources> \ No newline at end of file
+</resources>
diff --git a/packages/SettingsLib/SettingsTheme/res/values-v36/themes_expressive.xml b/packages/SettingsLib/SettingsTheme/res/values-v36/themes_expressive.xml
index 5173ebeaa9a1..ffbc65cc622b 100644
--- a/packages/SettingsLib/SettingsTheme/res/values-v36/themes_expressive.xml
+++ b/packages/SettingsLib/SettingsTheme/res/values-v36/themes_expressive.xml
@@ -32,9 +32,9 @@
<item name="preferenceTheme">@style/PreferenceTheme.SettingsLib.Expressive</item>
<!-- Set up Spinner style -->
- <!--item name="android:spinnerStyle"></item>
- <item name="android:spinnerItemStyle"></item>
- <item name="android:spinnerDropDownItemStyle"></item-->
+ <item name="android:spinnerStyle">@style/SettingslibSpinnerStyle.Expressive</item>
+ <!--<item name="android:spinnerItemStyle"></item>
+ <item name="android:spinnerDropDownItemStyle"></item>-->
<!-- Set up edge-to-edge configuration for top app bar -->
<item name="android:clipToPadding">false</item>
@@ -66,4 +66,4 @@
<item name="buttonBarNegativeButtonStyle">@style/Widget.SettingsLib.DialogButton.Outline</item>
<item name="buttonBarNeutralButtonStyle">@style/Widget.SettingsLib.DialogButton</item>
</style>
-</resources> \ No newline at end of file
+</resources>
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/dialog/SettingsAlterDialogContent.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/dialog/SettingsAlterDialogContent.kt
index 9f2210d852a9..058fe53f7201 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/dialog/SettingsAlterDialogContent.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/dialog/SettingsAlterDialogContent.kt
@@ -46,6 +46,7 @@ import androidx.compose.ui.layout.Placeable
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEach
import androidx.compose.ui.util.fastForEachIndexed
@@ -226,7 +227,12 @@ internal fun AlertDialogFlowRow(
val childrenMainAxisSizes =
IntArray(placeables.size) { j ->
placeables[j].width +
- if (j < placeables.lastIndex) mainAxisSpacing.roundToPx() else 0
+ if ((layoutDirection == LayoutDirection.Ltr && j < placeables.lastIndex)
+ || (layoutDirection == LayoutDirection.Rtl && j > 0)) {
+ mainAxisSpacing.roundToPx()
+ } else {
+ 0
+ }
}
val arrangement = Arrangement.End
val mainAxisPositions = IntArray(childrenMainAxisSizes.size) { 0 }
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppInfoPage.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppInfoPage.kt
index 5580d2e3211b..6dd5e371d8a6 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppInfoPage.kt
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppInfoPage.kt
@@ -37,6 +37,7 @@ import com.android.settingslib.spa.widget.preference.Preference
import com.android.settingslib.spa.widget.preference.PreferenceModel
import com.android.settingslib.spa.widget.preference.SwitchPreferenceModel
import com.android.settingslib.spa.widget.ui.AnnotatedText
+import com.android.settingslib.spa.widget.ui.Category
import com.android.settingslib.spaprivileged.model.app.AppRecord
import com.android.settingslib.spaprivileged.model.app.IPackageManagers
import com.android.settingslib.spaprivileged.model.app.PackageManagers
@@ -153,13 +154,15 @@ internal fun <T : AppRecord> TogglePermissionAppListModel<T>.TogglePermissionApp
override val changeable = { isChangeable }
override val onCheckedChange: (Boolean) -> Unit = { setAllowed(record, it) }
}
- RestrictedSwitchPreference(
- model = switchModel,
- restrictions = getRestrictions(userId, packageName, isAllowed()),
- ifBlockedByAdminOverrideCheckedValueTo = switchifBlockedByAdminOverrideCheckedValueTo,
- restrictionsProviderFactory = restrictionsProviderFactory,
- )
- InfoPageAdditionalContent(record, isAllowed)
+ Category {
+ RestrictedSwitchPreference(
+ model = switchModel,
+ restrictions = getRestrictions(userId, packageName, isAllowed()),
+ ifBlockedByAdminOverrideCheckedValueTo =
+ switchifBlockedByAdminOverrideCheckedValueTo,
+ restrictionsProviderFactory = restrictionsProviderFactory,
+ )
+ }
}
}
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppList.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppList.kt
index 771eb85ee21a..a3e4aa0420ff 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppList.kt
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppList.kt
@@ -91,9 +91,6 @@ interface TogglePermissionAppListModel<T : AppRecord> {
* Sets whether the permission is allowed for the given app.
*/
fun setAllowed(record: T, newAllowed: Boolean)
-
- @Composable
- fun InfoPageAdditionalContent(record: T, isAllowed: () -> Boolean?) {}
}
/**
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 e6c6638f7de4..c62aed1da352 100644
--- a/packages/SettingsLib/StatusBannerPreference/src/com/android/settingslib/widget/StatusBannerPreference.kt
+++ b/packages/SettingsLib/StatusBannerPreference/src/com/android/settingslib/widget/StatusBannerPreference.kt
@@ -49,6 +49,7 @@ class StatusBannerPreference @JvmOverloads constructor(
var iconLevel: BannerStatus = BannerStatus.GENERIC
set(value) {
field = value
+ updateIconTint(value)
notifyChanged()
}
var buttonLevel: BannerStatus = BannerStatus.GENERIC
@@ -81,7 +82,7 @@ class StatusBannerPreference @JvmOverloads constructor(
if (icon == null) {
icon = getIconDrawable(iconLevel)
} else {
- icon!!.setTintList(ColorStateList.valueOf(getBackgroundColor(iconLevel)))
+ updateIconTint(iconLevel)
}
buttonLevel = getInteger(R.styleable.StatusBanner_buttonLevel, 0).toBannerStatus()
buttonText = getString(R.styleable.StatusBanner_buttonText) ?: ""
@@ -252,4 +253,12 @@ class StatusBannerPreference @JvmOverloads constructor(
)
}
}
-} \ No newline at end of file
+
+ /**
+ * Sets the icon's tint color based on the icon level. If an icon is not defined, this is a
+ * no-op.
+ */
+ private fun updateIconTint(iconLevel: BannerStatus) {
+ icon?.setTintList(ColorStateList.valueOf(getBackgroundColor(iconLevel)))
+ }
+}
diff --git a/packages/SettingsLib/res/xml/timezones.xml b/packages/SettingsLib/res/xml/timezones.xml
index 6a8d7802f9fd..4cea32aa05f9 100644
--- a/packages/SettingsLib/res/xml/timezones.xml
+++ b/packages/SettingsLib/res/xml/timezones.xml
@@ -35,6 +35,7 @@
<timezone id="Europe/Brussels"></timezone>
<timezone id="Europe/Madrid"></timezone>
<timezone id="Europe/Sarajevo"></timezone>
+ <timezone id="Europe/Warsaw"></timezone>
<timezone id="Africa/Windhoek"></timezone>
<timezone id="Africa/Brazzaville"></timezone>
<timezone id="Asia/Amman"></timezone>
diff --git a/packages/SettingsLib/src/com/android/settingslib/RestrictedLockUtilsInternal.java b/packages/SettingsLib/src/com/android/settingslib/RestrictedLockUtilsInternal.java
index 4de64769b425..f89bd9c43a37 100644
--- a/packages/SettingsLib/src/com/android/settingslib/RestrictedLockUtilsInternal.java
+++ b/packages/SettingsLib/src/com/android/settingslib/RestrictedLockUtilsInternal.java
@@ -77,6 +77,10 @@ public class RestrictedLockUtilsInternal extends RestrictedLockUtils {
private static final String ROLE_DEVICE_LOCK_CONTROLLER =
"android.app.role.SYSTEM_FINANCED_DEVICE_CONTROLLER";
+ //TODO(b/378931989): Switch to android.app.admin.DevicePolicyIdentifiers.MEMORY_TAGGING_POLICY
+ //when the appropriate flag is launched.
+ private static final String MEMORY_TAGGING_POLICY = "memoryTagging";
+
/**
* @return drawables for displaying with settings that are locked by a device admin.
*/
@@ -244,14 +248,23 @@ public class RestrictedLockUtilsInternal extends RestrictedLockUtils {
*/
public static EnforcedAdmin checkIfKeyguardFeaturesDisabled(Context context,
int keyguardFeatures, final @UserIdInt int userId) {
- final LockSettingCheck check = (dpm, admin, checkUser) -> {
- int effectiveFeatures = dpm.getKeyguardDisabledFeatures(admin, checkUser);
- if (checkUser != userId) {
- effectiveFeatures &= PROFILE_KEYGUARD_FEATURES_AFFECT_OWNER;
- }
- return (effectiveFeatures & keyguardFeatures) != KEYGUARD_DISABLE_FEATURES_NONE;
- };
- if (UserManager.get(context).getUserInfo(userId).isManagedProfile()) {
+ UserInfo userInfo = UserManager.get(context).getUserInfo(userId);
+ if (userInfo == null) {
+ Log.w(LOG_TAG, "User " + userId + " does not exist");
+ return null;
+ }
+
+ final LockSettingCheck check =
+ (dpm, admin, checkUser) -> {
+ int effectiveFeatures = dpm.getKeyguardDisabledFeatures(admin, checkUser);
+ if (checkUser != userId) {
+ // This case is reached when {@code checkUser} is a managed profile and
+ // {@code userId} is the parent user.
+ effectiveFeatures &= PROFILE_KEYGUARD_FEATURES_AFFECT_OWNER;
+ }
+ return (effectiveFeatures & keyguardFeatures) != KEYGUARD_DISABLE_FEATURES_NONE;
+ };
+ if (userInfo.isManagedProfile()) {
DevicePolicyManager dpm =
(DevicePolicyManager) context.getSystemService(Context.DEVICE_POLICY_SERVICE);
return findEnforcedAdmin(dpm.getActiveAdminsAsUser(userId), dpm, userId, check);
@@ -838,14 +851,13 @@ public class RestrictedLockUtilsInternal extends RestrictedLockUtils {
if (dpm.getMtePolicy() == MTE_NOT_CONTROLLED_BY_POLICY) {
return null;
}
- EnforcedAdmin admin =
- RestrictedLockUtils.getProfileOrDeviceOwner(
- context, context.getUser());
- if (admin != null) {
- return admin;
+ EnforcingAdmin enforcingAdmin = context.getSystemService(DevicePolicyManager.class)
+ .getEnforcingAdmin(context.getUserId(), MEMORY_TAGGING_POLICY);
+ if (enforcingAdmin == null) {
+ Log.w(LOG_TAG, "MTE is controlled by policy but could not find enforcing admin.");
}
- int profileId = getManagedProfileId(context, context.getUserId());
- return RestrictedLockUtils.getProfileOrDeviceOwner(context, UserHandle.of(profileId));
+
+ return EnforcedAdmin.createDefaultEnforcedAdminWithRestriction(MEMORY_TAGGING_POLICY);
}
/**
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java
index b0f379605f5e..3ec4bb80b9cf 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java
@@ -107,6 +107,7 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile {
public static final int BROADCAST_STATE_UNKNOWN = 0;
public static final int BROADCAST_STATE_ON = 1;
public static final int BROADCAST_STATE_OFF = 2;
+ private static final int BROADCAST_NAME_PREFIX_MAX_LENGTH = 27;
@Retention(RetentionPolicy.SOURCE)
@IntDef(
@@ -1116,13 +1117,17 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile {
private String getDefaultValueOfBroadcastName() {
// set the default value;
int postfix = ThreadLocalRandom.current().nextInt(DEFAULT_CODE_MIN, DEFAULT_CODE_MAX);
- return BluetoothAdapter.getDefaultAdapter().getName() + UNDERLINE + postfix;
+ String name = BluetoothAdapter.getDefaultAdapter().getName();
+ return (name.length() < BROADCAST_NAME_PREFIX_MAX_LENGTH ? name : name.substring(0,
+ BROADCAST_NAME_PREFIX_MAX_LENGTH)) + UNDERLINE + postfix;
}
private String getDefaultValueOfProgramInfo() {
// set the default value;
int postfix = ThreadLocalRandom.current().nextInt(DEFAULT_CODE_MIN, DEFAULT_CODE_MAX);
- return BluetoothAdapter.getDefaultAdapter().getName() + UNDERLINE + postfix;
+ String name = BluetoothAdapter.getDefaultAdapter().getName();
+ return (name.length() < BROADCAST_NAME_PREFIX_MAX_LENGTH ? name : name.substring(0,
+ BROADCAST_NAME_PREFIX_MAX_LENGTH)) + UNDERLINE + postfix;
}
private byte[] getDefaultValueOfBroadcastCode() {
diff --git a/packages/SettingsLib/src/com/android/settingslib/display/DisplayDensityUtils.java b/packages/SettingsLib/src/com/android/settingslib/display/DisplayDensityUtils.java
index 985599c952d1..0d5f66794e3b 100644
--- a/packages/SettingsLib/src/com/android/settingslib/display/DisplayDensityUtils.java
+++ b/packages/SettingsLib/src/com/android/settingslib/display/DisplayDensityUtils.java
@@ -97,6 +97,12 @@ public class DisplayDensityUtils {
@Nullable
private final int[] mValues;
+ /**
+ * The density values before rounding to an integer.
+ */
+ @Nullable
+ private final float[] mFloatValues;
+
private final int mDefaultDensity;
private final int mCurrentIndex;
@@ -124,6 +130,7 @@ public class DisplayDensityUtils {
Log.w(LOG_TAG, "Cannot fetch display info for the default display");
mEntries = null;
mValues = null;
+ mFloatValues = null;
mDefaultDensity = 0;
mCurrentIndex = -1;
return;
@@ -154,6 +161,7 @@ public class DisplayDensityUtils {
Log.w(LOG_TAG, "No display satisfies the predicate");
mEntries = null;
mValues = null;
+ mFloatValues = null;
mDefaultDensity = 0;
mCurrentIndex = -1;
return;
@@ -165,6 +173,7 @@ public class DisplayDensityUtils {
Log.w(LOG_TAG, "Cannot fetch default density for display " + idOfSmallestDisplay);
mEntries = null;
mValues = null;
+ mFloatValues = null;
mDefaultDensity = 0;
mCurrentIndex = -1;
return;
@@ -197,18 +206,25 @@ public class DisplayDensityUtils {
String[] entries = new String[1 + numSmaller + numLarger];
int[] values = new int[entries.length];
+ float[] valuesFloat = new float[entries.length];
int curIndex = 0;
if (numSmaller > 0) {
final float interval = (1 - minScale) / numSmaller;
for (int i = numSmaller - 1; i >= 0; i--) {
+ // Save the float density value before rounding to be used to set the density ratio
+ // of overridden density to default density in WM.
+ final float densityFloat = defaultDensity * (1 - (i + 1) * interval);
// Round down to a multiple of 2 by truncating the low bit.
- final int density = ((int) (defaultDensity * (1 - (i + 1) * interval))) & ~1;
+ // LINT.IfChange
+ final int density = ((int) densityFloat) & ~1;
+ // LINT.ThenChange(/services/core/java/com/android/server/wm/DisplayContent.java:getBaseDensityFromRatio)
if (currentDensity == density) {
currentDensityIndex = curIndex;
}
- entries[curIndex] = res.getString(SUMMARIES_SMALLER[i]);
values[curIndex] = density;
+ valuesFloat[curIndex] = densityFloat;
+ entries[curIndex] = res.getString(SUMMARIES_SMALLER[i]);
curIndex++;
}
}
@@ -217,18 +233,25 @@ public class DisplayDensityUtils {
currentDensityIndex = curIndex;
}
values[curIndex] = defaultDensity;
+ valuesFloat[curIndex] = (float) defaultDensity;
entries[curIndex] = res.getString(SUMMARY_DEFAULT);
curIndex++;
if (numLarger > 0) {
final float interval = (maxScale - 1) / numLarger;
for (int i = 0; i < numLarger; i++) {
+ // Save the float density value before rounding to be used to set the density ratio
+ // of overridden density to default density in WM.
+ final float densityFloat = defaultDensity * (1 + (i + 1) * interval);
// Round down to a multiple of 2 by truncating the low bit.
- final int density = ((int) (defaultDensity * (1 + (i + 1) * interval))) & ~1;
+ // LINT.IfChange
+ final int density = ((int) densityFloat) & ~1;
+ // LINT.ThenChange(/services/core/java/com/android/server/wm/DisplayContent.java:getBaseDensityFromRatio)
if (currentDensity == density) {
currentDensityIndex = curIndex;
}
values[curIndex] = density;
+ valuesFloat[curIndex] = densityFloat;
entries[curIndex] = res.getString(SUMMARIES_LARGER[i]);
curIndex++;
}
@@ -244,6 +267,9 @@ public class DisplayDensityUtils {
values = Arrays.copyOf(values, newLength);
values[curIndex] = currentDensity;
+ valuesFloat = Arrays.copyOf(valuesFloat, newLength);
+ valuesFloat[curIndex] = (float) currentDensity;
+
entries = Arrays.copyOf(entries, newLength);
entries[curIndex] = res.getString(SUMMARY_CUSTOM, currentDensity);
@@ -254,6 +280,7 @@ public class DisplayDensityUtils {
mCurrentIndex = displayIndex;
mEntries = entries;
mValues = values;
+ mFloatValues = valuesFloat;
}
@Nullable
@@ -348,7 +375,14 @@ public class DisplayDensityUtils {
}
final IWindowManager wm = WindowManagerGlobal.getWindowManagerService();
- wm.setForcedDisplayDensityForUser(displayId, mValues[index], userId);
+ // Only set the ratio for external displays as Settings uses
+ // ScreenResolutionFragment to handle density update for internal display.
+ if (info.type == Display.TYPE_EXTERNAL) {
+ wm.setForcedDisplayDensityRatio(displayId,
+ mFloatValues[index] / mDefaultDensity, userId);
+ } else {
+ wm.setForcedDisplayDensityForUser(displayId, mValues[index], userId);
+ }
}
} catch (RemoteException exc) {
Log.w(LOG_TAG, "Unable to save forced display density setting");
diff --git a/packages/SettingsLib/src/com/android/settingslib/display/OWNERS b/packages/SettingsLib/src/com/android/settingslib/display/OWNERS
new file mode 100644
index 000000000000..aafc2f79611b
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/display/OWNERS
@@ -0,0 +1,5 @@
+# Default reviewers for this and subdirectories.
+pbdr@google.com
+ebrukurnaz@google.com
+lihongyu@google.com
+wilczynskip@google.com
diff --git a/packages/SettingsLib/src/com/android/settingslib/wifi/WifiUtils.kt b/packages/SettingsLib/src/com/android/settingslib/wifi/WifiUtils.kt
index 4d38f1d551bb..84d61fc86073 100644
--- a/packages/SettingsLib/src/com/android/settingslib/wifi/WifiUtils.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/wifi/WifiUtils.kt
@@ -501,6 +501,7 @@ open class WifiUtils {
dialogWindowType: Int,
onStartActivity: (intent: Intent) -> Unit,
onAllowed: () -> Unit,
+ onStartAapmActivity: (intent: Intent) -> Unit = onStartActivity,
): Job =
coroutineScope.launch {
val wifiManager = context.getSystemService(WifiManager::class.java) ?: return@launch
@@ -510,9 +511,9 @@ open class WifiUtils {
AdvancedProtectionManager.FEATURE_ID_DISALLOW_WEP,
AdvancedProtectionManager.SUPPORT_DIALOG_TYPE_BLOCKED_INTERACTION)
intent.putExtra(DIALOG_WINDOW_TYPE, dialogWindowType)
- onStartActivity(intent)
+ withContext(Dispatchers.Main) { onStartAapmActivity(intent) }
} else if (wifiManager.isWepSupported == true && wifiManager.queryWepAllowed()) {
- onAllowed()
+ withContext(Dispatchers.Main) { onAllowed() }
} else {
val intent = Intent(Intent.ACTION_MAIN).apply {
component = ComponentName(
@@ -522,7 +523,7 @@ open class WifiUtils {
putExtra(DIALOG_WINDOW_TYPE, dialogWindowType)
putExtra(SSID, ssid)
}.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
- onStartActivity(intent)
+ withContext(Dispatchers.Main) { onStartActivity(intent) }
}
}
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/RestrictedLockUtilsTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/RestrictedLockUtilsTest.java
index 785bcbf5a91c..71e11ba55850 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/RestrictedLockUtilsTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/RestrictedLockUtilsTest.java
@@ -458,6 +458,16 @@ public class RestrictedLockUtilsTest {
assertThat(intentCaptor.getValue().getExtra(EXTRA_RESTRICTION)).isNull();
}
+ /** See b/386971405. Ensure that the code does not crash when the user is not found. */
+ @Test
+ public void checkIfKeyguardFeaturesDisabled_returnsNull_whenUserDoesNotExist() {
+ when(mUserManager.getUserInfo(mUserId)).thenReturn(null);
+ assertThat(
+ RestrictedLockUtilsInternal.checkIfKeyguardFeaturesDisabled(
+ mContext, KEYGUARD_DISABLE_FINGERPRINT, mUserId))
+ .isNull();
+ }
+
private UserInfo setUpUser(int userId, ComponentName[] admins) {
UserInfo userInfo = new UserInfo(userId, "primary", 0);
when(mUserManager.getUserInfo(userId)).thenReturn(userInfo);
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/BannerMessagePreferenceTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/BannerMessagePreferenceTest.java
index 7f4bdaeac855..83471ae9513e 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/BannerMessagePreferenceTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/BannerMessagePreferenceTest.java
@@ -18,6 +18,7 @@ package com.android.settingslib.widget;
import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.robolectric.Robolectric.setupActivity;
@@ -25,6 +26,8 @@ import static org.robolectric.Shadows.shadowOf;
import android.app.Activity;
import android.content.Context;
+import android.content.res.ColorStateList;
+import android.content.res.Configuration;
import android.graphics.ColorFilter;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffColorFilter;
@@ -38,24 +41,34 @@ import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.ColorRes;
+import androidx.annotation.NonNull;
import androidx.preference.PreferenceViewHolder;
-import com.android.settingslib.testutils.OverpoweredReflectionHelper;
import com.android.settingslib.widget.preference.banner.R;
+import com.google.android.material.button.MaterialButton;
+
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
import org.robolectric.Robolectric;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
import org.robolectric.shadows.ShadowDrawable;
import org.robolectric.shadows.ShadowTouchDelegate;
import org.robolectric.util.ReflectionHelpers;
-@Ignore("b/359066481")
+import java.util.List;
+
@RunWith(RobolectricTestRunner.class)
+@Config(shadows = {BannerMessagePreferenceTest.ShadowSettingsThemeHelper.class})
public class BannerMessagePreferenceTest {
private Context mContext;
@@ -66,14 +79,23 @@ public class BannerMessagePreferenceTest {
private boolean mClickListenerCalled = false;
private final View.OnClickListener mClickListener = v -> mClickListenerCalled = true;
private final int mMinimumTargetSize =
- RuntimeEnvironment.application.getResources()
- .getDimensionPixelSize(com.android.settingslib.widget.theme.R.dimen.settingslib_preferred_minimum_touch_target);
+ RuntimeEnvironment.application
+ .getResources()
+ .getDimensionPixelSize(
+ com.android.settingslib.widget.theme.R.dimen
+ .settingslib_preferred_minimum_touch_target);
- private static final int TEST_STRING_RES_ID =
- R.string.accessibility_banner_message_dismiss;
+ private static final int TEST_STRING_RES_ID = R.string.accessibility_banner_message_dismiss;
+
+ @Mock private View mMockBackgroundView;
+ @Mock private Drawable mMockCardBackground;
+ @Mock private MaterialButton mMockPositiveBtn;
+ @Mock private MaterialButton mMockNegativeBtn;
@Before
public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ ShadowSettingsThemeHelper.setExpressiveTheme(false);
mContext = RuntimeEnvironment.application;
mClickListenerCalled = false;
mBannerPreference = new BannerMessagePreference(mContext);
@@ -90,6 +112,7 @@ public class BannerMessagePreferenceTest {
.isEqualTo("test");
}
+ @Ignore("b/359066481")
@Test
public void onBindViewHolder_andOnLayoutView_dismissButtonTouchDelegate_isCorrectSize() {
assumeAndroidS();
@@ -155,9 +178,8 @@ public class BannerMessagePreferenceTest {
@Test
public void onBindViewHolder_whenAtLeastS_whenSubtitleXmlAttribute_shouldSetSubtitle() {
assumeAndroidS();
- AttributeSet mAttributeSet = Robolectric.buildAttributeSet()
- .addAttribute(R.attr.subtitle, "Test")
- .build();
+ AttributeSet mAttributeSet =
+ Robolectric.buildAttributeSet().addAttribute(R.attr.subtitle, "Test").build();
mBannerPreference = new BannerMessagePreference(mContext, mAttributeSet);
mBannerPreference.onBindViewHolder(mHolder);
@@ -185,8 +207,7 @@ public class BannerMessagePreferenceTest {
ImageView mIcon = mRootView.findViewById(R.id.banner_icon);
ShadowDrawable shadowDrawable = shadowOf(mIcon.getDrawable());
- assertThat(shadowDrawable.getCreatedFromResId())
- .isEqualTo(R.drawable.settingslib_ic_cross);
+ assertThat(shadowDrawable.getCreatedFromResId()).isEqualTo(R.drawable.settingslib_ic_cross);
}
@Test
@@ -207,6 +228,7 @@ public class BannerMessagePreferenceTest {
Button mPositiveButton = mRootView.findViewById(R.id.banner_positive_btn);
assertThat(mPositiveButton.getVisibility()).isEqualTo(View.VISIBLE);
+
assertThat(mPositiveButton.getText()).isEqualTo(mContext.getString(TEST_STRING_RES_ID));
}
@@ -218,6 +240,7 @@ public class BannerMessagePreferenceTest {
Button mNegativeButton = mRootView.findViewById(R.id.banner_negative_btn);
assertThat(mNegativeButton.getVisibility()).isEqualTo(View.VISIBLE);
+
assertThat(mNegativeButton.getText()).isEqualTo(mContext.getString(TEST_STRING_RES_ID));
}
@@ -359,8 +382,6 @@ public class BannerMessagePreferenceTest {
@Test
public void onBindViewHolder_whenAtLeastS_whenAttentionUnset_setsHighTheme() {
assumeAndroidS();
- Drawable mCardBackgroundSpy = spy(mRootView.getBackground());
- mRootView.setBackground(mCardBackgroundSpy);
mBannerPreference.onBindViewHolder(mHolder);
@@ -370,17 +391,15 @@ public class BannerMessagePreferenceTest {
.isEqualTo(getColorId(R.color.banner_accent_attention_high));
assertThat(getButtonColor(R.id.banner_negative_btn))
.isEqualTo(getColorId(R.color.banner_accent_attention_high));
- verify(mCardBackgroundSpy).setTint(getColorId(R.color.banner_background_attention_high));
+
+ verify(mMockCardBackground).setTint(getColorId(R.color.banner_background_attention_high));
}
@Test
public void onBindViewHolder_whenAtLeastS_whenAttentionHighByXML_setsHighTheme() {
assumeAndroidS();
- Drawable mCardBackgroundSpy = spy(mRootView.getBackground());
- mRootView.setBackground(mCardBackgroundSpy);
- AttributeSet mAttributeSet = Robolectric.buildAttributeSet()
- .addAttribute(R.attr.attentionLevel, "high")
- .build();
+ AttributeSet mAttributeSet =
+ Robolectric.buildAttributeSet().addAttribute(R.attr.attentionLevel, "high").build();
mBannerPreference = new BannerMessagePreference(mContext, mAttributeSet);
mBannerPreference.onBindViewHolder(mHolder);
@@ -391,17 +410,17 @@ public class BannerMessagePreferenceTest {
.isEqualTo(getColorId(R.color.banner_accent_attention_high));
assertThat(getButtonColor(R.id.banner_negative_btn))
.isEqualTo(getColorId(R.color.banner_accent_attention_high));
- verify(mCardBackgroundSpy).setTint(getColorId(R.color.banner_background_attention_high));
+
+ verify(mMockCardBackground).setTint(getColorId(R.color.banner_background_attention_high));
}
@Test
public void onBindViewHolder_whenAtLeastS_whenAttentionMediumByXML_setsMediumTheme() {
assumeAndroidS();
- Drawable mCardBackgroundSpy = spy(mRootView.getBackground());
- mRootView.setBackground(mCardBackgroundSpy);
- AttributeSet mAttributeSet = Robolectric.buildAttributeSet()
- .addAttribute(R.attr.attentionLevel, "medium")
- .build();
+ AttributeSet mAttributeSet =
+ Robolectric.buildAttributeSet()
+ .addAttribute(R.attr.attentionLevel, "medium")
+ .build();
mBannerPreference = new BannerMessagePreference(mContext, mAttributeSet);
mBannerPreference.onBindViewHolder(mHolder);
@@ -412,17 +431,15 @@ public class BannerMessagePreferenceTest {
.isEqualTo(getColorId(R.color.banner_accent_attention_medium));
assertThat(getButtonColor(R.id.banner_negative_btn))
.isEqualTo(getColorId(R.color.banner_accent_attention_medium));
- verify(mCardBackgroundSpy).setTint(getColorId(R.color.banner_background_attention_medium));
+
+ verify(mMockCardBackground).setTint(getColorId(R.color.banner_background_attention_medium));
}
@Test
public void onBindViewHolder_whenAtLeastS_whenAttentionLowByXML_setsLowTheme() {
assumeAndroidS();
- Drawable mCardBackgroundSpy = spy(mRootView.getBackground());
- mRootView.setBackground(mCardBackgroundSpy);
- AttributeSet mAttributeSet = Robolectric.buildAttributeSet()
- .addAttribute(R.attr.attentionLevel, "low")
- .build();
+ AttributeSet mAttributeSet =
+ Robolectric.buildAttributeSet().addAttribute(R.attr.attentionLevel, "low").build();
mBannerPreference = new BannerMessagePreference(mContext, mAttributeSet);
mBannerPreference.onBindViewHolder(mHolder);
@@ -433,14 +450,13 @@ public class BannerMessagePreferenceTest {
.isEqualTo(getColorId(R.color.banner_accent_attention_low));
assertThat(getButtonColor(R.id.banner_negative_btn))
.isEqualTo(getColorId(R.color.banner_accent_attention_low));
- verify(mCardBackgroundSpy).setTint(getColorId(R.color.banner_background_attention_low));
+
+ verify(mMockCardBackground).setTint(getColorId(R.color.banner_background_attention_low));
}
@Test
public void setAttentionLevel_whenAtLeastS_whenHighAttention_setsHighTheme() {
assumeAndroidS();
- Drawable mCardBackgroundSpy = spy(mRootView.getBackground());
- mRootView.setBackground(mCardBackgroundSpy);
mBannerPreference.setAttentionLevel(BannerMessagePreference.AttentionLevel.HIGH);
mBannerPreference.onBindViewHolder(mHolder);
@@ -451,14 +467,44 @@ public class BannerMessagePreferenceTest {
.isEqualTo(getColorId(R.color.banner_accent_attention_high));
assertThat(getButtonColor(R.id.banner_negative_btn))
.isEqualTo(getColorId(R.color.banner_accent_attention_high));
- verify(mCardBackgroundSpy).setTint(getColorId(R.color.banner_background_attention_high));
+
+ verify(mMockCardBackground).setTint(getColorId(R.color.banner_background_attention_high));
}
@Test
- public void setAttentionLevel_whenAtLeastS_whenMedAttention_setsMediumTheme() {
+ public void setAttentionLevel_whenAtLeastS_whenHighAttentionAndExpressiveTheme_setsBtnTheme() {
+ setExpressiveTheme(true);
+ assumeAndroidS();
+ assertThat(SettingsThemeHelper.isExpressiveTheme(mContext)).isTrue();
+ assertThat(SettingsThemeHelper.isExpressiveTheme(mContext)).isTrue();
+ doReturn(mMockPositiveBtn).when(mHolder).findViewById(R.id.banner_positive_btn);
+ doReturn(mMockNegativeBtn).when(mHolder).findViewById(R.id.banner_negative_btn);
+ assertThat(SettingsThemeHelper.isExpressiveTheme(mContext)).isTrue();
+ mBannerPreference.setAttentionLevel(BannerMessagePreference.AttentionLevel.HIGH);
+ final ArgumentCaptor<ColorStateList> captor = ArgumentCaptor.forClass(ColorStateList.class);
+ ColorStateList filledBtnBackground =
+ getColorStateList(R.color.settingslib_banner_button_background_high);
+ ColorStateList filledBtnTextColor =
+ getColorStateList(R.color.settingslib_banner_filled_button_content_high);
+ ColorStateList outlineBtnTextColor =
+ getColorStateList(R.color.settingslib_banner_outline_button_content);
+
+ mBannerPreference.onBindViewHolder(mHolder);
+
+ verify(mMockPositiveBtn).setBackgroundTintList(captor.capture());
+ verify(mMockPositiveBtn).setTextColor(captor.capture());
+ verify(mMockNegativeBtn).setStrokeColor(captor.capture());
+ verify(mMockNegativeBtn).setTextColor(captor.capture());
+ List<ColorStateList> colors = captor.getAllValues();
+ assertThat(colors.get(0).getColors()).isEqualTo(filledBtnBackground.getColors());
+ assertThat(colors.get(1).getColors()).isEqualTo(filledBtnTextColor.getColors());
+ assertThat(colors.get(2).getColors()).isEqualTo(filledBtnBackground.getColors());
+ assertThat(colors.get(3).getColors()).isEqualTo(outlineBtnTextColor.getColors());
+ }
+
+ @Test
+ public void setAttentionLevel_whenAtLeastS_whenMedAttention_setsBtnMediumTheme() {
assumeAndroidS();
- Drawable mCardBackgroundSpy = spy(mRootView.getBackground());
- mRootView.setBackground(mCardBackgroundSpy);
mBannerPreference.setAttentionLevel(BannerMessagePreference.AttentionLevel.MEDIUM);
mBannerPreference.onBindViewHolder(mHolder);
@@ -469,14 +515,42 @@ public class BannerMessagePreferenceTest {
.isEqualTo(getColorId(R.color.banner_accent_attention_medium));
assertThat(getButtonColor(R.id.banner_negative_btn))
.isEqualTo(getColorId(R.color.banner_accent_attention_medium));
- verify(mCardBackgroundSpy).setTint(getColorId(R.color.banner_background_attention_medium));
+
+ verify(mMockCardBackground).setTint(getColorId(R.color.banner_background_attention_medium));
+ }
+
+ @Test
+ public void setAttentionLevel_whenAtLeastS_whenMedAttentionAndExpressiveTheme_setsBtnTheme() {
+ setExpressiveTheme(true);
+ mContext.getResources().getConfiguration().uiMode = Configuration.UI_MODE_NIGHT_NO;
+ assumeAndroidS();
+ doReturn(mMockPositiveBtn).when(mHolder).findViewById(R.id.banner_positive_btn);
+ doReturn(mMockNegativeBtn).when(mHolder).findViewById(R.id.banner_negative_btn);
+ mBannerPreference.setAttentionLevel(BannerMessagePreference.AttentionLevel.MEDIUM);
+ final ArgumentCaptor<ColorStateList> captor = ArgumentCaptor.forClass(ColorStateList.class);
+ ColorStateList filledBtnBackground =
+ getColorStateList(R.color.settingslib_banner_button_background_medium);
+ ColorStateList filledBtnTextColor =
+ getColorStateList(R.color.settingslib_banner_filled_button_content_medium);
+ ColorStateList outlineBtnTextColor =
+ getColorStateList(R.color.settingslib_banner_outline_button_content);
+
+ mBannerPreference.onBindViewHolder(mHolder);
+
+ verify(mMockPositiveBtn).setBackgroundTintList(captor.capture());
+ verify(mMockPositiveBtn).setTextColor(captor.capture());
+ verify(mMockNegativeBtn).setStrokeColor(captor.capture());
+ verify(mMockNegativeBtn).setTextColor(captor.capture());
+ List<ColorStateList> colors = captor.getAllValues();
+ assertThat(colors.get(0).getColors()).isEqualTo(filledBtnBackground.getColors());
+ assertThat(colors.get(1).getColors()).isEqualTo(filledBtnTextColor.getColors());
+ assertThat(colors.get(2).getColors()).isEqualTo(filledBtnBackground.getColors());
+ assertThat(colors.get(3).getColors()).isEqualTo(outlineBtnTextColor.getColors());
}
@Test
public void setAttentionLevel_whenAtLeastS_whenLowAttention_setsLowTheme() {
assumeAndroidS();
- Drawable mCardBackgroundSpy = spy(mRootView.getBackground());
- mRootView.setBackground(mCardBackgroundSpy);
mBannerPreference.setAttentionLevel(BannerMessagePreference.AttentionLevel.LOW);
mBannerPreference.onBindViewHolder(mHolder);
@@ -487,7 +561,37 @@ public class BannerMessagePreferenceTest {
.isEqualTo(getColorId(R.color.banner_accent_attention_low));
assertThat(getButtonColor(R.id.banner_negative_btn))
.isEqualTo(getColorId(R.color.banner_accent_attention_low));
- verify(mCardBackgroundSpy).setTint(getColorId(R.color.banner_background_attention_low));
+ verify(mMockCardBackground).setTint(getColorId(R.color.banner_background_attention_low));
+ }
+
+ @Test
+ public void
+ setAttentionLevel_whenAtLeastS_whenNormalAttentionAndExpressiveTheme_setsBtnTheme() {
+ setExpressiveTheme(true);
+ mContext.getResources().getConfiguration().uiMode = Configuration.UI_MODE_NIGHT_NO;
+ assumeAndroidS();
+ doReturn(mMockPositiveBtn).when(mHolder).findViewById(R.id.banner_positive_btn);
+ doReturn(mMockNegativeBtn).when(mHolder).findViewById(R.id.banner_negative_btn);
+ mBannerPreference.setAttentionLevel(BannerMessagePreference.AttentionLevel.NORMAL);
+ final ArgumentCaptor<ColorStateList> captor = ArgumentCaptor.forClass(ColorStateList.class);
+ ColorStateList filledBtnBackground =
+ getColorStateList(R.color.settingslib_banner_button_background_normal);
+ ColorStateList filledBtnTextColor =
+ getColorStateList(R.color.settingslib_banner_filled_button_content_normal);
+ ColorStateList outlineBtnStrokeColor =
+ getColorStateList(R.color.settingslib_banner_outline_button_stroke_normal);
+
+ mBannerPreference.onBindViewHolder(mHolder);
+
+ verify(mMockPositiveBtn).setBackgroundTintList(captor.capture());
+ verify(mMockPositiveBtn).setTextColor(captor.capture());
+ verify(mMockNegativeBtn).setStrokeColor(captor.capture());
+ verify(mMockNegativeBtn).setTextColor(captor.capture());
+ List<ColorStateList> colors = captor.getAllValues();
+ assertThat(colors.get(0).getColors()).isEqualTo(filledBtnBackground.getColors());
+ assertThat(colors.get(1).getColors()).isEqualTo(filledBtnTextColor.getColors());
+ assertThat(colors.get(2).getColors()).isEqualTo(outlineBtnStrokeColor.getColors());
+ assertThat(colors.get(3).getColors()).isEqualTo(filledBtnBackground.getColors());
}
private int getButtonColor(int buttonResId) {
@@ -495,6 +599,11 @@ public class BannerMessagePreferenceTest {
return mButton.getTextColors().getDefaultColor();
}
+ private ColorStateList getButtonTextColor(int buttonResId) {
+ Button mButton = mRootView.findViewById(buttonResId);
+ return mButton.getTextColors();
+ }
+
private ColorFilter getColorFilter(@ColorRes int colorResId) {
return new PorterDuffColorFilter(getColorId(colorResId), PorterDuff.Mode.SRC_IN);
}
@@ -503,28 +612,57 @@ public class BannerMessagePreferenceTest {
return mContext.getResources().getColor(colorResId, mContext.getTheme());
}
+ private ColorStateList getColorStateList(@ColorRes int colorResId) {
+ return mContext.getResources().getColorStateList(colorResId, mContext.getTheme());
+ }
+
private void assumeAndroidR() {
ReflectionHelpers.setStaticField(Build.VERSION.class, "SDK_INT", 30);
ReflectionHelpers.setStaticField(Build.VERSION.class, "CODENAME", "R");
- OverpoweredReflectionHelper
- .setStaticField(BannerMessagePreference.class, "IS_AT_LEAST_S", false);
- // Reset view holder to use correct layout.
- }
-
+ // Refresh the static final field IS_AT_LEAST_S
+ mBannerPreference = new BannerMessagePreference(mContext);
+ setUpViewHolder();
+ }
private void assumeAndroidS() {
ReflectionHelpers.setStaticField(Build.VERSION.class, "SDK_INT", 31);
ReflectionHelpers.setStaticField(Build.VERSION.class, "CODENAME", "S");
- OverpoweredReflectionHelper
- .setStaticField(BannerMessagePreference.class, "IS_AT_LEAST_S", true);
- // Re-inflate view to update layout.
+
+ // Refresh the static final field IS_AT_LEAST_S
+ mBannerPreference = new BannerMessagePreference(mContext);
setUpViewHolder();
}
+ private void setExpressiveTheme(boolean isExpressiveTheme) {
+ ShadowSettingsThemeHelper.setExpressiveTheme(isExpressiveTheme);
+ assertThat(SettingsThemeHelper.isExpressiveTheme(mContext)).isEqualTo(isExpressiveTheme);
+ if (isExpressiveTheme) {
+ doReturn(mContext).when(mMockPositiveBtn).getContext();
+ doReturn(mContext).when(mMockNegativeBtn).getContext();
+ }
+ }
+
private void setUpViewHolder() {
mRootView =
View.inflate(mContext, mBannerPreference.getLayoutResource(), null /* parent */);
- mHolder = PreferenceViewHolder.createInstanceForTests(mRootView);
+ mHolder = spy(PreferenceViewHolder.createInstanceForTests(mRootView));
+ doReturn(mMockBackgroundView).when(mHolder).findViewById(R.id.banner_background);
+ doReturn(mMockCardBackground).when(mMockBackgroundView).getBackground();
+ }
+
+ @Implements(SettingsThemeHelper.class)
+ public static class ShadowSettingsThemeHelper {
+ private static boolean sIsExpressiveTheme;
+
+ /** Shadow implementation of isExpressiveTheme */
+ @Implementation
+ public static boolean isExpressiveTheme(@NonNull Context context) {
+ return sIsExpressiveTheme;
+ }
+
+ static void setExpressiveTheme(boolean isExpressiveTheme) {
+ sIsExpressiveTheme = isExpressiveTheme;
+ }
}
}
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/wifi/WifiUtilsTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/wifi/WifiUtilsTest.java
index d8b6707b9118..97473fffaeb1 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/wifi/WifiUtilsTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/wifi/WifiUtilsTest.java
@@ -50,6 +50,7 @@ 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 org.robolectric.RobolectricTestRunner;
@@ -211,10 +212,12 @@ public class WifiUtilsTest {
WifiUtils.InternetIconInjector iconInjector = new WifiUtils.InternetIconInjector(mContext);
for (int level = 0; level <= 4; level++) {
+ Mockito.reset(mContext);
iconInjector.getIcon(false /* noInternet */, level);
verify(mContext).getDrawable(
WifiUtils.getInternetIconResource(level, false /* noInternet */));
+ Mockito.reset(mContext);
iconInjector.getIcon(true /* noInternet */, level);
verify(mContext).getDrawable(
WifiUtils.getInternetIconResource(level, true /* noInternet */));
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java
index 527a1f16a84f..5bbfdf7bab81 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java
@@ -672,6 +672,7 @@ public class SettingsHelper {
public static LocaleList resolveLocales(LocaleList restore, LocaleList current,
String[] supportedLocales) {
final HashMap<Locale, Locale> allLocales = new HashMap<>(supportedLocales.length);
+ final HashSet<String> existingLanguageAndScript = new HashSet<>();
for (String supportedLocaleStr : supportedLocales) {
final Locale locale = Locale.forLanguageTag(supportedLocaleStr);
allLocales.put(toFullLocale(locale), locale);
@@ -679,30 +680,26 @@ public class SettingsHelper {
// After restoring to reset locales, need to get extensions from restored locale. Get the
// first restored locale to check its extension.
- final Locale restoredLocale = restore.isEmpty()
+ final Locale firstRestoredLocale = restore.isEmpty()
? Locale.ROOT
: restore.get(0);
final ArrayList<Locale> filtered = new ArrayList<>(current.size());
for (int i = 0; i < current.size(); i++) {
- Locale locale = copyExtensionToTargetLocale(restoredLocale, current.get(i));
- allLocales.remove(toFullLocale(locale));
- filtered.add(locale);
+ Locale locale = copyExtensionToTargetLocale(firstRestoredLocale, current.get(i));
+
+ if (locale != null && existingLanguageAndScript.add(getLanguageAndScript(locale))) {
+ allLocales.remove(toFullLocale(locale));
+ 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) {
- String language = restoredLocaleWithExtension.getLanguage();
- String script = restoredLocaleWithExtension.getScript();
+ final Locale restoredLocaleWithExtension = copyExtensionToTargetLocale(
+ firstRestoredLocale, getFilteredLocale(restore.get(i), allLocales));
- String restoredLanguageAndScript =
- script == null ? language : language + "-" + script;
- if (existingLanguageAndScript.add(restoredLanguageAndScript)) {
- filtered.add(restoredLocaleWithExtension);
- }
+ if (restoredLocaleWithExtension != null && existingLanguageAndScript.add(
+ getLanguageAndScript(restoredLocaleWithExtension))) {
+ filtered.add(restoredLocaleWithExtension);
}
}
@@ -713,6 +710,16 @@ public class SettingsHelper {
return new LocaleList(filtered.toArray(new Locale[filtered.size()]));
}
+ private static String getLanguageAndScript(Locale locale) {
+ if (locale == null) {
+ return "";
+ }
+
+ String language = locale.getLanguage();
+ String script = locale.getScript();
+ return script == null ? language : String.join("-", language, script);
+ }
+
private static Locale copyExtensionToTargetLocale(Locale restoredLocale,
Locale targetLocale) {
if (!restoredLocale.hasExtensions()) {
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
index 65ede9d804d0..2dcaf088bf6c 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
@@ -4080,7 +4080,7 @@ public class SettingsProvider extends ContentProvider {
@VisibleForTesting
final class UpgradeController {
- private static final int SETTINGS_VERSION = 228;
+ private static final int SETTINGS_VERSION = 229;
private final int mUserId;
@@ -6336,6 +6336,52 @@ public class SettingsProvider extends ContentProvider {
currentVersion = 228;
}
+ // Version 228: Migrate WearOS time settings
+ if (currentVersion == 228) {
+ if (getContext()
+ .getPackageManager()
+ .hasSystemFeature(PackageManager.FEATURE_WATCH)) {
+
+ SettingsState global = getGlobalSettingsLocked();
+
+ Setting cwAutoTime =
+ global.getSettingLocked(Global.Wearable.CLOCKWORK_AUTO_TIME);
+ if (!cwAutoTime.isNull()) {
+ boolean phone =
+ String.valueOf(Global.Wearable.SYNC_TIME_FROM_PHONE)
+ .equals(cwAutoTime.getValue());
+ boolean network =
+ String.valueOf(Global.Wearable.SYNC_TIME_FROM_NETWORK)
+ .equals(cwAutoTime.getValue());
+ global.insertSettingLocked(
+ Global.AUTO_TIME,
+ phone || network ? "1" : "0",
+ null,
+ true,
+ SettingsState.SYSTEM_PACKAGE_NAME);
+ }
+
+ Setting cwAutoTimeZone =
+ global.getSettingLocked(Global.Wearable.CLOCKWORK_AUTO_TIME_ZONE);
+ if (!cwAutoTimeZone.isNull()) {
+ boolean phone =
+ String.valueOf(Global.Wearable.SYNC_TIME_ZONE_FROM_PHONE)
+ .equals(cwAutoTimeZone.getValue());
+ boolean network =
+ String.valueOf(Global.Wearable.SYNC_TIME_ZONE_FROM_NETWORK)
+ .equals(cwAutoTimeZone.getValue());
+ global.insertSettingLocked(
+ Global.AUTO_TIME_ZONE,
+ phone || network ? "1" : "0",
+ null,
+ true,
+ SettingsState.SYSTEM_PACKAGE_NAME);
+ }
+ }
+
+ currentVersion = 229;
+ }
+
// vXXX: Add new settings above this point.
if (currentVersion != newVersion) {
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 48c778542d66..2160d3164b17 100644
--- a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsHelperTest.java
+++ b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsHelperTest.java
@@ -388,11 +388,18 @@ 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"),
+ assertEquals(LocaleList.forLanguageTags("en-US,zh-Hans-TW,fr-FR"),
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
+ // restore
+ LocaleList.forLanguageTags("en-UK,en-GB,zh-Hans-HK,fr-FR"),
+
+ // current
+ LocaleList.forLanguageTags("en-US,zh-Hans-TW"),
+
+ // supported
+ new String[] {
+ "en-US" , "zh-Hans-TW" , "en-UK", "en-GB", "zh-Hans-HK", "fr-FR"
+ }));
}
@Test
diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml
index a178869d23d6..ea09d634b82b 100644
--- a/packages/Shell/AndroidManifest.xml
+++ b/packages/Shell/AndroidManifest.xml
@@ -961,6 +961,7 @@
android:featureFlag="android.security.aapm_api"/>
<uses-permission android:name="android.permission.QUERY_ADVANCED_PROTECTION_MODE"
android:featureFlag="android.security.aapm_api"/>
+ <uses-permission android:name="android.permission.MANAGE_DEVICE_POLICY_MTE" />
<!-- Permission required for CTS test - IntrusionDetectionManagerTest -->
<uses-permission android:name="android.permission.READ_INTRUSION_DETECTION_STATE"
diff --git a/packages/SystemUI/OWNERS b/packages/SystemUI/OWNERS
index ab3fa1b06255..728d206e2786 100644
--- a/packages/SystemUI/OWNERS
+++ b/packages/SystemUI/OWNERS
@@ -97,7 +97,6 @@ spdonghao@google.com
steell@google.com
stevenckng@google.com
stwu@google.com
-syeonlee@google.com
sunnygoyal@google.com
thiruram@google.com
tracyzhou@google.com
diff --git a/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/view/A11yMenuOverlayLayout.java b/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/view/A11yMenuOverlayLayout.java
index 60625f4fc703..db2fbd96408c 100644
--- a/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/view/A11yMenuOverlayLayout.java
+++ b/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/view/A11yMenuOverlayLayout.java
@@ -143,7 +143,7 @@ public class A11yMenuOverlayLayout {
final Display display = mDisplayManager.getDisplay(DEFAULT_DISPLAY);
final Context uiContext = mService.createWindowContext(
display, TYPE_ACCESSIBILITY_OVERLAY, /* options= */null);
- final WindowManager windowManager = uiContext.getSystemService(WindowManager.class);
+ final WindowManager windowManager = WindowManagerUtils.getWindowManager(uiContext);
mLayout = new A11yMenuFrameLayout(uiContext);
updateLayoutPosition(uiContext);
inflateLayoutAndSetOnTouchListener(mLayout, uiContext);
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index 17d8bbfd2240..3cda579aaf7b 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -512,13 +512,6 @@ flag {
}
flag {
- name: "status_bar_notification_chips"
- namespace: "systemui"
- description: "Show promoted ongoing notifications as chips in the status bar"
- bug: "364653005"
-}
-
-flag {
name: "status_bar_popup_chips"
namespace: "systemui"
description: "Show rich ongoing processes as chips in the status bar"
@@ -607,6 +600,16 @@ flag {
}
flag {
+ name: "avalanche_replace_hun_when_critical"
+ namespace: "systemui"
+ description: "Fix for replacing a sticky HUN when a critical HUN posted"
+ bug: "403301297"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+flag {
name: "indication_text_a11y_fix"
namespace: "systemui"
description: "add double shadow to the indication text at the bottom of the lock screen"
@@ -1390,13 +1393,6 @@ flag {
}
flag {
- name: "media_controls_posts_optimization"
- namespace: "systemui"
- description: "Ignore duplicate media notifications posted"
- bug: "358645640"
-}
-
-flag {
name: "media_controls_umo_inflation_in_background"
namespace: "systemui"
description: "Inflate UMO in background thread"
@@ -2038,13 +2034,6 @@ flag {
}
flag {
- name: "ui_rich_ongoing_force_expanded"
- namespace: "systemui"
- description: "Force promoted notifications to always be expanded"
- bug: "380901479"
-}
-
-flag {
name: "permission_helper_ui_rich_ongoing"
namespace: "systemui"
description: "[RONs] Guards inline permission helper for demoting RONs [Guts/card version]"
@@ -2059,13 +2048,6 @@ flag {
}
flag {
- name: "aod_ui_rich_ongoing"
- namespace: "systemui"
- description: "Show a rich ongoing notification on the always-on display (depends on ui_rich_ongoing)"
- bug: "369151941"
-}
-
-flag {
name: "stabilize_heads_up_group_v2"
namespace: "systemui"
description: "Stabilize heads up groups in VisualStabilityCoordinator"
@@ -2147,6 +2129,14 @@ flag {
}
flag {
+ name: "lockscreen_font"
+ namespace: "systemui"
+ description: "Read-only flag for lockscreen font"
+ bug: "393610165"
+ is_fixed_read_only: true
+}
+
+flag {
name: "always_compose_qs_ui_fragment"
namespace: "systemui"
description: "Have QQS and QS scenes in the Compose fragment always composed, not just when it should be visible."
@@ -2211,3 +2201,9 @@ flag {
bug: "403422950"
}
+flag {
+ name: "tv_global_actions_focus"
+ namespace: "systemui"
+ description: "Enables global actions focus on TV."
+ bug: "402759931"
+}
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/DialogTransitionAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/DialogTransitionAnimator.kt
index c88c4ebb1a8d..6a620b3b9c34 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/DialogTransitionAnimator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/DialogTransitionAnimator.kt
@@ -35,6 +35,7 @@ import android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALW
import com.android.app.animation.Interpolators
import com.android.internal.jank.Cuj.CujType
import com.android.internal.jank.InteractionJankMonitor
+import com.android.systemui.Flags
import com.android.systemui.util.maybeForceFullscreen
import com.android.systemui.util.registerAnimationOnBackInvoked
import java.util.concurrent.Executor
@@ -932,26 +933,29 @@ private class AnimatedDialog(
}
override fun onTransitionAnimationEnd(isExpandingFullyAbove: Boolean) {
- // onLaunchAnimationEnd is called by an Animator at the end of the animation,
- // on a Choreographer animation tick. The following calls will move the animated
- // content from the dialog overlay back to its original position, and this
- // change must be reflected in the next frame given that we then sync the next
- // frame of both the content and dialog ViewRoots. However, in case that content
- // is rendered by Compose, whose compositions are also scheduled on a
- // Choreographer frame, any state change made *right now* won't be reflected in
- // the next frame given that a Choreographer frame can't schedule another and
- // have it happen in the same frame. So we post the forwarded calls to
- // [Controller.onLaunchAnimationEnd], leaving this Choreographer frame, ensuring
- // that the move of the content back to its original window will be reflected in
- // the next frame right after [onLaunchAnimationEnd] is called.
- //
- // TODO(b/330672236): Move this to TransitionAnimator.
- dialog.context.mainExecutor.execute {
+ val onEnd = {
startController.onTransitionAnimationEnd(isExpandingFullyAbove)
endController.onTransitionAnimationEnd(isExpandingFullyAbove)
-
onLaunchAnimationEnd()
}
+ if (Flags.sceneContainer()) {
+ onEnd()
+ } else {
+ // onLaunchAnimationEnd is called by an Animator at the end of the
+ // animation, on a Choreographer animation tick. The following calls will
+ // move the animated content from the dialog overlay back to its original
+ // position, and this change must be reflected in the next frame given that
+ // we then sync the next frame of both the content and dialog ViewRoots.
+ // However, in case that content is rendered by Compose, whose compositions
+ // are also scheduled on a Choreographer frame, any state change made *right
+ // now* won't be reflected in the next frame given that a Choreographer
+ // frame can't schedule another and have it happen in the same frame. So we
+ // post the forwarded calls to [Controller.onLaunchAnimationEnd], leaving
+ // this Choreographer frame, ensuring that the move of the content back to
+ // its original window will be reflected in the next frame right after
+ // [onLaunchAnimationEnd] is called.
+ dialog.context.mainExecutor.execute { onEnd() }
+ }
}
override fun onTransitionAnimationProgress(
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/RemoteAnimationRunnerCompat.java b/packages/SystemUI/animation/src/com/android/systemui/animation/RemoteAnimationRunnerCompat.java
index 9e08317d2c6b..041ccb567146 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/RemoteAnimationRunnerCompat.java
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/RemoteAnimationRunnerCompat.java
@@ -199,8 +199,10 @@ public abstract class RemoteAnimationRunnerCompat extends IRemoteAnimationRunner
info.releaseAllSurfaces();
// Make sure that the transition leashes created are not leaked.
for (SurfaceControl leash : leashMap.values()) {
- if (leash.isValid()) {
+ try {
finishTransaction.reparent(leash, null);
+ } catch (Exception e) {
+ Log.e(TAG, "Failed to reparent leash", e);
}
}
// Don't release here since launcher might still be using them. Instead
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/TransitionAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/TransitionAnimator.kt
index a4a96d19e8bb..8886b9e5e275 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/TransitionAnimator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/TransitionAnimator.kt
@@ -39,6 +39,7 @@ import com.android.app.animation.Interpolators.LINEAR
import com.android.internal.annotations.VisibleForTesting
import com.android.internal.dynamicanimation.animation.SpringAnimation
import com.android.internal.dynamicanimation.animation.SpringForce
+import com.android.systemui.Flags
import com.android.systemui.Flags.moveTransitionAnimationLayer
import com.android.systemui.shared.Flags.returnAnimationFrameworkLibrary
import com.android.systemui.shared.Flags.returnAnimationFrameworkLongLived
@@ -1014,11 +1015,7 @@ class TransitionAnimator(
openingWindowSyncViewOverlay?.remove(windowBackgroundLayer)
}
}
- // TODO(b/330672236): Post this to the main thread for launches as well, so that they do not
- // flicker with Flexiglass enabled.
- if (controller.isLaunching) {
- onEnd()
- } else {
+ if (Flags.sceneContainer() || !controller.isLaunching) {
// onAnimationEnd is called at the end of the animation, on a Choreographer animation
// tick. During dialog launches, the following calls will move the animated content from
// the dialog overlay back to its original position, and this change must be reflected
@@ -1032,6 +1029,8 @@ class TransitionAnimator(
// Choreographer frame, ensuring that any state change applied by
// onTransitionAnimationEnd() will be reflected in the same frame.
mainExecutor.execute { onEnd() }
+ } else {
+ onEnd()
}
}
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/gesture/effect/ContentOverscrollEffect.kt b/packages/SystemUI/compose/core/src/com/android/compose/gesture/effect/ContentOverscrollEffect.kt
index cb713ece12a5..5ed72c7d94a2 100644
--- a/packages/SystemUI/compose/core/src/com/android/compose/gesture/effect/ContentOverscrollEffect.kt
+++ b/packages/SystemUI/compose/core/src/com/android/compose/gesture/effect/ContentOverscrollEffect.kt
@@ -55,7 +55,12 @@ open class BaseContentOverscrollEffect(
get() = animatable.value
override val isInProgress: Boolean
- get() = overscrollDistance != 0f
+ /**
+ * We need both checks, because [overscrollDistance] can be
+ * - zero while it is already being animated, if the animation starts from 0
+ * - greater than zero without an animation, if the content is still being dragged
+ */
+ get() = overscrollDistance != 0f || animatable.isRunning
override fun applyToScroll(
delta: Offset,
diff --git a/packages/SystemUI/compose/core/tests/src/com/android/compose/gesture/effect/OffsetOverscrollEffectTest.kt b/packages/SystemUI/compose/core/tests/src/com/android/compose/gesture/effect/OffsetOverscrollEffectTest.kt
index e7c47fb56130..8a1fa3724d15 100644
--- a/packages/SystemUI/compose/core/tests/src/com/android/compose/gesture/effect/OffsetOverscrollEffectTest.kt
+++ b/packages/SystemUI/compose/core/tests/src/com/android/compose/gesture/effect/OffsetOverscrollEffectTest.kt
@@ -16,12 +16,17 @@
package com.android.compose.gesture.effect
+import androidx.compose.foundation.OverscrollEffect
import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.gestures.ScrollableState
import androidx.compose.foundation.gestures.rememberScrollableState
import androidx.compose.foundation.gestures.scrollable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.overscroll
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.platform.LocalDensity
@@ -32,11 +37,14 @@ import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onRoot
import androidx.compose.ui.test.performTouchInput
+import androidx.compose.ui.test.swipeWithVelocity
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.truth.Truth.assertThat
import kotlin.properties.Delegates
+import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -47,7 +55,13 @@ class OffsetOverscrollEffectTest {
private val BOX_TAG = "box"
- private data class LayoutInfo(val layoutSize: Dp, val touchSlop: Float, val density: Density) {
+ private data class LayoutInfo(
+ val layoutSize: Dp,
+ val touchSlop: Float,
+ val density: Density,
+ val scrollableState: ScrollableState,
+ val overscrollEffect: OverscrollEffect,
+ ) {
fun expectedOffset(currentOffset: Dp): Dp {
return with(density) {
OffsetOverscrollEffect.computeOffset(this, currentOffset.toPx()).toDp()
@@ -55,22 +69,29 @@ class OffsetOverscrollEffectTest {
}
}
- private fun setupOverscrollableBox(scrollableOrientation: Orientation): LayoutInfo {
+ private fun setupOverscrollableBox(
+ scrollableOrientation: Orientation,
+ canScroll: () -> Boolean,
+ ): LayoutInfo {
val layoutSize: Dp = 200.dp
var touchSlop: Float by Delegates.notNull()
// The draggable touch slop, i.e. the min px distance a touch pointer must move before it is
// detected as a drag event.
lateinit var density: Density
+ lateinit var scrollableState: ScrollableState
+ lateinit var overscrollEffect: OverscrollEffect
+
rule.setContent {
density = LocalDensity.current
touchSlop = LocalViewConfiguration.current.touchSlop
- val overscrollEffect = rememberOffsetOverscrollEffect()
+ scrollableState = rememberScrollableState { if (canScroll()) it else 0f }
+ overscrollEffect = rememberOffsetOverscrollEffect()
Box(
Modifier.overscroll(overscrollEffect)
// A scrollable that does not consume the scroll gesture.
.scrollable(
- state = rememberScrollableState { 0f },
+ state = scrollableState,
orientation = scrollableOrientation,
overscrollEffect = overscrollEffect,
)
@@ -78,12 +99,16 @@ class OffsetOverscrollEffectTest {
.testTag(BOX_TAG)
)
}
- return LayoutInfo(layoutSize, touchSlop, density)
+ return LayoutInfo(layoutSize, touchSlop, density, scrollableState, overscrollEffect)
}
@Test
fun applyVerticalOffset_duringVerticalOverscroll() {
- val info = setupOverscrollableBox(scrollableOrientation = Orientation.Vertical)
+ val info =
+ setupOverscrollableBox(
+ scrollableOrientation = Orientation.Vertical,
+ canScroll = { false },
+ )
rule.onNodeWithTag(BOX_TAG).assertTopPositionInRootIsEqualTo(0.dp)
@@ -99,7 +124,11 @@ class OffsetOverscrollEffectTest {
@Test
fun applyNoOffset_duringHorizontalOverscroll() {
- val info = setupOverscrollableBox(scrollableOrientation = Orientation.Vertical)
+ val info =
+ setupOverscrollableBox(
+ scrollableOrientation = Orientation.Vertical,
+ canScroll = { false },
+ )
rule.onNodeWithTag(BOX_TAG).assertTopPositionInRootIsEqualTo(0.dp)
@@ -113,7 +142,11 @@ class OffsetOverscrollEffectTest {
@Test
fun backToZero_afterOverscroll() {
- val info = setupOverscrollableBox(scrollableOrientation = Orientation.Vertical)
+ val info =
+ setupOverscrollableBox(
+ scrollableOrientation = Orientation.Vertical,
+ canScroll = { false },
+ )
rule.onRoot().performTouchInput {
down(center)
@@ -131,7 +164,11 @@ class OffsetOverscrollEffectTest {
@Test
fun offsetOverscroll_followTheTouchPointer() {
- val info = setupOverscrollableBox(scrollableOrientation = Orientation.Vertical)
+ val info =
+ setupOverscrollableBox(
+ scrollableOrientation = Orientation.Vertical,
+ canScroll = { false },
+ )
// First gesture, drag down.
rule.onRoot().performTouchInput {
@@ -165,4 +202,130 @@ class OffsetOverscrollEffectTest {
.onNodeWithTag(BOX_TAG)
.assertTopPositionInRootIsEqualTo(info.expectedOffset(-info.layoutSize))
}
+
+ @Test
+ fun isScrollInProgress_overscroll() = runTest {
+ val info =
+ setupOverscrollableBox(
+ scrollableOrientation = Orientation.Vertical,
+ canScroll = { false },
+ )
+
+ // Start a swipe gesture, and swipe down to start an overscroll.
+ rule.onRoot().performTouchInput {
+ down(center)
+ moveBy(Offset(0f, info.touchSlop + info.layoutSize.toPx() / 2))
+ }
+
+ assertThat(info.scrollableState.isScrollInProgress).isTrue()
+ assertThat(info.overscrollEffect.isInProgress).isTrue()
+
+ // Finish the swipe gesture.
+ rule.onRoot().performTouchInput { up() }
+
+ assertThat(info.scrollableState.isScrollInProgress).isFalse()
+ assertThat(info.overscrollEffect.isInProgress).isTrue()
+
+ // Wait until the overscroll returns to idle.
+ rule.awaitIdle()
+
+ assertThat(info.scrollableState.isScrollInProgress).isFalse()
+ assertThat(info.overscrollEffect.isInProgress).isFalse()
+ }
+
+ @Test
+ fun isScrollInProgress_scroll() = runTest {
+ val info =
+ setupOverscrollableBox(
+ scrollableOrientation = Orientation.Vertical,
+ canScroll = { true },
+ )
+
+ rule.onNodeWithTag(BOX_TAG).assertTopPositionInRootIsEqualTo(0.dp)
+
+ // Start a swipe gesture, and swipe down to scroll.
+ rule.onRoot().performTouchInput {
+ down(center)
+ moveBy(Offset(0f, info.touchSlop + info.layoutSize.toPx() / 2))
+ }
+
+ assertThat(info.scrollableState.isScrollInProgress).isTrue()
+ assertThat(info.overscrollEffect.isInProgress).isFalse()
+
+ // Finish the swipe gesture.
+ rule.onRoot().performTouchInput { up() }
+
+ assertThat(info.scrollableState.isScrollInProgress).isFalse()
+ assertThat(info.overscrollEffect.isInProgress).isTrue()
+
+ // Wait until the overscroll returns to idle.
+ rule.awaitIdle()
+
+ assertThat(info.scrollableState.isScrollInProgress).isFalse()
+ assertThat(info.overscrollEffect.isInProgress).isFalse()
+ }
+
+ @Test
+ fun isScrollInProgress_flingToScroll() = runTest {
+ val info =
+ setupOverscrollableBox(
+ scrollableOrientation = Orientation.Vertical,
+ canScroll = { true },
+ )
+
+ rule.onNodeWithTag(BOX_TAG).assertTopPositionInRootIsEqualTo(0.dp)
+
+ // Swipe down and leave some velocity to start a fling.
+ rule.onRoot().performTouchInput {
+ swipeWithVelocity(
+ Offset.Zero,
+ Offset(0f, info.touchSlop + info.layoutSize.toPx() / 2),
+ endVelocity = 100f,
+ )
+ }
+
+ assertThat(info.scrollableState.isScrollInProgress).isTrue()
+ assertThat(info.overscrollEffect.isInProgress).isFalse()
+
+ // Wait until the fling is finished.
+ rule.awaitIdle()
+
+ assertThat(info.scrollableState.isScrollInProgress).isFalse()
+ assertThat(info.overscrollEffect.isInProgress).isFalse()
+ }
+
+ @Test
+ fun isScrollInProgress_flingToOverscroll() = runTest {
+ // Start with a scrollable state.
+ var canScroll by mutableStateOf(true)
+ val info =
+ setupOverscrollableBox(scrollableOrientation = Orientation.Vertical) { canScroll }
+
+ rule.onNodeWithTag(BOX_TAG).assertTopPositionInRootIsEqualTo(0.dp)
+
+ // Swipe down and leave some velocity to start a fling.
+ rule.onRoot().performTouchInput {
+ swipeWithVelocity(
+ Offset.Zero,
+ Offset(0f, info.touchSlop + info.layoutSize.toPx() / 2),
+ endVelocity = 100f,
+ )
+ }
+
+ assertThat(info.scrollableState.isScrollInProgress).isTrue()
+ assertThat(info.overscrollEffect.isInProgress).isFalse()
+
+ // The fling reaches the end of the scrollable region, and an overscroll starts.
+ canScroll = false
+ rule.mainClock.advanceTimeUntil { !info.scrollableState.isScrollInProgress }
+
+ assertThat(info.scrollableState.isScrollInProgress).isFalse()
+ assertThat(info.overscrollEffect.isInProgress).isTrue()
+
+ // Wait until the overscroll returns to idle.
+ rule.awaitIdle()
+
+ assertThat(info.scrollableState.isScrollInProgress).isFalse()
+ assertThat(info.overscrollEffect.isInProgress).isFalse()
+ }
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt
index 2b8fe39c4870..16a8f987ba83 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt
@@ -1,5 +1,6 @@
package com.android.systemui.communal.ui.compose
+import android.content.res.Configuration
import androidx.compose.animation.core.CubicBezierEasing
import androidx.compose.animation.core.RepeatMode
import androidx.compose.animation.core.animateFloat
@@ -15,6 +16,7 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
@@ -26,6 +28,7 @@ import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.BlendMode
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.semantics.clearAndSetSemantics
import androidx.compose.ui.semantics.disabled
@@ -55,6 +58,7 @@ import com.android.systemui.communal.ui.compose.Dimensions.Companion.SlideOffset
import com.android.systemui.communal.ui.compose.extensions.allowGestures
import com.android.systemui.communal.ui.viewmodel.CommunalViewModel
import com.android.systemui.communal.util.CommunalColors
+import com.android.systemui.keyguard.domain.interactor.FromGlanceableHubTransitionInteractor.Companion.TO_LOCKSCREEN_DURATION
import com.android.systemui.keyguard.domain.interactor.FromPrimaryBouncerTransitionInteractor.Companion.TO_GONE_DURATION
import com.android.systemui.scene.shared.model.SceneDataSourceDelegator
import com.android.systemui.scene.ui.composable.SceneTransitionLayoutDataSource
@@ -98,6 +102,17 @@ val sceneTransitionsV2 = transitions {
spec = tween(durationMillis = TO_GONE_DURATION.toInt(DurationUnit.MILLISECONDS))
fade(AllElements)
}
+ to(CommunalScenes.Blank, key = CommunalTransitionKeys.SwipeInLandscape) {
+ spec = tween(durationMillis = TO_LOCKSCREEN_DURATION.toInt(DurationUnit.MILLISECONDS))
+ translate(Communal.Elements.Grid, Edge.End)
+ timestampRange(endMillis = 167) {
+ fade(Communal.Elements.Grid)
+ fade(Communal.Elements.IndicationArea)
+ fade(Communal.Elements.LockIcon)
+ fade(Communal.Elements.StatusBar)
+ }
+ timestampRange(startMillis = 167, endMillis = 500) { fade(Communal.Elements.Scrim) }
+ }
to(CommunalScenes.Blank, key = CommunalTransitionKeys.Swipe) {
spec = tween(durationMillis = TransitionDuration.TO_GLANCEABLE_HUB_DURATION_MS)
translate(Communal.Elements.Grid, Edge.End)
@@ -214,6 +229,9 @@ fun CommunalContainer(
val blurRadius = with(LocalDensity.current) { viewModel.blurRadiusPx.toDp() }
+ val swipeFromHubInLandscape by
+ viewModel.swipeFromHubInLandscape.collectAsStateWithLifecycle(false)
+
SceneTransitionLayout(
state = state,
modifier = modifier.fillMaxSize().thenIf(isUiBlurred) { Modifier.blur(blurRadius) },
@@ -241,7 +259,14 @@ fun CommunalContainer(
userActions =
mapOf(
Swipe.End to
- UserActionResult(CommunalScenes.Blank, CommunalTransitionKeys.Swipe)
+ UserActionResult(
+ CommunalScenes.Blank,
+ if (swipeFromHubInLandscape) {
+ CommunalTransitionKeys.SwipeInLandscape
+ } else {
+ CommunalTransitionKeys.Swipe
+ },
+ )
),
) {
CommunalScene(
@@ -258,6 +283,20 @@ fun CommunalContainer(
Box(modifier = Modifier.fillMaxSize().allowGestures(touchesAllowed))
}
+/** Listens to orientation changes on communal scene and reset when scene is disposed. */
+@Composable
+fun ObserveOrientationChange(viewModel: CommunalViewModel) {
+ val configuration = LocalConfiguration.current
+
+ LaunchedEffect(configuration.orientation) {
+ viewModel.onOrientationChange(configuration.orientation)
+ }
+
+ DisposableEffect(Unit) {
+ onDispose { viewModel.onOrientationChange(Configuration.ORIENTATION_UNDEFINED) }
+ }
+}
+
/** Scene containing the glanceable hub UI. */
@Composable
fun ContentScope.CommunalScene(
@@ -269,6 +308,8 @@ fun ContentScope.CommunalScene(
) {
val isFocusable by viewModel.isFocusable.collectAsStateWithLifecycle(initialValue = false)
+ // Observe screen rotation while Communal Scene is active.
+ ObserveOrientationChange(viewModel)
Box(
modifier =
Modifier.element(Communal.Elements.Scrim)
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt
index d903c3d16fdb..748c3b89649a 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt
@@ -53,7 +53,7 @@ import com.android.systemui.statusbar.notification.icon.ui.viewbinder.Notificati
import com.android.systemui.statusbar.notification.icon.ui.viewbinder.StatusBarIconViewBindingFailureTracker
import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerAlwaysOnDisplayViewModel
import com.android.systemui.statusbar.notification.promoted.AODPromotedNotification
-import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUiAod
+import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUi
import com.android.systemui.statusbar.notification.promoted.ui.viewmodel.AODPromotedNotificationViewModel
import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout
import com.android.systemui.statusbar.notification.stack.ui.view.NotificationScrollView
@@ -111,7 +111,7 @@ constructor(
@Composable
fun AodPromotedNotificationArea(modifier: Modifier = Modifier) {
- if (!PromotedNotificationUiAod.isEnabled) {
+ if (!PromotedNotificationUi.isEnabled) {
return
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationStackNestedScrollConnection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationStackNestedScrollConnection.kt
deleted file mode 100644
index e1ee59ba0626..000000000000
--- a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationStackNestedScrollConnection.kt
+++ /dev/null
@@ -1,151 +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.notifications.ui.composable
-
-import androidx.compose.animation.core.Animatable
-import androidx.compose.animation.core.tween
-import androidx.compose.foundation.gestures.FlingBehavior
-import androidx.compose.foundation.gestures.Orientation
-import androidx.compose.foundation.gestures.ScrollableDefaults
-import androidx.compose.foundation.layout.offset
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.remember
-import androidx.compose.ui.Modifier
-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.platform.LocalConfiguration
-import androidx.compose.ui.platform.LocalDensity
-import androidx.compose.ui.unit.IntOffset
-import androidx.compose.ui.unit.Velocity
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.util.fastCoerceAtLeast
-import com.android.compose.nestedscroll.OnStopScope
-import com.android.compose.nestedscroll.PriorityNestedScrollConnection
-import com.android.compose.nestedscroll.ScrollController
-import kotlin.math.max
-import kotlin.math.roundToInt
-import kotlin.math.tanh
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.launch
-
-@Composable
-fun Modifier.stackVerticalOverscroll(
- coroutineScope: CoroutineScope,
- canScrollForward: () -> Boolean,
-): Modifier {
- val screenHeight =
- with(LocalDensity.current) { LocalConfiguration.current.screenHeightDp.dp.toPx() }
- val overscrollOffset = remember { Animatable(0f) }
- val flingBehavior = ScrollableDefaults.flingBehavior()
- val stackNestedScrollConnection =
- remember(flingBehavior) {
- NotificationStackNestedScrollConnection(
- stackOffset = { overscrollOffset.value },
- canScrollForward = canScrollForward,
- onScroll = { offsetAvailable ->
- coroutineScope.launch {
- val maxProgress = screenHeight * 0.2f
- val tilt = 3f
- var offset =
- overscrollOffset.value +
- maxProgress * tanh(x = offsetAvailable / (maxProgress * tilt))
- offset = max(offset, -1f * maxProgress)
- overscrollOffset.snapTo(offset)
- }
- },
- onStop = { velocityAvailable ->
- coroutineScope.launch {
- overscrollOffset.animateTo(
- targetValue = 0f,
- initialVelocity = velocityAvailable,
- animationSpec = tween(),
- )
- }
- },
- flingBehavior = flingBehavior,
- )
- }
-
- return this.then(
- Modifier.nestedScroll(
- remember {
- object : NestedScrollConnection {
- override suspend fun onPostFling(
- consumed: Velocity,
- available: Velocity,
- ): Velocity {
- return if (available.y < 0f && !canScrollForward()) {
- overscrollOffset.animateTo(
- targetValue = 0f,
- initialVelocity = available.y,
- animationSpec = tween(),
- )
- available
- } else {
- Velocity.Zero
- }
- }
- }
- }
- )
- .nestedScroll(stackNestedScrollConnection)
- .offset { IntOffset(x = 0, y = overscrollOffset.value.roundToInt()) }
- )
-}
-
-fun NotificationStackNestedScrollConnection(
- stackOffset: () -> Float,
- canScrollForward: () -> Boolean,
- onStart: (Float) -> Unit = {},
- onScroll: (Float) -> Unit,
- onStop: (Float) -> Unit = {},
- flingBehavior: FlingBehavior,
-): PriorityNestedScrollConnection {
- return PriorityNestedScrollConnection(
- orientation = Orientation.Vertical,
- canStartPreScroll = { _, _, _ -> false },
- canStartPostScroll = { offsetAvailable, offsetBeforeStart, _ ->
- offsetAvailable < 0f && offsetBeforeStart < 0f && !canScrollForward()
- },
- onStart = { firstScroll ->
- onStart(firstScroll)
- object : ScrollController {
- override fun onScroll(deltaScroll: Float, source: NestedScrollSource): Float {
- val minOffset = 0f
- val consumed = deltaScroll.fastCoerceAtLeast(minOffset - stackOffset())
- if (consumed != 0f) {
- onScroll(consumed)
- }
- return consumed
- }
-
- override suspend fun OnStopScope.onStop(initialVelocity: Float): Float {
- val consumedByScroll = flingToScroll(initialVelocity, flingBehavior)
- onStop(initialVelocity - consumedByScroll)
- return initialVelocity
- }
-
- override fun onCancel() {
- onStop(0f)
- }
-
- override fun canStopOnPreFling() = false
- }
- },
- )
-}
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 79b346439d5d..2f9cfb6aa211 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
@@ -78,6 +78,7 @@ import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.layout.positionInWindow
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntOffset
@@ -92,7 +93,11 @@ import com.android.compose.animation.scene.LowestZIndexContentPicker
import com.android.compose.animation.scene.SceneTransitionLayoutState
import com.android.compose.animation.scene.content.state.TransitionState
import com.android.compose.gesture.NestedScrollableBound
+import com.android.compose.gesture.effect.OffsetOverscrollEffect
+import com.android.compose.gesture.effect.rememberOffsetOverscrollEffect
import com.android.compose.modifiers.thenIf
+import com.android.internal.jank.InteractionJankMonitor
+import com.android.internal.jank.InteractionJankMonitor.CUJ_NOTIFICATION_SHADE_SCROLL_FLING
import com.android.systemui.common.ui.compose.windowinsets.LocalScreenCornerRadius
import com.android.systemui.res.R
import com.android.systemui.scene.session.ui.composable.SaveableSession
@@ -288,17 +293,19 @@ fun ContentScope.NotificationScrollingStack(
shadeSession: SaveableSession,
stackScrollView: NotificationScrollView,
viewModel: NotificationsPlaceholderViewModel,
+ jankMonitor: InteractionJankMonitor,
maxScrimTop: () -> Float,
shouldPunchHoleBehindScrim: Boolean,
stackTopPadding: Dp,
stackBottomPadding: Dp,
+ modifier: Modifier = Modifier,
shouldFillMaxSize: Boolean = true,
shouldIncludeHeadsUpSpace: Boolean = true,
shouldShowScrim: Boolean = true,
supportNestedScrolling: Boolean,
onEmptySpaceClick: (() -> Unit)? = null,
- modifier: Modifier = Modifier,
) {
+ val composeViewRoot = LocalView.current
val coroutineScope = shadeSession.sessionCoroutineScope()
val density = LocalDensity.current
val screenCornerRadius = LocalScreenCornerRadius.current
@@ -477,6 +484,21 @@ fun ContentScope.NotificationScrollingStack(
)
}
+ val overScrollEffect: OffsetOverscrollEffect = rememberOffsetOverscrollEffect()
+ // whether the stack is moving due to a swipe or fling
+ val isScrollInProgress =
+ scrollState.isScrollInProgress || overScrollEffect.isInProgress || scrimOffset.isRunning
+
+ LaunchedEffect(isScrollInProgress) {
+ if (isScrollInProgress) {
+ jankMonitor.begin(composeViewRoot, CUJ_NOTIFICATION_SHADE_SCROLL_FLING)
+ debugLog(viewModel) { "STACK scroll begins" }
+ } else {
+ debugLog(viewModel) { "STACK scroll ends" }
+ jankMonitor.end(CUJ_NOTIFICATION_SHADE_SCROLL_FLING)
+ }
+ }
+
Box(
modifier =
modifier
@@ -577,8 +599,7 @@ fun ContentScope.NotificationScrollingStack(
.thenIf(supportNestedScrolling) {
Modifier.nestedScroll(scrimNestedScrollConnection)
}
- .stackVerticalOverscroll(coroutineScope) { scrollState.canScrollForward }
- .verticalScroll(scrollState)
+ .verticalScroll(scrollState, overscrollEffect = overScrollEffect)
.padding(top = stackTopPadding, bottom = stackBottomPadding)
.fillMaxWidth()
.onGloballyPositioned { coordinates ->
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeOverlay.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeOverlay.kt
index 7cd6c6b47f2a..6d37e0affd6a 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeOverlay.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeOverlay.kt
@@ -29,6 +29,7 @@ import com.android.compose.animation.scene.ContentScope
import com.android.compose.animation.scene.ElementKey
import com.android.compose.animation.scene.UserAction
import com.android.compose.animation.scene.UserActionResult
+import com.android.internal.jank.InteractionJankMonitor
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.keyguard.ui.composable.blueprint.rememberBurnIn
import com.android.systemui.keyguard.ui.composable.section.DefaultClockSection
@@ -49,6 +50,7 @@ import com.android.systemui.scene.shared.model.Overlays
import com.android.systemui.scene.ui.composable.Overlay
import com.android.systemui.shade.ui.composable.OverlayShade
import com.android.systemui.shade.ui.composable.OverlayShadeHeader
+import com.android.systemui.shade.ui.composable.isFullWidthShade
import com.android.systemui.statusbar.notification.stack.ui.view.NotificationScrollView
import com.android.systemui.util.Utils
import dagger.Lazy
@@ -68,6 +70,7 @@ constructor(
private val keyguardClockViewModel: KeyguardClockViewModel,
private val mediaCarouselController: MediaCarouselController,
@Named(QUICK_QS_PANEL) private val mediaHost: Lazy<MediaHost>,
+ private val jankMonitor: InteractionJankMonitor,
) : Overlay {
override val key = Overlays.NotificationsShade
@@ -117,7 +120,7 @@ constructor(
) {
Box {
Column {
- if (viewModel.showClock) {
+ if (isFullWidthShade()) {
val burnIn = rememberBurnIn(keyguardClockViewModel)
with(clockSection) {
@@ -145,6 +148,7 @@ constructor(
shadeSession = shadeSession,
stackScrollView = stackScrollView.get(),
viewModel = placeholderViewModel,
+ jankMonitor = jankMonitor,
maxScrimTop = { 0f },
shouldPunchHoleBehindScrim = false,
stackTopPadding = notificationStackPadding,
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt
index 0a711487ccb1..d667f68e4fdd 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt
@@ -75,6 +75,7 @@ import com.android.compose.animation.scene.animateSceneFloatAsState
import com.android.compose.animation.scene.content.state.TransitionState
import com.android.compose.modifiers.thenIf
import com.android.compose.windowsizeclass.LocalWindowSizeClass
+import com.android.internal.jank.InteractionJankMonitor
import com.android.systemui.common.ui.compose.windowinsets.CutoutLocation
import com.android.systemui.common.ui.compose.windowinsets.LocalDisplayCutout
import com.android.systemui.compose.modifiers.sysuiResTag
@@ -126,6 +127,7 @@ constructor(
private val contentViewModelFactory: QuickSettingsSceneContentViewModel.Factory,
private val mediaCarouselController: MediaCarouselController,
@Named(MediaModule.QS_PANEL) private val mediaHost: MediaHost,
+ private val jankMonitor: InteractionJankMonitor,
) : ExclusiveActivatable(), Scene {
override val key = Scenes.QuickSettings
@@ -165,6 +167,7 @@ constructor(
mediaHost = mediaHost,
modifier = modifier,
shadeSession = shadeSession,
+ jankMonitor = jankMonitor,
)
}
@@ -186,6 +189,7 @@ private fun ContentScope.QuickSettingsScene(
mediaHost: MediaHost,
modifier: Modifier = Modifier,
shadeSession: SaveableSession,
+ jankMonitor: InteractionJankMonitor,
) {
val cutoutLocation = LocalDisplayCutout.current.location
val brightnessMirrorShowing by brightnessMirrorViewModel.isShowing.collectAsStateWithLifecycle()
@@ -432,6 +436,7 @@ private fun ContentScope.QuickSettingsScene(
shadeSession = shadeSession,
stackScrollView = notificationStackScrollView,
viewModel = notificationsPlaceholderViewModel,
+ jankMonitor = jankMonitor,
maxScrimTop = { minNotificationStackTop.toFloat() },
shouldPunchHoleBehindScrim = shouldPunchHoleBehindScrim,
stackTopPadding = notificationStackPadding,
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt
index 885d34fb95c9..60e32d7ce824 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt
@@ -73,6 +73,7 @@ import com.android.compose.animation.scene.animateSceneFloatAsState
import com.android.compose.animation.scene.content.state.TransitionState
import com.android.compose.modifiers.padding
import com.android.compose.modifiers.thenIf
+import com.android.internal.jank.InteractionJankMonitor
import com.android.systemui.battery.BatteryMeterViewController
import com.android.systemui.common.ui.compose.windowinsets.CutoutLocation
import com.android.systemui.common.ui.compose.windowinsets.LocalDisplayCutout
@@ -145,6 +146,7 @@ constructor(
private val mediaCarouselController: MediaCarouselController,
@Named(QUICK_QS_PANEL) private val qqsMediaHost: MediaHost,
@Named(QS_PANEL) private val qsMediaHost: MediaHost,
+ private val jankMonitor: InteractionJankMonitor,
) : ExclusiveActivatable(), Scene {
override val key = Scenes.Shade
@@ -182,6 +184,7 @@ constructor(
mediaCarouselController = mediaCarouselController,
qqsMediaHost = qqsMediaHost,
qsMediaHost = qsMediaHost,
+ jankMonitor = jankMonitor,
modifier = modifier,
shadeSession = shadeSession,
usingCollapsedLandscapeMedia =
@@ -212,6 +215,7 @@ private fun ContentScope.ShadeScene(
mediaCarouselController: MediaCarouselController,
qqsMediaHost: MediaHost,
qsMediaHost: MediaHost,
+ jankMonitor: InteractionJankMonitor,
modifier: Modifier = Modifier,
shadeSession: SaveableSession,
usingCollapsedLandscapeMedia: Boolean,
@@ -229,6 +233,7 @@ private fun ContentScope.ShadeScene(
modifier = modifier,
shadeSession = shadeSession,
usingCollapsedLandscapeMedia = usingCollapsedLandscapeMedia,
+ jankMonitor = jankMonitor,
)
is ShadeMode.Split ->
SplitShade(
@@ -240,6 +245,7 @@ private fun ContentScope.ShadeScene(
mediaHost = qsMediaHost,
modifier = modifier,
shadeSession = shadeSession,
+ jankMonitor = jankMonitor,
)
is ShadeMode.Dual -> error("Dual shade is implemented separately as an overlay.")
}
@@ -253,6 +259,7 @@ private fun ContentScope.SingleShade(
notificationsPlaceholderViewModel: NotificationsPlaceholderViewModel,
mediaCarouselController: MediaCarouselController,
mediaHost: MediaHost,
+ jankMonitor: InteractionJankMonitor,
modifier: Modifier = Modifier,
shadeSession: SaveableSession,
usingCollapsedLandscapeMedia: Boolean,
@@ -379,6 +386,7 @@ private fun ContentScope.SingleShade(
shadeSession = shadeSession,
stackScrollView = notificationStackScrollView,
viewModel = notificationsPlaceholderViewModel,
+ jankMonitor = jankMonitor,
maxScrimTop = { maxNotifScrimTop.toFloat() },
shouldPunchHoleBehindScrim = shouldPunchHoleBehindScrim,
stackTopPadding = notificationStackPadding,
@@ -419,6 +427,7 @@ private fun ContentScope.SplitShade(
mediaHost: MediaHost,
modifier: Modifier = Modifier,
shadeSession: SaveableSession,
+ jankMonitor: InteractionJankMonitor,
) {
val isCustomizing by viewModel.qsSceneAdapter.isCustomizing.collectAsStateWithLifecycle()
val isQsEnabled by viewModel.isQsEnabled.collectAsStateWithLifecycle()
@@ -596,6 +605,7 @@ private fun ContentScope.SplitShade(
shadeSession = shadeSession,
stackScrollView = notificationStackScrollView,
viewModel = notificationsPlaceholderViewModel,
+ jankMonitor = jankMonitor,
maxScrimTop = { 0f },
stackTopPadding = notificationStackPadding,
stackBottomPadding = notificationStackPadding,
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt
index 05958a212f47..9ba74749639a 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt
@@ -52,6 +52,7 @@ import androidx.compose.ui.util.fastForEach
import androidx.compose.ui.util.fastForEachIndexed
import androidx.compose.ui.util.fastForEachReversed
import androidx.compose.ui.util.lerp
+import com.android.compose.animation.scene.Element.Companion.SizeUnspecified
import com.android.compose.animation.scene.content.Content
import com.android.compose.animation.scene.content.state.TransitionState
import com.android.compose.animation.scene.transformation.CustomPropertyTransformation
@@ -105,6 +106,13 @@ internal class Element(val key: ElementKey) {
var targetSize by mutableStateOf(SizeUnspecified)
var targetOffset by mutableStateOf(Offset.Unspecified)
+ /**
+ * The *approach* state of this element in this content, i.e. the intermediate layout state
+ * during transitions, used for smooth animation. Note: These values are computed before
+ * measuring the children.
+ */
+ var approachSize by mutableStateOf(SizeUnspecified)
+
/** The last state this element had in this content. */
var lastOffset = Offset.Unspecified
var lastSize = SizeUnspecified
@@ -340,7 +348,11 @@ internal class ElementNode(
override fun isMeasurementApproachInProgress(lookaheadSize: IntSize): Boolean {
// TODO(b/324191441): Investigate whether making this check more complex (checking if this
// element is shared or transformed) would lead to better performance.
- return isAnyStateTransitioning()
+ val isTransitioning = isAnyStateTransitioning()
+ if (!isTransitioning) {
+ stateInContent.approachSize = SizeUnspecified
+ }
+ return isTransitioning
}
override fun Placeable.PlacementScope.isPlacementApproachInProgress(
@@ -392,6 +404,7 @@ internal class ElementNode(
// sharedElement isn't part of either but the element is still rendered as part of
// the underlying scene that is currently not being transitioned.
val currentState = currentTransitionStates.last().last()
+ stateInContent.approachSize = Element.SizeUnspecified
val shouldPlaceInThisContent =
elementContentWhenIdle(
layoutImpl,
@@ -409,7 +422,14 @@ internal class ElementNode(
val transition = elementState as? TransitionState.Transition
val placeable =
- measure(layoutImpl, element, transition, stateInContent, measurable, constraints)
+ approachMeasure(
+ layoutImpl = layoutImpl,
+ element = element,
+ transition = transition,
+ stateInContent = stateInContent,
+ measurable = measurable,
+ constraints = constraints,
+ )
stateInContent.lastSize = placeable.size()
return layout(placeable.width, placeable.height) { place(elementState, placeable) }
}
@@ -1183,7 +1203,7 @@ private fun interruptedAlpha(
)
}
-private fun measure(
+private fun approachMeasure(
layoutImpl: SceneTransitionLayoutImpl,
element: Element,
transition: TransitionState.Transition?,
@@ -1214,6 +1234,7 @@ private fun measure(
maybePlaceable?.let { placeable ->
stateInContent.sizeBeforeInterruption = Element.SizeUnspecified
stateInContent.sizeInterruptionDelta = IntSize.Zero
+ stateInContent.approachSize = Element.SizeUnspecified
return placeable
}
@@ -1236,6 +1257,10 @@ private fun measure(
)
},
)
+
+ // Important: Set approachSize before child measurement. Could be used for their calculations.
+ stateInContent.approachSize = interruptedSize
+
return measurable.measure(
Constraints.fixed(
interruptedSize.width.coerceAtLeast(0),
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
index 22688d310b44..cfd59fd316d3 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
@@ -160,6 +160,13 @@ interface ElementStateScope {
fun ElementKey.targetSize(content: ContentKey): IntSize?
/**
+ * Return the *approaching* size of [this] element in the given [content], i.e. thethe size the
+ * element when is transitioning, or `null` if the element is not composed and measured in that
+ * content (yet).
+ */
+ fun ElementKey.approachSize(content: ContentKey): IntSize?
+
+ /**
* Return the *target* offset of [this] element in the given [content], i.e. the size of the
* element when idle, or `null` if the element is not composed and placed in that content (yet).
*/
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/UserActionDistanceScopeImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/UserActionDistanceScopeImpl.kt
index 5d4232d8a8b7..0d5ae81c501d 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/UserActionDistanceScopeImpl.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/UserActionDistanceScopeImpl.kt
@@ -31,6 +31,12 @@ internal class ElementStateScopeImpl(private val layoutImpl: SceneTransitionLayo
}
}
+ override fun ElementKey.approachSize(content: ContentKey): IntSize? {
+ return layoutImpl.elements[this]?.stateByContent?.get(content)?.approachSize.takeIf {
+ it != Element.SizeUnspecified
+ }
+ }
+
override fun ElementKey.targetOffset(content: ContentKey): Offset? {
return layoutImpl.elements[this]?.stateByContent?.get(content)?.targetOffset.takeIf {
it != Offset.Unspecified
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt
index 86cbfe4f1a8b..aff5aa097a8e 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt
@@ -2312,4 +2312,76 @@ class ElementTest {
assertThat(compositions).isEqualTo(1)
}
+
+ @Test
+ fun measureElementApproachSizeBeforeChildren() {
+ val state =
+ rule.runOnUiThread {
+ MutableSceneTransitionLayoutStateForTests(SceneA, SceneTransitions.Empty)
+ }
+
+ lateinit var fooHeight: () -> Dp?
+ val fooHeightPreChildMeasure = mutableListOf<Dp?>()
+
+ val scope =
+ rule.setContentAndCreateMainScope {
+ val density = LocalDensity.current
+ SceneTransitionLayoutForTesting(state) {
+ scene(SceneA) {
+ fooHeight = {
+ with(density) { TestElements.Foo.approachSize(SceneA)?.height?.toDp() }
+ }
+ Box(Modifier.element(TestElements.Foo).size(200.dp)) {
+ Box(
+ Modifier.approachLayout(
+ isMeasurementApproachInProgress = { false },
+ approachMeasure = { measurable, constraints ->
+ fooHeightPreChildMeasure += fooHeight()
+ measurable.measure(constraints).run {
+ layout(width, height) {}
+ }
+ },
+ )
+ )
+ }
+ }
+ scene(SceneB) { Box(Modifier.element(TestElements.Foo).size(100.dp)) }
+ }
+ }
+
+ var progress by mutableFloatStateOf(0f)
+ val transition = transition(from = SceneA, to = SceneB, progress = { progress })
+ var countApproachPass = fooHeightPreChildMeasure.size
+
+ // Idle state: Scene A.
+ assertThat(state.isTransitioning()).isFalse()
+ assertThat(fooHeight()).isNull()
+
+ // Start transition: Scene A -> Scene B (progress 0%).
+ scope.launch { state.startTransition(transition) }
+ rule.waitForIdle()
+ assertThat(state.isTransitioning()).isTrue()
+ assertThat(fooHeightPreChildMeasure[countApproachPass]?.value).isWithin(.5f).of(200f)
+ assertThat(fooHeight()).isNotNull()
+ countApproachPass = fooHeightPreChildMeasure.size
+
+ // progress 50%: height is going from 200dp to 100dp, so 150dp is expected now.
+ progress = 0.5f
+ rule.waitForIdle()
+ assertThat(fooHeightPreChildMeasure[countApproachPass]?.value).isWithin(.5f).of(150f)
+ assertThat(fooHeight()).isNotNull()
+ countApproachPass = fooHeightPreChildMeasure.size
+
+ progress = 1f
+ rule.waitForIdle()
+ assertThat(fooHeightPreChildMeasure[countApproachPass]?.value).isWithin(.5f).of(100f)
+ assertThat(fooHeight()).isNotNull()
+ countApproachPass = fooHeightPreChildMeasure.size
+
+ transition.finish()
+ rule.waitForIdle()
+ assertThat(state.isTransitioning()).isFalse()
+ assertThat(fooHeight()).isNull()
+ assertThat(fooHeightPreChildMeasure.size).isEqualTo(countApproachPass)
+ }
}
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt
index 05c9818f0c57..655b79c5b635 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt
@@ -209,8 +209,7 @@ open class SimpleDigitalClockTextView(
lockScreenPaint.typeface = typefaceCache.getTypefaceForVariant(lsFontVariation)
typeface = lockScreenPaint.typeface
- textBounds = lockScreenPaint.getTextBounds(text)
- targetTextBounds = textBounds
+ updateTextBounds()
textAnimator.setTextStyle(
TextAnimator.Style(fVar = lsFontVariation),
@@ -253,9 +252,9 @@ open class SimpleDigitalClockTextView(
object : TextAnimatorListener {
override fun onInvalidate() = invalidate()
- override fun onRebased() = updateTextBounds()
+ override fun onRebased() = updateAnimationTextBounds()
- override fun onPaintModified() = updateTextBounds()
+ override fun onPaintModified() = updateAnimationTextBounds()
},
)
setInterpolatorPaint()
@@ -414,10 +413,7 @@ open class SimpleDigitalClockTextView(
}
fun refreshText() {
- textBounds = lockScreenPaint.getTextBounds(text)
- targetTextBounds =
- if (!this::textAnimator.isInitialized) textBounds
- else textAnimator.textInterpolator.targetPaint.getTextBounds(text)
+ updateTextBounds()
if (layout == null) {
requestLayout()
@@ -579,8 +575,7 @@ open class SimpleDigitalClockTextView(
if (fontSizePx > 0) {
setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSizePx)
lockScreenPaint.textSize = textSize
- textBounds = lockScreenPaint.getTextBounds(text)
- targetTextBounds = textBounds
+ updateTextBounds()
}
if (!constrainedByHeight) {
val lastUnconstrainedHeight = textBounds.height + lockScreenPaint.strokeWidth * 2
@@ -624,15 +619,26 @@ open class SimpleDigitalClockTextView(
}
}
+ /** Updates both the lockscreen text bounds and animation text bounds */
+ private fun updateTextBounds() {
+ textBounds = lockScreenPaint.getTextBounds(text)
+ updateAnimationTextBounds()
+ }
+
/**
* Called after textAnimator.setTextStyle textAnimator.setTextStyle will update targetPaint, and
* rebase if previous animator is canceled so basePaint will store the state we transition from
* and targetPaint will store the state we transition to
*/
- private fun updateTextBounds() {
+ private fun updateAnimationTextBounds() {
drawnProgress = null
- prevTextBounds = textAnimator.textInterpolator.basePaint.getTextBounds(text)
- targetTextBounds = textAnimator.textInterpolator.targetPaint.getTextBounds(text)
+ if (this::textAnimator.isInitialized) {
+ prevTextBounds = textAnimator.textInterpolator.basePaint.getTextBounds(text)
+ targetTextBounds = textAnimator.textInterpolator.targetPaint.getTextBounds(text)
+ } else {
+ prevTextBounds = textBounds
+ targetTextBounds = textBounds
+ }
}
/**
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/MagnificationSettingsControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/MagnificationSettingsControllerTest.java
index d0f8e7863537..81bc94943b71 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/MagnificationSettingsControllerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/MagnificationSettingsControllerTest.java
@@ -23,6 +23,7 @@ import static org.mockito.Mockito.verify;
import android.content.pm.ActivityInfo;
import android.testing.TestableLooper;
+import android.view.WindowManager;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
@@ -31,6 +32,7 @@ import com.android.internal.graphics.SfVsyncFrameCallbackProvider;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.accessibility.WindowMagnificationSettings.MagnificationSize;
import com.android.systemui.util.settings.SecureSettings;
+import com.android.systemui.utils.windowmanager.WindowManagerProvider;
import org.junit.After;
import org.junit.Before;
@@ -56,13 +58,15 @@ public class MagnificationSettingsControllerTest extends SysuiTestCase {
private SfVsyncFrameCallbackProvider mSfVsyncFrameProvider;
@Mock
private SecureSettings mSecureSettings;
+ @Mock
+ private WindowManagerProvider mWindowManagerProvider;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
mMagnificationSettingsController = new MagnificationSettingsController(
mContext, mSfVsyncFrameProvider,
- mMagnificationSettingControllerCallback, mSecureSettings,
+ mMagnificationSettingControllerCallback, mSecureSettings, mWindowManagerProvider,
mWindowMagnificationSettings);
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinderTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinderTest.kt
index e8c30bafbba0..c963157318ed 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinderTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinderTest.kt
@@ -191,7 +191,6 @@ class SideFpsOverlayViewBinderTest : SysuiTestCase() {
val clickListenerCaptor = ArgumentCaptor.forClass(View.OnClickListener::class.java)
verify(sideFpsView).setOnClickListener(clickListenerCaptor.capture())
clickListenerCaptor.value.onClick(sideFpsView)
- verify(lottieAnimationView).toggleAnimation()
}
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/clipboardoverlay/ActionIntentCreatorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/clipboardoverlay/ActionIntentCreatorTest.kt
index 652a2ff21e9b..87eea82ef30d 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/clipboardoverlay/ActionIntentCreatorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/clipboardoverlay/ActionIntentCreatorTest.kt
@@ -19,12 +19,17 @@ package com.android.systemui.clipboardoverlay
import android.content.ClipData
import android.content.ComponentName
import android.content.Intent
+import android.content.pm.ActivityInfo
+import android.content.pm.PackageInfo
+import android.content.pm.PackageManager
import android.net.Uri
import android.text.SpannableString
import androidx.test.filters.SmallTest
import androidx.test.runner.AndroidJUnit4
import com.android.systemui.SysuiTestCase
import com.android.systemui.res.R
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.mock
import kotlinx.coroutines.test.TestCoroutineScheduler
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.UnconfinedTestDispatcher
@@ -33,6 +38,8 @@ import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.kotlin.whenever
@SmallTest
@RunWith(AndroidJUnit4::class)
@@ -40,8 +47,10 @@ class ActionIntentCreatorTest : SysuiTestCase() {
private val scheduler = TestCoroutineScheduler()
private val mainDispatcher = UnconfinedTestDispatcher(scheduler)
private val testScope = TestScope(mainDispatcher)
+ val packageManager = mock<PackageManager>()
- val creator = ActionIntentCreator(testScope.backgroundScope)
+ val creator =
+ ActionIntentCreator(context, packageManager, testScope.backgroundScope, mainDispatcher)
@Test
fun test_getTextEditorIntent() {
@@ -73,7 +82,7 @@ class ActionIntentCreatorTest : SysuiTestCase() {
}
@Test
- fun test_getImageEditIntent() = runTest {
+ fun test_getImageEditIntent_noDefault() = runTest {
context.getOrCreateTestableResources().addOverride(R.string.config_screenshotEditor, "")
val fakeUri = Uri.parse("content://foo")
var intent = creator.getImageEditIntent(fakeUri, context)
@@ -83,18 +92,82 @@ class ActionIntentCreatorTest : SysuiTestCase() {
assertEquals(null, intent.component)
assertEquals("clipboard", intent.getStringExtra("edit_source"))
assertFlags(intent, EXTERNAL_INTENT_FLAGS)
+ }
+
+ @Test
+ fun test_getImageEditIntent_defaultProvided() = runTest {
+ val fakeUri = Uri.parse("content://foo")
- // try again with an editor component
val fakeComponent =
ComponentName("com.android.remotecopy", "com.android.remotecopy.RemoteCopyActivity")
context
.getOrCreateTestableResources()
.addOverride(R.string.config_screenshotEditor, fakeComponent.flattenToString())
- intent = creator.getImageEditIntent(fakeUri, context)
+ val intent = creator.getImageEditIntent(fakeUri, context)
assertEquals(fakeComponent, intent.component)
}
@Test
+ fun test_getImageEditIntent_preferredProvidedButDisabled() = runTest {
+ val fakeUri = Uri.parse("content://foo")
+
+ val defaultComponent = ComponentName("com.android.foo", "com.android.foo.Something")
+ val preferredComponent = ComponentName("com.android.bar", "com.android.bar.Something")
+
+ val packageInfo =
+ PackageInfo().apply {
+ activities = arrayOf() // no activities
+ }
+ whenever(packageManager.getPackageInfo(eq(preferredComponent.packageName), anyInt()))
+ .thenReturn(packageInfo)
+
+ context
+ .getOrCreateTestableResources()
+ .addOverride(R.string.config_screenshotEditor, defaultComponent.flattenToString())
+ context
+ .getOrCreateTestableResources()
+ .addOverride(
+ R.string.config_preferredScreenshotEditor,
+ preferredComponent.flattenToString(),
+ )
+ val intent = creator.getImageEditIntent(fakeUri, context)
+ assertEquals(defaultComponent, intent.component)
+ }
+
+ @Test
+ fun test_getImageEditIntent_preferredProvided() = runTest {
+ val fakeUri = Uri.parse("content://foo")
+
+ val defaultComponent = ComponentName("com.android.foo", "com.android.foo.Something")
+ val preferredComponent = ComponentName("com.android.bar", "com.android.bar.Something")
+
+ val packageInfo =
+ PackageInfo().apply {
+ activities =
+ arrayOf(
+ ActivityInfo().apply {
+ packageName = preferredComponent.packageName
+ name = preferredComponent.className
+ }
+ )
+ }
+ whenever(packageManager.getPackageInfo(eq(preferredComponent.packageName), anyInt()))
+ .thenReturn(packageInfo)
+
+ context
+ .getOrCreateTestableResources()
+ .addOverride(R.string.config_screenshotEditor, defaultComponent.flattenToString())
+ context
+ .getOrCreateTestableResources()
+ .addOverride(
+ R.string.config_preferredScreenshotEditor,
+ preferredComponent.flattenToString(),
+ )
+ val intent = creator.getImageEditIntent(fakeUri, context)
+ assertEquals(preferredComponent, intent.component)
+ }
+
+ @Test
fun test_getShareIntent_plaintext() {
val clipData = ClipData.newPlainText("Test", "Test Item")
val intent = creator.getShareIntent(clipData, context)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalSceneRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalSceneRepositoryImplTest.kt
index 293d32471713..51ad6a146d0e 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalSceneRepositoryImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalSceneRepositoryImplTest.kt
@@ -21,23 +21,18 @@ import androidx.test.filters.SmallTest
import com.android.compose.animation.scene.ObservableTransitionState
import com.android.systemui.SysuiTestCase
import com.android.systemui.communal.shared.model.CommunalScenes
-import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
import com.android.systemui.kosmos.Kosmos
-import com.android.systemui.kosmos.applicationCoroutineScope
import com.android.systemui.kosmos.backgroundScope
import com.android.systemui.kosmos.collectLastValue
import com.android.systemui.kosmos.runTest
import com.android.systemui.kosmos.useUnconfinedTestDispatcher
-import com.android.systemui.scene.shared.model.SceneDataSource
import com.android.systemui.scene.shared.model.SceneDataSourceDelegator
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.flow.flowOf
import org.junit.Test
import org.junit.runner.RunWith
-import org.mockito.kotlin.argumentCaptor
import org.mockito.kotlin.mock
-import org.mockito.kotlin.verify
@SmallTest
@RunWith(AndroidJUnit4::class)
@@ -49,10 +44,8 @@ class CommunalSceneRepositoryImplTest : SysuiTestCase() {
private val Kosmos.underTest by
Kosmos.Fixture {
CommunalSceneRepositoryImpl(
- applicationScope = applicationCoroutineScope,
backgroundScope = backgroundScope,
sceneDataSource = delegator,
- delegator = delegator,
)
}
@@ -90,18 +83,4 @@ class CommunalSceneRepositoryImplTest : SysuiTestCase() {
assertThat(transitionState)
.isEqualTo(ObservableTransitionState.Idle(CommunalScenes.Default))
}
-
- @Test
- fun showHubFromPowerButton() =
- kosmos.runTest {
- fakeKeyguardRepository.setKeyguardShowing(false)
-
- underTest.showHubFromPowerButton()
-
- argumentCaptor<SceneDataSource>().apply {
- verify(delegator).setDelegate(capture())
-
- assertThat(firstValue.currentScene.value).isEqualTo(CommunalScenes.Communal)
- }
- }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalAutoOpenInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalAutoOpenInteractorTest.kt
index 856a62e3f5a7..a6be3ce43b6a 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalAutoOpenInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalAutoOpenInteractorTest.kt
@@ -16,9 +16,11 @@
package com.android.systemui.communal.domain.interactor
+import android.platform.test.annotations.EnableFlags
import android.provider.Settings
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
+import com.android.systemui.Flags.FLAG_GLANCEABLE_HUB_V2
import com.android.systemui.SysuiTestCase
import com.android.systemui.common.data.repository.batteryRepository
import com.android.systemui.common.data.repository.fake
@@ -47,6 +49,7 @@ import org.junit.runner.RunWith
@SmallTest
@RunWith(AndroidJUnit4::class)
+@EnableFlags(FLAG_GLANCEABLE_HUB_V2)
class CommunalAutoOpenInteractorTest : SysuiTestCase() {
private val kosmos = testKosmos().useUnconfinedTestDispatcher()
@@ -54,6 +57,7 @@ class CommunalAutoOpenInteractorTest : SysuiTestCase() {
@Before
fun setUp() {
+ kosmos.setCommunalV2ConfigEnabled(true)
runBlocking { kosmos.fakeUserRepository.asMainUser() }
with(kosmos.fakeSettings) {
putIntForUser(
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt
index b65ecf46dcca..215d36fcb2a5 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt
@@ -36,6 +36,7 @@ import com.android.systemui.Flags.FLAG_COMMUNAL_HUB
import com.android.systemui.Flags.FLAG_COMMUNAL_RESPONSIVE_GRID
import com.android.systemui.Flags.FLAG_COMMUNAL_WIDGET_RESIZING
import com.android.systemui.Flags.FLAG_GLANCEABLE_HUB_V2
+import com.android.systemui.Flags.glanceableHubV2
import com.android.systemui.SysuiTestCase
import com.android.systemui.broadcast.broadcastDispatcher
import com.android.systemui.communal.data.model.CommunalSmartspaceTimer
@@ -90,6 +91,7 @@ import platform.test.runner.parameterized.Parameters
@SmallTest
@RunWith(ParameterizedAndroidJunit4::class)
+@EnableFlags(FLAG_COMMUNAL_HUB)
class CommunalInteractorTest(flags: FlagsParameterization) : SysuiTestCase() {
private val mainUser =
UserInfo(/* id= */ 0, /* name= */ "primary user", /* flags= */ UserInfo.FLAG_MAIN)
@@ -110,7 +112,9 @@ class CommunalInteractorTest(flags: FlagsParameterization) : SysuiTestCase() {
kosmos.fakeUserRepository.setUserInfos(listOf(mainUser, secondaryUser))
kosmos.fakeFeatureFlagsClassic.set(Flags.COMMUNAL_SERVICE_ENABLED, true)
- mSetFlagsRule.enableFlags(FLAG_COMMUNAL_HUB)
+ if (glanceableHubV2()) {
+ kosmos.setCommunalV2ConfigEnabled(true)
+ }
}
@Test
@@ -120,7 +124,9 @@ class CommunalInteractorTest(flags: FlagsParameterization) : SysuiTestCase() {
assertThat(underTest.isCommunalEnabled.value).isTrue()
}
+ /** Test not applicable when [FLAG_GLANCEABLE_HUB_V2] enabled */
@Test
+ @DisableFlags(FLAG_GLANCEABLE_HUB_V2)
fun isCommunalAvailable_whenKeyguardShowing_true() =
kosmos.runTest {
communalSettingsInteractor.setSuppressionReasons(emptyList())
@@ -1212,7 +1218,10 @@ class CommunalInteractorTest(flags: FlagsParameterization) : SysuiTestCase() {
@JvmStatic
@Parameters(name = "{0}")
fun getParams(): List<FlagsParameterization> {
- return FlagsParameterization.allCombinationsOf(FLAG_COMMUNAL_RESPONSIVE_GRID)
+ return FlagsParameterization.allCombinationsOf(
+ FLAG_COMMUNAL_RESPONSIVE_GRID,
+ FLAG_GLANCEABLE_HUB_V2,
+ )
}
private val MAIN_USER_INFO = UserInfo(0, "primary", UserInfo.FLAG_MAIN)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalSceneInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalSceneInteractorTest.kt
index dc21f0692c9e..7bdac476641b 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalSceneInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalSceneInteractorTest.kt
@@ -16,6 +16,8 @@
package com.android.systemui.communal.domain.interactor
+import android.content.res.Configuration.ORIENTATION_LANDSCAPE
+import android.content.res.Configuration.ORIENTATION_PORTRAIT
import android.platform.test.annotations.DisableFlags
import android.platform.test.annotations.EnableFlags
import android.platform.test.flag.junit.FlagsParameterization
@@ -33,11 +35,15 @@ import com.android.systemui.flags.andSceneContainer
import com.android.systemui.kosmos.testScope
import com.android.systemui.scene.initialSceneKey
import com.android.systemui.scene.shared.model.Scenes
+import com.android.systemui.statusbar.policy.KeyguardStateController
+import com.android.systemui.statusbar.policy.keyguardStateController
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.advanceTimeBy
+import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.junit.runner.RunWith
@@ -46,9 +52,11 @@ import org.mockito.kotlin.anyOrNull
import org.mockito.kotlin.mock
import org.mockito.kotlin.never
import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
import platform.test.runner.parameterized.ParameterizedAndroidJunit4
import platform.test.runner.parameterized.Parameters
+@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(ParameterizedAndroidJunit4::class)
class CommunalSceneInteractorTest(flags: FlagsParameterization) : SysuiTestCase() {
@@ -70,6 +78,7 @@ class CommunalSceneInteractorTest(flags: FlagsParameterization) : SysuiTestCase(
private val repository = kosmos.communalSceneRepository
private val underTest by lazy { kosmos.communalSceneInteractor }
+ private val keyguardStateController: KeyguardStateController = kosmos.keyguardStateController
@DisableFlags(FLAG_SCENE_CONTAINER)
@Test
@@ -551,4 +560,57 @@ class CommunalSceneInteractorTest(flags: FlagsParameterization) : SysuiTestCase(
transitionState.value = ObservableTransitionState.Idle(Scenes.Lockscreen)
assertThat(isCommunalVisible).isEqualTo(false)
}
+
+ @Test
+ fun willRotateToPortrait_whenKeyguardRotationNotAllowed() =
+ testScope.runTest {
+ whenever(keyguardStateController.isKeyguardScreenRotationAllowed()).thenReturn(false)
+ val willRotateToPortrait by collectLastValue(underTest.willRotateToPortrait)
+
+ repository.setCommunalContainerOrientation(ORIENTATION_LANDSCAPE)
+ runCurrent()
+
+ assertThat(willRotateToPortrait).isEqualTo(true)
+
+ repository.setCommunalContainerOrientation(ORIENTATION_PORTRAIT)
+ runCurrent()
+
+ assertThat(willRotateToPortrait).isEqualTo(false)
+ }
+
+ @Test
+ fun willRotateToPortrait_isFalse_whenKeyguardRotationIsAllowed() =
+ testScope.runTest {
+ whenever(keyguardStateController.isKeyguardScreenRotationAllowed()).thenReturn(true)
+ val willRotateToPortrait by collectLastValue(underTest.willRotateToPortrait)
+
+ repository.setCommunalContainerOrientation(ORIENTATION_LANDSCAPE)
+ runCurrent()
+
+ assertThat(willRotateToPortrait).isEqualTo(false)
+
+ repository.setCommunalContainerOrientation(ORIENTATION_PORTRAIT)
+ runCurrent()
+
+ assertThat(willRotateToPortrait).isEqualTo(false)
+ }
+
+ @Test
+ fun rotatedToPortrait() =
+ testScope.runTest {
+ val rotatedToPortrait by collectLastValue(underTest.rotatedToPortrait)
+ assertThat(rotatedToPortrait).isEqualTo(false)
+
+ repository.setCommunalContainerOrientation(ORIENTATION_PORTRAIT)
+ runCurrent()
+ assertThat(rotatedToPortrait).isEqualTo(false)
+
+ repository.setCommunalContainerOrientation(ORIENTATION_LANDSCAPE)
+ runCurrent()
+ assertThat(rotatedToPortrait).isEqualTo(false)
+
+ repository.setCommunalContainerOrientation(ORIENTATION_PORTRAIT)
+ runCurrent()
+ assertThat(rotatedToPortrait).isEqualTo(true)
+ }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalSettingsInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalSettingsInteractorTest.kt
index d6f7145bd770..c671aed1f4a1 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalSettingsInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalSettingsInteractorTest.kt
@@ -21,9 +21,11 @@ import android.app.admin.devicePolicyManager
import android.content.Intent
import android.content.pm.UserInfo
import android.os.UserManager
+import android.platform.test.annotations.EnableFlags
import android.provider.Settings
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
+import com.android.systemui.Flags.FLAG_GLANCEABLE_HUB_V2
import com.android.systemui.SysuiTestCase
import com.android.systemui.broadcast.broadcastDispatcher
import com.android.systemui.communal.shared.model.WhenToStartHub
@@ -86,8 +88,10 @@ class CommunalSettingsInteractorTest : SysuiTestCase() {
}
@Test
+ @EnableFlags(FLAG_GLANCEABLE_HUB_V2)
fun whenToStartHub_matchesRepository() =
kosmos.runTest {
+ setCommunalV2ConfigEnabled(true)
fakeSettings.putIntForUser(
Settings.Secure.WHEN_TO_START_GLANCEABLE_HUB,
Settings.Secure.GLANCEABLE_HUB_START_CHARGING,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/posturing/domain/interactor/PosturingInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/posturing/domain/interactor/PosturingInteractorTest.kt
index b4708d97c4c3..80f0005cb73f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/posturing/domain/interactor/PosturingInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/posturing/domain/interactor/PosturingInteractorTest.kt
@@ -53,7 +53,7 @@ class PosturingInteractorTest : SysuiTestCase() {
private val kosmos = testKosmos().useUnconfinedTestDispatcher()
- private val underTest by lazy { kosmos.posturingInteractor }
+ private val Kosmos.underTest by Kosmos.Fixture { kosmos.posturingInteractor }
@Test
fun testNoDebugOverride() =
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/util/UserTouchActivityNotifierTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/util/UserTouchActivityNotifierTest.kt
new file mode 100644
index 000000000000..581f3cb172fe
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/util/UserTouchActivityNotifierTest.kt
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.communal.util
+
+import android.testing.AndroidTestingRunner
+import android.view.MotionEvent
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.runTest
+import com.android.systemui.kosmos.useUnconfinedTestDispatcher
+import com.android.systemui.power.data.repository.fakePowerRepository
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlin.test.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidTestingRunner::class)
+@SmallTest
+class UserTouchActivityNotifierTest : SysuiTestCase() {
+ private val kosmos: Kosmos = testKosmos().useUnconfinedTestDispatcher()
+
+ @Test
+ fun firstEventTriggersNotify() =
+ kosmos.runTest { sendEventAndVerify(0, MotionEvent.ACTION_MOVE, true) }
+
+ @Test
+ fun secondEventTriggersRateLimited() =
+ kosmos.runTest {
+ var eventTime = 0L
+
+ sendEventAndVerify(eventTime, MotionEvent.ACTION_MOVE, true)
+ eventTime += 50
+ sendEventAndVerify(eventTime, MotionEvent.ACTION_MOVE, false)
+ eventTime += USER_TOUCH_ACTIVITY_RATE_LIMIT
+ sendEventAndVerify(eventTime, MotionEvent.ACTION_MOVE, true)
+ }
+
+ @Test
+ fun overridingActionNotifies() =
+ kosmos.runTest {
+ var eventTime = 0L
+ sendEventAndVerify(eventTime, MotionEvent.ACTION_MOVE, true)
+ sendEventAndVerify(eventTime, MotionEvent.ACTION_DOWN, true)
+ sendEventAndVerify(eventTime, MotionEvent.ACTION_UP, true)
+ sendEventAndVerify(eventTime, MotionEvent.ACTION_CANCEL, true)
+ }
+
+ private fun sendEventAndVerify(eventTime: Long, action: Int, shouldBeHandled: Boolean) {
+ kosmos.fakePowerRepository.userTouchRegistered = false
+ val motionEvent = MotionEvent.obtain(0, eventTime, action, 0f, 0f, 0)
+ kosmos.userTouchActivityNotifier.notifyActivity(motionEvent)
+
+ if (shouldBeHandled) {
+ assertThat(kosmos.fakePowerRepository.userTouchRegistered).isTrue()
+ } else {
+ assertThat(kosmos.fakePowerRepository.userTouchRegistered).isFalse()
+ }
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartableTest.kt
index b08e6761d92f..6b2207e0d754 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartableTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartableTest.kt
@@ -18,15 +18,18 @@ package com.android.systemui.communal.widgets
import android.appwidget.AppWidgetProviderInfo
import android.content.pm.UserInfo
-import androidx.test.ext.junit.runners.AndroidJUnit4
+import android.platform.test.annotations.EnableFlags
+import android.platform.test.flag.junit.FlagsParameterization
import androidx.test.filters.SmallTest
import com.android.systemui.Flags.FLAG_COMMUNAL_HUB
+import com.android.systemui.Flags.FLAG_GLANCEABLE_HUB_V2
import com.android.systemui.SysuiTestCase
import com.android.systemui.communal.data.repository.fakeCommunalWidgetRepository
import com.android.systemui.communal.domain.interactor.CommunalInteractor
import com.android.systemui.communal.domain.interactor.communalInteractor
import com.android.systemui.communal.domain.interactor.communalSettingsInteractor
import com.android.systemui.communal.domain.interactor.setCommunalEnabled
+import com.android.systemui.communal.domain.interactor.setCommunalV2ConfigEnabled
import com.android.systemui.communal.shared.model.FakeGlanceableHubMultiUserHelper
import com.android.systemui.communal.shared.model.fakeGlanceableHubMultiUserHelper
import com.android.systemui.coroutines.collectLastValue
@@ -54,10 +57,13 @@ import org.mockito.Mockito.never
import org.mockito.Mockito.spy
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4
+import platform.test.runner.parameterized.Parameters
@SmallTest
-@RunWith(AndroidJUnit4::class)
-class CommunalAppWidgetHostStartableTest : SysuiTestCase() {
+@RunWith(ParameterizedAndroidJunit4::class)
+@EnableFlags(FLAG_COMMUNAL_HUB)
+class CommunalAppWidgetHostStartableTest(flags: FlagsParameterization) : SysuiTestCase() {
private val kosmos = testKosmos()
@Mock private lateinit var appWidgetHost: CommunalAppWidgetHost
@@ -71,12 +77,27 @@ class CommunalAppWidgetHostStartableTest : SysuiTestCase() {
private lateinit var communalInteractorSpy: CommunalInteractor
private lateinit var underTest: CommunalAppWidgetHostStartable
+ init {
+ mSetFlagsRule.setFlagsParameterization(flags)
+ }
+
+ companion object {
+ private val MAIN_USER_INFO = UserInfo(0, "primary", UserInfo.FLAG_MAIN)
+ private val USER_INFO_WORK = UserInfo(10, "work", UserInfo.FLAG_PROFILE)
+
+ @JvmStatic
+ @Parameters(name = "{0}")
+ fun getParams(): List<FlagsParameterization> {
+ return FlagsParameterization.allCombinationsOf(FLAG_GLANCEABLE_HUB_V2)
+ }
+ }
+
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
kosmos.fakeUserRepository.setUserInfos(listOf(MAIN_USER_INFO, USER_INFO_WORK))
kosmos.fakeFeatureFlagsClassic.set(Flags.COMMUNAL_SERVICE_ENABLED, true)
- mSetFlagsRule.enableFlags(FLAG_COMMUNAL_HUB)
+ kosmos.setCommunalV2ConfigEnabled(true)
widgetManager = kosmos.mockGlanceableHubWidgetManager
helper = kosmos.fakeGlanceableHubMultiUserHelper
@@ -327,9 +348,4 @@ class CommunalAppWidgetHostStartableTest : SysuiTestCase() {
fakeKeyguardRepository.setKeyguardShowing(true)
}
}
-
- private companion object {
- val MAIN_USER_INFO = UserInfo(0, "primary", UserInfo.FLAG_MAIN)
- val USER_INFO_WORK = UserInfo(10, "work", UserInfo.FLAG_PROFILE)
- }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorTest.kt
index d2d8ab9d5cb7..e6153e8ab337 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorTest.kt
@@ -20,6 +20,8 @@ import android.content.pm.UserInfo
import android.hardware.biometrics.BiometricFaceConstants
import android.hardware.biometrics.BiometricSourceType
import android.os.PowerManager
+import android.platform.test.annotations.EnableFlags
+import android.service.dreams.Flags.FLAG_DREAMS_V2
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.compose.animation.scene.ObservableTransitionState
@@ -157,6 +159,33 @@ class DeviceEntryFaceAuthInteractorTest : SysuiTestCase() {
}
@Test
+ @EnableFlags(FLAG_DREAMS_V2)
+ fun faceAuthIsRequestedWhenTransitioningFromDreamToLockscreen() =
+ testScope.runTest {
+ underTest.start()
+ runCurrent()
+
+ powerInteractor.setAwakeForTest(reason = PowerManager.WAKE_REASON_LID)
+ faceWakeUpTriggersConfig.setTriggerFaceAuthOnWakeUpFrom(
+ setOf(WakeSleepReason.LID.powerManagerWakeReason)
+ )
+
+ keyguardTransitionRepository.sendTransitionStep(
+ TransitionStep(
+ KeyguardState.DREAMING,
+ KeyguardState.LOCKSCREEN,
+ transitionState = TransitionState.STARTED,
+ )
+ )
+
+ runCurrent()
+ assertThat(faceAuthRepository.runningAuthRequest.value)
+ .isEqualTo(
+ Pair(FaceAuthUiEvent.FACE_AUTH_UPDATED_KEYGUARD_VISIBILITY_CHANGED, true)
+ )
+ }
+
+ @Test
fun whenFaceIsLockedOutAnyAttemptsToTriggerFaceAuthMustProvideLockoutError() =
testScope.runTest {
underTest.start()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/ui/viewmodel/UdfpsAccessibilityOverlayViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/ui/viewmodel/UdfpsAccessibilityOverlayViewModelTest.kt
index 90500839c8ad..a7810a69265a 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/ui/viewmodel/UdfpsAccessibilityOverlayViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/ui/viewmodel/UdfpsAccessibilityOverlayViewModelTest.kt
@@ -16,13 +16,17 @@
package com.android.systemui.deviceentry.domain.ui.viewmodel
+import android.graphics.Point
import android.platform.test.flag.junit.FlagsParameterization
+import android.view.MotionEvent
+import android.view.View
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.accessibility.data.repository.fakeAccessibilityRepository
import com.android.systemui.biometrics.data.repository.fingerprintPropertyRepository
+import com.android.systemui.biometrics.udfpsUtils
import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository
+import com.android.systemui.deviceentry.data.ui.viewmodel.alternateBouncerUdfpsAccessibilityOverlayViewModel
import com.android.systemui.deviceentry.data.ui.viewmodel.deviceEntryUdfpsAccessibilityOverlayViewModel
import com.android.systemui.deviceentry.ui.viewmodel.DeviceEntryUdfpsAccessibilityOverlayViewModel
import com.android.systemui.flags.Flags.FULL_SCREEN_USER_SWITCHER
@@ -34,6 +38,7 @@ import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepos
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.model.TransitionState
import com.android.systemui.keyguard.shared.model.TransitionStep
+import com.android.systemui.keyguard.ui.viewmodel.accessibilityActionsViewModelKosmos
import com.android.systemui.keyguard.ui.viewmodel.fakeDeviceEntryIconViewModelTransition
import com.android.systemui.kosmos.testScope
import com.android.systemui.res.R
@@ -41,14 +46,22 @@ import com.android.systemui.shade.shadeTestUtil
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
import kotlin.test.Test
+import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.After
import org.junit.Before
import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.kotlin.any
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
import platform.test.runner.parameterized.ParameterizedAndroidJunit4
import platform.test.runner.parameterized.Parameters
+@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(ParameterizedAndroidJunit4::class)
class UdfpsAccessibilityOverlayViewModelTest(flags: FlagsParameterization) : SysuiTestCase() {
@@ -63,7 +76,6 @@ class UdfpsAccessibilityOverlayViewModelTest(flags: FlagsParameterization) : Sys
private val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository
private val fingerprintPropertyRepository = kosmos.fingerprintPropertyRepository
private val deviceEntryFingerprintAuthRepository = kosmos.deviceEntryFingerprintAuthRepository
- private val deviceEntryRepository = kosmos.fakeDeviceEntryRepository
private val shadeTestUtil by lazy { kosmos.shadeTestUtil }
@@ -83,6 +95,22 @@ class UdfpsAccessibilityOverlayViewModelTest(flags: FlagsParameterization) : Sys
@Before
fun setup() {
+ whenever(kosmos.udfpsUtils.isWithinSensorArea(any(), any(), any())).thenReturn(false)
+ whenever(
+ kosmos.udfpsUtils.getTouchInNativeCoordinates(anyInt(), any(), any(), anyBoolean())
+ )
+ .thenReturn(Point(0, 0))
+ whenever(
+ kosmos.udfpsUtils.onTouchOutsideOfSensorArea(
+ anyBoolean(),
+ eq(null),
+ anyInt(),
+ anyInt(),
+ any(),
+ anyBoolean(),
+ )
+ )
+ .thenReturn("Move left")
underTest = kosmos.deviceEntryUdfpsAccessibilityOverlayViewModel
overrideResource(R.integer.udfps_padding_debounce_duration, 0)
}
@@ -101,6 +129,55 @@ class UdfpsAccessibilityOverlayViewModelTest(flags: FlagsParameterization) : Sys
}
@Test
+ fun contentDescription_setOnUdfpsTouchOutsideSensorArea() =
+ testScope.runTest {
+ val contentDescription by collectLastValue(underTest.contentDescription)
+ setupVisibleStateOnLockscreen()
+ underTest.onHoverEvent(mock<View>(), mock<MotionEvent>())
+ runCurrent()
+ assertThat(contentDescription).isEqualTo("Move left")
+ }
+
+ @Test
+ fun clearAccessibilityOverlayMessageReason_updatesWhenFocusChangesFromUdfpsOverlayToLockscreen() =
+ testScope.runTest {
+ val clearAccessibilityOverlayMessageReason by
+ collectLastValue(underTest.clearAccessibilityOverlayMessageReason)
+ val contentDescription by collectLastValue(underTest.contentDescription)
+ setupVisibleStateOnLockscreen()
+ kosmos.accessibilityActionsViewModelKosmos.clearUdfpsAccessibilityOverlayMessage("test")
+ runCurrent()
+ assertThat(clearAccessibilityOverlayMessageReason).isEqualTo("test")
+
+ // UdfpsAccessibilityOverlayViewBinder collects clearAccessibilityOverlayMessageReason
+ // and calls
+ // viewModel.setContentDescription(null) - mock this here
+ underTest.setContentDescription(null)
+ runCurrent()
+ assertThat(contentDescription).isNull()
+ }
+
+ @Test
+ fun clearAccessibilityOverlayMessageReason_updatesAfterUdfpsOverlayFocusOnAlternateBouncer() =
+ testScope.runTest {
+ val clearAccessibilityOverlayMessageReason by
+ collectLastValue(underTest.clearAccessibilityOverlayMessageReason)
+ val contentDescription by collectLastValue(underTest.contentDescription)
+ setupVisibleStateOnLockscreen()
+ kosmos.alternateBouncerUdfpsAccessibilityOverlayViewModel
+ .clearUdfpsAccessibilityOverlayMessage("test")
+ runCurrent()
+ assertThat(clearAccessibilityOverlayMessageReason).isEqualTo("test")
+
+ // UdfpsAccessibilityOverlayViewBinder collects clearAccessibilityOverlayMessageReason
+ // and calls
+ // viewModel.setContentDescription(null) - mock this here
+ underTest.setContentDescription(null)
+ runCurrent()
+ assertThat(contentDescription).isNull()
+ }
+
+ @Test
fun touchExplorationNotEnabled_overlayNotVisible() =
testScope.runTest {
val visible by collectLastValue(underTest.visible)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayContainerViewControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayContainerViewControllerTest.java
index 3895595aaea6..33c4c44111aa 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayContainerViewControllerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayContainerViewControllerTest.java
@@ -16,6 +16,8 @@
package com.android.systemui.dreams;
+import static com.android.systemui.Flags.FLAG_BOUNCER_UI_REVAMP;
+
import static kotlinx.coroutines.flow.FlowKt.emptyFlow;
import static kotlinx.coroutines.flow.StateFlowKt.MutableStateFlow;
@@ -32,6 +34,7 @@ import android.app.DreamManager;
import android.content.res.Resources;
import android.graphics.Region;
import android.os.Handler;
+import android.platform.test.annotations.DisableFlags;
import android.platform.test.annotations.EnableFlags;
import android.testing.TestableLooper.RunWithLooper;
import android.view.AttachedSurfaceControl;
@@ -231,6 +234,7 @@ public class DreamOverlayContainerViewControllerTest extends SysuiTestCase {
}
@Test
+ @DisableFlags(FLAG_BOUNCER_UI_REVAMP)
public void testBouncerAnimation_updateBlur() {
final ArgumentCaptor<PrimaryBouncerExpansionCallback> bouncerExpansionCaptor =
ArgumentCaptor.forClass(PrimaryBouncerExpansionCallback.class);
@@ -253,6 +257,26 @@ public class DreamOverlayContainerViewControllerTest extends SysuiTestCase {
}
@Test
+ @EnableFlags(FLAG_BOUNCER_UI_REVAMP)
+ public void testBouncerAnimation_doesNotBlur_whenBouncerRevampEnabled() {
+ final ArgumentCaptor<PrimaryBouncerExpansionCallback> bouncerExpansionCaptor =
+ ArgumentCaptor.forClass(PrimaryBouncerExpansionCallback.class);
+ mController.onViewAttached();
+ verify(mPrimaryBouncerCallbackInteractor).addBouncerExpansionCallback(
+ bouncerExpansionCaptor.capture());
+
+ final float blurRadius = 1337f;
+ when(mBlurUtils.blurRadiusOfRatio(anyFloat())).thenReturn(blurRadius);
+
+ bouncerExpansionCaptor.getValue().onStartingToShow();
+ final float bouncerHideAmount = 0.05f;
+
+ bouncerExpansionCaptor.getValue().onExpansionChanged(bouncerHideAmount);
+ verify(mBlurUtils, never()).blurRadiusOfRatio(anyFloat());
+ verify(mBlurUtils, never()).applyBlur(eq(mViewRoot), anyInt(), anyBoolean());
+ }
+
+ @Test
public void testStartDreamEntryAnimationsOnAttachedNonLowLight() {
when(mStateController.isLowLightActive()).thenReturn(false);
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt
index fd99313a17b7..b74d53987503 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt
@@ -74,6 +74,7 @@ import com.android.systemui.kosmos.testScope
import com.android.systemui.navigationbar.gestural.domain.GestureInteractor
import com.android.systemui.navigationbar.gestural.domain.TaskInfo
import com.android.systemui.navigationbar.gestural.domain.TaskMatcher
+import com.android.systemui.power.domain.interactor.powerInteractor
import com.android.systemui.scene.data.repository.sceneContainerRepository
import com.android.systemui.scene.domain.interactor.sceneInteractor
import com.android.systemui.scene.shared.model.Overlays
@@ -265,6 +266,8 @@ class DreamOverlayServiceTest(flags: FlagsParameterization?) : SysuiTestCase() {
mDreamOverlayCallbackController,
kosmos.keyguardInteractor,
gestureInteractor,
+ kosmos.wakeGestureMonitor,
+ kosmos.powerInteractor,
WINDOW_NAME,
)
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/WakeGestureMonitorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/WakeGestureMonitorTest.kt
new file mode 100644
index 000000000000..b5f8f7884d7f
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/WakeGestureMonitorTest.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.systemui.dreams
+
+import android.hardware.Sensor
+import android.hardware.TriggerEventListener
+import android.hardware.display.ambientDisplayConfiguration
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.collectValues
+import com.android.systemui.kosmos.runTest
+import com.android.systemui.kosmos.useUnconfinedTestDispatcher
+import com.android.systemui.testKosmos
+import com.android.systemui.util.sensors.asyncSensorManager
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doAnswer
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.stub
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class WakeGestureMonitorTest : SysuiTestCase() {
+ private val kosmos = testKosmos().useUnconfinedTestDispatcher()
+
+ private val Kosmos.underTest by Kosmos.Fixture { wakeGestureMonitor }
+
+ @Test
+ fun testPickupGestureNotEnabled_doesNotSubscribeToSensor() =
+ kosmos.runTest {
+ ambientDisplayConfiguration.fakePickupGestureEnabled = false
+ val triggerSensor = stubSensorManager()
+
+ val wakeUpDetected by collectValues(underTest.wakeUpDetected)
+ triggerSensor()
+ assertThat(wakeUpDetected).isEmpty()
+ }
+
+ @Test
+ fun testPickupGestureEnabled_subscribesToSensor() =
+ kosmos.runTest {
+ ambientDisplayConfiguration.fakePickupGestureEnabled = true
+ val triggerSensor = stubSensorManager()
+
+ val wakeUpDetected by collectValues(underTest.wakeUpDetected)
+ triggerSensor()
+ assertThat(wakeUpDetected).hasSize(1)
+ triggerSensor()
+ assertThat(wakeUpDetected).hasSize(2)
+ }
+
+ private fun Kosmos.stubSensorManager(): () -> Unit {
+ val callbacks = mutableListOf<TriggerEventListener>()
+ val pickupSensor = mock<Sensor>()
+
+ asyncSensorManager.stub {
+ on { getDefaultSensor(Sensor.TYPE_PICK_UP_GESTURE) } doReturn pickupSensor
+ on { requestTriggerSensor(any(), eq(pickupSensor)) } doAnswer
+ {
+ val callback = it.arguments[0] as TriggerEventListener
+ callbacks.add(callback)
+ true
+ }
+ on { cancelTriggerSensor(any(), any()) } doAnswer
+ {
+ val callback = it.arguments[0] as TriggerEventListener
+ callbacks.remove(callback)
+ true
+ }
+ }
+
+ return {
+ val list = callbacks.toList()
+ // Simulate a trigger sensor which unregisters callbacks after triggering.
+ while (callbacks.isNotEmpty()) {
+ callbacks.removeLast()
+ }
+ list.forEach { it.onTrigger(mock()) }
+ }
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/source/TestShortcuts.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/source/TestShortcuts.kt
index af6c65ec6d6d..1f74ad496bbb 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/source/TestShortcuts.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/source/TestShortcuts.kt
@@ -533,7 +533,7 @@ object TestShortcuts {
val expectedShortcutCategoriesWithSimpleShortcutCombination =
listOf(
- simpleShortcutCategory(System, "System apps", "Open assistant"),
+ simpleShortcutCategory(System, "System apps", "Open digital assistant"),
simpleShortcutCategory(System, "System controls", "Go to home screen"),
simpleShortcutCategory(System, "System apps", "Open settings"),
simpleShortcutCategory(System, "System controls", "Lock screen"),
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractorTest.kt
index 57b12990fb97..f88b8529866b 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractorTest.kt
@@ -21,8 +21,12 @@ import android.platform.test.annotations.EnableFlags
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.internal.widget.LockPatternUtils
-import com.android.systemui.Flags
+import com.android.systemui.Flags.FLAG_GLANCEABLE_HUB_V2
+import com.android.systemui.Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR
import com.android.systemui.SysuiTestCase
+import com.android.systemui.communal.data.repository.communalSceneRepository
+import com.android.systemui.communal.domain.interactor.setCommunalV2Enabled
+import com.android.systemui.communal.shared.model.CommunalScenes
import com.android.systemui.keyguard.data.repository.fakeBiometricSettingsRepository
import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepositorySpy
@@ -32,10 +36,12 @@ import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.model.TransitionState
import com.android.systemui.keyguard.shared.model.TransitionStep
import com.android.systemui.keyguard.util.KeyguardTransitionRepositorySpySubject.Companion.assertThat
+import com.android.systemui.kosmos.collectLastValue
+import com.android.systemui.kosmos.runTest
import com.android.systemui.kosmos.testScope
+import com.android.systemui.kosmos.useUnconfinedTestDispatcher
import com.android.systemui.testKosmos
-import kotlinx.coroutines.test.runCurrent
-import kotlinx.coroutines.test.runTest
+import com.google.common.truth.Truth
import org.junit.Before
import org.junit.Ignore
import org.junit.Test
@@ -46,12 +52,10 @@ import org.mockito.Mockito.reset
@RunWith(AndroidJUnit4::class)
class FromGoneTransitionInteractorTest : SysuiTestCase() {
private val kosmos =
- testKosmos().apply {
+ testKosmos().useUnconfinedTestDispatcher().apply {
this.keyguardTransitionRepository = fakeKeyguardTransitionRepositorySpy
}
- private val testScope = kosmos.testScope
private val underTest = kosmos.fromGoneTransitionInteractor
- private val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepositorySpy
@Before
fun setUp() {
@@ -61,8 +65,8 @@ class FromGoneTransitionInteractorTest : SysuiTestCase() {
@Test
@Ignore("Fails due to fix for b/324432820 - will re-enable once permanent fix is submitted.")
fun testDoesNotTransitionToLockscreen_ifStartedButNotFinishedInGone() =
- testScope.runTest {
- keyguardTransitionRepository.sendTransitionSteps(
+ kosmos.runTest {
+ fakeKeyguardTransitionRepositorySpy.sendTransitionSteps(
listOf(
TransitionStep(
from = KeyguardState.LOCKSCREEN,
@@ -77,54 +81,74 @@ class FromGoneTransitionInteractorTest : SysuiTestCase() {
),
testScope,
)
- reset(keyguardTransitionRepository)
- kosmos.fakeKeyguardRepository.setKeyguardShowing(true)
- runCurrent()
+ reset(fakeKeyguardTransitionRepositorySpy)
+ fakeKeyguardRepository.setKeyguardShowing(true)
// We're in the middle of a LOCKSCREEN -> GONE transition.
- assertThat(keyguardTransitionRepository).noTransitionsStarted()
+ assertThat(fakeKeyguardTransitionRepositorySpy).noTransitionsStarted()
}
@Test
- @DisableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR)
+ @DisableFlags(FLAG_KEYGUARD_WM_STATE_REFACTOR)
fun testTransitionsToLockscreen_ifFinishedInGone() =
- testScope.runTest {
- keyguardTransitionRepository.sendTransitionSteps(
+ kosmos.runTest {
+ fakeKeyguardTransitionRepositorySpy.sendTransitionSteps(
from = KeyguardState.LOCKSCREEN,
to = KeyguardState.GONE,
testScope,
)
- reset(keyguardTransitionRepository)
- kosmos.fakeKeyguardRepository.setKeyguardShowing(true)
- runCurrent()
+ reset(fakeKeyguardTransitionRepositorySpy)
+ fakeKeyguardRepository.setKeyguardShowing(true)
// We're in the middle of a GONE -> LOCKSCREEN transition.
- assertThat(keyguardTransitionRepository)
+ assertThat(fakeKeyguardTransitionRepositorySpy)
.startedTransition(to = KeyguardState.LOCKSCREEN)
}
@Test
- @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR)
+ @EnableFlags(FLAG_KEYGUARD_WM_STATE_REFACTOR)
fun testTransitionsToLockscreen_ifFinishedInGone_wmRefactor() =
- testScope.runTest {
- keyguardTransitionRepository.sendTransitionSteps(
+ kosmos.runTest {
+ fakeKeyguardTransitionRepositorySpy.sendTransitionSteps(
from = KeyguardState.LOCKSCREEN,
to = KeyguardState.GONE,
testScope,
)
- reset(keyguardTransitionRepository)
+ reset(fakeKeyguardTransitionRepositorySpy)
// Trigger lockdown.
- kosmos.fakeBiometricSettingsRepository.setAuthenticationFlags(
+ fakeBiometricSettingsRepository.setAuthenticationFlags(
AuthenticationFlags(
0,
LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN,
)
)
- runCurrent()
// We're in the middle of a GONE -> LOCKSCREEN transition.
- assertThat(keyguardTransitionRepository)
+ assertThat(fakeKeyguardTransitionRepositorySpy)
.startedTransition(to = KeyguardState.LOCKSCREEN)
}
+
+ @Test
+ @EnableFlags(FLAG_GLANCEABLE_HUB_V2)
+ @DisableFlags(FLAG_KEYGUARD_WM_STATE_REFACTOR)
+ fun testTransitionToGlanceableHub() =
+ kosmos.runTest {
+ val currentScene by collectLastValue(communalSceneRepository.currentScene)
+
+ fakeKeyguardTransitionRepositorySpy.sendTransitionSteps(
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.GONE,
+ testScope,
+ )
+ reset(fakeKeyguardTransitionRepositorySpy)
+ // Communal is enabled
+ setCommunalV2Enabled(true)
+ Truth.assertThat(currentScene).isEqualTo(CommunalScenes.Blank)
+
+ fakeKeyguardRepository.setKeyguardShowing(true)
+
+ Truth.assertThat(currentScene).isEqualTo(CommunalScenes.Communal)
+ assertThat(fakeKeyguardTransitionRepositorySpy).noTransitionsStarted()
+ }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/binder/WindowManagerLockscreenVisibilityManagerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/binder/WindowManagerLockscreenVisibilityManagerTest.kt
index d0762a3797c0..807cab7cf1b6 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/binder/WindowManagerLockscreenVisibilityManagerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/binder/WindowManagerLockscreenVisibilityManagerTest.kt
@@ -43,12 +43,15 @@ import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.eq
import org.mockito.Mock
import org.mockito.Mockito.anyInt
+import org.mockito.Mockito.times
import org.mockito.Mockito.verify
import org.mockito.Mockito.verifyNoMoreInteractions
import org.mockito.MockitoAnnotations
import org.mockito.kotlin.any
+import org.mockito.kotlin.clearInvocations
import org.mockito.kotlin.doAnswer
import org.mockito.kotlin.mock
+import org.mockito.kotlin.never
import org.mockito.kotlin.whenever
@SmallTest
@@ -59,6 +62,7 @@ class WindowManagerLockscreenVisibilityManagerTest : SysuiTestCase() {
private lateinit var underTest: WindowManagerLockscreenVisibilityManager
private lateinit var executor: FakeExecutor
+ private lateinit var uiBgExecutor: FakeExecutor
@Mock private lateinit var activityTaskManagerService: IActivityTaskManager
@Mock private lateinit var keyguardStateController: KeyguardStateController
@@ -74,10 +78,12 @@ class WindowManagerLockscreenVisibilityManagerTest : SysuiTestCase() {
fun setUp() {
MockitoAnnotations.initMocks(this)
executor = FakeExecutor(FakeSystemClock())
+ uiBgExecutor = FakeExecutor(FakeSystemClock())
underTest =
WindowManagerLockscreenVisibilityManager(
executor = executor,
+ uiBgExecutor = uiBgExecutor,
activityTaskManagerService = activityTaskManagerService,
keyguardStateController = keyguardStateController,
keyguardSurfaceBehindAnimator = keyguardSurfaceBehindAnimator,
@@ -93,8 +99,10 @@ class WindowManagerLockscreenVisibilityManagerTest : SysuiTestCase() {
@RequiresFlagsDisabled(Flags.FLAG_ENSURE_KEYGUARD_DOES_TRANSITION_STARTING)
fun testLockscreenVisible_andAodVisible_without_keyguard_shell_transitions() {
underTest.setLockscreenShown(true)
+ uiBgExecutor.runAllReady()
verify(activityTaskManagerService).setLockScreenShown(true, false)
underTest.setAodVisible(true)
+ uiBgExecutor.runAllReady()
verify(activityTaskManagerService).setLockScreenShown(true, true)
verifyNoMoreInteractions(activityTaskManagerService)
@@ -104,8 +112,10 @@ class WindowManagerLockscreenVisibilityManagerTest : SysuiTestCase() {
@RequiresFlagsEnabled(Flags.FLAG_ENSURE_KEYGUARD_DOES_TRANSITION_STARTING)
fun testLockscreenVisible_andAodVisible_with_keyguard_shell_transitions() {
underTest.setLockscreenShown(true)
+ uiBgExecutor.runAllReady()
verify(keyguardTransitions).startKeyguardTransition(true, false)
underTest.setAodVisible(true)
+ uiBgExecutor.runAllReady()
verify(keyguardTransitions).startKeyguardTransition(true, true)
verifyNoMoreInteractions(keyguardTransitions)
@@ -115,13 +125,16 @@ class WindowManagerLockscreenVisibilityManagerTest : SysuiTestCase() {
@RequiresFlagsDisabled(Flags.FLAG_ENSURE_KEYGUARD_DOES_TRANSITION_STARTING)
fun testGoingAway_whenLockscreenVisible_thenSurfaceMadeVisible_without_keyguard_shell_transitions() {
underTest.setLockscreenShown(true)
+ uiBgExecutor.runAllReady()
verify(activityTaskManagerService).setLockScreenShown(true, false)
underTest.setAodVisible(true)
+ uiBgExecutor.runAllReady()
verify(activityTaskManagerService).setLockScreenShown(true, true)
verifyNoMoreInteractions(activityTaskManagerService)
underTest.setSurfaceBehindVisibility(true)
+ uiBgExecutor.runAllReady()
verify(activityTaskManagerService).keyguardGoingAway(anyInt())
verifyNoMoreInteractions(activityTaskManagerService)
@@ -131,13 +144,16 @@ class WindowManagerLockscreenVisibilityManagerTest : SysuiTestCase() {
@RequiresFlagsEnabled(Flags.FLAG_ENSURE_KEYGUARD_DOES_TRANSITION_STARTING)
fun testGoingAway_whenLockscreenVisible_thenSurfaceMadeVisible_with_keyguard_shell_transitions() {
underTest.setLockscreenShown(true)
+ uiBgExecutor.runAllReady()
verify(keyguardTransitions).startKeyguardTransition(true, false)
underTest.setAodVisible(true)
+ uiBgExecutor.runAllReady()
verify(keyguardTransitions).startKeyguardTransition(true, true)
verifyNoMoreInteractions(keyguardTransitions)
underTest.setSurfaceBehindVisibility(true)
+ uiBgExecutor.runAllReady()
verify(keyguardTransitions).startKeyguardTransition(false, false)
verifyNoMoreInteractions(keyguardTransitions)
@@ -148,11 +164,13 @@ class WindowManagerLockscreenVisibilityManagerTest : SysuiTestCase() {
fun testSurfaceVisible_whenLockscreenNotShowing_doesNotTriggerGoingAway_without_keyguard_shell_transitions() {
underTest.setLockscreenShown(false)
underTest.setAodVisible(false)
+ uiBgExecutor.runAllReady()
verify(activityTaskManagerService).setLockScreenShown(false, false)
verifyNoMoreInteractions(activityTaskManagerService)
underTest.setSurfaceBehindVisibility(true)
+ uiBgExecutor.runAllReady()
verifyNoMoreInteractions(activityTaskManagerService)
}
@@ -162,11 +180,13 @@ class WindowManagerLockscreenVisibilityManagerTest : SysuiTestCase() {
fun testSurfaceVisible_whenLockscreenNotShowing_doesNotTriggerGoingAway_with_keyguard_shell_transitions() {
underTest.setLockscreenShown(false)
underTest.setAodVisible(false)
+ uiBgExecutor.runAllReady()
verify(keyguardTransitions).startKeyguardTransition(false, false)
verifyNoMoreInteractions(keyguardTransitions)
underTest.setSurfaceBehindVisibility(true)
+ uiBgExecutor.runAllReady()
verifyNoMoreInteractions(keyguardTransitions)
}
@@ -175,9 +195,11 @@ class WindowManagerLockscreenVisibilityManagerTest : SysuiTestCase() {
@RequiresFlagsDisabled(Flags.FLAG_ENSURE_KEYGUARD_DOES_TRANSITION_STARTING)
fun testAodVisible_noLockscreenShownCallYet_doesNotShowLockscreenUntilLater_without_keyguard_shell_transitions() {
underTest.setAodVisible(false)
+ uiBgExecutor.runAllReady()
verifyNoMoreInteractions(activityTaskManagerService)
underTest.setLockscreenShown(true)
+ uiBgExecutor.runAllReady()
verify(activityTaskManagerService).setLockScreenShown(true, false)
verifyNoMoreInteractions(activityTaskManagerService)
}
@@ -186,9 +208,11 @@ class WindowManagerLockscreenVisibilityManagerTest : SysuiTestCase() {
@RequiresFlagsEnabled(Flags.FLAG_ENSURE_KEYGUARD_DOES_TRANSITION_STARTING)
fun testAodVisible_noLockscreenShownCallYet_doesNotShowLockscreenUntilLater_with_keyguard_shell_transitions() {
underTest.setAodVisible(false)
+ uiBgExecutor.runAllReady()
verifyNoMoreInteractions(keyguardTransitions)
underTest.setLockscreenShown(true)
+ uiBgExecutor.runAllReady()
verify(keyguardTransitions).startKeyguardTransition(true, false)
verifyNoMoreInteractions(activityTaskManagerService)
}
@@ -197,10 +221,13 @@ class WindowManagerLockscreenVisibilityManagerTest : SysuiTestCase() {
@RequiresFlagsDisabled(Flags.FLAG_ENSURE_KEYGUARD_DOES_TRANSITION_STARTING)
fun setSurfaceBehindVisibility_goesAwayFirst_andIgnoresSecondCall_without_keyguard_shell_transitions() {
underTest.setLockscreenShown(true)
+ uiBgExecutor.runAllReady()
underTest.setSurfaceBehindVisibility(true)
+ uiBgExecutor.runAllReady()
verify(activityTaskManagerService).keyguardGoingAway(0)
underTest.setSurfaceBehindVisibility(true)
+ uiBgExecutor.runAllReady()
verifyNoMoreInteractions(keyguardTransitions)
}
@@ -208,22 +235,31 @@ class WindowManagerLockscreenVisibilityManagerTest : SysuiTestCase() {
@RequiresFlagsEnabled(Flags.FLAG_ENSURE_KEYGUARD_DOES_TRANSITION_STARTING)
fun setSurfaceBehindVisibility_goesAwayFirst_andIgnoresSecondCall_with_keyguard_shell_transitions() {
underTest.setLockscreenShown(true)
+ uiBgExecutor.runAllReady()
verify(keyguardTransitions).startKeyguardTransition(true, false)
underTest.setSurfaceBehindVisibility(true)
+ uiBgExecutor.runAllReady()
verify(keyguardTransitions).startKeyguardTransition(false, false)
underTest.setSurfaceBehindVisibility(true)
+ uiBgExecutor.runAllReady()
verifyNoMoreInteractions(keyguardTransitions)
}
@Test
@RequiresFlagsDisabled(Flags.FLAG_ENSURE_KEYGUARD_DOES_TRANSITION_STARTING)
fun setSurfaceBehindVisibility_falseSetsLockscreenVisibility_without_keyguard_shell_transitions() {
- // Show the surface behind, then hide it.
underTest.setLockscreenShown(true)
+ uiBgExecutor.runAllReady()
+ verify(activityTaskManagerService).setLockScreenShown(eq(true), any())
+
+ // Show the surface behind, then hide it.
underTest.setSurfaceBehindVisibility(true)
+ uiBgExecutor.runAllReady()
underTest.setSurfaceBehindVisibility(false)
- verify(activityTaskManagerService).setLockScreenShown(eq(true), any())
+ uiBgExecutor.runAllReady()
+
+ verify(activityTaskManagerService, times(2)).setLockScreenShown(eq(true), any())
}
@Test
@@ -233,6 +269,7 @@ class WindowManagerLockscreenVisibilityManagerTest : SysuiTestCase() {
underTest.setLockscreenShown(true)
underTest.setSurfaceBehindVisibility(true)
underTest.setSurfaceBehindVisibility(false)
+ uiBgExecutor.runAllReady()
verify(keyguardTransitions).startKeyguardTransition(eq(true), any())
}
@@ -258,4 +295,33 @@ class WindowManagerLockscreenVisibilityManagerTest : SysuiTestCase() {
verify(mockedCallback).onAnimationFinished()
verifyNoMoreInteractions(mockedCallback)
}
+
+ @Test
+ fun lockscreenEventuallyShown_ifReshown_afterGoingAwayExecutionDelayed() {
+ underTest.setLockscreenShown(true)
+ uiBgExecutor.runAllReady()
+ clearInvocations(activityTaskManagerService)
+
+ // Trigger keyguardGoingAway, then immediately setLockScreenShown before going away runs on
+ // the uiBgExecutor.
+ underTest.setSurfaceBehindVisibility(true)
+ underTest.setLockscreenShown(true)
+
+ // Next ready should be the keyguardGoingAway call.
+ uiBgExecutor.runNextReady()
+ verify(activityTaskManagerService).keyguardGoingAway(anyInt())
+ verify(activityTaskManagerService, never()).setLockScreenShown(any(), any())
+ clearInvocations(activityTaskManagerService)
+
+ // Then, the setLockScreenShown call, which should have been enqueued when we called
+ // setLockScreenShown(true) even though keyguardGoingAway() hadn't yet been called.
+ uiBgExecutor.runNextReady()
+ verify(activityTaskManagerService).setLockScreenShown(eq(true), any())
+ verify(activityTaskManagerService, never()).keyguardGoingAway(anyInt())
+ clearInvocations(activityTaskManagerService)
+
+ // Shouldn't be anything left in the queue.
+ uiBgExecutor.runAllReady()
+ verifyNoMoreInteractions(activityTaskManagerService)
+ }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToPrimaryBouncerTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToPrimaryBouncerTransitionViewModelTest.kt
new file mode 100644
index 000000000000..c515fc394bda
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToPrimaryBouncerTransitionViewModelTest.kt
@@ -0,0 +1,63 @@
+/*
+ * 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.ui.viewmodel
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectValues
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.TransitionState
+import com.android.systemui.keyguard.shared.model.TransitionState.RUNNING
+import com.android.systemui.keyguard.shared.model.TransitionStep
+import com.android.systemui.keyguard.ui.transitions.blurConfig
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.testKosmos
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class DreamingToPrimaryBouncerTransitionViewModelTest : SysuiTestCase() {
+ private val kosmos = testKosmos()
+ private val testScope = kosmos.testScope
+ private val underTest by lazy { kosmos.dreamingToPrimaryBouncerViewModel }
+
+ @Test
+ fun dreamingToPrimaryBouncerChangesBlurToMax() =
+ testScope.runTest {
+ val values by collectValues(underTest.windowBlurRadius)
+
+ kosmos.keyguardWindowBlurTestUtil.assertTransitionToBlurRadius(
+ transitionProgress = listOf(0.0f, 0.0f, 0.3f, 0.4f, 0.5f, 1.0f),
+ startValue = kosmos.blurConfig.maxBlurRadiusPx,
+ endValue = kosmos.blurConfig.maxBlurRadiusPx,
+ transitionFactory = ::step,
+ actualValuesProvider = { values },
+ checkInterpolatedValues = false,
+ )
+ }
+
+ private fun step(value: Float, transitionState: TransitionState = RUNNING) =
+ TransitionStep(
+ from = KeyguardState.DREAMING,
+ to = KeyguardState.PRIMARY_BOUNCER,
+ value = value,
+ transitionState = transitionState,
+ ownerName = "dreamingToPrimaryBouncerTransitionViewModelTest",
+ )
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModelTest.kt
index 3ab920a46084..cdd093a410df 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModelTest.kt
@@ -17,11 +17,20 @@
package com.android.systemui.keyguard.ui.viewmodel
import android.content.res.Configuration
+import android.content.res.mainResources
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
+import android.platform.test.flag.junit.FlagsParameterization
import android.util.LayoutDirection
-import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
+import com.android.compose.animation.scene.ObservableTransitionState
+import com.android.systemui.Flags.FLAG_GLANCEABLE_HUB_V2
import com.android.systemui.SysuiTestCase
import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository
+import com.android.systemui.communal.data.repository.communalSceneRepository
+import com.android.systemui.communal.domain.interactor.communalSceneInteractor
+import com.android.systemui.communal.domain.interactor.setCommunalV2ConfigEnabled
+import com.android.systemui.communal.shared.model.CommunalScenes
import com.android.systemui.flags.DisableSceneContainer
import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
import com.android.systemui.keyguard.shared.model.KeyguardState
@@ -29,30 +38,53 @@ import com.android.systemui.keyguard.shared.model.TransitionState
import com.android.systemui.keyguard.shared.model.TransitionStep
import com.android.systemui.keyguard.ui.transitions.blurConfig
import com.android.systemui.kosmos.collectValues
+import com.android.systemui.kosmos.runCurrent
import com.android.systemui.kosmos.runTest
import com.android.systemui.kosmos.testScope
import com.android.systemui.res.R
+import com.android.systemui.statusbar.policy.KeyguardStateController
+import com.android.systemui.statusbar.policy.keyguardStateController
import com.android.systemui.testKosmos
import com.google.common.collect.Range
import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.flowOf
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.mock
import org.mockito.kotlin.whenever
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4
+import platform.test.runner.parameterized.Parameters
@SmallTest
-@RunWith(AndroidJUnit4::class)
-class GlanceableHubToLockscreenTransitionViewModelTest : SysuiTestCase() {
- val kosmos = testKosmos()
+@RunWith(ParameterizedAndroidJunit4::class)
+class GlanceableHubToLockscreenTransitionViewModelTest(flags: FlagsParameterization) :
+ SysuiTestCase() {
+ val kosmos = testKosmos().apply { mainResources = mContext.orCreateTestableResources.resources }
val testScope = kosmos.testScope
val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository
val configurationRepository = kosmos.fakeConfigurationRepository
+ val keyguardStateController: KeyguardStateController = kosmos.keyguardStateController
val underTest by lazy { kosmos.glanceableHubToLockscreenTransitionViewModel }
+ companion object {
+ @JvmStatic
+ @Parameters(name = "{0}")
+ fun getParams(): List<FlagsParameterization> {
+ return FlagsParameterization.allCombinationsOf(FLAG_GLANCEABLE_HUB_V2)
+ }
+ }
+
+ init {
+ mSetFlagsRule.setFlagsParameterization(flags)
+ }
+
@Test
fun lockscreenFadeIn() =
kosmos.runTest {
+ communalSceneInteractor.changeScene(CommunalScenes.Communal, "test")
+
val values by collectValues(underTest.keyguardAlpha)
assertThat(values).isEmpty()
@@ -79,6 +111,116 @@ class GlanceableHubToLockscreenTransitionViewModelTest : SysuiTestCase() {
}
@Test
+ @EnableFlags(FLAG_GLANCEABLE_HUB_V2)
+ fun lockscreenFadeIn_fromHubInLandscape() =
+ kosmos.runTest {
+ kosmos.setCommunalV2ConfigEnabled(true)
+ whenever(keyguardStateController.isKeyguardScreenRotationAllowed).thenReturn(false)
+ communalSceneInteractor.changeScene(CommunalScenes.Communal, "test")
+ communalSceneRepository.setCommunalContainerOrientation(
+ Configuration.ORIENTATION_LANDSCAPE
+ )
+
+ val values by collectValues(underTest.keyguardAlpha)
+ assertThat(values).isEmpty()
+
+ // Exit hub to lockscreen
+ val progress = MutableStateFlow(0f)
+ val transitionState =
+ MutableStateFlow(
+ ObservableTransitionState.Transition(
+ fromScene = CommunalScenes.Communal,
+ toScene = CommunalScenes.Blank,
+ currentScene = flowOf(CommunalScenes.Blank),
+ progress = progress,
+ isInitiatedByUserInput = false,
+ isUserInputOngoing = flowOf(false),
+ )
+ )
+ communalSceneInteractor.setTransitionState(transitionState)
+ progress.value = .2f
+
+ // Still in landscape
+ keyguardTransitionRepository.sendTransitionSteps(
+ listOf(
+ step(0f, TransitionState.STARTED),
+ step(0.1f),
+ // start here..
+ step(0.5f),
+ ),
+ testScope,
+ )
+
+ // Communal container is rotated to portrait
+ communalSceneRepository.setCommunalContainerOrientation(
+ Configuration.ORIENTATION_PORTRAIT
+ )
+ runCurrent()
+
+ keyguardTransitionRepository.sendTransitionSteps(
+ listOf(
+ step(0.6f),
+ step(0.7f),
+ // should stop here..
+ step(0.8f),
+ step(1f),
+ ),
+ testScope,
+ )
+ // Scene transition finished.
+ progress.value = 1f
+ keyguardTransitionRepository.sendTransitionSteps(
+ listOf(step(1f, TransitionState.FINISHED)),
+ testScope,
+ )
+
+ assertThat(values).hasSize(4)
+ // onStart
+ assertThat(values[0]).isEqualTo(0f)
+ assertThat(values[1]).isEqualTo(0f)
+ assertThat(values[2]).isEqualTo(1f)
+ // onFinish
+ assertThat(values[3]).isEqualTo(1f)
+ }
+
+ @Test
+ @DisableFlags(FLAG_GLANCEABLE_HUB_V2)
+ fun lockscreenFadeIn_v2FlagDisabledAndFromHubInLandscape() =
+ kosmos.runTest {
+ whenever(keyguardStateController.isKeyguardScreenRotationAllowed).thenReturn(false)
+ communalSceneInteractor.changeScene(CommunalScenes.Communal, "test")
+ // Rotation is not enabled so communal container is in portrait.
+ communalSceneRepository.setCommunalContainerOrientation(
+ Configuration.ORIENTATION_PORTRAIT
+ )
+
+ val values by collectValues(underTest.keyguardAlpha)
+ assertThat(values).isEmpty()
+
+ // Exit hub to lockscreen
+ keyguardTransitionRepository.sendTransitionSteps(
+ listOf(
+ step(0f, TransitionState.STARTED),
+ // Should start running here...
+ step(0.1f),
+ step(0.2f),
+ step(0.3f),
+ step(0.4f),
+ // ...up to here
+ step(0.5f),
+ step(0.6f),
+ step(0.7f),
+ step(0.8f),
+ step(1f),
+ ),
+ testScope,
+ )
+
+ assertThat(values).hasSize(4)
+ values.forEach { assertThat(it).isIn(Range.closed(0f, 1f)) }
+ }
+
+ @Test
fun lockscreenTranslationX() =
kosmos.runTest {
val config: Configuration = mock()
@@ -89,6 +231,8 @@ class GlanceableHubToLockscreenTransitionViewModelTest : SysuiTestCase() {
R.dimen.hub_to_lockscreen_transition_lockscreen_translation_x,
100,
)
+ communalSceneInteractor.changeScene(CommunalScenes.Communal, "test")
+
val values by collectValues(underTest.keyguardTranslationX)
assertThat(values).isEmpty()
@@ -108,6 +252,44 @@ class GlanceableHubToLockscreenTransitionViewModelTest : SysuiTestCase() {
}
@Test
+ @EnableFlags(FLAG_GLANCEABLE_HUB_V2)
+ fun lockscreenTranslationX_fromHubInLandscape() =
+ kosmos.runTest {
+ kosmos.setCommunalV2ConfigEnabled(true)
+ val config: Configuration = mock()
+ whenever(config.layoutDirection).thenReturn(LayoutDirection.LTR)
+ configurationRepository.onConfigurationChange(config)
+
+ configurationRepository.setDimensionPixelSize(
+ R.dimen.hub_to_lockscreen_transition_lockscreen_translation_x,
+ 100,
+ )
+ whenever(keyguardStateController.isKeyguardScreenRotationAllowed).thenReturn(false)
+
+ communalSceneInteractor.changeScene(CommunalScenes.Communal, "test")
+ communalSceneRepository.setCommunalContainerOrientation(
+ Configuration.ORIENTATION_LANDSCAPE
+ )
+
+ val values by collectValues(underTest.keyguardTranslationX)
+ assertThat(values).isEmpty()
+
+ keyguardTransitionRepository.sendTransitionSteps(
+ listOf(
+ step(0f, TransitionState.STARTED),
+ step(0.3f),
+ step(0.5f),
+ step(0.7f),
+ step(1f),
+ step(1f, TransitionState.FINISHED),
+ ),
+ testScope,
+ )
+ // no translation-x animation
+ values.forEach { assertThat(it.value).isEqualTo(0f) }
+ }
+
+ @Test
fun lockscreenTranslationX_resetsAfterCancellation() =
kosmos.runTest {
val config: Configuration = mock()
@@ -118,6 +300,9 @@ class GlanceableHubToLockscreenTransitionViewModelTest : SysuiTestCase() {
R.dimen.hub_to_lockscreen_transition_lockscreen_translation_x,
100,
)
+
+ communalSceneInteractor.changeScene(CommunalScenes.Communal, "test")
+
val values by collectValues(underTest.keyguardTranslationX)
assertThat(values).isEmpty()
@@ -137,6 +322,42 @@ class GlanceableHubToLockscreenTransitionViewModelTest : SysuiTestCase() {
}
@Test
+ @EnableFlags(FLAG_GLANCEABLE_HUB_V2)
+ fun lockscreenTranslationX_resetsAfterCancellation_fromHubInLandscape() =
+ kosmos.runTest {
+ kosmos.setCommunalV2ConfigEnabled(true)
+ val config: Configuration = mock()
+ whenever(config.layoutDirection).thenReturn(LayoutDirection.LTR)
+ configurationRepository.onConfigurationChange(config)
+
+ configurationRepository.setDimensionPixelSize(
+ R.dimen.hub_to_lockscreen_transition_lockscreen_translation_x,
+ 100,
+ )
+ whenever(keyguardStateController.isKeyguardScreenRotationAllowed).thenReturn(false)
+
+ communalSceneInteractor.changeScene(CommunalScenes.Communal, "test")
+ communalSceneRepository.setCommunalContainerOrientation(
+ Configuration.ORIENTATION_LANDSCAPE
+ )
+
+ val values by collectValues(underTest.keyguardTranslationX)
+ assertThat(values).isEmpty()
+
+ keyguardTransitionRepository.sendTransitionSteps(
+ listOf(
+ step(0f, TransitionState.STARTED),
+ step(0.3f),
+ step(0.6f),
+ step(0.9f, TransitionState.CANCELED),
+ ),
+ testScope,
+ )
+ // no translation-x animation
+ values.forEach { assertThat(it.value).isEqualTo(0f) }
+ }
+
+ @Test
@DisableSceneContainer
fun blurBecomesMinValueImmediately() =
kosmos.runTest {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt
index fe213a6ebbf0..71e09d982494 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt
@@ -17,12 +17,19 @@
package com.android.systemui.keyguard.ui.viewmodel
+import android.content.res.Configuration
+import android.content.res.mainResources
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
import android.platform.test.flag.junit.FlagsParameterization
import android.view.View
import androidx.test.filters.SmallTest
import com.android.compose.animation.scene.ObservableTransitionState
+import com.android.systemui.Flags.FLAG_GLANCEABLE_HUB_V2
import com.android.systemui.SysuiTestCase
import com.android.systemui.communal.data.repository.communalSceneRepository
+import com.android.systemui.communal.domain.interactor.communalSceneInteractor
+import com.android.systemui.communal.domain.interactor.setCommunalV2ConfigEnabled
import com.android.systemui.communal.shared.model.CommunalScenes
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository
@@ -35,6 +42,9 @@ import com.android.systemui.keyguard.domain.interactor.pulseExpansionInteractor
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.model.TransitionState
import com.android.systemui.keyguard.shared.model.TransitionStep
+import com.android.systemui.kosmos.collectLastValue
+import com.android.systemui.kosmos.runCurrent
+import com.android.systemui.kosmos.runTest
import com.android.systemui.kosmos.testScope
import com.android.systemui.scene.data.repository.Idle
import com.android.systemui.scene.data.repository.sceneContainerRepository
@@ -48,6 +58,7 @@ import com.android.systemui.statusbar.notification.data.repository.activeNotific
import com.android.systemui.statusbar.notification.stack.domain.interactor.notificationsKeyguardInteractor
import com.android.systemui.statusbar.phone.dozeParameters
import com.android.systemui.statusbar.phone.screenOffAnimationController
+import com.android.systemui.statusbar.policy.keyguardStateController
import com.android.systemui.testKosmos
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
@@ -69,7 +80,8 @@ import platform.test.runner.parameterized.Parameters
@SmallTest
@RunWith(ParameterizedAndroidJunit4::class)
class KeyguardRootViewModelTest(flags: FlagsParameterization) : SysuiTestCase() {
- private val kosmos = testKosmos()
+ private val kosmos =
+ testKosmos().apply { mainResources = mContext.orCreateTestableResources.resources }
private val testScope = kosmos.testScope
private val keyguardTransitionRepository by lazy { kosmos.fakeKeyguardTransitionRepository }
private val keyguardRepository by lazy { kosmos.fakeKeyguardRepository }
@@ -419,6 +431,7 @@ class KeyguardRootViewModelTest(flags: FlagsParameterization) : SysuiTestCase()
}
@Test
+ @DisableFlags(FLAG_GLANCEABLE_HUB_V2)
fun alpha_transitionFromHubToLockscreen_isOne() =
testScope.runTest {
val alpha by collectLastValue(underTest.alpha(viewState))
@@ -439,6 +452,84 @@ class KeyguardRootViewModelTest(flags: FlagsParameterization) : SysuiTestCase()
}
@Test
+ @DisableSceneContainer
+ @EnableFlags(FLAG_GLANCEABLE_HUB_V2)
+ fun alpha_transitionFromHubToLockscreenInLandscape_isOne() =
+ kosmos.runTest {
+ setCommunalV2ConfigEnabled(true)
+ whenever(keyguardStateController.isKeyguardScreenRotationAllowed).thenReturn(false)
+ communalSceneInteractor.changeScene(CommunalScenes.Communal, "test")
+ communalSceneRepository.setCommunalContainerOrientation(
+ Configuration.ORIENTATION_LANDSCAPE
+ )
+
+ val alpha by collectLastValue(underTest.alpha(viewState))
+
+ // Transition to the glanceable hub and back.
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.GLANCEABLE_HUB,
+ testScope,
+ )
+
+ communalSceneInteractor.changeScene(CommunalScenes.Communal, "test")
+ runCurrent()
+
+ // Exit hub to lockscreen
+ val progress = MutableStateFlow(0f)
+ val transitionState =
+ MutableStateFlow(
+ ObservableTransitionState.Transition(
+ fromScene = CommunalScenes.Communal,
+ toScene = CommunalScenes.Blank,
+ currentScene = flowOf(CommunalScenes.Blank),
+ progress = progress,
+ isInitiatedByUserInput = false,
+ isUserInputOngoing = flowOf(false),
+ )
+ )
+ communalSceneInteractor.setTransitionState(transitionState)
+ progress.value = .4f
+
+ keyguardTransitionRepository.sendTransitionSteps(
+ listOf(
+ TransitionStep(
+ from = KeyguardState.GLANCEABLE_HUB,
+ to = KeyguardState.LOCKSCREEN,
+ transitionState = TransitionState.STARTED,
+ value = 0f,
+ ),
+ TransitionStep(
+ from = KeyguardState.GLANCEABLE_HUB,
+ to = KeyguardState.LOCKSCREEN,
+ transitionState = TransitionState.RUNNING,
+ value = 0.4f,
+ ),
+ ),
+ testScope,
+ )
+
+ communalSceneRepository.setCommunalContainerOrientation(
+ Configuration.ORIENTATION_PORTRAIT
+ )
+ runCurrent()
+
+ keyguardTransitionRepository.sendTransitionSteps(
+ listOf(
+ TransitionStep(
+ from = KeyguardState.GLANCEABLE_HUB,
+ to = KeyguardState.LOCKSCREEN,
+ transitionState = TransitionState.FINISHED,
+ value = 1f,
+ )
+ ),
+ testScope,
+ )
+
+ assertThat(alpha).isEqualTo(1.0f)
+ }
+
+ @Test
fun alpha_emitsOnShadeExpansion() =
testScope.runTest {
val alpha by collectLastValue(underTest.alpha(viewState))
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardSmartspaceViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardSmartspaceViewModelTest.kt
index adce9d65cbe0..e89c05f3a84d 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardSmartspaceViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardSmartspaceViewModelTest.kt
@@ -16,6 +16,9 @@
package com.android.systemui.keyguard.ui.viewmodel
+import android.content.res.Configuration
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
@@ -44,7 +47,7 @@ class KeyguardSmartspaceViewModelTest : SysuiTestCase() {
val kosmos = testKosmos()
val testScope = kosmos.testScope
val underTest = kosmos.keyguardSmartspaceViewModel
- val res = context.resources
+ @Mock private lateinit var mockConfiguration: Configuration
@Mock(answer = Answers.RETURNS_DEEP_STUBS) private lateinit var clockController: ClockController
@@ -119,4 +122,63 @@ class KeyguardSmartspaceViewModelTest : SysuiTestCase() {
assertThat(isShadeLayoutWide).isFalse()
}
}
+
+ @Test
+ @DisableFlags(com.android.systemui.shared.Flags.FLAG_CLOCK_REACTIVE_SMARTSPACE_LAYOUT)
+ fun dateWeatherBelowSmallClock_smartspacelayoutflag_off_true() {
+ val result = KeyguardSmartspaceViewModel.dateWeatherBelowSmallClock(mockConfiguration)
+
+ assertThat(result).isTrue()
+ }
+
+ @Test
+ @EnableFlags(com.android.systemui.shared.Flags.FLAG_CLOCK_REACTIVE_SMARTSPACE_LAYOUT)
+ fun dateWeatherBelowSmallClock_defaultFontAndDisplaySize_false() {
+ val fontScale = 1.0f
+ val screenWidthDp = 347
+ mockConfiguration.fontScale = fontScale
+ mockConfiguration.screenWidthDp = screenWidthDp
+
+ val result = KeyguardSmartspaceViewModel.dateWeatherBelowSmallClock(mockConfiguration)
+
+ assertThat(result).isFalse()
+ }
+
+ @Test
+ @EnableFlags(com.android.systemui.shared.Flags.FLAG_CLOCK_REACTIVE_SMARTSPACE_LAYOUT)
+ fun dateWeatherBelowSmallClock_variousFontAndDisplaySize_false() {
+ mockConfiguration.fontScale = 1.0f
+ mockConfiguration.screenWidthDp = 347
+ val result1 = KeyguardSmartspaceViewModel.dateWeatherBelowSmallClock(mockConfiguration)
+ assertThat(result1).isFalse()
+
+ mockConfiguration.fontScale = 1.2f
+ mockConfiguration.screenWidthDp = 347
+ val result2 = KeyguardSmartspaceViewModel.dateWeatherBelowSmallClock(mockConfiguration)
+ assertThat(result2).isFalse()
+
+ mockConfiguration.fontScale = 1.7f
+ mockConfiguration.screenWidthDp = 412
+ val result3 = KeyguardSmartspaceViewModel.dateWeatherBelowSmallClock(mockConfiguration)
+ assertThat(result3).isFalse()
+ }
+
+ @Test
+ @EnableFlags(com.android.systemui.shared.Flags.FLAG_CLOCK_REACTIVE_SMARTSPACE_LAYOUT)
+ fun dateWeatherBelowSmallClock_variousFontAndDisplaySize_true() {
+ mockConfiguration.fontScale = 1.0f
+ mockConfiguration.screenWidthDp = 310
+ val result1 = KeyguardSmartspaceViewModel.dateWeatherBelowSmallClock(mockConfiguration)
+ assertThat(result1).isTrue()
+
+ mockConfiguration.fontScale = 1.5f
+ mockConfiguration.screenWidthDp = 347
+ val result2 = KeyguardSmartspaceViewModel.dateWeatherBelowSmallClock(mockConfiguration)
+ assertThat(result2).isTrue()
+
+ mockConfiguration.fontScale = 2.0f
+ mockConfiguration.screenWidthDp = 411
+ val result3 = KeyguardSmartspaceViewModel.dateWeatherBelowSmallClock(mockConfiguration)
+ assertThat(result3).isTrue()
+ }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToDreamingTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToDreamingTransitionViewModelTest.kt
new file mode 100644
index 000000000000..9c2c3c3f1498
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToDreamingTransitionViewModelTest.kt
@@ -0,0 +1,74 @@
+/*
+ * 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.ui.viewmodel
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectValues
+import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.TransitionState
+import com.android.systemui.keyguard.shared.model.TransitionState.RUNNING
+import com.android.systemui.keyguard.shared.model.TransitionStep
+import com.android.systemui.keyguard.ui.transitions.blurConfig
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.testKosmos
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class PrimaryBouncerToDreamingTransitionViewModelTest : SysuiTestCase() {
+ private val kosmos = testKosmos()
+ private val testScope = kosmos.testScope
+ private lateinit var keyguardTransitionRepository: FakeKeyguardTransitionRepository
+ private lateinit var underTest: PrimaryBouncerToDreamingTransitionViewModel
+
+ @Before
+ fun setUp() {
+ keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository
+ underTest = kosmos.primaryBouncerToDreamingTransitionViewModel
+ }
+
+ @Test
+ fun blurRadiusGoesToMinImmediately() =
+ testScope.runTest {
+ val values by collectValues(underTest.windowBlurRadius)
+
+ kosmos.keyguardWindowBlurTestUtil.assertTransitionToBlurRadius(
+ transitionProgress = listOf(0.0f, 0.2f, 0.3f, 0.65f, 0.7f, 1.0f),
+ startValue = kosmos.blurConfig.maxBlurRadiusPx,
+ endValue = kosmos.blurConfig.minBlurRadiusPx,
+ actualValuesProvider = { values },
+ transitionFactory = ::step,
+ )
+ }
+
+ private fun step(value: Float, state: TransitionState = RUNNING): TransitionStep {
+ return TransitionStep(
+ from = KeyguardState.PRIMARY_BOUNCER,
+ to = KeyguardState.DREAMING,
+ value = value,
+ transitionState = state,
+ ownerName = "PrimaryBouncerToDreamingTransitionViewModelTest",
+ )
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/lowlightclock/LowLightMonitorTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/lowlightclock/LowLightMonitorTest.java
deleted file mode 100644
index b177e07d09b6..000000000000
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/lowlightclock/LowLightMonitorTest.java
+++ /dev/null
@@ -1,198 +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.lowlightclock;
-
-import static com.android.dream.lowlight.LowLightDreamManager.AMBIENT_LIGHT_MODE_LOW_LIGHT;
-import static com.android.dream.lowlight.LowLightDreamManager.AMBIENT_LIGHT_MODE_REGULAR;
-import static com.android.systemui.keyguard.ScreenLifecycle.SCREEN_ON;
-
-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.never;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import android.content.ComponentName;
-import android.content.pm.PackageManager;
-import android.testing.TestableLooper;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
-
-import com.android.dream.lowlight.LowLightDreamManager;
-import com.android.systemui.SysuiTestCase;
-import com.android.systemui.keyguard.ScreenLifecycle;
-import com.android.systemui.shared.condition.Condition;
-import com.android.systemui.shared.condition.Monitor;
-import com.android.systemui.util.concurrency.FakeExecutor;
-import com.android.systemui.util.time.FakeSystemClock;
-
-import dagger.Lazy;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Captor;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-import java.util.Set;
-
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-@TestableLooper.RunWithLooper()
-public class LowLightMonitorTest extends SysuiTestCase {
-
- @Mock
- private Lazy<LowLightDreamManager> mLowLightDreamManagerLazy;
- @Mock
- private LowLightDreamManager mLowLightDreamManager;
- @Mock
- private Monitor mMonitor;
- @Mock
- private ScreenLifecycle mScreenLifecycle;
- @Mock
- private LowLightLogger mLogger;
-
- private LowLightMonitor mLowLightMonitor;
-
- @Mock
- Lazy<Set<Condition>> mLazyConditions;
-
- @Mock
- private PackageManager mPackageManager;
-
- @Mock
- private ComponentName mDreamComponent;
-
- FakeExecutor mBackgroundExecutor = new FakeExecutor(new FakeSystemClock());
-
- Condition mCondition = mock(Condition.class);
- Set<Condition> mConditionSet = Set.of(mCondition);
-
- @Captor
- ArgumentCaptor<Monitor.Subscription> mPreconditionsSubscriptionCaptor;
-
- @Before
- public void setUp() {
- MockitoAnnotations.initMocks(this);
- when(mLowLightDreamManagerLazy.get()).thenReturn(mLowLightDreamManager);
- when(mLazyConditions.get()).thenReturn(mConditionSet);
- mLowLightMonitor = new LowLightMonitor(mLowLightDreamManagerLazy,
- mMonitor, mLazyConditions, mScreenLifecycle, mLogger, mDreamComponent,
- mPackageManager, mBackgroundExecutor);
- }
-
- @Test
- public void testSetAmbientLowLightWhenInLowLight() {
- mLowLightMonitor.onConditionsChanged(true);
- mBackgroundExecutor.runAllReady();
- // Verify setting low light when condition is true
- verify(mLowLightDreamManager).setAmbientLightMode(AMBIENT_LIGHT_MODE_LOW_LIGHT);
- }
-
- @Test
- public void testExitAmbientLowLightWhenNotInLowLight() {
- mLowLightMonitor.onConditionsChanged(true);
- mLowLightMonitor.onConditionsChanged(false);
- mBackgroundExecutor.runAllReady();
- // Verify ambient light toggles back to light mode regular
- verify(mLowLightDreamManager).setAmbientLightMode(AMBIENT_LIGHT_MODE_REGULAR);
- }
-
- @Test
- public void testStartMonitorLowLightConditionsWhenScreenTurnsOn() {
- mLowLightMonitor.onScreenTurnedOn();
- mBackgroundExecutor.runAllReady();
-
- // Verify subscribing to low light conditions monitor when screen turns on.
- verify(mMonitor).addSubscription(any());
- }
-
- @Test
- public void testStopMonitorLowLightConditionsWhenScreenTurnsOff() {
- final Monitor.Subscription.Token token = mock(Monitor.Subscription.Token.class);
- when(mMonitor.addSubscription(any())).thenReturn(token);
- mLowLightMonitor.onScreenTurnedOn();
-
- // Verify removing subscription when screen turns off.
- mLowLightMonitor.onScreenTurnedOff();
- mBackgroundExecutor.runAllReady();
- verify(mMonitor).removeSubscription(token);
- }
-
- @Test
- public void testSubscribeToLowLightConditionsOnlyOnceWhenScreenTurnsOn() {
- final Monitor.Subscription.Token token = mock(Monitor.Subscription.Token.class);
- when(mMonitor.addSubscription(any())).thenReturn(token);
-
- mLowLightMonitor.onScreenTurnedOn();
- mLowLightMonitor.onScreenTurnedOn();
- mBackgroundExecutor.runAllReady();
- // Verify subscription is only added once.
- verify(mMonitor, times(1)).addSubscription(any());
- }
-
- @Test
- public void testSubscribedToExpectedConditions() {
- final Monitor.Subscription.Token token = mock(Monitor.Subscription.Token.class);
- when(mMonitor.addSubscription(any())).thenReturn(token);
-
- mLowLightMonitor.onScreenTurnedOn();
- mLowLightMonitor.onScreenTurnedOn();
- mBackgroundExecutor.runAllReady();
- Set<Condition> conditions = captureConditions();
- // Verify Monitor is subscribed to the expected conditions
- assertThat(conditions).isEqualTo(mConditionSet);
- }
-
- @Test
- public void testNotUnsubscribeIfNotSubscribedWhenScreenTurnsOff() {
- mLowLightMonitor.onScreenTurnedOff();
- mBackgroundExecutor.runAllReady();
- // Verify doesn't remove subscription since there is none.
- verify(mMonitor, never()).removeSubscription(any());
- }
-
- @Test
- public void testSubscribeIfScreenIsOnWhenStarting() {
- when(mScreenLifecycle.getScreenState()).thenReturn(SCREEN_ON);
- mLowLightMonitor.start();
- mBackgroundExecutor.runAllReady();
- // Verify to add subscription on start if the screen state is on
- verify(mMonitor, times(1)).addSubscription(any());
- }
-
- @Test
- public void testNoSubscribeIfDreamNotPresent() {
- LowLightMonitor lowLightMonitor = new LowLightMonitor(mLowLightDreamManagerLazy,
- mMonitor, mLazyConditions, mScreenLifecycle, mLogger, null, mPackageManager,
- mBackgroundExecutor);
- when(mScreenLifecycle.getScreenState()).thenReturn(SCREEN_ON);
- lowLightMonitor.start();
- mBackgroundExecutor.runAllReady();
- verify(mScreenLifecycle, never()).addObserver(any());
- }
-
- private Set<Condition> captureConditions() {
- verify(mMonitor).addSubscription(mPreconditionsSubscriptionCaptor.capture());
- return mPreconditionsSubscriptionCaptor.getValue().getConditions();
- }
-}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/lowlightclock/LowLightMonitorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/lowlightclock/LowLightMonitorTest.kt
new file mode 100644
index 000000000000..11f0f4394a85
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/lowlightclock/LowLightMonitorTest.kt
@@ -0,0 +1,271 @@
+/*
+ * 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.lowlightclock
+
+import android.content.ComponentName
+import android.content.pm.PackageManager
+import android.testing.TestableLooper.RunWithLooper
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.dream.lowlight.LowLightDreamManager
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.biometrics.domain.interactor.displayStateInteractor
+import com.android.systemui.display.data.repository.displayRepository
+import com.android.systemui.kosmos.runCurrent
+import com.android.systemui.kosmos.runTest
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.kosmos.useUnconfinedTestDispatcher
+import com.android.systemui.shared.condition.Condition
+import com.android.systemui.shared.condition.Monitor
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth
+import dagger.Lazy
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers
+import org.mockito.Captor
+import org.mockito.Mock
+import org.mockito.Mockito
+import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.argumentCaptor
+import org.mockito.kotlin.clearInvocations
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.never
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@RunWithLooper
+class LowLightMonitorTest : SysuiTestCase() {
+ val kosmos = testKosmos().useUnconfinedTestDispatcher()
+
+ @Mock private lateinit var lowLightDreamManagerLazy: Lazy<LowLightDreamManager>
+
+ @Mock private lateinit var lowLightDreamManager: LowLightDreamManager
+
+ private val monitor: Monitor = prepareMonitor()
+
+ @Mock private lateinit var logger: LowLightLogger
+
+ private lateinit var lowLightMonitor: LowLightMonitor
+
+ @Mock private lateinit var lazyConditions: Lazy<Set<Condition>>
+
+ @Mock private lateinit var packageManager: PackageManager
+
+ @Mock private lateinit var dreamComponent: ComponentName
+
+ private val condition = mock<Condition>()
+
+ private val conditionSet = setOf(condition)
+
+ @Captor
+ private lateinit var preconditionsSubscriptionCaptor: ArgumentCaptor<Monitor.Subscription>
+
+ private fun prepareMonitor(): Monitor {
+ val monitor = mock<Monitor>()
+ whenever(monitor.addSubscription(ArgumentMatchers.any())).thenReturn(mock())
+
+ return monitor
+ }
+
+ private fun setDisplayOn(screenOn: Boolean) {
+ kosmos.displayRepository.setDefaultDisplayOff(!screenOn)
+ }
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+ whenever(lowLightDreamManagerLazy.get()).thenReturn(lowLightDreamManager)
+ whenever(lazyConditions.get()).thenReturn(conditionSet)
+ lowLightMonitor =
+ LowLightMonitor(
+ lowLightDreamManagerLazy,
+ monitor,
+ lazyConditions,
+ kosmos.displayStateInteractor,
+ logger,
+ dreamComponent,
+ packageManager,
+ kosmos.testScope.backgroundScope,
+ )
+ whenever(monitor.addSubscription(ArgumentMatchers.any())).thenReturn(mock())
+ val subscriptionCaptor = argumentCaptor<Monitor.Subscription>()
+
+ setDisplayOn(false)
+
+ lowLightMonitor.start()
+ verify(monitor).addSubscription(subscriptionCaptor.capture())
+ clearInvocations(monitor)
+
+ subscriptionCaptor.firstValue.callback.onConditionsChanged(true)
+ }
+
+ private fun getConditionCallback(monitor: Monitor): Monitor.Callback {
+ val subscriptionCaptor = argumentCaptor<Monitor.Subscription>()
+ verify(monitor).addSubscription(subscriptionCaptor.capture())
+ return subscriptionCaptor.firstValue.callback
+ }
+
+ @Test
+ fun testSetAmbientLowLightWhenInLowLight() =
+ kosmos.runTest {
+ // Turn on screen
+ setDisplayOn(true)
+
+ // Set conditions to true
+ val callback = getConditionCallback(monitor)
+ callback.onConditionsChanged(true)
+
+ // Verify setting low light when condition is true
+ Mockito.verify(lowLightDreamManager)
+ .setAmbientLightMode(LowLightDreamManager.AMBIENT_LIGHT_MODE_LOW_LIGHT)
+ }
+
+ @Test
+ fun testExitAmbientLowLightWhenNotInLowLight() =
+ kosmos.runTest {
+ // Turn on screen
+ setDisplayOn(true)
+
+ // Set conditions to true then false
+ val callback = getConditionCallback(monitor)
+ callback.onConditionsChanged(true)
+ clearInvocations(lowLightDreamManager)
+ callback.onConditionsChanged(false)
+
+ // Verify ambient light toggles back to light mode regular
+ Mockito.verify(lowLightDreamManager)
+ .setAmbientLightMode(LowLightDreamManager.AMBIENT_LIGHT_MODE_REGULAR)
+ }
+
+ @Test
+ fun testStopMonitorLowLightConditionsWhenScreenTurnsOff() =
+ kosmos.runTest {
+ val token = mock<Monitor.Subscription.Token>()
+ whenever(monitor.addSubscription(ArgumentMatchers.any())).thenReturn(token)
+
+ setDisplayOn(true)
+
+ // Verify removing subscription when screen turns off.
+ setDisplayOn(false)
+ Mockito.verify(monitor).removeSubscription(token)
+ }
+
+ @Test
+ fun testSubscribeToLowLightConditionsOnlyOnceWhenScreenTurnsOn() =
+ kosmos.runTest {
+ val token = mock<Monitor.Subscription.Token>()
+ whenever(monitor.addSubscription(ArgumentMatchers.any())).thenReturn(token)
+
+ setDisplayOn(true)
+ setDisplayOn(true)
+ // Verify subscription is only added once.
+ Mockito.verify(monitor, Mockito.times(1)).addSubscription(ArgumentMatchers.any())
+ }
+
+ @Test
+ fun testSubscribedToExpectedConditions() =
+ kosmos.runTest {
+ val token = mock<Monitor.Subscription.Token>()
+ whenever(monitor.addSubscription(ArgumentMatchers.any())).thenReturn(token)
+
+ setDisplayOn(true)
+
+ val conditions = captureConditions()
+ // Verify Monitor is subscribed to the expected conditions
+ Truth.assertThat(conditions).isEqualTo(conditionSet)
+ }
+
+ @Test
+ fun testNotUnsubscribeIfNotSubscribedWhenScreenTurnsOff() =
+ kosmos.runTest {
+ setDisplayOn(true)
+ clearInvocations(monitor)
+ setDisplayOn(false)
+ runCurrent()
+ // Verify doesn't remove subscription since there is none.
+ Mockito.verify(monitor).removeSubscription(ArgumentMatchers.any())
+ }
+
+ @Test
+ fun testSubscribeIfScreenIsOnWhenStarting() =
+ kosmos.runTest {
+ val monitor = prepareMonitor()
+
+ setDisplayOn(true)
+
+ val targetMonitor =
+ LowLightMonitor(
+ lowLightDreamManagerLazy,
+ monitor,
+ lazyConditions,
+ displayStateInteractor,
+ logger,
+ dreamComponent,
+ packageManager,
+ testScope.backgroundScope,
+ )
+
+ // start
+ targetMonitor.start()
+
+ val callback = getConditionCallback(monitor)
+ clearInvocations(monitor)
+ callback.onConditionsChanged(true)
+
+ // Verify to add subscription on start and when the screen state is on
+ Mockito.verify(monitor).addSubscription(ArgumentMatchers.any())
+ }
+
+ @Test
+ fun testNoSubscribeIfDreamNotPresent() =
+ kosmos.runTest {
+ val monitor = prepareMonitor()
+
+ setDisplayOn(true)
+
+ val lowLightMonitor =
+ LowLightMonitor(
+ lowLightDreamManagerLazy,
+ monitor,
+ lazyConditions,
+ displayStateInteractor,
+ logger,
+ null,
+ packageManager,
+ testScope,
+ )
+
+ // start
+ lowLightMonitor.start()
+
+ val callback = getConditionCallback(monitor)
+ clearInvocations(monitor)
+ callback.onConditionsChanged(true)
+
+ // Verify to add subscription on start and when the screen state is on
+ Mockito.verify(monitor, never()).addSubscription(ArgumentMatchers.any())
+ }
+
+ private fun captureConditions(): Set<Condition?> {
+ Mockito.verify(monitor).addSubscription(preconditionsSubscriptionCaptor.capture())
+ return preconditionsSubscriptionCaptor.value.conditions
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/MediaTestHelper.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/MediaTestHelper.kt
deleted file mode 100644
index 720bcb52d95d..000000000000
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/MediaTestHelper.kt
+++ /dev/null
@@ -1,33 +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.media.controls
-
-import android.app.smartspace.SmartspaceAction
-import android.graphics.drawable.Icon
-import com.android.systemui.util.mockito.mock
-import com.android.systemui.util.mockito.whenever
-
-class MediaTestHelper {
- companion object {
- /** Returns a list of three mocked recommendations */
- fun getValidRecommendationList(mediaIcon: Icon): List<SmartspaceAction> {
- val mediaRecommendationItem =
- mock<SmartspaceAction> { whenever(icon).thenReturn(mediaIcon) }
- return listOf(mediaRecommendationItem, mediaRecommendationItem, mediaRecommendationItem)
- }
- }
-}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/data/repository/MediaDataRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/data/repository/MediaDataRepositoryTest.kt
index 7e6f5fc71b15..2eed13287218 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/data/repository/MediaDataRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/data/repository/MediaDataRepositoryTest.kt
@@ -16,16 +16,12 @@
package com.android.systemui.media.controls.data.repository
-import android.R
-import android.graphics.drawable.Icon
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.kosmos.testScope
-import com.android.systemui.media.controls.MediaTestHelper
import com.android.systemui.media.controls.shared.model.MediaData
-import com.android.systemui.media.controls.shared.model.SmartspaceMediaData
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.test.runTest
@@ -42,17 +38,6 @@ class MediaDataRepositoryTest : SysuiTestCase() {
private val underTest: MediaDataRepository = kosmos.mediaDataRepository
@Test
- fun setRecommendation() =
- testScope.runTest {
- val smartspaceData by collectLastValue(underTest.smartspaceMediaData)
- val recommendation = SmartspaceMediaData(isActive = true)
-
- underTest.setRecommendation(recommendation)
-
- assertThat(smartspaceData).isEqualTo(recommendation)
- }
-
- @Test
fun addAndRemoveMediaData() =
testScope.runTest {
val entries by collectLastValue(underTest.mediaEntries)
@@ -75,29 +60,4 @@ class MediaDataRepositoryTest : SysuiTestCase() {
assertThat(entries!!.size).isEqualTo(1)
assertThat(entries!![secondKey]).isEqualTo(secondData)
}
-
- @Test
- fun dismissRecommendation() =
- testScope.runTest {
- val smartspaceData by collectLastValue(underTest.smartspaceMediaData)
- val icon = Icon.createWithResource(context, R.drawable.ic_media_play)
- val recommendation =
- SmartspaceMediaData(
- targetId = KEY_MEDIA_SMARTSPACE,
- isActive = true,
- recommendations = MediaTestHelper.getValidRecommendationList(icon),
- )
-
- underTest.setRecommendation(recommendation)
-
- assertThat(smartspaceData).isEqualTo(recommendation)
-
- underTest.dismissSmartspaceRecommendation(KEY_MEDIA_SMARTSPACE)
-
- assertThat(smartspaceData!!.isActive).isFalse()
- }
-
- companion object {
- private const val KEY_MEDIA_SMARTSPACE = "MEDIA_SMARTSPACE_ID"
- }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/data/repository/MediaFilterRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/data/repository/MediaFilterRepositoryTest.kt
index b1427f21345c..0a4911fec2b4 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/data/repository/MediaFilterRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/data/repository/MediaFilterRepositoryTest.kt
@@ -16,20 +16,15 @@
package com.android.systemui.media.controls.data.repository
-import android.R
-import android.graphics.drawable.Icon
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.internal.logging.InstanceId
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.kosmos.testScope
-import com.android.systemui.media.controls.MediaTestHelper
import com.android.systemui.media.controls.shared.model.MediaCommonModel
import com.android.systemui.media.controls.shared.model.MediaData
import com.android.systemui.media.controls.shared.model.MediaDataLoadingModel
-import com.android.systemui.media.controls.shared.model.SmartspaceMediaData
-import com.android.systemui.media.controls.shared.model.SmartspaceMediaLoadingModel
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.test.runTest
@@ -42,13 +37,6 @@ class MediaFilterRepositoryTest : SysuiTestCase() {
private val kosmos = testKosmos()
private val testScope = kosmos.testScope
- private val icon = Icon.createWithResource(context, R.drawable.ic_media_play)
- private val mediaRecommendation =
- SmartspaceMediaData(
- targetId = KEY_MEDIA_SMARTSPACE,
- isActive = true,
- recommendations = MediaTestHelper.getValidRecommendationList(icon),
- )
private val underTest: MediaFilterRepository = with(kosmos) { mediaFilterRepository }
@@ -140,21 +128,6 @@ class MediaFilterRepositoryTest : SysuiTestCase() {
}
@Test
- fun addActiveRecommendation_thenInactive() =
- testScope.runTest {
- val smartspaceMediaData by collectLastValue(underTest.smartspaceMediaData)
-
- underTest.setRecommendation(mediaRecommendation)
-
- assertThat(smartspaceMediaData).isEqualTo(mediaRecommendation)
-
- underTest.setRecommendation(mediaRecommendation.copy(isActive = false))
-
- assertThat(smartspaceMediaData).isNotEqualTo(mediaRecommendation)
- assertThat(underTest.isRecommendationActive()).isFalse()
- }
-
- @Test
fun addMediaControlPlayingThenRemote() =
testScope.runTest {
val currentMedia by collectLastValue(underTest.currentMedia)
@@ -163,10 +136,6 @@ class MediaFilterRepositoryTest : SysuiTestCase() {
val playingData = createMediaData("app1", true, LOCAL, false, playingInstanceId)
val remoteData = createMediaData("app2", true, REMOTE, false, remoteInstanceId)
- underTest.setRecommendation(mediaRecommendation)
- underTest.setRecommendationsLoadingState(
- SmartspaceMediaLoadingModel.Loaded(KEY_MEDIA_SMARTSPACE, true)
- )
underTest.addSelectedUserMediaEntry(playingData)
underTest.addMediaDataLoadingState(
MediaDataLoadingModel.Loaded(playingInstanceId),
@@ -179,14 +148,11 @@ class MediaFilterRepositoryTest : SysuiTestCase() {
false,
)
- assertThat(currentMedia?.size).isEqualTo(3)
+ assertThat(currentMedia?.size).isEqualTo(2)
assertThat(currentMedia)
.containsExactly(
- MediaCommonModel.MediaControl(MediaDataLoadingModel.Loaded(playingInstanceId)),
- MediaCommonModel.MediaControl(MediaDataLoadingModel.Loaded(remoteInstanceId)),
- MediaCommonModel.MediaRecommendations(
- SmartspaceMediaLoadingModel.Loaded(KEY_MEDIA_SMARTSPACE, true)
- ),
+ MediaCommonModel(MediaDataLoadingModel.Loaded(playingInstanceId)),
+ MediaCommonModel(MediaDataLoadingModel.Loaded(remoteInstanceId)),
)
.inOrder()
}
@@ -208,8 +174,8 @@ class MediaFilterRepositoryTest : SysuiTestCase() {
assertThat(currentMedia?.size).isEqualTo(2)
assertThat(currentMedia)
.containsExactly(
- MediaCommonModel.MediaControl(MediaDataLoadingModel.Loaded(playingInstanceId1)),
- MediaCommonModel.MediaControl(MediaDataLoadingModel.Loaded(playingInstanceId2)),
+ MediaCommonModel(MediaDataLoadingModel.Loaded(playingInstanceId1)),
+ MediaCommonModel(MediaDataLoadingModel.Loaded(playingInstanceId2)),
)
.inOrder()
@@ -224,8 +190,8 @@ class MediaFilterRepositoryTest : SysuiTestCase() {
assertThat(currentMedia?.size).isEqualTo(2)
assertThat(currentMedia)
.containsExactly(
- MediaCommonModel.MediaControl(MediaDataLoadingModel.Loaded(playingInstanceId1)),
- MediaCommonModel.MediaControl(MediaDataLoadingModel.Loaded(playingInstanceId2)),
+ MediaCommonModel(MediaDataLoadingModel.Loaded(playingInstanceId1)),
+ MediaCommonModel(MediaDataLoadingModel.Loaded(playingInstanceId2)),
)
.inOrder()
@@ -234,8 +200,8 @@ class MediaFilterRepositoryTest : SysuiTestCase() {
assertThat(currentMedia?.size).isEqualTo(2)
assertThat(currentMedia)
.containsExactly(
- MediaCommonModel.MediaControl(MediaDataLoadingModel.Loaded(playingInstanceId2)),
- MediaCommonModel.MediaControl(MediaDataLoadingModel.Loaded(playingInstanceId1)),
+ MediaCommonModel(MediaDataLoadingModel.Loaded(playingInstanceId2)),
+ MediaCommonModel(MediaDataLoadingModel.Loaded(playingInstanceId1)),
)
.inOrder()
}
@@ -270,81 +236,16 @@ class MediaFilterRepositoryTest : SysuiTestCase() {
underTest.addSelectedUserMediaEntry(playingAndRemoteData)
underTest.addMediaDataLoadingState(MediaDataLoadingModel.Loaded(instanceId2))
- underTest.setRecommendation(mediaRecommendation)
- underTest.setRecommendationsLoadingState(
- SmartspaceMediaLoadingModel.Loaded(KEY_MEDIA_SMARTSPACE, true)
- )
underTest.setOrderedMedia()
- assertThat(currentMedia?.size).isEqualTo(6)
- assertThat(currentMedia)
- .containsExactly(
- MediaCommonModel.MediaControl(MediaDataLoadingModel.Loaded(instanceId1)),
- MediaCommonModel.MediaControl(MediaDataLoadingModel.Loaded(instanceId2)),
- MediaCommonModel.MediaRecommendations(
- SmartspaceMediaLoadingModel.Loaded(KEY_MEDIA_SMARTSPACE, true)
- ),
- MediaCommonModel.MediaControl(MediaDataLoadingModel.Loaded(instanceId4)),
- MediaCommonModel.MediaControl(MediaDataLoadingModel.Loaded(instanceId3)),
- MediaCommonModel.MediaControl(MediaDataLoadingModel.Loaded(instanceId5)),
- )
- .inOrder()
- }
-
- @Test
- fun loadMediaFromRec() =
- testScope.runTest {
- val currentMedia by collectLastValue(underTest.currentMedia)
- val instanceId1 = InstanceId.fakeInstanceId(123)
- val instanceId2 = InstanceId.fakeInstanceId(456)
- val data =
- MediaData(
- active = true,
- instanceId = instanceId1,
- packageName = PACKAGE_NAME,
- isPlaying = true,
- notificationKey = KEY,
- )
- val newData =
- MediaData(
- active = true,
- instanceId = instanceId2,
- isPlaying = true,
- notificationKey = KEY_2,
- )
-
- underTest.setMediaFromRecPackageName(PACKAGE_NAME)
- underTest.addSelectedUserMediaEntry(data)
- underTest.setRecommendation(mediaRecommendation)
- underTest.setRecommendationsLoadingState(
- SmartspaceMediaLoadingModel.Loaded(KEY_MEDIA_SMARTSPACE)
- )
- underTest.addMediaDataLoadingState(MediaDataLoadingModel.Loaded(instanceId1))
-
- assertThat(currentMedia)
- .containsExactly(
- MediaCommonModel.MediaControl(
- MediaDataLoadingModel.Loaded(instanceId1),
- isMediaFromRec = true,
- ),
- MediaCommonModel.MediaRecommendations(
- SmartspaceMediaLoadingModel.Loaded(KEY_MEDIA_SMARTSPACE)
- ),
- )
- .inOrder()
-
- underTest.addSelectedUserMediaEntry(newData)
- underTest.addSelectedUserMediaEntry(data.copy(isPlaying = false))
- underTest.addMediaDataLoadingState(MediaDataLoadingModel.Loaded(instanceId2))
- underTest.addMediaDataLoadingState(MediaDataLoadingModel.Loaded(instanceId1))
-
+ assertThat(currentMedia?.size).isEqualTo(5)
assertThat(currentMedia)
.containsExactly(
- MediaCommonModel.MediaControl(MediaDataLoadingModel.Loaded(instanceId2)),
- MediaCommonModel.MediaControl(MediaDataLoadingModel.Loaded(instanceId1)),
- MediaCommonModel.MediaRecommendations(
- SmartspaceMediaLoadingModel.Loaded(KEY_MEDIA_SMARTSPACE)
- ),
+ MediaCommonModel(MediaDataLoadingModel.Loaded(instanceId1)),
+ MediaCommonModel(MediaDataLoadingModel.Loaded(instanceId2)),
+ MediaCommonModel(MediaDataLoadingModel.Loaded(instanceId4)),
+ MediaCommonModel(MediaDataLoadingModel.Loaded(instanceId3)),
+ MediaCommonModel(MediaDataLoadingModel.Loaded(instanceId5)),
)
.inOrder()
}
@@ -377,8 +278,5 @@ class MediaFilterRepositoryTest : SysuiTestCase() {
private const val LOCAL = MediaData.PLAYBACK_LOCAL
private const val REMOTE = MediaData.PLAYBACK_CAST_LOCAL
private const val KEY = "KEY"
- private const val KEY_2 = "KEY_2"
- private const val KEY_MEDIA_SMARTSPACE = "MEDIA_SMARTSPACE_ID"
- private const val PACKAGE_NAME = "com.android.example"
}
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/interactor/MediaControlInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/interactor/MediaControlInteractorTest.kt
index ba987c11f3e0..1a570d3b2d9b 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/interactor/MediaControlInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/interactor/MediaControlInteractorTest.kt
@@ -16,9 +16,7 @@
package com.android.systemui.media.controls.domain.interactor
-import android.R
import android.app.PendingIntent
-import android.graphics.drawable.Icon
import android.os.Bundle
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
@@ -32,7 +30,6 @@ import com.android.systemui.bluetooth.mockBroadcastDialogController
import com.android.systemui.concurrency.fakeExecutor
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.kosmos.testScope
-import com.android.systemui.media.controls.MediaTestHelper
import com.android.systemui.media.controls.data.repository.mediaDataRepository
import com.android.systemui.media.controls.domain.pipeline.MediaDataFilterImpl
import com.android.systemui.media.controls.domain.pipeline.MediaDataProcessor
@@ -41,7 +38,6 @@ import com.android.systemui.media.controls.domain.pipeline.interactor.mediaContr
import com.android.systemui.media.controls.domain.pipeline.mediaDataFilter
import com.android.systemui.media.controls.domain.pipeline.mediaDataProcessor
import com.android.systemui.media.controls.shared.model.MediaData
-import com.android.systemui.media.controls.shared.model.SmartspaceMediaData
import com.android.systemui.media.controls.util.mediaInstanceId
import com.android.systemui.media.mediaOutputDialogManager
import com.android.systemui.mockActivityIntentHelper
@@ -72,13 +68,6 @@ class MediaControlInteractorTest : SysuiTestCase() {
private val keyguardStateController = kosmos.keyguardStateController
private val instanceId: InstanceId = kosmos.mediaInstanceId
private val notificationLockscreenUserManager = kosmos.notificationLockscreenUserManager
- private val icon = Icon.createWithResource(context, R.drawable.ic_media_play)
- private val mediaRecommendation =
- SmartspaceMediaData(
- targetId = KEY_MEDIA_SMARTSPACE,
- isActive = true,
- recommendations = MediaTestHelper.getValidRecommendationList(icon),
- )
private val underTest: MediaControlInteractor =
with(kosmos) {
@@ -159,7 +148,6 @@ class MediaControlInteractorTest : SysuiTestCase() {
whenever(expandable.activityTransitionController(any())).thenReturn(activityController)
val mediaData = MediaData(userId = USER_ID, instanceId = instanceId, artist = ARTIST)
- mediaDataFilter.onSmartspaceMediaDataLoaded(KEY_MEDIA_SMARTSPACE, mediaRecommendation, true)
mediaDataFilter.onMediaDataLoaded(KEY, null, mediaData)
underTest.startClickIntent(expandable, clickIntent)
@@ -240,7 +228,7 @@ class MediaControlInteractorTest : SysuiTestCase() {
}
@Test
- fun removeMediaControl_noRecommendation() {
+ fun removeMediaControl() {
whenever(notificationLockscreenUserManager.isCurrentProfile(USER_ID)).thenReturn(true)
whenever(notificationLockscreenUserManager.isProfileAvailable(USER_ID)).thenReturn(true)
val listener = mock<MediaDataProcessor.Listener>()
@@ -257,25 +245,6 @@ class MediaControlInteractorTest : SysuiTestCase() {
verify(listener).onMediaDataRemoved(eq(KEY), eq(true))
}
- @Test
- fun removeMediaControl_recommendationsExist() {
- whenever(notificationLockscreenUserManager.isCurrentProfile(USER_ID)).thenReturn(true)
- whenever(notificationLockscreenUserManager.isProfileAvailable(USER_ID)).thenReturn(true)
- val listener = mock<MediaDataProcessor.Listener>()
- kosmos.mediaDataProcessor.addInternalListener(listener)
-
- val mediaData = MediaData(userId = USER_ID, instanceId = instanceId, artist = ARTIST)
- kosmos.mediaDataRepository.addMediaEntry(KEY, mediaData)
- mediaDataFilter.onSmartspaceMediaDataLoaded(KEY_MEDIA_SMARTSPACE, mediaRecommendation, true)
- mediaDataFilter.onMediaDataLoaded(KEY, null, mediaData)
-
- underTest.removeMediaControl(null, instanceId, 0L)
- kosmos.fakeExecutor.advanceClockToNext()
- kosmos.fakeExecutor.runAllReady()
-
- verify(listener).onMediaDataRemoved(eq(KEY), eq(true))
- }
-
companion object {
private const val USER_ID = 0
private const val KEY = "key"
@@ -283,6 +252,5 @@ class MediaControlInteractorTest : SysuiTestCase() {
private const val APP_NAME = "app"
private const val ARTIST = "artist"
private const val ARTIST_2 = "artist2"
- private const val KEY_MEDIA_SMARTSPACE = "MEDIA_SMARTSPACE_ID"
}
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoaderTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoaderTest.kt
index 1f1a74b6c389..63c0f4371c62 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoaderTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoaderTest.kt
@@ -34,7 +34,6 @@ import androidx.media.utils.MediaConstants
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
-import com.android.systemui.flags.Flags.MEDIA_RESUME_PROGRESS
import com.android.systemui.flags.fakeFeatureFlagsClassic
import com.android.systemui.graphics.ImageLoader
import com.android.systemui.graphics.imageLoader
@@ -167,8 +166,6 @@ class MediaDataLoaderTest : SysuiTestCase() {
@Test
fun loadMediaDataForResumption_returnsMediaData() =
testScope.runTest {
- fakeFeatureFlags.set(MEDIA_RESUME_PROGRESS, true)
-
val song = "THIS_IS_A_SONG"
val artist = "THIS_IS_AN_ARTIST"
val albumArt = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListenerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListenerTest.kt
index a3b3f5c6845d..ff131954716e 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListenerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListenerTest.kt
@@ -25,9 +25,7 @@ import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.media.controls.MediaTestUtils
import com.android.systemui.media.controls.shared.model.MediaData
-import com.android.systemui.media.controls.shared.model.SmartspaceMediaData
import com.android.systemui.media.controls.util.MediaControllerFactory
-import com.android.systemui.media.controls.util.MediaFlags
import com.android.systemui.plugins.statusbar.StatusBarStateController
import com.android.systemui.statusbar.SysuiStatusBarStateController
import com.android.systemui.util.concurrency.FakeExecutor
@@ -58,7 +56,6 @@ private const val PACKAGE = "PKG"
private const val SESSION_KEY = "SESSION_KEY"
private const val SESSION_ARTIST = "SESSION_ARTIST"
private const val SESSION_TITLE = "SESSION_TITLE"
-private const val SMARTSPACE_KEY = "SMARTSPACE_KEY"
private fun <T> anyObject(): T {
return ArgumentMatchers.any<T>()
@@ -90,8 +87,6 @@ class MediaTimeoutListenerTest : SysuiTestCase() {
private lateinit var mainExecutor: FakeExecutor
private lateinit var bgExecutor: FakeExecutor
private lateinit var uiExecutor: FakeExecutor
- @Mock private lateinit var mediaFlags: MediaFlags
- @Mock private lateinit var smartspaceData: SmartspaceMediaData
@Before
fun setup() {
@@ -108,7 +103,6 @@ class MediaTimeoutListenerTest : SysuiTestCase() {
logger,
statusBarStateController,
clock,
- mediaFlags,
)
mediaTimeoutListener.timeoutCallback = timeoutCallback
mediaTimeoutListener.stateCallback = stateCallback
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/dialog/MediaOutputAdapterLegacyTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/dialog/MediaOutputAdapterLegacyTest.java
index 9c4d93c17d00..f7298dd0bf36 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/dialog/MediaOutputAdapterLegacyTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/dialog/MediaOutputAdapterLegacyTest.java
@@ -67,6 +67,7 @@ import java.util.concurrent.Executor;
import java.util.stream.Collectors;
@SmallTest
+@DisableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_REDESIGN)
@RunWith(AndroidJUnit4.class)
@TestableLooper.RunWithLooper(setAsMainLooper = true)
public class MediaOutputAdapterLegacyTest extends SysuiTestCase {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.kt
new file mode 100644
index 000000000000..70adfd324e94
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.kt
@@ -0,0 +1,763 @@
+/*
+ * 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.dialog
+
+import android.content.Context
+import android.graphics.drawable.Icon
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
+import android.testing.TestableLooper.RunWithLooper
+import android.view.View.GONE
+import android.view.View.VISIBLE
+import android.widget.LinearLayout
+import androidx.appcompat.view.ContextThemeWrapper
+import androidx.core.graphics.drawable.IconCompat
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.internal.widget.RecyclerView
+import com.android.media.flags.Flags
+import com.android.settingslib.media.LocalMediaManager.MediaDeviceState.STATE_CONNECTED
+import com.android.settingslib.media.LocalMediaManager.MediaDeviceState.STATE_CONNECTING
+import com.android.settingslib.media.LocalMediaManager.MediaDeviceState.STATE_CONNECTING_FAILED
+import com.android.settingslib.media.LocalMediaManager.MediaDeviceState.STATE_DISCONNECTED
+import com.android.settingslib.media.LocalMediaManager.MediaDeviceState.STATE_GROUPING
+import com.android.settingslib.media.MediaDevice
+import com.android.settingslib.media.MediaDevice.SelectionBehavior.SELECTION_BEHAVIOR_GO_TO_APP
+import com.android.settingslib.media.MediaDevice.SelectionBehavior.SELECTION_BEHAVIOR_NONE
+import com.android.settingslib.media.MediaDevice.SelectionBehavior.SELECTION_BEHAVIOR_TRANSFER
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.media.dialog.MediaItem.MediaItemType
+import com.android.systemui.media.dialog.MediaItem.createDeviceMediaItem
+import com.android.systemui.media.dialog.MediaOutputAdapter.MediaDeviceViewHolder
+import com.android.systemui.media.dialog.MediaOutputAdapter.MediaGroupDividerViewHolder
+import com.android.systemui.res.R
+import com.google.android.material.slider.Slider
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doNothing
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.never
+import org.mockito.kotlin.spy
+import org.mockito.kotlin.stub
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+@SmallTest
+@EnableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_REDESIGN)
+@RunWith(AndroidJUnit4::class)
+@RunWithLooper(setAsMainLooper = true)
+class MediaOutputAdapterTest : SysuiTestCase() {
+ private val mMediaSwitchingController = mock<MediaSwitchingController>()
+ private val mMediaDevice1: MediaDevice = mock<MediaDevice>()
+ private val mMediaDevice2: MediaDevice = mock<MediaDevice>()
+ private val mIcon: Icon = mock<Icon>()
+ private val mIconCompat: IconCompat = mock<IconCompat>()
+ private lateinit var mMediaOutputAdapter: MediaOutputAdapter
+ private val mMediaItems: MutableList<MediaItem> = ArrayList()
+
+ @Before
+ fun setUp() {
+ mMediaSwitchingController.stub {
+ on { getMediaItemList(false) } doReturn mMediaItems
+ on { hasAdjustVolumeUserRestriction() } doReturn false
+ on { isAnyDeviceTransferring } doReturn false
+ on { currentConnectedMediaDevice } doReturn mMediaDevice1
+ on { connectedSpeakersExpandableGroupDivider }
+ .doReturn(
+ MediaItem.createExpandableGroupDividerMediaItem(
+ mContext.getString(R.string.media_output_group_title_connected_speakers)
+ )
+ )
+ on { sessionVolumeMax } doReturn TEST_MAX_VOLUME
+ on { sessionVolume } doReturn TEST_CURRENT_VOLUME
+ on { sessionName } doReturn TEST_SESSION_NAME
+ on { colorSchemeLegacy } doReturn mock<MediaOutputColorSchemeLegacy>()
+ on { colorScheme } doReturn mock<MediaOutputColorScheme>()
+ }
+
+ mIconCompat.stub { on { toIcon(mContext) } doReturn mIcon }
+
+ mMediaDevice1
+ .stub {
+ on { id } doReturn TEST_DEVICE_ID_1
+ on { name } doReturn TEST_DEVICE_NAME_1
+ }
+ .also {
+ whenever(mMediaSwitchingController.getDeviceIconCompat(it)) doReturn mIconCompat
+ }
+
+ mMediaDevice2
+ .stub {
+ on { id } doReturn TEST_DEVICE_ID_2
+ on { name } doReturn TEST_DEVICE_NAME_2
+ }
+ .also {
+ whenever(mMediaSwitchingController.getDeviceIconCompat(it)) doReturn mIconCompat
+ }
+
+ mMediaOutputAdapter = MediaOutputAdapter(mMediaSwitchingController)
+ }
+
+ @Test
+ fun getItemCount_returnsMediaItemSize() {
+ updateAdapterWithDevices(listOf(mMediaDevice1, mMediaDevice2))
+
+ assertThat(mMediaOutputAdapter.itemCount).isEqualTo(mMediaItems.size)
+ }
+
+ @Test
+ fun getItemId_forDifferentItemsTypes_returnCorrespondingHashCode() {
+ updateAdapterWithDevices(listOf(mMediaDevice1, mMediaDevice2))
+
+ assertThat(mMediaOutputAdapter.getItemId(0))
+ .isEqualTo(mMediaItems[0].mediaDevice.get().id.hashCode())
+ }
+
+ @Test
+ fun getItemId_invalidPosition_returnPosition() {
+ updateAdapterWithDevices(listOf(mMediaDevice1, mMediaDevice2))
+ val invalidPosition = mMediaItems.size + 1
+
+ assertThat(mMediaOutputAdapter.getItemId(invalidPosition)).isEqualTo(RecyclerView.NO_ID)
+ }
+
+ @Test
+ fun onBindViewHolder_bindDisconnectedDevice_verifyView() {
+ mMediaDevice2.stub { on { state } doReturn STATE_DISCONNECTED }
+ updateAdapterWithDevices(listOf(mMediaDevice2))
+
+ createAndBindDeviceViewHolder(position = 0).apply {
+ assertThat(mTitleIcon.visibility).isEqualTo(VISIBLE)
+ assertThat(mTitleText.visibility).isEqualTo(VISIBLE)
+ assertThat(mTitleText.text.toString()).isEqualTo(TEST_DEVICE_NAME_2)
+ assertThat(mSlider.visibility).isEqualTo(GONE)
+ }
+ }
+
+ @Test
+ fun onBindViewHolder_bindConnectedDevice_verifyView() {
+ mMediaDevice1.stub { on { state } doReturn STATE_CONNECTED }
+ updateAdapterWithDevices(listOf(mMediaDevice1))
+
+ createAndBindDeviceViewHolder(position = 0).apply {
+ assertThat(mTitleIcon.visibility).isEqualTo(GONE)
+ assertThat(mTitleText.visibility).isEqualTo(VISIBLE)
+ assertThat(mTitleText.text.toString()).isEqualTo(TEST_DEVICE_NAME_1)
+ assertThat(mSlider.visibility).isEqualTo(VISIBLE)
+ }
+ }
+
+ @Test
+ fun onBindViewHolder_isMutingExpectedDevice_verifyView() {
+ mMediaDevice1.stub {
+ on { isMutingExpectedDevice } doReturn true
+ on { state } doReturn STATE_DISCONNECTED
+ }
+ mMediaSwitchingController.stub { on { isCurrentConnectedDeviceRemote } doReturn false }
+ updateAdapterWithDevices(listOf(mMediaDevice1))
+
+ createAndBindDeviceViewHolder(position = 0).apply {
+ assertThat(mTitleText.text.toString()).isEqualTo(TEST_DEVICE_NAME_1)
+ assertThat(mLoadingIndicator.visibility).isEqualTo(GONE)
+ assertThat(mSlider.visibility).isEqualTo(GONE)
+ assertThat(mGroupButton.visibility).isEqualTo(GONE)
+ assertThat(mTitleText.visibility).isEqualTo(VISIBLE)
+ }
+ }
+
+ @Test
+ fun onBindViewHolder_bindConnectedDeviceWithMutingExpectedDeviceExist_verifyView() {
+ mMediaDevice1.stub {
+ on { isMutingExpectedDevice } doReturn true
+ on { state } doReturn STATE_CONNECTED
+ }
+ mMediaSwitchingController.stub {
+ on { hasMutingExpectedDevice() } doReturn true
+ on { isCurrentConnectedDeviceRemote } doReturn false
+ }
+ updateAdapterWithDevices(listOf(mMediaDevice1))
+
+ createAndBindDeviceViewHolder(position = 0).apply {
+ assertThat(mLoadingIndicator.visibility).isEqualTo(GONE)
+ assertThat(mSlider.visibility).isEqualTo(GONE)
+ assertThat(mGroupButton.visibility).isEqualTo(GONE)
+ assertThat(mTitleText.visibility).isEqualTo(VISIBLE)
+ assertThat(mTitleText.text.toString()).isEqualTo(TEST_DEVICE_NAME_1)
+ }
+ }
+
+ @Test
+ fun onBindViewHolder_initSeekbar_setsVolume() {
+ mMediaDevice1.stub {
+ on { state } doReturn STATE_CONNECTED
+ on { maxVolume } doReturn TEST_MAX_VOLUME
+ on { currentVolume } doReturn TEST_CURRENT_VOLUME
+ }
+ updateAdapterWithDevices(listOf(mMediaDevice1))
+
+ createAndBindDeviceViewHolder(position = 0).apply {
+ assertThat(mSlider.visibility).isEqualTo(VISIBLE)
+ assertThat(mSlider.value).isEqualTo(TEST_CURRENT_VOLUME)
+ assertThat(mSlider.valueFrom).isEqualTo(0)
+ assertThat(mSlider.valueTo).isEqualTo(TEST_MAX_VOLUME)
+ }
+ }
+
+ @Test
+ fun onBindViewHolder_dragSeekbar_adjustsVolume() {
+ mMediaDevice1.stub {
+ on { maxVolume } doReturn TEST_MAX_VOLUME
+ on { currentVolume } doReturn TEST_CURRENT_VOLUME
+ }
+ updateAdapterWithDevices(listOf(mMediaDevice1))
+
+ val viewHolder =
+ mMediaOutputAdapter.onCreateViewHolder(
+ LinearLayout(mContext),
+ MediaItemType.TYPE_DEVICE,
+ ) as MediaDeviceViewHolder
+
+ var sliderChangeListener: Slider.OnChangeListener? = null
+ viewHolder.mSlider =
+ object : Slider(contextWithTheme(mContext)) {
+ override fun addOnChangeListener(listener: OnChangeListener) {
+ sliderChangeListener = listener
+ }
+ }
+ mMediaOutputAdapter.onBindViewHolder(viewHolder, 0)
+ sliderChangeListener?.onValueChange(viewHolder.mSlider, 5f, true)
+
+ verify(mMediaSwitchingController).adjustVolume(mMediaDevice1, 5)
+ }
+
+ @Test
+ fun onBindViewHolder_dragSeekbar_logsInteraction() {
+ mMediaDevice1
+ .stub {
+ on { maxVolume } doReturn TEST_MAX_VOLUME
+ on { currentVolume } doReturn TEST_CURRENT_VOLUME
+ }
+ .also { mMediaItems.add(createDeviceMediaItem(it)) }
+ updateAdapterWithDevices(listOf(mMediaDevice1))
+
+ val viewHolder =
+ mMediaOutputAdapter.onCreateViewHolder(
+ LinearLayout(mContext),
+ MediaItemType.TYPE_DEVICE,
+ ) as MediaDeviceViewHolder
+
+ var sliderTouchListener: Slider.OnSliderTouchListener? = null
+ viewHolder.mSlider =
+ object : Slider(contextWithTheme(mContext)) {
+ override fun addOnSliderTouchListener(listener: OnSliderTouchListener) {
+ sliderTouchListener = listener
+ }
+ }
+ mMediaOutputAdapter.onBindViewHolder(viewHolder, 0)
+ sliderTouchListener?.onStopTrackingTouch(viewHolder.mSlider)
+
+ verify(mMediaSwitchingController).logInteractionAdjustVolume(mMediaDevice1)
+ }
+
+ @Test
+ fun onBindViewHolder_bindSelectableDevice_verifyView() {
+ mMediaSwitchingController.stub {
+ on { selectableMediaDevice } doReturn listOf(mMediaDevice2)
+ }
+ updateAdapterWithDevices(listOf(mMediaDevice1, mMediaDevice2))
+
+ createAndBindDeviceViewHolder(position = 1).apply {
+ assertThat(mLoadingIndicator.visibility).isEqualTo(GONE)
+ assertThat(mDivider.visibility).isEqualTo(VISIBLE)
+ assertThat(mGroupButton.visibility).isEqualTo(VISIBLE)
+ assertThat(mGroupButton.contentDescription)
+ .isEqualTo(mContext.getString(R.string.accessibility_add_device_to_group))
+ assertThat(mTitleText.visibility).isEqualTo(VISIBLE)
+ assertThat(mTitleText.text.toString()).isEqualTo(TEST_DEVICE_NAME_2)
+
+ mGroupButton.performClick()
+ }
+ verify(mMediaSwitchingController).addDeviceToPlayMedia(mMediaDevice2)
+ }
+
+ @Test
+ fun onBindViewHolder_bindDeselectableDevice_verifyView() {
+ mMediaSwitchingController.stub {
+ on { selectedMediaDevice } doReturn listOf(mMediaDevice1, mMediaDevice2)
+ on { deselectableMediaDevice } doReturn listOf(mMediaDevice1, mMediaDevice2)
+ }
+ updateAdapterWithDevices(listOf(mMediaDevice1, mMediaDevice2))
+
+ createAndBindDeviceViewHolder(position = 1).apply {
+ assertThat(mGroupButton.visibility).isEqualTo(VISIBLE)
+ assertThat(mGroupButton.contentDescription)
+ .isEqualTo(mContext.getString(R.string.accessibility_remove_device_from_group))
+ mGroupButton.performClick()
+ }
+
+ verify(mMediaSwitchingController).removeDeviceFromPlayMedia(mMediaDevice2)
+ }
+
+ @Test
+ fun onBindViewHolder_bindNonDeselectableDevice_verifyView() {
+ mMediaSwitchingController.stub {
+ on { selectedMediaDevice } doReturn listOf(mMediaDevice1)
+ on { deselectableMediaDevice } doReturn ArrayList()
+ }
+ updateAdapterWithDevices(listOf(mMediaDevice1, mMediaDevice2))
+
+ createAndBindDeviceViewHolder(position = 0).apply {
+ assertThat(mTitleText.text.toString()).isEqualTo(TEST_DEVICE_NAME_1)
+ assertThat(mGroupButton.visibility).isEqualTo(GONE)
+ }
+ }
+
+ @Test
+ fun onBindViewHolder_bindFailedStateDevice_verifyView() {
+ mMediaDevice2.stub { on { state } doReturn STATE_CONNECTING_FAILED }
+ updateAdapterWithDevices(listOf(mMediaDevice2))
+
+ createAndBindDeviceViewHolder(position = 0).apply {
+ assertThat(mStatusIcon.visibility).isEqualTo(VISIBLE)
+ assertThat(mSubTitleText.visibility).isEqualTo(VISIBLE)
+ assertThat(mSubTitleText.text.toString())
+ .isEqualTo(mContext.getText(R.string.media_output_dialog_connect_failed).toString())
+ }
+ }
+
+ @Test
+ fun onBindViewHolder_deviceHasSubtext_displaySubtitle() {
+ mMediaDevice2.stub {
+ on { state } doReturn STATE_DISCONNECTED
+ on { hasSubtext() } doReturn true
+ on { subtextString } doReturn TEST_CUSTOM_SUBTEXT
+ }
+ updateAdapterWithDevices(listOf(mMediaDevice2))
+
+ createAndBindDeviceViewHolder(position = 0).apply {
+ assertThat(mTitleText.text.toString()).isEqualTo(TEST_DEVICE_NAME_2)
+ assertThat(mSubTitleText.visibility).isEqualTo(VISIBLE)
+ assertThat(mSubTitleText.text.toString()).isEqualTo(TEST_CUSTOM_SUBTEXT)
+ }
+ }
+
+ @Test
+ fun onBindViewHolder_deviceWithOngoingSession_displaysGoToAppButton() {
+ mMediaDevice2.stub {
+ on { state } doReturn STATE_DISCONNECTED
+ on { hasOngoingSession() } doReturn true
+ }
+ updateAdapterWithDevices(listOf(mMediaDevice2))
+
+ val viewHolder =
+ createAndBindDeviceViewHolder(position = 0).apply {
+ assertThat(mTitleText.text.toString()).isEqualTo(TEST_DEVICE_NAME_2)
+ assertThat(mOngoingSessionButton.visibility).isEqualTo(VISIBLE)
+ assertThat(mOngoingSessionButton.contentDescription)
+ .isEqualTo(mContext.getString(R.string.accessibility_open_application))
+ mOngoingSessionButton.performClick()
+ }
+
+ verify(mMediaSwitchingController)
+ .tryToLaunchInAppRoutingIntent(TEST_DEVICE_ID_2, viewHolder.mOngoingSessionButton)
+ }
+
+ @Test
+ fun onItemClick_selectionBehaviorTransfer_connectsDevice() {
+ mMediaDevice2.stub {
+ on { state } doReturn STATE_DISCONNECTED
+ on { selectionBehavior } doReturn SELECTION_BEHAVIOR_TRANSFER
+ }
+ updateAdapterWithDevices(listOf(mMediaDevice2))
+
+ createAndBindDeviceViewHolder(position = 0).apply { mMainContent.performClick() }
+
+ verify(mMediaSwitchingController).connectDevice(mMediaDevice2)
+ }
+
+ @Test
+ fun onItemClick_selectionBehaviorTransferAndSessionHost_showsEndSessionDialog() {
+ mMediaSwitchingController.stub {
+ on { isCurrentOutputDeviceHasSessionOngoing() } doReturn true
+ }
+ mMediaDevice2.stub {
+ on { state } doReturn STATE_DISCONNECTED
+ on { selectionBehavior } doReturn SELECTION_BEHAVIOR_TRANSFER
+ }
+ updateAdapterWithDevices(listOf(mMediaDevice2))
+
+ val viewHolder =
+ mMediaOutputAdapter.onCreateViewHolder(
+ LinearLayout(mContext),
+ MediaItemType.TYPE_DEVICE,
+ ) as MediaDeviceViewHolder
+ val spyMediaDeviceViewHolder = spy(viewHolder)
+ doNothing().whenever(spyMediaDeviceViewHolder).showCustomEndSessionDialog(mMediaDevice2)
+
+ mMediaOutputAdapter.onBindViewHolder(spyMediaDeviceViewHolder, 0)
+ spyMediaDeviceViewHolder.mMainContent.performClick()
+
+ verify(mMediaSwitchingController, never()).connectDevice(ArgumentMatchers.any())
+ verify(spyMediaDeviceViewHolder).showCustomEndSessionDialog(mMediaDevice2)
+ }
+
+ @Test
+ fun onItemClick_selectionBehaviorGoToApp_sendsLaunchIntent() {
+ mMediaDevice2.stub {
+ on { state } doReturn STATE_DISCONNECTED
+ on { selectionBehavior } doReturn SELECTION_BEHAVIOR_GO_TO_APP
+ }
+ updateAdapterWithDevices(listOf(mMediaDevice2))
+
+ val viewHolder =
+ createAndBindDeviceViewHolder(position = 0).apply { mMainContent.performClick() }
+ verify(mMediaSwitchingController)
+ .tryToLaunchInAppRoutingIntent(TEST_DEVICE_ID_2, viewHolder.mMainContent)
+ }
+
+ @Test
+ fun onItemClick_selectionBehaviorNone_doesNothing() {
+ mMediaDevice2.stub {
+ on { state } doReturn STATE_DISCONNECTED
+ on { selectionBehavior } doReturn SELECTION_BEHAVIOR_NONE
+ }
+ updateAdapterWithDevices(listOf(mMediaDevice2))
+ createAndBindDeviceViewHolder(position = 0).apply { mMainContent.performClick() }
+
+ verify(mMediaSwitchingController, never()).tryToLaunchInAppRoutingIntent(any(), any())
+ verify(mMediaSwitchingController, never()).connectDevice(any())
+ }
+
+ @DisableFlags(Flags.FLAG_DISABLE_TRANSFER_WHEN_APPS_DO_NOT_SUPPORT)
+ @Test
+ fun clickFullItemOfSelectableDevice_flagOff_verifyConnectDevice() {
+ mMediaSwitchingController.stub {
+ on { selectableMediaDevice } doReturn listOf(mMediaDevice2)
+ }
+ updateAdapterWithDevices(listOf(mMediaDevice2))
+
+ createAndBindDeviceViewHolder(position = 0).apply {
+ assertThat(mTitleText.text.toString()).isEqualTo(TEST_DEVICE_NAME_2)
+ mMainContent.performClick()
+ }
+ verify(mMediaSwitchingController).connectDevice(mMediaDevice2)
+ }
+
+ @EnableFlags(Flags.FLAG_DISABLE_TRANSFER_WHEN_APPS_DO_NOT_SUPPORT)
+ @Test
+ fun clickFullItemOfSelectableDevice_flagOn_hasListingPreference_verifyConnectDevice() {
+ mMediaDevice2.stub { on { hasRouteListingPreferenceItem() } doReturn true }
+ mMediaSwitchingController.stub {
+ on { selectableMediaDevice } doReturn listOf(mMediaDevice2)
+ }
+ updateAdapterWithDevices(listOf(mMediaDevice2))
+
+ createAndBindDeviceViewHolder(position = 0).apply {
+ assertThat(mTitleText.text.toString()).isEqualTo(TEST_DEVICE_NAME_2)
+ mMainContent.performClick()
+ }
+ verify(mMediaSwitchingController).connectDevice(mMediaDevice2)
+ }
+
+ @EnableFlags(Flags.FLAG_DISABLE_TRANSFER_WHEN_APPS_DO_NOT_SUPPORT)
+ @Test
+ fun clickFullItemOfSelectableDevice_flagOn_isTransferable_verifyConnectDevice() {
+ mMediaSwitchingController.stub {
+ on { selectableMediaDevice } doReturn listOf(mMediaDevice2)
+ on { transferableMediaDevices } doReturn listOf(mMediaDevice2)
+ }
+ updateAdapterWithDevices(listOf(mMediaDevice2))
+
+ createAndBindDeviceViewHolder(position = 0).apply {
+ assertThat(mTitleText.text.toString()).isEqualTo(TEST_DEVICE_NAME_2)
+ mMainContent.performClick()
+ }
+ verify(mMediaSwitchingController).connectDevice(mMediaDevice2)
+ }
+
+ @EnableFlags(Flags.FLAG_DISABLE_TRANSFER_WHEN_APPS_DO_NOT_SUPPORT)
+ @Test
+ fun clickFullItemOfSelectableDevice_flagOn_notTransferable_verifyNotConnectDevice() {
+ mMediaDevice2.stub { on { hasRouteListingPreferenceItem() } doReturn false }
+ mMediaSwitchingController.stub {
+ on { selectableMediaDevice } doReturn listOf(mMediaDevice2)
+ on { transferableMediaDevices } doReturn listOf()
+ }
+ updateAdapterWithDevices(listOf(mMediaDevice2))
+
+ createAndBindDeviceViewHolder(position = 0).apply {
+ assertThat(mTitleText.text.toString()).isEqualTo(TEST_DEVICE_NAME_2)
+ mMainContent.performClick()
+ }
+ verify(mMediaSwitchingController, never()).connectDevice(any())
+ }
+
+ @Test
+ fun onBindViewHolder_inTransferring_bindTransferringDevice_verifyView() {
+ mMediaSwitchingController.stub { on { isAnyDeviceTransferring() } doReturn true }
+ mMediaDevice2.stub { on { state } doReturn STATE_CONNECTING }
+ updateAdapterWithDevices(listOf(mMediaDevice1, mMediaDevice2))
+
+ // Connected device, looks like disconnected during transfer
+ createAndBindDeviceViewHolder(position = 0).apply {
+ assertThat(mTitleText.visibility).isEqualTo(VISIBLE)
+ assertThat(mTitleText.text.toString()).isEqualTo(TEST_DEVICE_NAME_1)
+ assertThat(mSlider.visibility).isEqualTo(GONE)
+ assertThat(mLoadingIndicator.visibility).isEqualTo(GONE)
+ }
+
+ // Connecting device
+ createAndBindDeviceViewHolder(position = 1).apply {
+ assertThat(mTitleText.visibility).isEqualTo(VISIBLE)
+ assertThat(mTitleText.text.toString()).isEqualTo(TEST_DEVICE_NAME_2)
+ assertThat(mSlider.visibility).isEqualTo(GONE)
+ assertThat(mLoadingIndicator.visibility).isEqualTo(VISIBLE)
+ }
+ }
+
+ @Test
+ fun onBindViewHolder_bindGroupingDevice_verifyView() {
+ mMediaDevice1.stub { on { state } doReturn STATE_GROUPING }
+ updateAdapterWithDevices(listOf(mMediaDevice1))
+
+ createAndBindDeviceViewHolder(position = 0).apply {
+ assertThat(mTitleText.visibility).isEqualTo(VISIBLE)
+ assertThat(mTitleText.text.toString()).isEqualTo(TEST_DEVICE_NAME_1)
+ assertThat(mSlider.visibility).isEqualTo(GONE)
+ assertThat(mSubTitleText.visibility).isEqualTo(GONE)
+ assertThat(mGroupButton.visibility).isEqualTo(GONE)
+ assertThat(mLoadingIndicator.visibility).isEqualTo(VISIBLE)
+ }
+ }
+
+ @Test
+ fun onItemClick_clicksWithMutingExpectedDeviceExist_cancelsMuteAwaitConnection() {
+ mMediaSwitchingController.stub {
+ on { hasMutingExpectedDevice() } doReturn true
+ on { isCurrentConnectedDeviceRemote() } doReturn false
+ }
+ mMediaDevice1.stub { on { isMutingExpectedDevice } doReturn false }
+ updateAdapterWithDevices(listOf(mMediaDevice1))
+
+ createAndBindDeviceViewHolder(position = 0).apply { mMainContent.performClick() }
+ verify(mMediaSwitchingController).cancelMuteAwaitConnection()
+ }
+
+ @Test
+ fun onGroupActionTriggered_clicksSelectableDevice_triggerGrouping() {
+ mMediaSwitchingController.stub {
+ on { selectableMediaDevice } doReturn listOf(mMediaDevice2)
+ }
+ updateAdapterWithDevices(listOf(mMediaDevice2))
+
+ createAndBindDeviceViewHolder(position = 0).apply { mGroupButton.performClick() }
+ verify(mMediaSwitchingController).addDeviceToPlayMedia(mMediaDevice2)
+ }
+
+ @Test
+ fun onGroupActionTriggered_clickSelectedRemoteDevice_triggerUngrouping() {
+ mMediaSwitchingController.stub {
+ on { selectableMediaDevice } doReturn listOf(mMediaDevice2)
+ on { selectedMediaDevice } doReturn listOf(mMediaDevice1)
+ on { deselectableMediaDevice } doReturn listOf(mMediaDevice1)
+ on { isCurrentConnectedDeviceRemote } doReturn true
+ }
+ updateAdapterWithDevices(listOf(mMediaDevice1, mMediaDevice2))
+
+ createAndBindDeviceViewHolder(position = 0).apply { mGroupButton.performClick() }
+ verify(mMediaSwitchingController).removeDeviceFromPlayMedia(mMediaDevice1)
+ }
+
+ @Test
+ fun onBindViewHolder_hasVolumeAdjustmentRestriction_verifySeekbarDisabled() {
+ mMediaSwitchingController.stub {
+ on { isCurrentConnectedDeviceRemote } doReturn true
+ on { hasAdjustVolumeUserRestriction() } doReturn true
+ }
+ mMediaDevice1.stub { on { state } doReturn STATE_CONNECTED }
+ updateAdapterWithDevices(listOf(mMediaDevice1))
+
+ createAndBindDeviceViewHolder(position = 0).apply {
+ assertThat(mSlider.visibility).isEqualTo(GONE)
+ }
+ }
+
+ @Test
+ fun onBindViewHolder_volumeControlChangeToEnabled_enableSeekbarAgain() {
+ mMediaSwitchingController.stub {
+ on { isVolumeControlEnabled(mMediaDevice1) } doReturn false
+ }
+ mMediaDevice1.stub {
+ on { state } doReturn STATE_CONNECTED
+ on { currentVolume } doReturn TEST_CURRENT_VOLUME
+ on { maxVolume } doReturn TEST_MAX_VOLUME
+ }
+ updateAdapterWithDevices(listOf(mMediaDevice1))
+
+ createAndBindDeviceViewHolder(position = 0).apply {
+ assertThat(mSlider.visibility).isEqualTo(VISIBLE)
+ assertThat(mSlider.isEnabled).isFalse()
+ }
+
+ mMediaSwitchingController.stub {
+ on { isVolumeControlEnabled(mMediaDevice1) } doReturn true
+ }
+ createAndBindDeviceViewHolder(position = 0).apply {
+ assertThat(mSlider.visibility).isEqualTo(VISIBLE)
+ assertThat(mSlider.isEnabled).isTrue()
+ }
+ }
+
+ @Test
+ fun updateItems_controllerItemsUpdated_notUpdatesInAdapterUntilUpdateItems() {
+ mMediaOutputAdapter.updateItems()
+ val updatedList: MutableList<MediaItem> = ArrayList()
+ updatedList.add(MediaItem.createDeviceGroupMediaItem())
+ whenever(mMediaSwitchingController.getMediaItemList(false)).doReturn(updatedList)
+ assertThat(mMediaOutputAdapter.itemCount).isEqualTo(mMediaItems.size)
+
+ mMediaOutputAdapter.updateItems()
+ assertThat(mMediaOutputAdapter.itemCount).isEqualTo(updatedList.size)
+ }
+
+ @Test
+ fun multipleSelectedDevices_listCollapsed_verifyItemTypes() {
+ mMediaSwitchingController.stub { on { isGroupListCollapsed } doReturn true }
+ initializeSession()
+
+ with(mMediaOutputAdapter) {
+ assertThat(itemCount).isEqualTo(2)
+ assertThat(getItemViewType(0)).isEqualTo(MediaItemType.TYPE_GROUP_DIVIDER)
+ assertThat(getItemViewType(1)).isEqualTo(MediaItemType.TYPE_DEVICE_GROUP)
+ }
+ }
+
+ @Test
+ fun multipleSelectedDevices_listCollapsed_verifySessionControl() {
+ mMediaSwitchingController.stub { on { isGroupListCollapsed } doReturn true }
+ initializeSession()
+
+ createAndBindDeviceViewHolder(position = 1).apply {
+ assertThat(mTitleText.text.toString()).isEqualTo(TEST_SESSION_NAME)
+ assertThat(mSlider.visibility).isEqualTo(VISIBLE)
+ assertThat(mTitleText.visibility).isEqualTo(VISIBLE)
+ assertThat(mSlider.value).isEqualTo(TEST_CURRENT_VOLUME)
+ }
+
+ val viewHolder =
+ mMediaOutputAdapter.onCreateViewHolder(
+ LinearLayout(mContext),
+ MediaItemType.TYPE_DEVICE_GROUP,
+ ) as MediaDeviceViewHolder
+
+ var sliderChangeListener: Slider.OnChangeListener? = null
+ viewHolder.mSlider =
+ object : Slider(contextWithTheme(mContext)) {
+ override fun addOnChangeListener(listener: OnChangeListener) {
+ sliderChangeListener = listener
+ }
+ }
+ mMediaOutputAdapter.onBindViewHolder(viewHolder, 1)
+ sliderChangeListener?.onValueChange(viewHolder.mSlider, 7f, true)
+
+ verify(mMediaSwitchingController).adjustSessionVolume(7)
+ }
+
+ @Test
+ fun multipleSelectedDevices_expandIconClicked_verifyIndividualDevices() {
+ mMediaSwitchingController.stub { on { isGroupListCollapsed } doReturn true }
+ initializeSession()
+
+ val groupDividerViewHolder =
+ mMediaOutputAdapter.onCreateViewHolder(
+ LinearLayout(mContext),
+ MediaItemType.TYPE_GROUP_DIVIDER,
+ ) as MediaGroupDividerViewHolder
+ mMediaOutputAdapter.onBindViewHolder(groupDividerViewHolder, 0)
+
+ mMediaSwitchingController.stub { on { isGroupListCollapsed } doReturn false }
+ groupDividerViewHolder.mExpandButton.performClick()
+
+ createAndBindDeviceViewHolder(position = 1).apply {
+ assertThat(mTitleText.text.toString()).isEqualTo(TEST_DEVICE_NAME_1)
+ assertThat(mSlider.visibility).isEqualTo(VISIBLE)
+ assertThat(mTitleText.visibility).isEqualTo(VISIBLE)
+ assertThat(mGroupButton.visibility).isEqualTo(VISIBLE)
+ }
+
+ createAndBindDeviceViewHolder(position = 2).apply {
+ assertThat(mTitleText.text.toString()).isEqualTo(TEST_DEVICE_NAME_2)
+ assertThat(mSlider.visibility).isEqualTo(VISIBLE)
+ assertThat(mTitleText.visibility).isEqualTo(VISIBLE)
+ assertThat(mGroupButton.visibility).isEqualTo(VISIBLE)
+ }
+ }
+
+ private fun contextWithTheme(context: Context) =
+ ContextThemeWrapper(
+ context,
+ com.google.android.material.R.style.Theme_Material3_DynamicColors_DayNight,
+ )
+
+ private fun updateAdapterWithDevices(deviceList: List<MediaDevice>) {
+ for (device in deviceList) {
+ mMediaItems.add(createDeviceMediaItem(device))
+ }
+ mMediaOutputAdapter.updateItems()
+ }
+
+ private fun createAndBindDeviceViewHolder(position: Int): MediaDeviceViewHolder {
+ val viewHolder =
+ mMediaOutputAdapter.onCreateViewHolder(
+ LinearLayout(mContext),
+ mMediaOutputAdapter.getItemViewType(position),
+ )
+ if (viewHolder is MediaDeviceViewHolder) {
+ mMediaOutputAdapter.onBindViewHolder(viewHolder, position)
+ return viewHolder
+ } else {
+ throw RuntimeException("ViewHolder for position $position is not MediaDeviceViewHolder")
+ }
+ }
+
+ private fun initializeSession() {
+ val selectedDevices = listOf(mMediaDevice1, mMediaDevice2)
+ mMediaSwitchingController.stub {
+ on { selectableMediaDevice } doReturn selectedDevices
+ on { selectedMediaDevice } doReturn selectedDevices
+ on { deselectableMediaDevice } doReturn selectedDevices
+ }
+ mMediaOutputAdapter = MediaOutputAdapter(mMediaSwitchingController)
+ updateAdapterWithDevices(listOf(mMediaDevice1, mMediaDevice2))
+ }
+
+ companion object {
+ private const val TEST_DEVICE_NAME_1 = "test_device_name_1"
+ private const val TEST_DEVICE_NAME_2 = "test_device_name_2"
+ private const val TEST_DEVICE_ID_1 = "test_device_id_1"
+ private const val TEST_DEVICE_ID_2 = "test_device_id_2"
+ private const val TEST_SESSION_NAME = "test_session_name"
+ private const val TEST_CUSTOM_SUBTEXT = "custom subtext"
+
+ private const val TEST_MAX_VOLUME = 20
+ private const val TEST_CURRENT_VOLUME = 10
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/mediaprojection/permission/ShareToAppPermissionDialogDelegateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/mediaprojection/permission/ShareToAppPermissionDialogDelegateTest.kt
index 04ef1be9c057..ab605c0ea14e 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/mediaprojection/permission/ShareToAppPermissionDialogDelegateTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/mediaprojection/permission/ShareToAppPermissionDialogDelegateTest.kt
@@ -18,12 +18,17 @@ package com.android.systemui.mediaprojection.permission
import android.app.AlertDialog
import android.media.projection.MediaProjectionConfig
+import android.platform.test.annotations.RequiresFlagsDisabled
+import android.platform.test.annotations.RequiresFlagsEnabled
+import android.platform.test.flag.junit.CheckFlagsRule
+import android.platform.test.flag.junit.DeviceFlagsValueProvider
import android.testing.TestableLooper
import android.view.WindowManager
import android.widget.Spinner
import android.widget.TextView
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
+import com.android.systemui.Flags
import com.android.systemui.SysuiTestCase
import com.android.systemui.mediaprojection.MediaProjectionMetricsLogger
import com.android.systemui.res.R
@@ -32,6 +37,7 @@ import com.android.systemui.statusbar.phone.SystemUIDialog
import com.google.common.truth.Truth.assertThat
import kotlin.test.assertEquals
import org.junit.After
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.mock
@@ -41,6 +47,8 @@ import org.mockito.kotlin.mock
@TestableLooper.RunWithLooper(setAsMainLooper = true)
class ShareToAppPermissionDialogDelegateTest : SysuiTestCase() {
+ @get:Rule val checkFlagRule: CheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule()
+
private lateinit var dialog: AlertDialog
private val appName = "Test App"
@@ -51,6 +59,8 @@ class ShareToAppPermissionDialogDelegateTest : SysuiTestCase() {
R.string.media_projection_entry_app_permission_dialog_option_text_entire_screen
private val resIdSingleAppDisabled =
R.string.media_projection_entry_app_permission_dialog_single_app_disabled
+ private val resIdSingleAppNotSupported =
+ R.string.media_projection_entry_app_permission_dialog_single_app_not_supported
@After
fun teardown() {
@@ -78,6 +88,7 @@ class ShareToAppPermissionDialogDelegateTest : SysuiTestCase() {
}
@Test
+ @RequiresFlagsDisabled(Flags.FLAG_MEDIA_PROJECTION_GREY_ERROR_TEXT)
fun showDialog_disableSingleApp() {
setUpAndShowDialog(
mediaProjectionConfig = MediaProjectionConfig.createConfigForDefaultDisplay()
@@ -98,10 +109,34 @@ class ShareToAppPermissionDialogDelegateTest : SysuiTestCase() {
}
@Test
+ @RequiresFlagsEnabled(Flags.FLAG_MEDIA_PROJECTION_GREY_ERROR_TEXT)
+ fun showDialog_disableSingleApp_appNotSupported() {
+ setUpAndShowDialog(
+ mediaProjectionConfig = MediaProjectionConfig.createConfigForDefaultDisplay()
+ )
+
+ val spinner = dialog.requireViewById<Spinner>(R.id.screen_share_mode_options)
+ val secondOptionWarningText =
+ spinner.adapter
+ .getDropDownView(1, null, spinner)
+ .findViewById<TextView>(android.R.id.text2)
+ ?.text
+
+ // check that the first option is full screen and enabled
+ assertEquals(context.getString(resIdFullScreen), spinner.selectedItem)
+
+ // check that the second option is single app and disabled
+ assertEquals(
+ context.getString(resIdSingleAppNotSupported, appName),
+ secondOptionWarningText,
+ )
+ }
+
+ @Test
fun showDialog_disableSingleApp_forceShowPartialScreenShareTrue() {
setUpAndShowDialog(
mediaProjectionConfig = MediaProjectionConfig.createConfigForDefaultDisplay(),
- overrideDisableSingleAppOption = true
+ overrideDisableSingleAppOption = true,
)
val spinner = dialog.requireViewById<Spinner>(R.id.screen_share_mode_options)
@@ -161,7 +196,7 @@ class ShareToAppPermissionDialogDelegateTest : SysuiTestCase() {
appName,
overrideDisableSingleAppOption,
hostUid = 12345,
- mediaProjectionMetricsLogger = mock<MediaProjectionMetricsLogger>()
+ mediaProjectionMetricsLogger = mock<MediaProjectionMetricsLogger>(),
)
dialog = AlertDialogWithDelegate(context, R.style.Theme_SystemUI_Dialog, delegate)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/mediaprojection/permission/SystemCastPermissionDialogDelegateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/mediaprojection/permission/SystemCastPermissionDialogDelegateTest.kt
index 6495b66cc148..17cdb8dd592d 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/mediaprojection/permission/SystemCastPermissionDialogDelegateTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/mediaprojection/permission/SystemCastPermissionDialogDelegateTest.kt
@@ -18,12 +18,17 @@ package com.android.systemui.mediaprojection.permission
import android.app.AlertDialog
import android.media.projection.MediaProjectionConfig
+import android.platform.test.annotations.RequiresFlagsDisabled
+import android.platform.test.annotations.RequiresFlagsEnabled
+import android.platform.test.flag.junit.CheckFlagsRule
+import android.platform.test.flag.junit.DeviceFlagsValueProvider
import android.testing.TestableLooper
import android.view.WindowManager
import android.widget.Spinner
import android.widget.TextView
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
+import com.android.systemui.Flags
import com.android.systemui.SysuiTestCase
import com.android.systemui.mediaprojection.MediaProjectionMetricsLogger
import com.android.systemui.res.R
@@ -32,6 +37,7 @@ import com.android.systemui.statusbar.phone.SystemUIDialog
import com.google.common.truth.Truth.assertThat
import kotlin.test.assertEquals
import org.junit.After
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.mock
@@ -41,6 +47,8 @@ import org.mockito.kotlin.mock
@TestableLooper.RunWithLooper(setAsMainLooper = true)
class SystemCastPermissionDialogDelegateTest : SysuiTestCase() {
+ @get:Rule val checkFlagRule: CheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule()
+
private lateinit var dialog: AlertDialog
private val appName = "Test App"
@@ -51,6 +59,8 @@ class SystemCastPermissionDialogDelegateTest : SysuiTestCase() {
R.string.media_projection_entry_cast_permission_dialog_option_text_entire_screen
private val resIdSingleAppDisabled =
R.string.media_projection_entry_app_permission_dialog_single_app_disabled
+ private val resIdSingleAppNotSupported =
+ R.string.media_projection_entry_app_permission_dialog_single_app_not_supported
@After
fun teardown() {
@@ -78,6 +88,7 @@ class SystemCastPermissionDialogDelegateTest : SysuiTestCase() {
}
@Test
+ @RequiresFlagsDisabled(Flags.FLAG_MEDIA_PROJECTION_GREY_ERROR_TEXT)
fun showDialog_disableSingleApp() {
setUpAndShowDialog(
mediaProjectionConfig = MediaProjectionConfig.createConfigForDefaultDisplay()
@@ -98,6 +109,30 @@ class SystemCastPermissionDialogDelegateTest : SysuiTestCase() {
}
@Test
+ @RequiresFlagsEnabled(Flags.FLAG_MEDIA_PROJECTION_GREY_ERROR_TEXT)
+ fun showDialog_disableSingleApp_appNotSupported() {
+ setUpAndShowDialog(
+ mediaProjectionConfig = MediaProjectionConfig.createConfigForDefaultDisplay()
+ )
+
+ val spinner = dialog.requireViewById<Spinner>(R.id.screen_share_mode_options)
+ val secondOptionWarningText =
+ spinner.adapter
+ .getDropDownView(1, null, spinner)
+ .findViewById<TextView>(android.R.id.text2)
+ ?.text
+
+ // check that the first option is full screen and enabled
+ assertEquals(context.getString(resIdFullScreen), spinner.selectedItem)
+
+ // check that the second option is single app and disabled
+ assertEquals(
+ context.getString(resIdSingleAppNotSupported, appName),
+ secondOptionWarningText,
+ )
+ }
+
+ @Test
fun showDialog_disableSingleApp_forceShowPartialScreenShareTrue() {
setUpAndShowDialog(
mediaProjectionConfig = MediaProjectionConfig.createConfigForDefaultDisplay(),
@@ -169,7 +204,7 @@ class SystemCastPermissionDialogDelegateTest : SysuiTestCase() {
SystemUIDialog.setDialogSize(dialog)
dialog.window?.addSystemFlags(
- WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS,
+ WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS
)
delegate.onCreate(dialog, savedInstanceState = null)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModelTest.kt
index ffcd95bc7a4a..cd7b658518b6 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModelTest.kt
@@ -38,13 +38,10 @@ import com.android.systemui.scene.domain.interactor.sceneInteractor
import com.android.systemui.scene.domain.startable.sceneContainerStartable
import com.android.systemui.scene.shared.model.Overlays
import com.android.systemui.scene.shared.model.Scenes
-import com.android.systemui.shade.data.repository.shadeRepository
import com.android.systemui.shade.domain.interactor.enableDualShade
import com.android.systemui.shade.domain.interactor.shadeInteractor
import com.android.systemui.shade.ui.viewmodel.notificationsShadeOverlayContentViewModel
import com.android.systemui.statusbar.disableflags.data.repository.fakeDisableFlagsRepository
-import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository
-import com.android.systemui.statusbar.notification.data.repository.setActiveNotifs
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -116,38 +113,6 @@ class NotificationsShadeOverlayContentViewModelTest : SysuiTestCase() {
}
@Test
- fun showClock_showsOnNarrowScreen() =
- testScope.runTest {
- kosmos.shadeRepository.setShadeLayoutWide(false)
-
- // Shown when notifications are present.
- kosmos.activeNotificationListRepository.setActiveNotifs(1)
- runCurrent()
- assertThat(underTest.showClock).isTrue()
-
- // Hidden when notifications are not present.
- kosmos.activeNotificationListRepository.setActiveNotifs(0)
- runCurrent()
- assertThat(underTest.showClock).isFalse()
- }
-
- @Test
- fun showClock_hidesOnWideScreen() =
- testScope.runTest {
- kosmos.shadeRepository.setShadeLayoutWide(true)
-
- // Hidden when notifications are present.
- kosmos.activeNotificationListRepository.setActiveNotifs(1)
- runCurrent()
- assertThat(underTest.showClock).isFalse()
-
- // Hidden when notifications are not present.
- kosmos.activeNotificationListRepository.setActiveNotifs(0)
- runCurrent()
- assertThat(underTest.showClock).isFalse()
- }
-
- @Test
fun showMedia_activeMedia_true() =
testScope.runTest {
kosmos.mediaFilterRepository.addSelectedUserMediaEntry(MediaData(active = true))
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/screenshot/scroll/ScrollCaptureControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/screenshot/scroll/ScrollCaptureControllerTest.java
index a831e6344a66..fd796a56652b 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/screenshot/scroll/ScrollCaptureControllerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/screenshot/scroll/ScrollCaptureControllerTest.java
@@ -204,6 +204,21 @@ public class ScrollCaptureControllerTest extends SysuiTestCase {
assertEquals("bottom", 200, screenshot.getBottom());
}
+ @Test
+ public void testCancellation() {
+ ScrollCaptureController controller = new TestScenario()
+ .withPageHeight(100)
+ .withMaxPages(2.5f)
+ .withTileHeight(10)
+ .withAvailableRange(-10, Integer.MAX_VALUE)
+ .createController(mContext);
+
+ ScrollCaptureController.LongScreenshot screenshot =
+ getUnchecked(controller.run(EMPTY_RESPONSE));
+
+ assertEquals("top", -10, screenshot.getTop());
+ assertEquals("bottom", 240, screenshot.getBottom());
+ }
/**
* Build and configure a stubbed controller for each test case.
*/
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 b376558371f3..0289c58f6e93 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ShadeControllerSceneImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ShadeControllerSceneImplTest.kt
@@ -34,6 +34,7 @@ import com.android.systemui.scene.domain.interactor.sceneInteractor
import com.android.systemui.scene.shared.model.Overlays
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.shade.domain.interactor.ShadeInteractor
+import com.android.systemui.shade.domain.interactor.disableDualShade
import com.android.systemui.shade.domain.interactor.enableDualShade
import com.android.systemui.shade.domain.interactor.shadeInteractor
import com.android.systemui.statusbar.CommandQueue
@@ -214,6 +215,21 @@ class ShadeControllerSceneImplTest : SysuiTestCase() {
assertThat(currentOverlays).isEmpty()
}
+ @Test
+ fun instantCollapseShade_singleShade_doesntSwitchToShadeScene() =
+ testScope.runTest {
+ kosmos.disableDualShade()
+ runCurrent()
+ val currentScene by collectLastValue(sceneInteractor.currentScene)
+ val homeScene = currentScene
+ sceneInteractor.changeScene(Scenes.QuickSettings, "")
+ assertThat(currentScene).isEqualTo(Scenes.QuickSettings)
+
+ underTest.instantCollapseShade()
+
+ assertThat(currentScene).isEqualTo(homeScene)
+ }
+
private fun setScene(key: SceneKey) {
sceneInteractor.changeScene(key, "test")
sceneInteractor.setTransitionState(
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/BlurUtilsTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/BlurUtilsTest.kt
index 402b53c12bda..7664e2ad3072 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/BlurUtilsTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/BlurUtilsTest.kt
@@ -19,22 +19,23 @@ package com.android.systemui.statusbar
import android.content.res.Resources
import android.view.CrossWindowBlurListeners
import android.view.SurfaceControl
+import android.view.SyncRtSurfaceTransactionApplier
import android.view.ViewRootImpl
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.dump.DumpManager
import com.android.systemui.keyguard.ui.transitions.BlurConfig
+import com.google.common.truth.Truth.assertThat
+import junit.framework.TestCase.assertEquals
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.Captor
import org.mockito.Mock
-import org.mockito.Mockito.any
-import org.mockito.Mockito.anyInt
import org.mockito.Mockito.clearInvocations
-import org.mockito.Mockito.eq
import org.mockito.Mockito.mock
-import org.mockito.Mockito.never
import org.mockito.Mockito.verify
import org.mockito.Mockito.`when`
import org.mockito.MockitoAnnotations
@@ -45,10 +46,13 @@ class BlurUtilsTest : SysuiTestCase() {
val blurConfig: BlurConfig = BlurConfig(minBlurRadiusPx = 1.0f, maxBlurRadiusPx = 100.0f)
@Mock lateinit var dumpManager: DumpManager
- @Mock lateinit var transaction: SurfaceControl.Transaction
@Mock lateinit var crossWindowBlurListeners: CrossWindowBlurListeners
@Mock lateinit var resources: Resources
- lateinit var blurUtils: TestableBlurUtils
+ @Mock lateinit var syncRTTransactionApplier: SyncRtSurfaceTransactionApplier
+ @Mock lateinit var transaction: SurfaceControl.Transaction
+ @Captor
+ private lateinit var captor: ArgumentCaptor<SyncRtSurfaceTransactionApplier.SurfaceParams>
+ private lateinit var blurUtils: TestableBlurUtils
@Before
fun setup() {
@@ -77,9 +81,10 @@ class BlurUtilsTest : SysuiTestCase() {
`when`(viewRootImpl.surfaceControl).thenReturn(surfaceControl)
`when`(surfaceControl.isValid).thenReturn(true)
blurUtils.applyBlur(viewRootImpl, radius, true /* opaque */)
- verify(transaction).setBackgroundBlurRadius(eq(surfaceControl), eq(radius))
- verify(transaction).setOpaque(eq(surfaceControl), eq(true))
- verify(transaction).apply()
+
+ verify(syncRTTransactionApplier).scheduleApply(captor.capture())
+ assertThat(captor.value.opaque).isTrue()
+ assertEquals(radius, captor.value.backgroundBlurRadius)
}
@Test
@@ -92,9 +97,10 @@ class BlurUtilsTest : SysuiTestCase() {
blurUtils.blursEnabled = false
blurUtils.applyBlur(viewRootImpl, radius, true /* opaque */)
- verify(transaction).setOpaque(eq(surfaceControl), eq(true))
- verify(transaction, never()).setBackgroundBlurRadius(any(), anyInt())
- verify(transaction).apply()
+
+ verify(syncRTTransactionApplier).scheduleApply(captor.capture())
+ assertThat(captor.value.opaque).isTrue()
+ assertEquals(0 /* unset value */, captor.value.backgroundBlurRadius)
}
@Test
@@ -102,24 +108,32 @@ class BlurUtilsTest : SysuiTestCase() {
val radius = 10
val surfaceControl = mock(SurfaceControl::class.java)
val viewRootImpl = mock(ViewRootImpl::class.java)
+ val tmpFloatArray = FloatArray(0)
`when`(viewRootImpl.surfaceControl).thenReturn(surfaceControl)
`when`(surfaceControl.isValid).thenReturn(true)
blurUtils.applyBlur(viewRootImpl, radius, true /* opaque */)
+
+ verify(syncRTTransactionApplier).scheduleApply(captor.capture())
+ assertThat(captor.value.opaque).isTrue()
+ SyncRtSurfaceTransactionApplier.applyParams(transaction, captor.value, tmpFloatArray)
verify(transaction).setEarlyWakeupStart()
+
+ clearInvocations(syncRTTransactionApplier)
clearInvocations(transaction)
blurUtils.applyBlur(viewRootImpl, 0, true /* opaque */)
+ verify(syncRTTransactionApplier).scheduleApply(captor.capture())
+ SyncRtSurfaceTransactionApplier.applyParams(transaction, captor.value, tmpFloatArray)
verify(transaction).setEarlyWakeupEnd()
}
- inner class TestableBlurUtils : BlurUtils(resources, blurConfig, crossWindowBlurListeners, dumpManager) {
+ inner class TestableBlurUtils :
+ BlurUtils(resources, blurConfig, crossWindowBlurListeners, dumpManager) {
var blursEnabled = true
+ override val transactionApplier: SyncRtSurfaceTransactionApplier
+ get() = syncRTTransactionApplier
override fun supportsBlursOnWindows(): Boolean {
return blursEnabled
}
-
- override fun createTransaction(): SurfaceControl.Transaction {
- return transaction
- }
}
}
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 039a32ba9127..b4c6b33463b0 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationShadeDepthControllerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationShadeDepthControllerTest.kt
@@ -48,6 +48,7 @@ import com.android.wm.shell.appzoomout.AppZoomOut
import com.google.common.truth.Truth.assertThat
import java.util.Optional
import java.util.function.Consumer
+import kotlinx.coroutines.flow.MutableStateFlow
import org.junit.Before
import org.junit.Rule
import org.junit.Test
@@ -120,6 +121,8 @@ class NotificationShadeDepthControllerTest : SysuiTestCase() {
`when`(blurUtils.supportsBlursOnWindows()).thenReturn(true)
`when`(blurUtils.maxBlurRadius).thenReturn(maxBlur.toFloat())
`when`(blurUtils.maxBlurRadius).thenReturn(maxBlur.toFloat())
+ `when`(windowRootViewBlurInteractor.isBlurCurrentlySupported)
+ .thenReturn(MutableStateFlow(true))
notificationShadeDepthController =
NotificationShadeDepthController(
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/OperatorNameViewControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/OperatorNameViewControllerTest.kt
index 72d21f1064af..81f2bc94a307 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/OperatorNameViewControllerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/OperatorNameViewControllerTest.kt
@@ -16,10 +16,12 @@
package com.android.systemui.statusbar
+import android.content.res.Resources
import android.telephony.ServiceState
import android.telephony.SubscriptionInfo
import android.telephony.TelephonyManager
import android.telephony.telephonyManager
+import android.testing.TestableResources
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.keyguard.keyguardUpdateMonitor
@@ -70,10 +72,18 @@ class OperatorNameViewControllerTest : SysuiTestCase() {
private val airplaneModeRepository = FakeAirplaneModeRepository()
private val connectivityRepository = FakeConnectivityRepository()
+ @Mock private lateinit var resources: Resources
+ private lateinit var testableResources: TestableResources
+
@Before
fun setup() {
MockitoAnnotations.initMocks(this)
+ testableResources = mContext.getOrCreateTestableResources()
+ testableResources.addOverride(
+ com.android.internal.R.integer.config_showOperatorNameDefault,
+ 1)
+
airplaneModeInteractor =
AirplaneModeInteractor(
airplaneModeRepository,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryTest.java
index bfd700dcc302..52996ee1e369 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryTest.java
@@ -309,24 +309,6 @@ public class NotificationEntryTest extends SysuiTestCase {
}
@Test
- @EnableFlags(PromotedNotificationUi.FLAG_NAME)
- @DisableFlags(StatusBarNotifChips.FLAG_NAME)
- public void isPromotedOngoing_uiFlagOnAndNotifHasFlag_true() {
- mEntry.getSbn().getNotification().flags |= FLAG_PROMOTED_ONGOING;
-
- assertTrue(mEntry.isPromotedOngoing());
- }
-
- @Test
- @EnableFlags(StatusBarNotifChips.FLAG_NAME)
- @DisableFlags(PromotedNotificationUi.FLAG_NAME)
- public void isPromotedOngoing_statusBarNotifChipsFlagOnAndNotifHasFlag_true() {
- mEntry.getSbn().getNotification().flags |= FLAG_PROMOTED_ONGOING;
-
- assertTrue(mEntry.isPromotedOngoing());
- }
-
- @Test
public void testIsNotificationVisibilityPrivate_true() {
assertTrue(mEntry.isNotificationVisibilityPrivate());
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImplTest.kt
index 8560b66d961f..5b0e4e139d4e 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImplTest.kt
@@ -749,20 +749,6 @@ class HeadsUpManagerImplTest(flags: FlagsParameterization) : SysuiTestCase() {
assertThat(getIsSticky_promotedAndExpanded()).isFalse()
}
- @Test
- @EnableFlags(PromotedNotificationUi.FLAG_NAME)
- @DisableFlags(StatusBarNotifChips.FLAG_NAME)
- fun testIsSticky_promotedAndExpanded_promotedUiFlagOn_false() {
- assertThat(getIsSticky_promotedAndExpanded()).isFalse()
- }
-
- @Test
- @EnableFlags(StatusBarNotifChips.FLAG_NAME)
- @DisableFlags(PromotedNotificationUi.FLAG_NAME)
- fun testIsSticky_promotedAndExpanded_notifChipsFlagOn_false() {
- assertThat(getIsSticky_promotedAndExpanded()).isFalse()
- }
-
private fun getIsSticky_promotedAndExpanded(): Boolean {
val notif = Notification.Builder(mContext, "").setSmallIcon(R.drawable.ic_person).build()
notif.flags = FLAG_PROMOTED_ONGOING
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 cc016b9768b7..df77b5ad46e8 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,6 +33,8 @@ import android.platform.test.annotations.EnableFlags
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.runTest
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
@@ -58,132 +60,122 @@ import org.junit.runner.RunWith
class PromotedNotificationContentExtractorImplTest : SysuiTestCase() {
private val kosmos = testKosmos().apply { systemClock = fakeSystemClock }
- private val underTest = kosmos.promotedNotificationContentExtractor
- private val systemClock = kosmos.fakeSystemClock
- private val rowImageInflater =
- RowImageInflater.newInstance(previousIndex = null, reinflating = false)
- private val imageModelProvider by lazy { rowImageInflater.useForContentModel() }
+ private val Kosmos.underTest by Kosmos.Fixture { promotedNotificationContentExtractor }
+ private val Kosmos.rowImageInflater by
+ Kosmos.Fixture { RowImageInflater.newInstance(previousIndex = null, reinflating = false) }
+ private val Kosmos.imageModelProvider by
+ Kosmos.Fixture { rowImageInflater.useForContentModel() }
@Test
@DisableFlags(PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME)
- fun shouldNotExtract_bothFlagsDisabled() {
- val notif = createEntry()
- val content = extractContent(notif)
- assertThat(content).isNull()
- }
-
- @Test
- @EnableFlags(PromotedNotificationUi.FLAG_NAME)
- @DisableFlags(StatusBarNotifChips.FLAG_NAME)
- fun shouldExtract_promotedNotificationUiFlagEnabled() {
- val entry = createEntry()
- val content = extractContent(entry)
- assertThat(content).isNotNull()
- }
-
- @Test
- @EnableFlags(StatusBarNotifChips.FLAG_NAME)
- @DisableFlags(PromotedNotificationUi.FLAG_NAME)
- fun shouldExtract_statusBarNotifChipsFlagEnabled() {
- val entry = createEntry()
- val content = extractContent(entry)
- assertThat(content).isNotNull()
- }
+ fun shouldNotExtract_bothFlagsDisabled() =
+ kosmos.runTest {
+ val notif = createEntry()
+ val content = extractContent(notif)
+ assertThat(content).isNull()
+ }
@Test
@EnableFlags(PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME)
- fun shouldExtract_bothFlagsEnabled() {
- val entry = createEntry()
- val content = extractContent(entry)
- assertThat(content).isNotNull()
- }
+ fun shouldExtract_bothFlagsEnabled() =
+ kosmos.runTest {
+ val entry = createEntry()
+ val content = extractContent(entry)
+ assertThat(content).isNotNull()
+ }
@Test
@EnableFlags(PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME)
- fun shouldNotExtract_becauseNotPromoted() {
- val entry = createEntry(promoted = false)
- val content = extractContent(entry)
- assertThat(content).isNull()
- }
+ fun shouldNotExtract_becauseNotPromoted() =
+ kosmos.runTest {
+ val entry = createEntry(promoted = false)
+ val content = extractContent(entry)
+ assertThat(content).isNull()
+ }
@Test
@EnableFlags(PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME)
- fun extractsContent_commonFields() {
- val entry = createEntry {
- setSubText(TEST_SUB_TEXT)
- setContentTitle(TEST_CONTENT_TITLE)
- setContentText(TEST_CONTENT_TEXT)
- }
+ fun extractsContent_commonFields() =
+ kosmos.runTest {
+ val entry = createEntry {
+ setSubText(TEST_SUB_TEXT)
+ setContentTitle(TEST_CONTENT_TITLE)
+ setContentText(TEST_CONTENT_TEXT)
+ }
- val content = requireContent(entry)
+ val content = requireContent(entry)
- content.privateVersion.apply {
- assertThat(subText).isEqualTo(TEST_SUB_TEXT)
- assertThat(title).isEqualTo(TEST_CONTENT_TITLE)
- assertThat(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()
+ 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)
- }
+ fun extractsContent_commonFields_noRedaction() =
+ kosmos.runTest {
+ val entry = createEntry {
+ setSubText(TEST_SUB_TEXT)
+ setContentTitle(TEST_CONTENT_TITLE)
+ setContentText(TEST_CONTENT_TEXT)
+ }
- val content = requireContent(entry, redactionType = REDACTION_TYPE_NONE)
+ 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.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)
+ content.publicVersion.apply {
+ assertThat(subText).isEqualTo(TEST_SUB_TEXT)
+ assertThat(title).isEqualTo(TEST_CONTENT_TITLE)
+ assertThat(text).isEqualTo(TEST_CONTENT_TEXT)
+ }
}
- }
@Test
@EnableFlags(PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME)
- fun extractContent_wasPromotedAutomatically_false() {
- val entry = createEntry { extras.putBoolean(EXTRA_WAS_AUTOMATICALLY_PROMOTED, false) }
+ fun extractContent_wasPromotedAutomatically_false() =
+ kosmos.runTest {
+ val entry = createEntry { extras.putBoolean(EXTRA_WAS_AUTOMATICALLY_PROMOTED, false) }
- val content = requireContent(entry).privateVersion
+ val content = requireContent(entry).privateVersion
- assertThat(content.wasPromotedAutomatically).isFalse()
- }
+ assertThat(content.wasPromotedAutomatically).isFalse()
+ }
@Test
@EnableFlags(PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME)
- fun extractContent_wasPromotedAutomatically_true() {
- val entry = createEntry { extras.putBoolean(EXTRA_WAS_AUTOMATICALLY_PROMOTED, true) }
+ fun extractContent_wasPromotedAutomatically_true() =
+ kosmos.runTest {
+ val entry = createEntry { extras.putBoolean(EXTRA_WAS_AUTOMATICALLY_PROMOTED, true) }
- val content = requireContent(entry).privateVersion
+ val content = requireContent(entry).privateVersion
- assertThat(content.wasPromotedAutomatically).isTrue()
- }
+ assertThat(content.wasPromotedAutomatically).isTrue()
+ }
@Test
@EnableFlags(PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME)
@DisableFlags(android.app.Flags.FLAG_API_RICH_ONGOING)
- fun extractContent_apiFlagOff_shortCriticalTextNotExtracted() {
- val entry = createEntry { setShortCriticalText(TEST_SHORT_CRITICAL_TEXT) }
+ fun extractContent_apiFlagOff_shortCriticalTextNotExtracted() =
+ kosmos.runTest {
+ val entry = createEntry { setShortCriticalText(TEST_SHORT_CRITICAL_TEXT) }
- val content = requireContent(entry).privateVersion
+ val content = requireContent(entry).privateVersion
- assertThat(content.text).isNull()
- }
+ assertThat(content.text).isNull()
+ }
@Test
@EnableFlags(
@@ -191,13 +183,14 @@ class PromotedNotificationContentExtractorImplTest : SysuiTestCase() {
StatusBarNotifChips.FLAG_NAME,
android.app.Flags.FLAG_API_RICH_ONGOING,
)
- fun extractContent_apiFlagOn_shortCriticalTextExtracted() {
- val entry = createEntry { setShortCriticalText(TEST_SHORT_CRITICAL_TEXT) }
+ fun extractContent_apiFlagOn_shortCriticalTextExtracted() =
+ kosmos.runTest {
+ val entry = createEntry { setShortCriticalText(TEST_SHORT_CRITICAL_TEXT) }
- val content = requireContent(entry).privateVersion
+ val content = requireContent(entry).privateVersion
- assertThat(content.shortCriticalText).isEqualTo(TEST_SHORT_CRITICAL_TEXT)
- }
+ assertThat(content.shortCriticalText).isEqualTo(TEST_SHORT_CRITICAL_TEXT)
+ }
@Test
@EnableFlags(
@@ -205,165 +198,188 @@ class PromotedNotificationContentExtractorImplTest : SysuiTestCase() {
StatusBarNotifChips.FLAG_NAME,
android.app.Flags.FLAG_API_RICH_ONGOING,
)
- fun extractContent_noShortCriticalTextSet_textIsNull() {
- val entry = createEntry { setShortCriticalText(null) }
+ fun extractContent_noShortCriticalTextSet_textIsNull() =
+ kosmos.runTest {
+ val entry = createEntry { setShortCriticalText(null) }
- val content = requireContent(entry).privateVersion
+ val content = requireContent(entry).privateVersion
- assertThat(content.shortCriticalText).isNull()
- }
+ assertThat(content.shortCriticalText).isNull()
+ }
@Test
@EnableFlags(PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME)
- fun extractTime_none() {
- assertExtractedTime(hasTime = false, hasChronometer = false, expected = ExpectedTime.Null)
- }
+ fun extractTime_none() =
+ kosmos.runTest {
+ assertExtractedTime(
+ hasTime = false,
+ hasChronometer = false,
+ expected = ExpectedTime.Null,
+ )
+ }
@Test
@EnableFlags(PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME)
- fun extractTime_basicTimeZero() {
- assertExtractedTime(
- hasTime = true,
- hasChronometer = false,
- provided = ProvidedTime.Value(0L),
- expected = ExpectedTime.Time,
- )
- }
+ fun extractTime_basicTimeZero() =
+ kosmos.runTest {
+ assertExtractedTime(
+ hasTime = true,
+ hasChronometer = false,
+ provided = ProvidedTime.Value(0L),
+ expected = ExpectedTime.Time,
+ )
+ }
@Test
@EnableFlags(PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME)
- fun extractTime_basicTimeNow() {
- assertExtractedTime(
- hasTime = true,
- hasChronometer = false,
- provided = ProvidedTime.Offset(Duration.ZERO),
- expected = ExpectedTime.Time,
- )
- }
+ fun extractTime_basicTimeNow() =
+ kosmos.runTest {
+ assertExtractedTime(
+ hasTime = true,
+ hasChronometer = false,
+ provided = ProvidedTime.Offset(Duration.ZERO),
+ expected = ExpectedTime.Time,
+ )
+ }
@Test
@EnableFlags(PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME)
- fun extractTime_basicTimePast() {
- assertExtractedTime(
- hasTime = true,
- hasChronometer = false,
- provided = ProvidedTime.Offset((-5).minutes),
- expected = ExpectedTime.Time,
- )
- }
+ fun extractTime_basicTimePast() =
+ kosmos.runTest {
+ assertExtractedTime(
+ hasTime = true,
+ hasChronometer = false,
+ provided = ProvidedTime.Offset((-5).minutes),
+ expected = ExpectedTime.Time,
+ )
+ }
@Test
@EnableFlags(PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME)
- fun extractTime_basicTimeFuture() {
- assertExtractedTime(
- hasTime = true,
- hasChronometer = false,
- provided = ProvidedTime.Offset(5.minutes),
- expected = ExpectedTime.Time,
- )
- }
+ fun extractTime_basicTimeFuture() =
+ kosmos.runTest {
+ assertExtractedTime(
+ hasTime = true,
+ hasChronometer = false,
+ provided = ProvidedTime.Offset(5.minutes),
+ expected = ExpectedTime.Time,
+ )
+ }
@Test
@EnableFlags(PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME)
- fun extractTime_countUpZero() {
- assertExtractedTime(
- hasTime = false,
- hasChronometer = true,
- isCountDown = false,
- provided = ProvidedTime.Value(0L),
- expected = ExpectedTime.CountUp,
- )
- }
+ fun extractTime_countUpZero() =
+ kosmos.runTest {
+ assertExtractedTime(
+ hasTime = false,
+ hasChronometer = true,
+ isCountDown = false,
+ provided = ProvidedTime.Value(0L),
+ expected = ExpectedTime.CountUp,
+ )
+ }
@Test
@EnableFlags(PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME)
- fun extractTime_countUpNow() {
- assertExtractedTime(
- hasTime = false,
- hasChronometer = true,
- isCountDown = false,
- provided = ProvidedTime.Offset(Duration.ZERO),
- expected = ExpectedTime.CountUp,
- )
- }
+ fun extractTime_countUpNow() =
+ kosmos.runTest {
+ assertExtractedTime(
+ hasTime = false,
+ hasChronometer = true,
+ isCountDown = false,
+ provided = ProvidedTime.Offset(Duration.ZERO),
+ expected = ExpectedTime.CountUp,
+ )
+ }
@Test
@EnableFlags(PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME)
- fun extractTime_countUpPast() {
- assertExtractedTime(
- hasTime = false,
- hasChronometer = true,
- isCountDown = false,
- provided = ProvidedTime.Offset((-5).minutes),
- expected = ExpectedTime.CountUp,
- )
- }
+ fun extractTime_countUpPast() =
+ kosmos.runTest {
+ assertExtractedTime(
+ hasTime = false,
+ hasChronometer = true,
+ isCountDown = false,
+ provided = ProvidedTime.Offset((-5).minutes),
+ expected = ExpectedTime.CountUp,
+ )
+ }
@Test
@EnableFlags(PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME)
- fun extractTime_countUpFuture() {
- assertExtractedTime(
- hasTime = false,
- hasChronometer = true,
- isCountDown = false,
- provided = ProvidedTime.Offset(5.minutes),
- expected = ExpectedTime.CountUp,
- )
- }
+ fun extractTime_countUpFuture() =
+ kosmos.runTest {
+ assertExtractedTime(
+ hasTime = false,
+ hasChronometer = true,
+ isCountDown = false,
+ provided = ProvidedTime.Offset(5.minutes),
+ expected = ExpectedTime.CountUp,
+ )
+ }
@Test
@EnableFlags(PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME)
- fun extractTime_countDownZero() {
- assertExtractedTime(
- hasTime = false,
- hasChronometer = true,
- isCountDown = true,
- provided = ProvidedTime.Value(0L),
- expected = ExpectedTime.CountDown,
- )
- }
+ fun extractTime_countDownZero() =
+ kosmos.runTest {
+ assertExtractedTime(
+ hasTime = false,
+ hasChronometer = true,
+ isCountDown = true,
+ provided = ProvidedTime.Value(0L),
+ expected = ExpectedTime.CountDown,
+ )
+ }
@Test
@EnableFlags(PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME)
- fun extractTime_countDownNow() {
- assertExtractedTime(
- hasTime = false,
- hasChronometer = true,
- isCountDown = true,
- provided = ProvidedTime.Offset(Duration.ZERO),
- expected = ExpectedTime.CountDown,
- )
- }
+ fun extractTime_countDownNow() =
+ kosmos.runTest {
+ assertExtractedTime(
+ hasTime = false,
+ hasChronometer = true,
+ isCountDown = true,
+ provided = ProvidedTime.Offset(Duration.ZERO),
+ expected = ExpectedTime.CountDown,
+ )
+ }
@Test
@EnableFlags(PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME)
- fun extractTime_countDownPast() {
- assertExtractedTime(
- hasTime = false,
- hasChronometer = true,
- isCountDown = true,
- provided = ProvidedTime.Offset((-5).minutes),
- expected = ExpectedTime.CountDown,
- )
- }
+ fun extractTime_countDownPast() =
+ kosmos.runTest {
+ assertExtractedTime(
+ hasTime = false,
+ hasChronometer = true,
+ isCountDown = true,
+ provided = ProvidedTime.Offset((-5).minutes),
+ expected = ExpectedTime.CountDown,
+ )
+ }
@Test
@EnableFlags(PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME)
- fun extractTime_countDownFuture() {
- assertExtractedTime(
- hasTime = false,
- hasChronometer = true,
- isCountDown = true,
- provided = ProvidedTime.Offset(5.minutes),
- expected = ExpectedTime.CountDown,
- )
- }
+ fun extractTime_countDownFuture() =
+ kosmos.runTest {
+ assertExtractedTime(
+ hasTime = false,
+ hasChronometer = true,
+ isCountDown = true,
+ provided = ProvidedTime.Offset(5.minutes),
+ expected = ExpectedTime.CountDown,
+ )
+ }
@Test
@EnableFlags(PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME)
- fun extractTime_prefersChronometerToWhen() {
- assertExtractedTime(hasTime = true, hasChronometer = true, expected = ExpectedTime.CountUp)
- }
+ fun extractTime_prefersChronometerToWhen() =
+ kosmos.runTest {
+ assertExtractedTime(
+ hasTime = true,
+ hasChronometer = true,
+ expected = ExpectedTime.CountUp,
+ )
+ }
private sealed class ProvidedTime {
data class Value(val value: Long) : ProvidedTime()
@@ -378,7 +394,7 @@ class PromotedNotificationContentExtractorImplTest : SysuiTestCase() {
CountDown,
}
- private fun assertExtractedTime(
+ private fun Kosmos.assertExtractedTime(
hasTime: Boolean = false,
hasChronometer: Boolean = false,
isCountDown: Boolean = false,
@@ -387,8 +403,8 @@ class PromotedNotificationContentExtractorImplTest : SysuiTestCase() {
) {
// Set the two timebases to different (arbitrary) numbers, so we can verify whether the
// extractor is doing the timebase adjustment correctly.
- systemClock.setCurrentTimeMillis(1_739_570_992_579L)
- systemClock.setElapsedRealtime(1_380_967_080L)
+ fakeSystemClock.setCurrentTimeMillis(1_739_570_992_579L)
+ fakeSystemClock.setElapsedRealtime(1_380_967_080L)
val providedCurrentTime =
when (provided) {
@@ -437,122 +453,130 @@ class PromotedNotificationContentExtractorImplTest : SysuiTestCase() {
@Test
@EnableFlags(PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME)
- fun extractContent_fromBaseStyle() {
- val entry = createEntry { setStyle(null) }
+ fun extractContent_fromBaseStyle() =
+ kosmos.runTest {
+ val entry = createEntry { setStyle(null) }
- val content = requireContent(entry)
+ val content = requireContent(entry)
- assertThat(content.privateVersion.style).isEqualTo(Style.Base)
- assertThat(content.publicVersion.style).isEqualTo(Style.Base)
- }
+ assertThat(content.privateVersion.style).isEqualTo(Style.Base)
+ assertThat(content.publicVersion.style).isEqualTo(Style.Base)
+ }
@Test
@EnableFlags(PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME)
- fun extractContent_fromBigPictureStyle() {
- val entry = createEntry { setStyle(BigPictureStyle()) }
+ fun extractContent_fromBigPictureStyle() =
+ kosmos.runTest {
+ val entry = createEntry { setStyle(BigPictureStyle()) }
- val content = requireContent(entry)
+ val content = requireContent(entry)
- assertThat(content.privateVersion.style).isEqualTo(Style.BigPicture)
- assertThat(content.publicVersion.style).isEqualTo(Style.Base)
- }
+ assertThat(content.privateVersion.style).isEqualTo(Style.BigPicture)
+ assertThat(content.publicVersion.style).isEqualTo(Style.Base)
+ }
@Test
@EnableFlags(PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME)
- fun extractContent_fromBigTextStyle() {
- val entry = createEntry {
- setContentTitle(TEST_CONTENT_TITLE)
- setContentText(TEST_CONTENT_TEXT)
- setStyle(
- BigTextStyle()
- .bigText(TEST_BIG_TEXT)
- .setBigContentTitle(TEST_BIG_CONTENT_TITLE)
- .setSummaryText(TEST_SUMMARY_TEXT)
- )
- }
+ fun extractContent_fromBigTextStyle() =
+ kosmos.runTest {
+ val entry = createEntry {
+ setContentTitle(TEST_CONTENT_TITLE)
+ setContentText(TEST_CONTENT_TEXT)
+ setStyle(
+ BigTextStyle()
+ .bigText(TEST_BIG_TEXT)
+ .setBigContentTitle(TEST_BIG_CONTENT_TITLE)
+ .setSummaryText(TEST_SUMMARY_TEXT)
+ )
+ }
- val content = requireContent(entry)
+ val content = requireContent(entry)
- 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.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()
- }
+ assertThat(content.publicVersion.style).isEqualTo(Style.Base)
+ assertThat(content.publicVersion.title).isNull()
+ assertThat(content.publicVersion.text).isNull()
+ }
@Test
@EnableFlags(PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME)
- fun extractContent_fromBigTextStyle_fallbackToContentTitle() {
- val entry = createEntry {
- setContentTitle(TEST_CONTENT_TITLE)
- setContentText(TEST_CONTENT_TEXT)
- setStyle(
- BigTextStyle()
- .bigText(TEST_BIG_TEXT)
- // bigContentTitle unset
- .setSummaryText(TEST_SUMMARY_TEXT)
- )
- }
+ fun extractContent_fromBigTextStyle_fallbackToContentTitle() =
+ kosmos.runTest {
+ val entry = createEntry {
+ setContentTitle(TEST_CONTENT_TITLE)
+ setContentText(TEST_CONTENT_TEXT)
+ setStyle(
+ BigTextStyle()
+ .bigText(TEST_BIG_TEXT)
+ // bigContentTitle unset
+ .setSummaryText(TEST_SUMMARY_TEXT)
+ )
+ }
- val content = requireContent(entry)
+ val content = requireContent(entry)
- 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.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()
- }
+ assertThat(content.publicVersion.style).isEqualTo(Style.Base)
+ assertThat(content.publicVersion.title).isNull()
+ assertThat(content.publicVersion.text).isNull()
+ }
@Test
@EnableFlags(PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME)
- fun extractContent_fromBigTextStyle_fallbackToContentText() {
- val entry = createEntry {
- setContentTitle(TEST_CONTENT_TITLE)
- setContentText(TEST_CONTENT_TEXT)
- setStyle(
- BigTextStyle()
- // bigText unset
- .setBigContentTitle(TEST_BIG_CONTENT_TITLE)
- .setSummaryText(TEST_SUMMARY_TEXT)
- )
- }
+ fun extractContent_fromBigTextStyle_fallbackToContentText() =
+ kosmos.runTest {
+ val entry = createEntry {
+ setContentTitle(TEST_CONTENT_TITLE)
+ setContentText(TEST_CONTENT_TEXT)
+ setStyle(
+ BigTextStyle()
+ // bigText unset
+ .setBigContentTitle(TEST_BIG_CONTENT_TITLE)
+ .setSummaryText(TEST_SUMMARY_TEXT)
+ )
+ }
- val content = requireContent(entry)
+ val content = requireContent(entry)
- 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.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()
- }
+ assertThat(content.publicVersion.style).isEqualTo(Style.Base)
+ assertThat(content.publicVersion.title).isNull()
+ assertThat(content.publicVersion.text).isNull()
+ }
@Test
@EnableFlags(PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME)
- fun extractContent_fromCallStyle() {
- val hangUpIntent =
- PendingIntent.getBroadcast(
- context,
- 0,
- Intent("hangup_action"),
- PendingIntent.FLAG_IMMUTABLE,
- )
- val entry = createEntry { setStyle(CallStyle.forOngoingCall(TEST_PERSON, hangUpIntent)) }
+ fun extractContent_fromCallStyle() =
+ kosmos.runTest {
+ val hangUpIntent =
+ PendingIntent.getBroadcast(
+ context,
+ 0,
+ Intent("hangup_action"),
+ PendingIntent.FLAG_IMMUTABLE,
+ )
+ val entry = createEntry {
+ setStyle(CallStyle.forOngoingCall(TEST_PERSON, hangUpIntent))
+ }
- val content = requireContent(entry)
+ val content = requireContent(entry)
- assertThat(content.privateVersion.style).isEqualTo(Style.Call)
- assertThat(content.privateVersion.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()
- }
+ assertThat(content.publicVersion.style).isEqualTo(Style.Base)
+ assertThat(content.publicVersion.title).isNull()
+ assertThat(content.publicVersion.text).isNull()
+ }
@Test
@EnableFlags(
@@ -560,75 +584,79 @@ class PromotedNotificationContentExtractorImplTest : SysuiTestCase() {
StatusBarNotifChips.FLAG_NAME,
android.app.Flags.FLAG_API_RICH_ONGOING,
)
- fun extractContent_fromProgressStyle() {
- val entry = createEntry {
- setStyle(ProgressStyle().addProgressSegment(Segment(100)).setProgress(75))
- }
+ fun extractContent_fromProgressStyle() =
+ kosmos.runTest {
+ val entry = createEntry {
+ setStyle(ProgressStyle().addProgressSegment(Segment(100)).setProgress(75))
+ }
- val content = requireContent(entry)
+ val content = requireContent(entry)
- 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.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()
- }
+ assertThat(content.publicVersion.style).isEqualTo(Style.Base)
+ assertThat(content.publicVersion.title).isNull()
+ assertThat(content.publicVersion.text).isNull()
+ assertThat(content.publicVersion.newProgress).isNull()
+ }
@Test
@EnableFlags(PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME)
- fun extractContent_fromIneligibleStyle() {
- val entry = createEntry {
- setStyle(MessagingStyle(TEST_PERSON).addMessage("message text", 0L, TEST_PERSON))
- }
+ fun extractContent_fromIneligibleStyle() =
+ kosmos.runTest {
+ val entry = createEntry {
+ setStyle(MessagingStyle(TEST_PERSON).addMessage("message text", 0L, TEST_PERSON))
+ }
- val content = requireContent(entry)
+ val content = requireContent(entry)
- assertThat(content.privateVersion.style).isEqualTo(Style.Ineligible)
+ assertThat(content.privateVersion.style).isEqualTo(Style.Ineligible)
- assertThat(content.publicVersion.style).isEqualTo(Style.Ineligible)
- }
+ assertThat(content.publicVersion.style).isEqualTo(Style.Ineligible)
+ }
@Test
@EnableFlags(PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME)
- fun extractContent_fromOldProgressDeterminate() {
- val entry = createEntry {
- setProgress(TEST_PROGRESS_MAX, TEST_PROGRESS, /* indeterminate= */ false)
- }
+ fun extractContent_fromOldProgressDeterminate() =
+ kosmos.runTest {
+ val entry = createEntry {
+ setProgress(TEST_PROGRESS_MAX, TEST_PROGRESS, /* indeterminate= */ false)
+ }
- val content = requireContent(entry)
+ val content = requireContent(entry)
- val oldProgress = assertNotNull(content.privateVersion.oldProgress)
+ val oldProgress = assertNotNull(content.privateVersion.oldProgress)
- assertThat(oldProgress.progress).isEqualTo(TEST_PROGRESS)
- assertThat(oldProgress.max).isEqualTo(TEST_PROGRESS_MAX)
- assertThat(oldProgress.isIndeterminate).isFalse()
- }
+ assertThat(oldProgress.progress).isEqualTo(TEST_PROGRESS)
+ assertThat(oldProgress.max).isEqualTo(TEST_PROGRESS_MAX)
+ assertThat(oldProgress.isIndeterminate).isFalse()
+ }
@Test
@EnableFlags(PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME)
- fun extractContent_fromOldProgressIndeterminate() {
- val entry = createEntry {
- setProgress(TEST_PROGRESS_MAX, TEST_PROGRESS, /* indeterminate= */ true)
- }
+ fun extractContent_fromOldProgressIndeterminate() =
+ kosmos.runTest {
+ val entry = createEntry {
+ setProgress(TEST_PROGRESS_MAX, TEST_PROGRESS, /* indeterminate= */ true)
+ }
- val content = requireContent(entry)
- val oldProgress = assertNotNull(content.privateVersion.oldProgress)
+ val content = requireContent(entry)
+ val oldProgress = assertNotNull(content.privateVersion.oldProgress)
- assertThat(oldProgress.progress).isEqualTo(TEST_PROGRESS)
- assertThat(oldProgress.max).isEqualTo(TEST_PROGRESS_MAX)
- assertThat(oldProgress.isIndeterminate).isTrue()
- }
+ assertThat(oldProgress.progress).isEqualTo(TEST_PROGRESS)
+ assertThat(oldProgress.max).isEqualTo(TEST_PROGRESS_MAX)
+ assertThat(oldProgress.isIndeterminate).isTrue()
+ }
- private fun requireContent(
+ private fun Kosmos.requireContent(
entry: NotificationEntry,
redactionType: Int = REDACTION_TYPE_PUBLIC,
): PromotedNotificationContentModels = assertNotNull(extractContent(entry, redactionType))
- private fun extractContent(
+ private fun Kosmos.extractContent(
entry: NotificationEntry,
redactionType: Int = REDACTION_TYPE_PUBLIC,
): PromotedNotificationContentModels? {
@@ -636,7 +664,7 @@ class PromotedNotificationContentExtractorImplTest : SysuiTestCase() {
return underTest.extractContent(entry, recoveredBuilder, redactionType, imageModelProvider)
}
- private fun createEntry(
+ private fun Kosmos.createEntry(
promoted: Boolean = true,
builderBlock: Notification.Builder.() -> Unit = {},
): NotificationEntry {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/AODPromotedNotificationsInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/AODPromotedNotificationsInteractorTest.kt
new file mode 100644
index 000000000000..915edc03952d
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/AODPromotedNotificationsInteractorTest.kt
@@ -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.systemui.statusbar.notification.promoted.domain.interactor
+
+import android.app.Notification
+import android.content.applicationContext
+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.dump.dumpManager
+import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
+import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.kosmos.collectLastValue
+import com.android.systemui.kosmos.runTest
+import com.android.systemui.kosmos.useUnconfinedTestDispatcher
+import com.android.systemui.statusbar.chips.notification.domain.interactor.statusBarNotificationChipsInteractor
+import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import com.android.systemui.statusbar.notification.collection.buildPromotedOngoingEntry
+import com.android.systemui.statusbar.notification.domain.interactor.renderNotificationListInteractor
+import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUi
+import com.android.systemui.statusbar.phone.ongoingcall.EnableChipsModernization
+import com.android.systemui.statusbar.policy.domain.interactor.sensitiveNotificationProtectionInteractor
+import com.android.systemui.statusbar.policy.mockSensitiveNotificationProtectionController
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.any
+import org.mockito.kotlin.whenever
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@EnableFlags(PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME)
+@EnableChipsModernization
+class AODPromotedNotificationsInteractorTest : SysuiTestCase() {
+ private val kosmos = testKosmos().useUnconfinedTestDispatcher()
+
+ private val Kosmos.underTest by Fixture {
+ AODPromotedNotificationInteractor(
+ promotedNotificationsInteractor = promotedNotificationsInteractor,
+ keyguardInteractor = keyguardInteractor,
+ sensitiveNotificationProtectionInteractor = sensitiveNotificationProtectionInteractor,
+ dumpManager = dumpManager,
+ )
+ }
+
+ @Before
+ fun setUp() {
+ kosmos.statusBarNotificationChipsInteractor.start()
+ }
+
+ private fun Kosmos.buildPublicPrivatePromotedOngoing(): NotificationEntry =
+ buildPromotedOngoingEntry {
+ modifyNotification(applicationContext)
+ .setContentTitle("SENSITIVE")
+ .setPublicVersion(
+ Notification.Builder(applicationContext, "channel")
+ .setContentTitle("REDACTED")
+ .build()
+ )
+ }
+
+ @Test
+ fun content_sensitive_unlocked() =
+ kosmos.runTest {
+ // GIVEN a promoted entry
+ val ronEntry = buildPublicPrivatePromotedOngoing()
+
+ setKeyguardLocked(false)
+ setScreenSharingProtectionActive(false)
+
+ renderNotificationListInteractor.setRenderedList(listOf(ronEntry))
+
+ // THEN aod content is sensitive
+ val content by collectLastValue(underTest.content)
+ assertThat(content?.title).isEqualTo("SENSITIVE")
+ }
+
+ @Test
+ fun content_sensitive_locked() =
+ kosmos.runTest {
+ // GIVEN a promoted entry
+ val ronEntry = buildPublicPrivatePromotedOngoing()
+
+ setKeyguardLocked(true)
+ setScreenSharingProtectionActive(false)
+
+ renderNotificationListInteractor.setRenderedList(listOf(ronEntry))
+
+ // THEN aod content is redacted
+ val content by collectLastValue(underTest.content)
+ assertThat(content).isNotNull()
+ assertThat(content!!.title).isEqualTo("REDACTED")
+ }
+
+ @Test
+ fun content_sensitive_unlocked_screensharing() =
+ kosmos.runTest {
+ // GIVEN a promoted entry
+ val ronEntry = buildPublicPrivatePromotedOngoing()
+
+ setKeyguardLocked(false)
+ setScreenSharingProtectionActive(true)
+
+ renderNotificationListInteractor.setRenderedList(listOf(ronEntry))
+
+ // THEN aod content is redacted
+ val content by collectLastValue(underTest.content)
+ assertThat(content).isNotNull()
+ assertThat(content!!.title).isEqualTo("REDACTED")
+ }
+
+ private fun Kosmos.setKeyguardLocked(locked: Boolean) {
+ fakeKeyguardRepository.setKeyguardDismissible(!locked)
+ }
+
+ private fun Kosmos.setScreenSharingProtectionActive(active: Boolean) {
+ whenever(mockSensitiveNotificationProtectionController.isSensitiveStateActive)
+ .thenReturn(active)
+ whenever(mockSensitiveNotificationProtectionController.shouldProtectNotification(any()))
+ .thenReturn(active)
+ }
+}
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 19b1046f1931..4aa21a68b2e0 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
@@ -399,35 +399,6 @@ public class NotificationContentInflaterTest extends SysuiTestCase {
}
@Test
- @EnableFlags(PromotedNotificationUi.FLAG_NAME)
- @DisableFlags(StatusBarNotifChips.FLAG_NAME)
- public void testExtractsPromotedContent_whePromotedNotificationUiFlagEnabled()
- throws Exception {
- 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().getPromotedNotificationContentModels());
- }
-
- @Test
- @EnableFlags(StatusBarNotifChips.FLAG_NAME)
- @DisableFlags(PromotedNotificationUi.FLAG_NAME)
- public void testExtractsPromotedContent_whenStatusBarNotifChipsFlagEnabled() throws Exception {
- 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().getPromotedNotificationContentModels());
- }
-
- @Test
@EnableFlags({PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME})
public void testExtractsPromotedContent_whenBothFlagsEnabled() throws Exception {
final PromotedNotificationContentModels content =
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 dcba3e447dda..21b0c9013b5f 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
@@ -465,32 +465,6 @@ class NotificationRowContentBinderImplTest : SysuiTestCase() {
}
@Test
- @EnableFlags(PromotedNotificationUi.FLAG_NAME)
- @DisableFlags(StatusBarNotifChips.FLAG_NAME)
- fun testExtractsPromotedContent_whenPromotedNotificationUiFlagEnabled() {
- val content = PromotedNotificationContentBuilder("key").build()
- promotedNotificationContentExtractor.resetForEntry(row.entry, content)
-
- inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_ALL, row)
-
- promotedNotificationContentExtractor.verifyOneExtractCall()
- Assert.assertEquals(content, row.entry.promotedNotificationContentModels)
- }
-
- @Test
- @EnableFlags(StatusBarNotifChips.FLAG_NAME)
- @DisableFlags(PromotedNotificationUi.FLAG_NAME)
- fun testExtractsPromotedContent_whenStatusBarNotifChipsFlagEnabled() {
- val content = PromotedNotificationContentBuilder("key").build()
- promotedNotificationContentExtractor.resetForEntry(row.entry, content)
-
- inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_ALL, row)
-
- promotedNotificationContentExtractor.verifyOneExtractCall()
- Assert.assertEquals(content, row.entry.promotedNotificationContentModels)
- }
-
- @Test
@EnableFlags(PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME)
fun testExtractsPromotedContent_whenBothFlagsEnabled() {
val content = PromotedNotificationContentBuilder("key").build()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerTest.java
index 761ed6186afc..ca4dc0e5e546 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerTest.java
@@ -16,6 +16,8 @@
package com.android.systemui.statusbar.notification.stack;
+import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_ALL;
+
import static org.junit.Assert.assertNull;
import android.app.Notification;
@@ -64,6 +66,7 @@ public class NotificationChildrenContainerTest extends SysuiTestCase {
mContext,
mDependency,
TestableLooper.get(this));
+ mNotificationTestHelper.setDefaultInflationFlags(FLAG_CONTENT_VIEW_ALL);
mGroup = mNotificationTestHelper.createGroup();
mChildrenContainer = mGroup.getChildrenContainer();
}
@@ -172,9 +175,12 @@ public class NotificationChildrenContainerTest extends SysuiTestCase {
@Test
@EnableFlags(AsyncGroupHeaderViewInflation.FLAG_NAME)
public void testSetLowPriorityWithAsyncInflation_noHeaderReInflation() {
+ mChildrenContainer.setLowPriorityGroupHeader(null, null);
mChildrenContainer.setIsMinimized(true);
+
+ // THEN
assertNull("We don't inflate header from the main thread with Async "
- + "Inflation enabled", mChildrenContainer.getCurrentHeaderView());
+ + "Inflation enabled", mChildrenContainer.getMinimizedNotificationHeader());
}
@Test
@@ -182,7 +188,7 @@ public class NotificationChildrenContainerTest extends SysuiTestCase {
public void setLowPriorityBeforeLowPriorityHeaderSet() {
//Given: the children container does not have a low-priority header, and is not low-priority
- assertNull(mChildrenContainer.getMinimizedGroupHeaderWrapper());
+ mChildrenContainer.setLowPriorityGroupHeader(null, null);
mGroup.setIsMinimized(false);
//When: set the children container to be low-priority and set the low-priority header
@@ -214,8 +220,8 @@ public class NotificationChildrenContainerTest extends SysuiTestCase {
public void changeLowPriorityAfterHeaderSet() {
//Given: the children container does not have headers, and is not low-priority
- assertNull(mChildrenContainer.getMinimizedGroupHeaderWrapper());
- assertNull(mChildrenContainer.getNotificationHeaderWrapper());
+ mChildrenContainer.setLowPriorityGroupHeader(null, null);
+ mChildrenContainer.setGroupHeader(null, null);
mGroup.setIsMinimized(false);
//When: set the set the normal header
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculatorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculatorTest.kt
index f7bbf989ad3f..e03dbf54e101 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculatorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculatorTest.kt
@@ -32,7 +32,7 @@ import com.android.systemui.statusbar.SysuiStatusBarStateController
import com.android.systemui.statusbar.notification.collection.EntryAdapter
import com.android.systemui.statusbar.notification.collection.NotificationEntry
import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor
-import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUiForceExpanded
+import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUi
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
import com.android.systemui.statusbar.notification.row.ExpandableView
import com.android.systemui.statusbar.policy.ResourcesSplitShadeStateController
@@ -155,7 +155,7 @@ class NotificationStackSizeCalculatorTest : SysuiTestCase() {
}
@Test
- @EnableFlags(PromotedNotificationUiForceExpanded.FLAG_NAME)
+ @EnableFlags(PromotedNotificationUi.FLAG_NAME)
fun maxKeyguardNotificationsForPromotedOngoing_onLockscreenSpaceForMinHeightButNotIntrinsicHeight_returnsOne() {
setGapHeight(0f)
// No divider height since we're testing one element where index = 0
@@ -283,7 +283,7 @@ class NotificationStackSizeCalculatorTest : SysuiTestCase() {
}
@Test
- @EnableFlags(PromotedNotificationUiForceExpanded.FLAG_NAME)
+ @EnableFlags(PromotedNotificationUi.FLAG_NAME)
fun getSpaceNeeded_onLockscreenEnoughSpacePromotedOngoing_intrinsicHeight() {
setGapHeight(0f)
// No divider height since we're testing one element where index = 0
@@ -342,7 +342,7 @@ class NotificationStackSizeCalculatorTest : SysuiTestCase() {
}
@Test
- @EnableFlags(PromotedNotificationUiForceExpanded.FLAG_NAME)
+ @EnableFlags(PromotedNotificationUi.FLAG_NAME)
fun getSpaceNeeded_onLockscreenSavingSpacePromotedOngoing_minHeight() {
setGapHeight(0f)
// No divider height since we're testing one element where index = 0
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorKairosAdapterTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorKairosAdapterTest.kt
index e72d0c27e632..8aff622ee772 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorKairosAdapterTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorKairosAdapterTest.kt
@@ -97,7 +97,7 @@ class MobileIconInteractorKairosAdapterTest : MobileIconInteractorTestBase() {
}
.asIncremental()
.applyLatestSpecForKey(),
- isStackable = interactor.isStackable.toState(),
+ isStackable = interactor.isStackable.toState(false),
activeDataConnectionHasDataEnabled =
interactor.activeDataConnectionHasDataEnabled.toState(),
activeDataIconInteractor =
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModelTest.kt
index 3d37914b1a7d..7dbcb270190c 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModelTest.kt
@@ -60,7 +60,6 @@ class MobileIconsViewModelTest : SysuiTestCase() {
private val interactor = FakeMobileIconsInteractor(FakeMobileMappingsProxy(), mock())
private lateinit var airplaneModeInteractor: AirplaneModeInteractor
- @Mock private lateinit var constants: ConnectivityConstants
@Mock private lateinit var logger: MobileViewLogger
@Mock private lateinit var verboseLogger: VerboseMobileViewLogger
@@ -84,7 +83,10 @@ class MobileIconsViewModelTest : SysuiTestCase() {
verboseLogger,
interactor,
airplaneModeInteractor,
- constants,
+ object : ConnectivityConstants {
+ override val hasDataCapabilities = true
+ override val shouldShowActivityConfig = false
+ },
testScope.backgroundScope,
)
@@ -349,7 +351,42 @@ class MobileIconsViewModelTest : SysuiTestCase() {
// WHEN sub2 becomes last and sub2 has a network type icon
interactor.filteredSubscriptions.value = listOf(SUB_1, SUB_2)
- // THEN the flow updates
+ assertThat(latest).isTrue()
+ job.cancel()
+ }
+
+ @Test
+ fun isStackable_apmEnabled_false() =
+ testScope.runTest {
+ var latest: Boolean? = null
+ val job = underTest.isStackable.onEach { latest = it }.launchIn(this)
+
+ // Set the interactor to true to test APM
+ interactor.isStackable.value = true
+
+ // Enable APM
+ airplaneModeInteractor.setIsAirplaneMode(true)
+
+ interactor.filteredSubscriptions.value = listOf(SUB_1, SUB_2)
+
+ assertThat(latest).isFalse()
+ job.cancel()
+ }
+
+ @Test
+ fun isStackable_apmDisabled_true() =
+ testScope.runTest {
+ var latest: Boolean? = null
+ val job = underTest.isStackable.onEach { latest = it }.launchIn(this)
+
+ // Set the interactor to true to test APM
+ interactor.isStackable.value = true
+
+ // Disable APM
+ airplaneModeInteractor.setIsAirplaneMode(false)
+
+ interactor.filteredSubscriptions.value = listOf(SUB_1, SUB_2)
+
assertThat(latest).isTrue()
job.cancel()
diff --git a/packages/SystemUI/res-keyguard/values/styles.xml b/packages/SystemUI/res-keyguard/values/styles.xml
index e7d6b2fe08f4..877360594fc6 100644
--- a/packages/SystemUI/res-keyguard/values/styles.xml
+++ b/packages/SystemUI/res-keyguard/values/styles.xml
@@ -137,7 +137,13 @@
<item name="android:gravity">start</item>
<item name="android:ellipsize">end</item>
<item name="android:maxLines">2</item>
- <item name="android:fontFamily">@*android:string/config_headlineFontFamily</item>
+ <item name="android:fontFamily" android:featureFlag="!com.android.systemui.lockscreen_font">
+ @*android:string/config_headlineFontFamily
+ </item>
+ <item name="android:fontFamily" android:featureFlag="com.android.systemui.lockscreen_font">
+ variable-title-small
+ </item>
+ <item name="android:fontFamily"></item>
<item name="android:shadowColor">@color/keyguard_shadow_color</item>
<item name="android:shadowRadius">?attr/shadowRadius</item>
</style>
diff --git a/packages/SystemUI/res/drawable/global_actions_lite_button_background.xml b/packages/SystemUI/res/drawable/global_actions_lite_button_background.xml
new file mode 100644
index 000000000000..a40fbd368a70
--- /dev/null
+++ b/packages/SystemUI/res/drawable/global_actions_lite_button_background.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2025 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_focused="false" >
+ <shape android:shape="oval">
+ <solid android:color="@color/global_actions_lite_button_background"/>
+ </shape>
+ </item>
+ <item android:state_focused="true" >
+ <shape android:shape="oval">
+ <solid android:color="@color/global_actions_lite_button_background_focused"/>
+ </shape>
+ </item>
+</selector>
diff --git a/packages/SystemUI/res/drawable/ic_add_circle_rounded.xml b/packages/SystemUI/res/drawable/ic_add_circle_rounded.xml
new file mode 100644
index 000000000000..467a3813e461
--- /dev/null
+++ b/packages/SystemUI/res/drawable/ic_add_circle_rounded.xml
@@ -0,0 +1,27 @@
+<!--
+ ~ Copyright (C) 2025 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <group>
+ <path
+ android:pathData="M11,13V16C11,16.283 11.092,16.525 11.275,16.725C11.475,16.908 11.717,17 12,17C12.283,17 12.517,16.908 12.7,16.725C12.9,16.525 13,16.283 13,16V13H16C16.283,13 16.517,12.908 16.7,12.725C16.9,12.525 17,12.283 17,12C17,11.717 16.9,11.483 16.7,11.3C16.517,11.1 16.283,11 16,11H13V8C13,7.717 12.9,7.483 12.7,7.3C12.517,7.1 12.283,7 12,7C11.717,7 11.475,7.1 11.275,7.3C11.092,7.483 11,7.717 11,8V11H8C7.717,11 7.475,11.1 7.275,11.3C7.092,11.483 7,11.717 7,12C7,12.283 7.092,12.525 7.275,12.725C7.475,12.908 7.717,13 8,13H11ZM12,22C10.617,22 9.317,21.742 8.1,21.225C6.883,20.692 5.825,19.975 4.925,19.075C4.025,18.175 3.308,17.117 2.775,15.9C2.258,14.683 2,13.383 2,12C2,10.617 2.258,9.317 2.775,8.1C3.308,6.883 4.025,5.825 4.925,4.925C5.825,4.025 6.883,3.317 8.1,2.8C9.317,2.267 10.617,2 12,2C13.383,2 14.683,2.267 15.9,2.8C17.117,3.317 18.175,4.025 19.075,4.925C19.975,5.825 20.683,6.883 21.2,8.1C21.733,9.317 22,10.617 22,12C22,13.383 21.733,14.683 21.2,15.9C20.683,17.117 19.975,18.175 19.075,19.075C18.175,19.975 17.117,20.692 15.9,21.225C14.683,21.742 13.383,22 12,22ZM12,20C14.233,20 16.125,19.225 17.675,17.675C19.225,16.125 20,14.233 20,12C20,9.767 19.225,7.875 17.675,6.325C16.125,4.775 14.233,4 12,4C9.767,4 7.875,4.775 6.325,6.325C4.775,7.875 4,9.767 4,12C4,14.233 4.775,16.125 6.325,17.675C7.875,19.225 9.767,20 12,20Z"
+ android:fillColor="#ffffff"/>
+ </group>
+</vector>
diff --git a/packages/SystemUI/res/drawable/ic_check_circle_filled.xml b/packages/SystemUI/res/drawable/ic_check_circle_filled.xml
new file mode 100644
index 000000000000..935733c3333d
--- /dev/null
+++ b/packages/SystemUI/res/drawable/ic_check_circle_filled.xml
@@ -0,0 +1,27 @@
+<!--
+ ~ Copyright (C) 2025 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <group>
+ <path
+ android:pathData="M10.6,13.8L8.45,11.65C8.267,11.467 8.033,11.375 7.75,11.375C7.467,11.375 7.233,11.467 7.05,11.65C6.867,11.833 6.775,12.067 6.775,12.35C6.775,12.633 6.867,12.867 7.05,13.05L9.9,15.9C10.1,16.1 10.333,16.2 10.6,16.2C10.867,16.2 11.1,16.1 11.3,15.9L16.95,10.25C17.133,10.067 17.225,9.833 17.225,9.55C17.225,9.267 17.133,9.033 16.95,8.85C16.767,8.667 16.533,8.575 16.25,8.575C15.967,8.575 15.733,8.667 15.55,8.85L10.6,13.8ZM12,22C10.617,22 9.317,21.742 8.1,21.225C6.883,20.692 5.825,19.975 4.925,19.075C4.025,18.175 3.308,17.117 2.775,15.9C2.258,14.683 2,13.383 2,12C2,10.617 2.258,9.317 2.775,8.1C3.308,6.883 4.025,5.825 4.925,4.925C5.825,4.025 6.883,3.317 8.1,2.8C9.317,2.267 10.617,2 12,2C13.383,2 14.683,2.267 15.9,2.8C17.117,3.317 18.175,4.025 19.075,4.925C19.975,5.825 20.683,6.883 21.2,8.1C21.733,9.317 22,10.617 22,12C22,13.383 21.733,14.683 21.2,15.9C20.683,17.117 19.975,18.175 19.075,19.075C18.175,19.975 17.117,20.692 15.9,21.225C14.683,21.742 13.383,22 12,22Z"
+ android:fillColor="#ffffff"/>
+ </group>
+</vector>
diff --git a/packages/SystemUI/res/drawable/ic_expand_less_rounded.xml b/packages/SystemUI/res/drawable/ic_expand_less_rounded.xml
new file mode 100644
index 000000000000..5570fcfdab28
--- /dev/null
+++ b/packages/SystemUI/res/drawable/ic_expand_less_rounded.xml
@@ -0,0 +1,25 @@
+<!--
+ ~ Copyright (C) 2025 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="16dp"
+ android:height="16dp"
+ android:viewportWidth="16"
+ android:viewportHeight="16">
+ <path
+ android:pathData="M8,5.25C8.097,5.25 8.194,5.264 8.292,5.292C8.375,5.333 8.451,5.389 8.521,5.458L12.479,9.417C12.632,9.569 12.708,9.743 12.708,9.938C12.694,10.146 12.611,10.326 12.458,10.479C12.306,10.632 12.132,10.708 11.938,10.708C11.729,10.708 11.549,10.632 11.396,10.479L8,7.063L4.583,10.479C4.431,10.632 4.257,10.701 4.063,10.688C3.854,10.688 3.674,10.611 3.521,10.458C3.368,10.306 3.292,10.125 3.292,9.917C3.292,9.722 3.368,9.549 3.521,9.396L7.479,5.458C7.549,5.389 7.632,5.333 7.729,5.292C7.813,5.264 7.903,5.25 8,5.25Z"
+ android:fillColor="#ffffffff"/>
+</vector>
diff --git a/packages/SystemUI/res/drawable/ic_expand_more_rounded.xml b/packages/SystemUI/res/drawable/ic_expand_more_rounded.xml
new file mode 100644
index 000000000000..dec620e54995
--- /dev/null
+++ b/packages/SystemUI/res/drawable/ic_expand_more_rounded.xml
@@ -0,0 +1,25 @@
+<!--
+ ~ Copyright (C) 2025 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="16dp"
+ android:height="16dp"
+ android:viewportHeight="16"
+ android:viewportWidth="16">
+ <path
+ android:pathData="M8,10.75C7.903,10.75 7.806,10.736 7.708,10.708C7.625,10.667 7.549,10.611 7.479,10.542L3.521,6.583C3.368,6.431 3.292,6.257 3.292,6.063C3.306,5.854 3.389,5.674 3.542,5.521C3.694,5.368 3.868,5.292 4.063,5.292C4.271,5.292 4.451,5.368 4.604,5.521L8,8.938L11.417,5.521C11.569,5.368 11.743,5.299 11.938,5.313C12.146,5.313 12.326,5.389 12.479,5.542C12.632,5.694 12.708,5.875 12.708,6.083C12.708,6.278 12.632,6.451 12.479,6.604L8.521,10.542C8.451,10.611 8.368,10.667 8.271,10.708C8.188,10.736 8.097,10.75 8,10.75Z"
+ android:fillColor="#ffffffff"/>
+</vector>
diff --git a/packages/SystemUI/res/drawable/ic_qs_category_accessibility.xml b/packages/SystemUI/res/drawable/ic_qs_category_accessibility.xml
new file mode 100644
index 000000000000..bc62d38f1932
--- /dev/null
+++ b/packages/SystemUI/res/drawable/ic_qs_category_accessibility.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ Copyright (C) 2025 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:tint="?attr/colorControlNormal"
+ android:viewportHeight="960"
+ android:viewportWidth="960">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M480,240Q447,240 423.5,216.5Q400,193 400,160Q400,127 423.5,103.5Q447,80 480,80Q513,80 536.5,103.5Q560,127 560,160Q560,193 536.5,216.5Q513,240 480,240ZM360,840L360,360Q311,356 261,349Q211,342 163,331Q146,327 135.5,312Q125,297 130,280Q135,263 151,255Q167,247 185,251Q255,266 330.5,273Q406,280 480,280Q554,280 629.5,273Q705,266 775,251Q793,247 809,255Q825,263 830,280Q835,297 824.5,312Q814,327 797,331Q749,342 699,349Q649,356 600,360L600,840Q600,857 588.5,868.5Q577,880 560,880Q543,880 531.5,868.5Q520,857 520,840L520,640L440,640L440,840Q440,857 428.5,868.5Q417,880 400,880Q383,880 371.5,868.5Q360,857 360,840Z" />
+</vector> \ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/ic_qs_category_connectivty.xml b/packages/SystemUI/res/drawable/ic_qs_category_connectivty.xml
new file mode 100644
index 000000000000..91644873c064
--- /dev/null
+++ b/packages/SystemUI/res/drawable/ic_qs_category_connectivty.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ Copyright (C) 2025 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:tint="?attr/colorControlNormal"
+ android:viewportHeight="960"
+ android:viewportWidth="960">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M480,840Q438,840 409,811Q380,782 380,740Q380,698 409,669Q438,640 480,640Q522,640 551,669Q580,698 580,740Q580,782 551,811Q522,840 480,840ZM480,400Q555,400 622.5,424Q690,448 745,490Q765,505 765.5,529.5Q766,554 748,572Q731,589 706,589.5Q681,590 661,576Q623,550 577,535Q531,520 480,520Q429,520 383,535Q337,550 299,576Q279,590 254,589Q229,588 212,571Q195,553 195,528.5Q195,504 215,489Q270,447 337.5,423.5Q405,400 480,400ZM480,160Q605,160 715.5,201Q826,242 914,317Q934,334 935,359Q936,384 918,402Q901,419 876,419.5Q851,420 831,404Q759,345 669.5,312.5Q580,280 480,280Q380,280 290.5,312.5Q201,345 129,404Q109,420 84,419.5Q59,419 42,402Q24,384 25,359Q26,334 46,317Q134,242 244.5,201Q355,160 480,160Z" />
+</vector> \ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/ic_qs_category_display.xml b/packages/SystemUI/res/drawable/ic_qs_category_display.xml
new file mode 100644
index 000000000000..c238e940eb01
--- /dev/null
+++ b/packages/SystemUI/res/drawable/ic_qs_category_display.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ Copyright (C) 2025 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:tint="?attr/colorControlNormal"
+ android:viewportHeight="960"
+ android:viewportWidth="960">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M346,800L240,800Q207,800 183.5,776.5Q160,753 160,720L160,614L83,536Q72,524 66,509.5Q60,495 60,480Q60,465 66,450.5Q72,436 83,424L160,346L160,240Q160,207 183.5,183.5Q207,160 240,160L346,160L424,83Q436,72 450.5,66Q465,60 480,60Q495,60 509.5,66Q524,72 536,83L614,160L720,160Q753,160 776.5,183.5Q800,207 800,240L800,346L877,424Q888,436 894,450.5Q900,465 900,480Q900,495 894,509.5Q888,524 877,536L800,614L800,720Q800,753 776.5,776.5Q753,800 720,800L614,800L536,877Q524,888 509.5,894Q495,900 480,900Q465,900 450.5,894Q436,888 424,877L346,800ZM380,720L480,820Q480,820 480,820Q480,820 480,820L580,720L720,720Q720,720 720,720Q720,720 720,720L720,580L820,480Q820,480 820,480Q820,480 820,480L720,380L720,240Q720,240 720,240Q720,240 720,240L580,240L480,140Q480,140 480,140Q480,140 480,140L380,240L240,240Q240,240 240,240Q240,240 240,240L240,380L140,480Q140,480 140,480Q140,480 140,480L240,580L240,720Q240,720 240,720Q240,720 240,720L380,720ZM480,680Q563,680 621.5,621.5Q680,563 680,480Q680,397 621.5,338.5Q563,280 480,280L480,680Z" />
+</vector> \ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/ic_qs_category_privacy.xml b/packages/SystemUI/res/drawable/ic_qs_category_privacy.xml
new file mode 100644
index 000000000000..915cf41ba1f6
--- /dev/null
+++ b/packages/SystemUI/res/drawable/ic_qs_category_privacy.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ Copyright (C) 2025 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:tint="?attr/colorControlNormal"
+ android:viewportHeight="960"
+ android:viewportWidth="960">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M444,600L516,600Q525,600 531.5,592.5Q538,585 536,576L517,471Q537,461 548.5,442Q560,423 560,400Q560,367 536.5,343.5Q513,320 480,320Q447,320 423.5,343.5Q400,367 400,400Q400,423 411.5,442Q423,461 443,471L424,576Q422,585 428.5,592.5Q435,600 444,600ZM480,876Q473,876 467,875Q461,874 455,872Q320,827 240,705.5Q160,584 160,444L160,255Q160,230 174.5,210Q189,190 212,181L452,91Q466,86 480,86Q494,86 508,91L748,181Q771,190 785.5,210Q800,230 800,255L800,444Q800,584 720,705.5Q640,827 505,872Q499,874 493,875Q487,876 480,876Z" />
+</vector> \ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/ic_qs_category_provided_by_apps.xml b/packages/SystemUI/res/drawable/ic_qs_category_provided_by_apps.xml
new file mode 100644
index 000000000000..cea43ae1bc2f
--- /dev/null
+++ b/packages/SystemUI/res/drawable/ic_qs_category_provided_by_apps.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ Copyright (C) 2025 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:tint="?attr/colorControlNormal"
+ android:viewportHeight="960"
+ android:viewportWidth="960">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M240,800Q207,800 183.5,776.5Q160,753 160,720Q160,687 183.5,663.5Q207,640 240,640Q273,640 296.5,663.5Q320,687 320,720Q320,753 296.5,776.5Q273,800 240,800ZM480,800Q447,800 423.5,776.5Q400,753 400,720Q400,687 423.5,663.5Q447,640 480,640Q513,640 536.5,663.5Q560,687 560,720Q560,753 536.5,776.5Q513,800 480,800ZM720,800Q687,800 663.5,776.5Q640,753 640,720Q640,687 663.5,663.5Q687,640 720,640Q753,640 776.5,663.5Q800,687 800,720Q800,753 776.5,776.5Q753,800 720,800ZM240,560Q207,560 183.5,536.5Q160,513 160,480Q160,447 183.5,423.5Q207,400 240,400Q273,400 296.5,423.5Q320,447 320,480Q320,513 296.5,536.5Q273,560 240,560ZM480,560Q447,560 423.5,536.5Q400,513 400,480Q400,447 423.5,423.5Q447,400 480,400Q513,400 536.5,423.5Q560,447 560,480Q560,513 536.5,536.5Q513,560 480,560ZM720,560Q687,560 663.5,536.5Q640,513 640,480Q640,447 663.5,423.5Q687,400 720,400Q753,400 776.5,423.5Q800,447 800,480Q800,513 776.5,536.5Q753,560 720,560ZM240,320Q207,320 183.5,296.5Q160,273 160,240Q160,207 183.5,183.5Q207,160 240,160Q273,160 296.5,183.5Q320,207 320,240Q320,273 296.5,296.5Q273,320 240,320ZM480,320Q447,320 423.5,296.5Q400,273 400,240Q400,207 423.5,183.5Q447,160 480,160Q513,160 536.5,183.5Q560,207 560,240Q560,273 536.5,296.5Q513,320 480,320ZM720,320Q687,320 663.5,296.5Q640,273 640,240Q640,207 663.5,183.5Q687,160 720,160Q753,160 776.5,183.5Q800,207 800,240Q800,273 776.5,296.5Q753,320 720,320Z" />
+</vector> \ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/ic_qs_category_unknown.xml b/packages/SystemUI/res/drawable/ic_qs_category_unknown.xml
new file mode 100644
index 000000000000..ec2ce15e2d01
--- /dev/null
+++ b/packages/SystemUI/res/drawable/ic_qs_category_unknown.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ Copyright (C) 2025 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:tint="?attr/colorControlNormal"
+ android:viewportHeight="960"
+ android:viewportWidth="960">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M200,840Q167,840 143.5,816.5Q120,793 120,760L120,640Q120,623 131.5,611.5Q143,600 160,600Q177,600 188.5,611.5Q200,623 200,640L200,760Q200,760 200,760Q200,760 200,760L320,760Q337,760 348.5,771.5Q360,783 360,800Q360,817 348.5,828.5Q337,840 320,840L200,840ZM760,840L640,840Q623,840 611.5,828.5Q600,817 600,800Q600,783 611.5,771.5Q623,760 640,760L760,760Q760,760 760,760Q760,760 760,760L760,640Q760,623 771.5,611.5Q783,600 800,600Q817,600 828.5,611.5Q840,623 840,640L840,760Q840,793 816.5,816.5Q793,840 760,840ZM120,200Q120,167 143.5,143.5Q167,120 200,120L320,120Q337,120 348.5,131.5Q360,143 360,160Q360,177 348.5,188.5Q337,200 320,200L200,200Q200,200 200,200Q200,200 200,200L200,320Q200,337 188.5,348.5Q177,360 160,360Q143,360 131.5,348.5Q120,337 120,320L120,200ZM840,200L840,320Q840,337 828.5,348.5Q817,360 800,360Q783,360 771.5,348.5Q760,337 760,320L760,200Q760,200 760,200Q760,200 760,200L640,200Q623,200 611.5,188.5Q600,177 600,160Q600,143 611.5,131.5Q623,120 640,120L760,120Q793,120 816.5,143.5Q840,167 840,200ZM480,720Q501,720 515.5,705.5Q530,691 530,670Q530,649 515.5,634.5Q501,620 480,620Q459,620 444.5,634.5Q430,649 430,670Q430,691 444.5,705.5Q459,720 480,720ZM480,308Q506,308 525.5,324Q545,340 545,365Q545,388 530.5,406Q516,424 499,439Q473,462 459.5,482.5Q446,503 444,532Q443,546 454,556.5Q465,567 480,567Q494,567 505.5,557Q517,547 519,532Q521,515 531,502Q541,489 560,470Q595,435 606.5,413.5Q618,392 618,362Q618,308 579,274Q540,240 480,240Q439,240 406.5,258.5Q374,277 357,311Q351,323 356.5,335.5Q362,348 375,353Q388,358 401.5,353Q415,348 423,337Q434,323 448.5,315.5Q463,308 480,308Z" />
+</vector> \ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/ic_qs_category_utilities.xml b/packages/SystemUI/res/drawable/ic_qs_category_utilities.xml
new file mode 100644
index 000000000000..4dfac8393b8e
--- /dev/null
+++ b/packages/SystemUI/res/drawable/ic_qs_category_utilities.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ Copyright (C) 2025 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:tint="?attr/colorControlNormal"
+ android:viewportHeight="960"
+ android:viewportWidth="960">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M433,880Q406,880 386.5,862Q367,844 363,818L354,752Q341,747 329.5,740Q318,733 307,725L245,751Q220,762 195,753Q170,744 156,721L109,639Q95,616 101,590Q107,564 128,547L181,507Q180,500 180,493.5Q180,487 180,480Q180,473 180,466.5Q180,460 181,453L128,413Q107,396 101,370Q95,344 109,321L156,239Q170,216 195,207Q220,198 245,209L307,235Q318,227 330,220Q342,213 354,208L363,142Q367,116 386.5,98Q406,80 433,80L527,80Q554,80 573.5,98Q593,116 597,142L606,208Q619,213 630.5,220Q642,227 653,235L715,209Q740,198 765,207Q790,216 804,239L851,321Q865,344 859,370Q853,396 832,413L779,453Q780,460 780,466.5Q780,473 780,480Q780,487 780,493.5Q780,500 778,507L831,547Q852,564 858,590Q864,616 850,639L802,721Q788,744 763,753Q738,762 713,751L653,725Q642,733 630,740Q618,747 606,752L597,818Q593,844 573.5,862Q554,880 527,880L433,880ZM482,620Q540,620 581,579Q622,538 622,480Q622,422 581,381Q540,340 482,340Q423,340 382.5,381Q342,422 342,480Q342,538 382.5,579Q423,620 482,620Z" />
+</vector> \ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/media_output_dialog_background_reduced_radius.xml b/packages/SystemUI/res/drawable/media_output_dialog_background_reduced_radius.xml
new file mode 100644
index 000000000000..f78212b44828
--- /dev/null
+++ b/packages/SystemUI/res/drawable/media_output_dialog_background_reduced_radius.xml
@@ -0,0 +1,20 @@
+<!--
+ ~ Copyright (C) 2025 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <corners android:radius="@dimen/media_output_dialog_corner_radius" />
+ <solid android:color="@color/media_dialog_surface_container" />
+</shape> \ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/media_output_dialog_footer_background.xml b/packages/SystemUI/res/drawable/media_output_dialog_footer_background.xml
new file mode 100644
index 000000000000..2d27ac1612a9
--- /dev/null
+++ b/packages/SystemUI/res/drawable/media_output_dialog_footer_background.xml
@@ -0,0 +1,22 @@
+<!--
+ ~ Copyright (C) 2025 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <corners
+ android:bottomLeftRadius="@dimen/media_output_dialog_corner_radius"
+ android:bottomRightRadius="@dimen/media_output_dialog_corner_radius" />
+ <solid android:color="@color/media_dialog_surface_container" />
+</shape> \ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/media_output_dialog_item_fixed_volume_background.xml b/packages/SystemUI/res/drawable/media_output_dialog_item_fixed_volume_background.xml
new file mode 100644
index 000000000000..38db2da37f7c
--- /dev/null
+++ b/packages/SystemUI/res/drawable/media_output_dialog_item_fixed_volume_background.xml
@@ -0,0 +1,20 @@
+<!--
+ ~ Copyright (C) 2025 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <corners android:radius="20dp" />
+ <solid android:color="@color/media_dialog_primary" />
+</shape> \ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/media_output_dialog_round_button_ripple.xml b/packages/SystemUI/res/drawable/media_output_dialog_round_button_ripple.xml
new file mode 100644
index 000000000000..d23c1837c501
--- /dev/null
+++ b/packages/SystemUI/res/drawable/media_output_dialog_round_button_ripple.xml
@@ -0,0 +1,24 @@
+<!--
+ ~ 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.
+ -->
+<ripple android:color="?android:colorControlHighlight"
+ xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:id="@android:id/mask">
+ <shape android:shape="rectangle">
+ <solid android:color="@android:color/white" />
+ <corners android:radius="20dp" />
+ </shape>
+ </item>
+</ripple> \ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/media_output_item_expandable_button_background.xml b/packages/SystemUI/res/drawable/media_output_item_expandable_button_background.xml
new file mode 100644
index 000000000000..8fc8744a3827
--- /dev/null
+++ b/packages/SystemUI/res/drawable/media_output_item_expandable_button_background.xml
@@ -0,0 +1,24 @@
+<!--
+ ~ Copyright (C) 2025 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <corners
+ android:radius="@dimen/media_output_item_expand_icon_height"/>
+ <size
+ android:width="@dimen/media_output_item_expand_icon_width"
+ android:height="@dimen/media_output_item_expand_icon_height" />
+ <solid android:color="@color/media_dialog_on_surface" />
+</shape> \ No newline at end of file
diff --git a/packages/SystemUI/res/layout/biometric_prompt_one_pane_layout.xml b/packages/SystemUI/res/layout/biometric_prompt_one_pane_layout.xml
index 4002f7808637..1e4a07f5fc30 100644
--- a/packages/SystemUI/res/layout/biometric_prompt_one_pane_layout.xml
+++ b/packages/SystemUI/res/layout/biometric_prompt_one_pane_layout.xml
@@ -22,7 +22,7 @@
android:layout_width="0dp"
android:layout_height="0dp"
android:accessibilityLiveRegion="assertive"
- android:importantForAccessibility="yes"
+ android:importantForAccessibility="auto"
android:clickable="false"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/rightGuideline"
diff --git a/packages/SystemUI/res/layout/biometric_prompt_two_pane_layout.xml b/packages/SystemUI/res/layout/biometric_prompt_two_pane_layout.xml
index 3c8cb6860a41..8234c24a7e17 100644
--- a/packages/SystemUI/res/layout/biometric_prompt_two_pane_layout.xml
+++ b/packages/SystemUI/res/layout/biometric_prompt_two_pane_layout.xml
@@ -23,7 +23,7 @@ android:layout_height="match_parent">
android:layout_width="0dp"
android:layout_height="0dp"
android:accessibilityLiveRegion="assertive"
- android:importantForAccessibility="yes"
+ android:importantForAccessibility="auto"
android:clickable="false"
android:paddingHorizontal="16dp"
android:paddingVertical="16dp"
diff --git a/packages/SystemUI/res/layout/combined_qs_header.xml b/packages/SystemUI/res/layout/combined_qs_header.xml
index 5c06585de99c..65d17042412b 100644
--- a/packages/SystemUI/res/layout/combined_qs_header.xml
+++ b/packages/SystemUI/res/layout/combined_qs_header.xml
@@ -85,7 +85,7 @@ frame when animating QS <-> QQS transition
android:paddingEnd="@dimen/status_bar_left_clock_end_padding"
android:singleLine="true"
android:textDirection="locale"
- android:textAppearance="@style/TextAppearance.QS.Status"
+ android:textAppearance="@style/TextAppearance.QS.Status.Clock"
android:fontFeatureSettings="tnum"
android:transformPivotX="0dp"
android:transformPivotY="24dp"
diff --git a/packages/SystemUI/res/layout/media_output_dialog.xml b/packages/SystemUI/res/layout/media_output_dialog.xml
index 9b629ace76af..15657284030d 100644
--- a/packages/SystemUI/res/layout/media_output_dialog.xml
+++ b/packages/SystemUI/res/layout/media_output_dialog.xml
@@ -97,6 +97,23 @@
</LinearLayout>
</LinearLayout>
+ <LinearLayout
+ android:id="@+id/quick_access_shelf"
+ android:paddingHorizontal="@dimen/media_output_dialog_margin_horizontal"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:visibility="gone">
+
+ <com.google.android.material.button.MaterialButton
+ android:id="@+id/connect_device"
+ app:icon="@drawable/ic_add"
+ style="@style/MediaOutput.Dialog.QuickAccessButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/media_output_dialog_button_connect_device"
+ android:layout_marginBottom="8dp"/>
+ </LinearLayout>
+
<ViewStub
android:id="@+id/broadcast_qrcode"
android:layout="@layout/media_output_broadcast_area"
@@ -123,13 +140,15 @@
</androidx.constraintlayout.widget.ConstraintLayout>
<LinearLayout
+ android:id="@+id/dialog_footer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:layout_marginTop="4dp"
- android:layout_marginStart="@dimen/dialog_side_padding"
- android:layout_marginEnd="@dimen/dialog_side_padding"
- android:layout_marginBottom="@dimen/dialog_bottom_padding"
- android:orientation="horizontal">
+ android:paddingTop="4dp"
+ android:paddingStart="@dimen/dialog_side_padding"
+ android:paddingEnd="@dimen/dialog_side_padding"
+ android:paddingBottom="@dimen/dialog_bottom_padding"
+ android:orientation="horizontal"
+ android:gravity="end">
<Button
android:id="@+id/stop"
@@ -140,6 +159,7 @@
android:visibility="gone"/>
<Space
+ android:id="@+id/footer_spacer"
android:layout_weight="1"
android:layout_width="0dp"
android:layout_height="match_parent"/>
diff --git a/packages/SystemUI/res/layout/media_output_list_item_device.xml b/packages/SystemUI/res/layout/media_output_list_item_device.xml
new file mode 100644
index 000000000000..29d5bfcc1743
--- /dev/null
+++ b/packages/SystemUI/res/layout/media_output_list_item_device.xml
@@ -0,0 +1,141 @@
+<?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.
+ -->
+<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/item_layout"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:baselineAligned="false"
+ android:orientation="vertical">
+
+ <LinearLayout
+ android:id="@+id/main_content"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="start|center_vertical"
+ android:background="?android:attr/selectableItemBackground"
+ android:focusable="true"
+ android:orientation="horizontal"
+ android:layout_marginHorizontal="@dimen/media_output_dialog_margin_horizontal"
+ android:paddingVertical="@dimen/media_output_item_content_vertical_margin">
+
+ <ImageView
+ android:id="@+id/title_icon"
+ style="@style/MediaOutput.Item.Icon"
+ android:layout_marginEnd="@dimen/media_output_item_horizontal_gap"
+ android:importantForAccessibility="no"
+ tools:src="@drawable/ic_smartphone"
+ tools:visibility="visible"/>
+
+ <LinearLayout
+ android:id="@+id/text_container"
+ android:layout_width="0dp"
+ android:layout_weight="1"
+ android:layout_height="wrap_content"
+ android:minHeight="@dimen/media_output_item_icon_size"
+ android:layout_gravity="start"
+ android:gravity="center_vertical|start"
+ android:orientation="vertical">
+
+ <TextView
+ android:id="@+id/title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:fontFamily="variable-title-small"
+ android:ellipsize="end"
+ android:maxLines="1" />
+
+ <TextView
+ android:id="@+id/subtitle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:fontFamily="variable-title-small"
+ android:alpha="@dimen/media_output_item_subtitle_alpha"
+ android:maxLines="1"
+ android:singleLine="true" />
+ </LinearLayout>
+
+ <ImageView
+ android:id="@+id/status_icon"
+ style="@style/MediaOutput.Item.Icon"
+ android:layout_marginStart="@dimen/media_output_item_horizontal_gap"
+ android:importantForAccessibility="no"
+ android:visibility="gone"
+ app:tint="@color/media_dialog_on_surface_variant"
+ tools:src="@drawable/media_output_status_failed"
+ tools:visibility="visible" />
+
+ <ProgressBar
+ android:id="@+id/loading_indicator"
+ style="?android:attr/progressBarStyleSmallTitle"
+ android:layout_width="@dimen/media_output_item_icon_size"
+ android:layout_height="@dimen/media_output_item_icon_size"
+ android:padding="@dimen/media_output_item_icon_padding"
+ android:scaleType="fitCenter"
+ android:layout_marginStart="@dimen/media_output_item_horizontal_gap"
+ android:indeterminate="true"
+ android:indeterminateOnly="true"
+ android:visibility="gone"
+ tools:indeterminateTint="@color/media_dialog_on_surface_variant"
+ tools:visibility="visible" />
+
+ <View
+ android:id="@+id/divider"
+ android:layout_width="1dp"
+ android:layout_height="@dimen/media_output_item_icon_size"
+ android:layout_marginStart="@dimen/media_output_item_horizontal_gap"
+ android:background="@color/media_dialog_outline"
+ android:visibility="visible"
+ />
+
+ <ImageButton
+ android:id="@+id/ongoing_session_button"
+ style="@style/MediaOutput.Item.Icon"
+ android:src="@drawable/ic_sound_bars_anim"
+ android:background="?android:attr/selectableItemBackgroundBorderless"
+ android:focusable="true"
+ android:contentDescription="@string/accessibility_open_application"
+ android:layout_marginStart="@dimen/media_output_item_horizontal_gap"
+ android:visibility="gone"
+ tools:visibility="visible"/>
+
+ <ImageButton
+ android:id="@+id/group_button"
+ style="@style/MediaOutput.Item.Icon"
+ android:layout_marginStart="@dimen/media_output_item_horizontal_gap"
+ android:src="@drawable/ic_add_circle_rounded"
+ android:background="@drawable/media_output_dialog_round_button_ripple"
+ android:focusable="true"
+ android:contentDescription="@null"
+ android:visibility="gone"
+ tools:visibility="visible"/>
+ </LinearLayout>
+
+ <com.google.android.material.slider.Slider
+ android:id="@+id/volume_seekbar"
+ android:layout_width="match_parent"
+ android:layout_height="44dp"
+ android:layout_marginVertical="3dp"
+ android:theme="@style/Theme.Material3.DynamicColors.DayNight"
+ app:labelBehavior="gone"
+ app:tickVisible="false"
+ app:trackCornerSize="12dp"
+ app:trackHeight="32dp"
+ app:trackIconSize="20dp"
+ app:trackStopIndicatorSize="0dp" />
+</LinearLayout> \ No newline at end of file
diff --git a/packages/SystemUI/res/layout/media_output_list_item_group_divider.xml b/packages/SystemUI/res/layout/media_output_list_item_group_divider.xml
new file mode 100644
index 000000000000..f8c6c1f9f616
--- /dev/null
+++ b/packages/SystemUI/res/layout/media_output_list_item_group_divider.xml
@@ -0,0 +1,70 @@
+<?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.
+ -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginHorizontal="@dimen/media_output_dialog_margin_horizontal"
+ android:orientation="vertical">
+
+ <View
+ android:id="@+id/top_separator"
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:layout_marginVertical="8dp"
+ android:background="@color/media_dialog_outline_variant"
+ android:visibility="gone" />
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:minHeight="40dp"
+ android:orientation="horizontal"
+ android:gravity="center_vertical">
+
+ <TextView
+ android:id="@+id/title"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:accessibilityHeading="true"
+ android:ellipsize="end"
+ android:maxLines="1"
+ android:fontFamily="variable-label-large-emphasized"
+ android:gravity="center_vertical|start" />
+
+ <FrameLayout
+ android:id="@+id/expand_button"
+ android:layout_width="40dp"
+ android:layout_height="40dp"
+ android:layout_marginStart="@dimen/media_output_item_horizontal_gap"
+ android:contentDescription="@string/accessibility_open_application"
+ android:focusable="true"
+ android:visibility="gone">
+
+ <ImageView
+ android:id="@+id/expand_button_icon"
+ android:layout_width="@dimen/media_output_item_expand_icon_width"
+ android:layout_height="@dimen/media_output_item_expand_icon_height"
+ android:layout_gravity="center"
+ android:background="@drawable/media_output_item_expandable_button_background"
+ android:contentDescription="@null"
+ android:focusable="false"
+ android:scaleType="centerInside" />
+ </FrameLayout>
+ </LinearLayout>
+</LinearLayout>
+
diff --git a/packages/SystemUI/res/layout/notification_conversation_info.xml b/packages/SystemUI/res/layout/notification_conversation_info.xml
index 9560be0d6969..56660139f823 100644
--- a/packages/SystemUI/res/layout/notification_conversation_info.xml
+++ b/packages/SystemUI/res/layout/notification_conversation_info.xml
@@ -391,6 +391,16 @@
android:paddingEnd="4dp"
>
<TextView
+ android:id="@+id/inline_dismiss"
+ android:text="@string/notification_inline_dismiss"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentStart="true"
+ android:gravity="center_vertical"
+ android:minWidth="@dimen/notification_importance_toggle_size"
+ android:minHeight="@dimen/notification_importance_toggle_size"
+ style="@style/TextAppearance.NotificationInfo.Button"/>
+ <TextView
android:id="@+id/done"
android:text="@string/inline_ok_button"
android:layout_width="wrap_content"
diff --git a/packages/SystemUI/res/values-night/colors.xml b/packages/SystemUI/res/values-night/colors.xml
index 85182a02faaf..3dd01996afb8 100644
--- a/packages/SystemUI/res/values-night/colors.xml
+++ b/packages/SystemUI/res/values-night/colors.xml
@@ -76,6 +76,16 @@
<color name="media_dialog_seekbar_progress">@color/material_dynamic_secondary40</color>
<color name="media_dialog_button_background">@color/material_dynamic_primary70</color>
<color name="media_dialog_solid_button_text">@color/material_dynamic_secondary20</color>
+ <color name="media_dialog_primary">@android:color/system_primary_dark</color>
+ <color name="media_dialog_on_primary">@android:color/system_on_primary_dark</color>
+ <color name="media_dialog_secondary">@android:color/system_secondary_dark</color>
+ <color name="media_dialog_secondary_container">@android:color/system_secondary_container_dark</color>
+ <color name="media_dialog_surface_container">@android:color/system_surface_container_dark</color>
+ <color name="media_dialog_surface_container_high">@android:color/system_surface_container_high_dark</color>
+ <color name="media_dialog_on_surface">@android:color/system_on_surface_dark</color>
+ <color name="media_dialog_on_surface_variant">@android:color/system_on_surface_variant_dark</color>
+ <color name="media_dialog_outline">@android:color/system_outline_dark</color>
+ <color name="media_dialog_outline_variant">@android:color/system_outline_variant_dark</color>
<!-- Biometric dialog colors -->
<color name="biometric_dialog_gray">#ffcccccc</color>
diff --git a/packages/SystemUI/res/values/colors.xml b/packages/SystemUI/res/values/colors.xml
index ff16e063f5b1..e1318dd43c88 100644
--- a/packages/SystemUI/res/values/colors.xml
+++ b/packages/SystemUI/res/values/colors.xml
@@ -50,6 +50,7 @@
<!-- Colors for Power Menu Lite -->
<color name="global_actions_lite_background">#191C18</color>
<color name="global_actions_lite_button_background">#303030</color>
+ <color name="global_actions_lite_button_background_focused">#808080</color>
<color name="global_actions_lite_text">#F0F0F0</color>
<color name="global_actions_lite_emergency_background">#F85D4D</color>
<color name="global_actions_lite_emergency_icon">@color/GM2_grey_900</color>
@@ -148,6 +149,7 @@
<!-- Animated Action colors -->
<color name="animated_action_button_text_color">@androidprv:color/materialColorOnSurface</color>
<color name="animated_action_button_stroke_color">@androidprv:color/materialColorOnSurface</color>
+ <color name="animated_action_button_attribution_color">@androidprv:color/materialColorOnSurfaceVariant</color>
<!-- Biometric dialog colors -->
<color name="biometric_dialog_gray">#ff757575</color>
@@ -209,6 +211,16 @@
<color name="media_dialog_seekbar_progress">@android:color/system_accent1_200</color>
<color name="media_dialog_button_background">@color/material_dynamic_primary40</color>
<color name="media_dialog_solid_button_text">@color/material_dynamic_neutral95</color>
+ <color name="media_dialog_primary">@android:color/system_primary_light</color>
+ <color name="media_dialog_on_primary">@android:color/system_on_primary_light</color>
+ <color name="media_dialog_secondary">@android:color/system_secondary_light</color>
+ <color name="media_dialog_secondary_container">@android:color/system_secondary_container_light</color>
+ <color name="media_dialog_surface_container">@android:color/system_surface_container_light</color>
+ <color name="media_dialog_surface_container_high">@android:color/system_surface_container_high_light</color>
+ <color name="media_dialog_on_surface">@android:color/system_on_surface_light</color>
+ <color name="media_dialog_on_surface_variant">@android:color/system_on_surface_variant_light</color>
+ <color name="media_dialog_outline">@android:color/system_outline_light</color>
+ <color name="media_dialog_outline_variant">@android:color/system_outline_variant_light</color>
<!-- controls -->
<color name="control_primary_text">#E6FFFFFF</color>
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index ca984881713b..f062bd1d4990 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -1567,8 +1567,20 @@
<dimen name="media_output_dialog_item_height">64dp</dimen>
<dimen name="media_output_dialog_margin_horizontal">16dp</dimen>
<dimen name="media_output_dialog_list_padding_top">8dp</dimen>
+ <dimen name="media_output_dialog_app_icon_size">16dp</dimen>
+ <dimen name="media_output_dialog_app_icon_bottom_margin">11dp</dimen>
<dimen name="media_output_dialog_icon_left_radius">@dimen/media_output_dialog_active_background_radius</dimen>
<dimen name="media_output_dialog_icon_right_radius">0dp</dimen>
+ <dimen name="media_output_dialog_corner_radius">20dp</dimen>
+ <dimen name="media_output_dialog_button_gap">8dp</dimen>
+ <dimen name="media_output_item_content_vertical_margin">8dp</dimen>
+ <dimen name="media_output_item_content_vertical_margin_active">4dp</dimen>
+ <dimen name="media_output_item_horizontal_gap">12dp</dimen>
+ <dimen name="media_output_item_icon_size">40dp</dimen>
+ <dimen name="media_output_item_icon_padding">8dp</dimen>
+ <dimen name="media_output_item_expand_icon_width">28dp</dimen>
+ <dimen name="media_output_item_expand_icon_height">20dp</dimen>
+ <item name="media_output_item_subtitle_alpha" format="float" type="dimen">0.8</item>
<!-- Distance that the full shade transition takes in order to complete by tapping on a button
like "expand". -->
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 2d40c32e29e9..bbf56936d560 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -592,6 +592,9 @@
<!-- Content description of the button to expand the group of devices. [CHAR LIMIT=NONE] -->
<string name="accessibility_expand_group">Expand group.</string>
+ <!-- Content description of the button to collapse the group of devices. [CHAR LIMIT=NONE] -->
+ <string name="accessibility_collapse_group">Collapse group.</string>
+
<!-- Content description of the button to add a device to a group. [CHAR LIMIT=NONE] -->
<string name="accessibility_add_device_to_group">Add device to group.</string>
@@ -2354,8 +2357,8 @@
<string name="group_system_access_all_apps_search">Open apps list</string>
<!-- User visible title for the keyboard shortcut that accesses [system] settings. [CHAR LIMIT=70] -->
<string name="group_system_access_system_settings">Open settings</string>
- <!-- User visible title for the keyboard shortcut that accesses Assistant app. [CHAR LIMIT=70] -->
- <string name="group_system_access_google_assistant">Open assistant</string>
+ <!-- User visible title for the keyboard shortcut that accesses the default digital assistant app. [CHAR LIMIT=70] -->
+ <string name="group_system_access_google_assistant">Open digital assistant</string>
<!-- User visible title for the keyboard shortcut that locks screen. [CHAR LIMIT=70] -->
<string name="group_system_lock_screen">Lock screen</string>
<!-- User visible title for the keyboard shortcut that pulls up Notes app for quick memo. [CHAR LIMIT=70] -->
@@ -3293,6 +3296,8 @@
<string name="media_output_dialog_connect_failed">Can\'t switch. Tap to try again.</string>
<!-- Title for connecting item [CHAR LIMIT=60] -->
<string name="media_output_dialog_pairing_new">Connect a device</string>
+ <!-- Button text for connecting a new device [CHAR LIMIT=60] -->
+ <string name="media_output_dialog_button_connect_device">Connect Device</string>
<!-- App name when can't get app name [CHAR LIMIT=60] -->
<string name="media_output_dialog_unknown_launch_app_name">Unknown app</string>
<!-- Button text for stopping casting [CHAR LIMIT=60] -->
@@ -3303,6 +3308,8 @@
<string name="media_output_dialog_accessibility_seekbar">Volume</string>
<!-- Summary for media output volume of a device in percentage [CHAR LIMIT=NONE] -->
<string name="media_output_dialog_volume_percentage"><xliff:g id="percentage" example="10">%1$d</xliff:g>%%</string>
+ <!-- Title for Connected speakers expandable group. [CHAR LIMIT=NONE] -->
+ <string name="media_output_group_title_connected_speakers">Connected speakers</string>
<!-- Title for Speakers and Displays group. [CHAR LIMIT=NONE] -->
<string name="media_output_group_title_speakers_and_displays">Speakers &amp; Displays</string>
<!-- Title for Suggested Devices group. [CHAR LIMIT=NONE] -->
@@ -3452,6 +3459,10 @@
<string name="keyguard_try_fingerprint">Use fingerprint to open</string>
<!-- Accessibility announcement to inform user to unlock using the fingerprint sensor [CHAR LIMIT=NONE] -->
<string name="accessibility_fingerprint_bouncer">Authentication required. Touch the fingerprint sensor to authenticate.</string>
+ <!-- Accessibility action label for resuming animation -->
+ <string name="resume_animation">Resume animation</string>
+ <!-- Accessibility action label for pausing animation -->
+ <string name="pause_animation">Pause animation</string>
<!-- Content description for a chip in the status bar showing that the user is currently on a call. [CHAR LIMIT=NONE] -->
<string name="ongoing_call_content_description">Ongoing call</string>
diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml
index 0e1f99f28850..fb72123a0a3b 100644
--- a/packages/SystemUI/res/values/styles.xml
+++ b/packages/SystemUI/res/values/styles.xml
@@ -181,15 +181,19 @@
<item name="android:textColor">@androidprv:color/materialColorOnSurfaceVariant</item>
</style>
- <!-- This is hard coded to be sans-serif-condensed to match the icons -->
-
<style name="TextAppearance.QS.Status">
- <item name="android:fontFamily">@*android:string/config_headlineFontFamily</item>
+ <item name="android:fontFamily" android:featureFlag="!com.android.systemui.shade_header_font_update">@*android:string/config_headlineFontFamily</item>
+ <item name="android:fontFamily" android:featureFlag="com.android.systemui.shade_header_font_update">variable-body-medium-emphasized</item>
<item name="android:textColor">@color/shade_header_text_color</item>
<item name="android:textSize">14sp</item>
<item name="android:letterSpacing">0.01</item>
</style>
+ <style name="TextAppearance.QS.Status.Clock">
+ <item name="android:fontFamily" android:featureFlag="!com.android.systemui.shade_header_font_update">@*android:string/config_headlineFontFamily</item>
+ <item name="android:fontFamily" android:featureFlag="com.android.systemui.shade_header_font_update">variable-display-small-emphasized</item>
+ </style>
+
<style name="TextAppearance.QS.Status.Build">
<item name="android:textColor">?attr/onShadeInactiveVariant</item>
</style>
@@ -704,6 +708,33 @@
<item name="android:colorBackground">@color/media_dialog_background</item>
</style>
+ <style name="MediaOutput" />
+ <style name="MediaOutput.Dialog" />
+ <style name="MediaOutput.Dialog.QuickAccessButton" parent="@style/Widget.Material3.Button.OutlinedButton.Icon">
+ <item name="theme">@style/Theme.Material3.DynamicColors.DayNight</item>
+ <item name="android:paddingTop">6dp</item>
+ <item name="android:minHeight">32dp</item>
+ <item name="android:paddingBottom">6dp</item>
+ <item name="android:paddingStart">8dp</item>
+ <item name="android:paddingEnd">12dp</item>
+ <item name="android:insetTop">0dp</item>
+ <item name="android:insetBottom">0dp</item>
+ <item name="android:textColor">@color/media_dialog_on_surface_variant</item>
+ <item name="iconSize">18dp</item>
+ <item name="iconTint">@color/media_dialog_primary</item>
+ <item name="shapeAppearance">@style/ShapeAppearance.Material3.Corner.Small</item>
+ <item name="strokeColor">@color/media_dialog_outline_variant</item>
+ </style>
+
+ <style name="MediaOutput.Item" />
+ <style name="MediaOutput.Item.Icon">
+ <item name="android:layout_width">@dimen/media_output_item_icon_size</item>
+ <item name="android:layout_height">@dimen/media_output_item_icon_size</item>
+ <item name="android:padding">@dimen/media_output_item_icon_padding</item>
+ <item name="android:scaleType">fitCenter</item>
+ <item name="tint">@color/media_dialog_on_surface</item>
+ </style>
+
<style name="MediaOutputItemInactiveTitle">
<item name="android:textSize">16sp</item>
<item name="android:textColor">@color/media_dialog_item_main_content</item>
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/model/Task.java b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/model/Task.java
index a518c57bdd16..96307c7c301f 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/model/Task.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/model/Task.java
@@ -309,6 +309,13 @@ public class Task {
taskInfo.topActivity);
}
+ /**
+ * Creates a task object from the given [taskInfo].
+ */
+ public static Task from(TaskInfo taskInfo) {
+ return from(new TaskKey(taskInfo), taskInfo, /* isLocked= */ false);
+ }
+
public Task(TaskKey key) {
this.key = key;
this.taskDescription = new TaskDescription();
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationImpl.java b/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationImpl.java
index c6071a006408..63d56e662a50 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationImpl.java
@@ -53,6 +53,7 @@ import com.android.systemui.recents.LauncherProxyService;
import com.android.systemui.settings.DisplayTracker;
import com.android.systemui.statusbar.CommandQueue;
import com.android.systemui.util.settings.SecureSettings;
+import com.android.systemui.utils.windowmanager.WindowManagerProvider;
import java.io.PrintWriter;
import java.util.concurrent.Executor;
@@ -96,17 +97,19 @@ public class MagnificationImpl implements Magnification, CommandQueue.Callbacks
private final WindowMagnifierCallback mWindowMagnifierCallback;
private final SysUiState mSysUiState;
private final SecureSettings mSecureSettings;
+ private final WindowManagerProvider mWindowManagerProvider;
WindowMagnificationControllerSupplier(Context context, Handler handler,
WindowMagnifierCallback windowMagnifierCallback,
DisplayManager displayManager, SysUiState sysUiState,
- SecureSettings secureSettings) {
+ SecureSettings secureSettings, WindowManagerProvider windowManagerProvider) {
super(displayManager);
mContext = context;
mHandler = handler;
mWindowMagnifierCallback = windowMagnifierCallback;
mSysUiState = sysUiState;
mSecureSettings = secureSettings;
+ mWindowManagerProvider = windowManagerProvider;
}
@Override
@@ -114,8 +117,9 @@ public class MagnificationImpl implements Magnification, CommandQueue.Callbacks
final Context windowContext = mContext.createWindowContext(display,
TYPE_ACCESSIBILITY_OVERLAY,
/* options */ null);
+ final WindowManager windowManager = mWindowManagerProvider
+ .getWindowManager(windowContext);
windowContext.setTheme(com.android.systemui.res.R.style.Theme_SystemUI);
- final WindowManager windowManager = windowContext.getSystemService(WindowManager.class);
Supplier<SurfaceControlViewHost> scvhSupplier = () ->
new SurfaceControlViewHost(mContext,
@@ -146,17 +150,20 @@ public class MagnificationImpl implements Magnification, CommandQueue.Callbacks
private final Executor mExecutor;
private final DisplayManager mDisplayManager;
private final IWindowManager mIWindowManager;
+ private final WindowManagerProvider mWindowManagerProvider;
FullscreenMagnificationControllerSupplier(Context context,
DisplayManager displayManager,
Handler handler,
- Executor executor, IWindowManager iWindowManager) {
+ Executor executor, IWindowManager iWindowManager,
+ WindowManagerProvider windowManagerProvider) {
super(displayManager);
mContext = context;
mHandler = handler;
mExecutor = executor;
mDisplayManager = displayManager;
mIWindowManager = iWindowManager;
+ mWindowManagerProvider = windowManagerProvider;
}
@Override
@@ -172,7 +179,7 @@ public class MagnificationImpl implements Magnification, CommandQueue.Callbacks
mExecutor,
mDisplayManager,
windowContext.getSystemService(AccessibilityManager.class),
- windowContext.getSystemService(WindowManager.class),
+ mWindowManagerProvider.getWindowManager(windowContext),
mIWindowManager,
scvhSupplier);
}
@@ -188,15 +195,17 @@ public class MagnificationImpl implements Magnification, CommandQueue.Callbacks
private final Context mContext;
private final MagnificationSettingsController.Callback mSettingsControllerCallback;
private final SecureSettings mSecureSettings;
+ private final WindowManagerProvider mWindowManagerProvider;
SettingsSupplier(Context context,
MagnificationSettingsController.Callback settingsControllerCallback,
DisplayManager displayManager,
- SecureSettings secureSettings) {
+ SecureSettings secureSettings, WindowManagerProvider windowManagerProvider) {
super(displayManager);
mContext = context;
mSettingsControllerCallback = settingsControllerCallback;
mSecureSettings = secureSettings;
+ mWindowManagerProvider = windowManagerProvider;
}
@Override
@@ -204,12 +213,12 @@ public class MagnificationImpl implements Magnification, CommandQueue.Callbacks
final Context windowContext = mContext.createWindowContext(display,
TYPE_ACCESSIBILITY_OVERLAY, /* options */ null);
windowContext.setTheme(com.android.systemui.res.R.style.Theme_SystemUI);
-
return new MagnificationSettingsController(
windowContext,
new SfVsyncFrameCallbackProvider(),
mSettingsControllerCallback,
- mSecureSettings);
+ mSecureSettings,
+ mWindowManagerProvider);
}
}
@@ -223,10 +232,12 @@ public class MagnificationImpl implements Magnification, CommandQueue.Callbacks
SysUiState sysUiState, LauncherProxyService launcherProxyService,
SecureSettings secureSettings, DisplayTracker displayTracker,
DisplayManager displayManager, AccessibilityLogger a11yLogger,
- IWindowManager iWindowManager, AccessibilityManager accessibilityManager) {
+ IWindowManager iWindowManager, AccessibilityManager accessibilityManager,
+ WindowManagerProvider windowManagerProvider) {
this(context, mainHandler.getLooper(), executor, commandQueue,
modeSwitchesController, sysUiState, launcherProxyService, secureSettings,
- displayTracker, displayManager, a11yLogger, iWindowManager, accessibilityManager);
+ displayTracker, displayManager, a11yLogger, iWindowManager, accessibilityManager,
+ windowManagerProvider);
}
@VisibleForTesting
@@ -236,7 +247,8 @@ public class MagnificationImpl implements Magnification, CommandQueue.Callbacks
SecureSettings secureSettings, DisplayTracker displayTracker,
DisplayManager displayManager, AccessibilityLogger a11yLogger,
IWindowManager iWindowManager,
- AccessibilityManager accessibilityManager) {
+ AccessibilityManager accessibilityManager,
+ WindowManagerProvider windowManagerProvider) {
mHandler = new Handler(looper) {
@Override
public void handleMessage(@NonNull Message msg) {
@@ -255,11 +267,13 @@ public class MagnificationImpl implements Magnification, CommandQueue.Callbacks
mA11yLogger = a11yLogger;
mWindowMagnificationControllerSupplier = new WindowMagnificationControllerSupplier(context,
mHandler, mWindowMagnifierCallback,
- displayManager, sysUiState, secureSettings);
+ displayManager, sysUiState, secureSettings, windowManagerProvider);
mFullscreenMagnificationControllerSupplier = new FullscreenMagnificationControllerSupplier(
- context, displayManager, mHandler, mExecutor, iWindowManager);
+ context, displayManager, mHandler, mExecutor, iWindowManager,
+ windowManagerProvider);
mMagnificationSettingsSupplier = new SettingsSupplier(context,
- mMagnificationSettingsControllerCallback, displayManager, secureSettings);
+ mMagnificationSettingsControllerCallback, displayManager, secureSettings,
+ windowManagerProvider);
mModeSwitchesController.setClickListenerDelegate(
displayId -> mHandler.post(() -> {
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationSettingsController.java b/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationSettingsController.java
index 5af34f4ddb34..95206b88f6aa 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationSettingsController.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationSettingsController.java
@@ -30,6 +30,7 @@ import com.android.internal.accessibility.common.MagnificationConstants;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.graphics.SfVsyncFrameCallbackProvider;
import com.android.systemui.util.settings.SecureSettings;
+import com.android.systemui.utils.windowmanager.WindowManagerProvider;
/**
* A class to control {@link WindowMagnificationSettings} and receive settings panel callbacks by
@@ -60,8 +61,10 @@ public class MagnificationSettingsController implements ComponentCallbacks {
@UiContext Context context,
SfVsyncFrameCallbackProvider sfVsyncFrameProvider,
@NonNull Callback settingsControllerCallback,
- SecureSettings secureSettings) {
- this(context, sfVsyncFrameProvider, settingsControllerCallback, secureSettings, null);
+ SecureSettings secureSettings,
+ WindowManagerProvider windowManagerProvider) {
+ this(context, sfVsyncFrameProvider, settingsControllerCallback, secureSettings,
+ windowManagerProvider, null);
}
@VisibleForTesting
@@ -70,6 +73,7 @@ public class MagnificationSettingsController implements ComponentCallbacks {
SfVsyncFrameCallbackProvider sfVsyncFrameProvider,
@NonNull Callback settingsControllerCallback,
SecureSettings secureSettings,
+ WindowManagerProvider windowManagerProvider,
WindowMagnificationSettings windowMagnificationSettings) {
mContext = context.createWindowContext(
context.getDisplay(),
@@ -82,10 +86,10 @@ public class MagnificationSettingsController implements ComponentCallbacks {
if (windowMagnificationSettings != null) {
mWindowMagnificationSettings = windowMagnificationSettings;
} else {
- WindowManager wm = mContext.getSystemService(WindowManager.class);
+ WindowManager windowManager = windowManagerProvider.getWindowManager(mContext);
mWindowMagnificationSettings = new WindowMagnificationSettings(mContext,
mWindowMagnificationSettingsCallback,
- sfVsyncFrameProvider, secureSettings, wm);
+ sfVsyncFrameProvider, secureSettings, windowManager);
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/UdfpsOverlayInteractor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/UdfpsOverlayInteractor.kt
index 8a5e011cd3ce..2bb9809af30e 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/UdfpsOverlayInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/UdfpsOverlayInteractor.kt
@@ -16,6 +16,7 @@
package com.android.systemui.biometrics.domain.interactor
+import android.annotation.SuppressLint
import android.content.Context
import android.hardware.fingerprint.FingerprintManager
import android.util.Log
@@ -32,10 +33,14 @@ import javax.inject.Inject
import kotlin.math.max
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
@@ -131,6 +136,25 @@ constructor(
}
.distinctUntilChanged()
+ /**
+ * Event flow that emits every time the user taps the screen and a UDFPS guidance message is
+ * surfaced and then cleared. Modeled as a SharedFlow because a StateFlow fails to emit every
+ * event to the subscriber, causing missed Talkback feedback and incorrect focusability state of
+ * the UDFPS accessibility overlay.
+ */
+ @SuppressLint("SharedFlowCreation")
+ private val _clearAccessibilityOverlayMessageReason = MutableSharedFlow<String?>()
+
+ /** Indicates the reason for clearing the UDFPS accessibility overlay content description */
+ val clearAccessibilityOverlayMessageReason: SharedFlow<String?> =
+ _clearAccessibilityOverlayMessageReason.asSharedFlow()
+
+ suspend fun clearUdfpsAccessibilityOverlayMessage(reason: String) {
+ // Add delay to make sure we read the guidance message before clearing it
+ delay(1000)
+ _clearAccessibilityOverlayMessageReason.emit(reason)
+ }
+
companion object {
private const val TAG = "UdfpsOverlayInteractor"
}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt
index 3b22e13f29a2..80d06f4a2d37 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt
@@ -27,6 +27,7 @@ import android.util.Log
import android.view.MotionEvent
import android.view.View
import android.view.View.IMPORTANT_FOR_ACCESSIBILITY_NO
+import android.view.View.IMPORTANT_FOR_ACCESSIBILITY_YES
import android.view.accessibility.AccessibilityManager
import android.widget.Button
import android.widget.ImageView
@@ -43,7 +44,6 @@ import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.airbnb.lottie.LottieAnimationView
import com.airbnb.lottie.LottieCompositionFactory
-import com.android.app.tracing.coroutines.launchTraced as launch
import com.android.systemui.biometrics.Utils.ellipsize
import com.android.systemui.biometrics.shared.model.BiometricModalities
import com.android.systemui.biometrics.shared.model.BiometricModality
@@ -63,6 +63,7 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.launch
private const val TAG = "BiometricViewBinder"
@@ -123,25 +124,6 @@ object BiometricViewBinder {
val confirmationButton = view.requireViewById<Button>(R.id.button_confirm)
val retryButton = view.requireViewById<Button>(R.id.button_try_again)
- // Handles custom "Cancel Authentication" talkback action
- val cancelDelegate: AccessibilityDelegateCompat =
- object : AccessibilityDelegateCompat() {
- override fun onInitializeAccessibilityNodeInfo(
- host: View,
- info: AccessibilityNodeInfoCompat,
- ) {
- super.onInitializeAccessibilityNodeInfo(host, info)
- info.addAction(
- AccessibilityActionCompat(
- AccessibilityNodeInfoCompat.ACTION_CLICK,
- view.context.getString(R.string.biometric_dialog_cancel_authentication),
- )
- )
- }
- }
- ViewCompat.setAccessibilityDelegate(backgroundView, cancelDelegate)
- ViewCompat.setAccessibilityDelegate(cancelButton, cancelDelegate)
-
// TODO(b/330788871): temporary workaround for the unsafe callbacks & legacy controllers
val adapter =
Spaghetti(
@@ -155,6 +137,33 @@ object BiometricViewBinder {
var boundSize = false
view.repeatWhenAttached {
+ // Handles custom "Cancel Authentication" talkback action
+ val cancelDelegate: AccessibilityDelegateCompat =
+ object : AccessibilityDelegateCompat() {
+ override fun onInitializeAccessibilityNodeInfo(
+ host: View,
+ info: AccessibilityNodeInfoCompat,
+ ) {
+ super.onInitializeAccessibilityNodeInfo(host, info)
+ lifecycleScope.launch {
+ // Clears UDFPS guidance hint after focus moves to cancel view
+ viewModel.onClearUdfpsGuidanceHint(
+ accessibilityManager.isTouchExplorationEnabled
+ )
+ }
+ info.addAction(
+ AccessibilityActionCompat(
+ AccessibilityNodeInfoCompat.ACTION_CLICK,
+ view.context.getString(
+ R.string.biometric_dialog_cancel_authentication
+ ),
+ )
+ )
+ }
+ }
+ ViewCompat.setAccessibilityDelegate(backgroundView, cancelDelegate)
+ ViewCompat.setAccessibilityDelegate(cancelButton, cancelDelegate)
+
// these do not change and need to be set before any size transitions
val modalities = viewModel.modalities.first()
@@ -404,11 +413,16 @@ object BiometricViewBinder {
}
false
}
+
launch {
viewModel.accessibilityHint.collect { message ->
- if (message.isNotBlank()) {
- udfpsGuidanceView.contentDescription = message
- }
+ udfpsGuidanceView.importantForAccessibility =
+ if (message == null) {
+ IMPORTANT_FOR_ACCESSIBILITY_NO
+ } else {
+ IMPORTANT_FOR_ACCESSIBILITY_YES
+ }
+ udfpsGuidanceView.contentDescription = message
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinder.kt
index ceb2b10ab517..1d7562ea64b9 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinder.kt
@@ -25,6 +25,9 @@ import android.view.LayoutInflater
import android.view.View
import android.view.WindowManager
import android.view.accessibility.AccessibilityEvent
+import androidx.core.view.AccessibilityDelegateCompat
+import androidx.core.view.ViewCompat
+import androidx.core.view.accessibility.AccessibilityNodeInfoCompat
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.repeatOnLifecycle
import com.airbnb.lottie.LottieAnimationView
@@ -67,6 +70,59 @@ constructor(
private val sfpsSensorInteractor: Lazy<SideFpsSensorInteractor>,
private val windowManager: Lazy<WindowManager>,
) : CoreStartable {
+ private val pauseDelegate: AccessibilityDelegateCompat =
+ object : AccessibilityDelegateCompat() {
+ override fun onInitializeAccessibilityNodeInfo(
+ host: View,
+ info: AccessibilityNodeInfoCompat,
+ ) {
+ super.onInitializeAccessibilityNodeInfo(host, info)
+ info.addAction(
+ AccessibilityNodeInfoCompat.AccessibilityActionCompat(
+ AccessibilityNodeInfoCompat.ACTION_CLICK,
+ host.context.getString(R.string.pause_animation),
+ )
+ )
+ }
+
+ override fun dispatchPopulateAccessibilityEvent(
+ host: View,
+ event: AccessibilityEvent,
+ ): Boolean {
+ return if (event.getEventType() === AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {
+ true
+ } else {
+ super.dispatchPopulateAccessibilityEvent(host, event)
+ }
+ }
+ }
+
+ private val resumeDelegate: AccessibilityDelegateCompat =
+ object : AccessibilityDelegateCompat() {
+ override fun onInitializeAccessibilityNodeInfo(
+ host: View,
+ info: AccessibilityNodeInfoCompat,
+ ) {
+ super.onInitializeAccessibilityNodeInfo(host, info)
+ info.addAction(
+ AccessibilityNodeInfoCompat.AccessibilityActionCompat(
+ AccessibilityNodeInfoCompat.ACTION_CLICK,
+ host.context.getString(R.string.resume_animation),
+ )
+ )
+ }
+
+ override fun dispatchPopulateAccessibilityEvent(
+ host: View,
+ event: AccessibilityEvent,
+ ): Boolean {
+ return if (event.getEventType() === AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {
+ true
+ } else {
+ super.dispatchPopulateAccessibilityEvent(host, event)
+ }
+ }
+ }
override fun start() {
applicationScope.launch {
@@ -135,6 +191,7 @@ constructor(
overlayView!!.setOnClickListener { v ->
v.requireViewById<LottieAnimationView>(R.id.sidefps_animation).toggleAnimation()
}
+ ViewCompat.setAccessibilityDelegate(overlayView!!, pauseDelegate)
Log.d(TAG, "show(): adding overlayView $overlayView")
windowManager.get().addView(overlayView, overlayViewModel.defaultOverlayViewParams)
}
@@ -177,29 +234,6 @@ constructor(
overlayShowAnimator.start()
- /**
- * Intercepts TYPE_WINDOW_STATE_CHANGED accessibility event, preventing Talkback
- * from speaking @string/accessibility_fingerprint_label twice when sensor location
- * indicator is in focus
- */
- it.setAccessibilityDelegate(
- object : View.AccessibilityDelegate() {
- override fun dispatchPopulateAccessibilityEvent(
- host: View,
- event: AccessibilityEvent,
- ): Boolean {
- return if (
- event.getEventType() ===
- AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED
- ) {
- true
- } else {
- super.dispatchPopulateAccessibilityEvent(host, event)
- }
- }
- }
- )
-
repeatOnLifecycle(Lifecycle.State.STARTED) {
launch {
viewModel.lottieCallbacks.collect { callbacks ->
@@ -224,6 +258,16 @@ constructor(
}
}
}
+
+ private fun LottieAnimationView.toggleAnimation() {
+ if (isAnimating) {
+ pauseAnimation()
+ ViewCompat.setAccessibilityDelegate(this, resumeDelegate)
+ } else {
+ resumeAnimation()
+ ViewCompat.setAccessibilityDelegate(this, pauseDelegate)
+ }
+ }
}
private fun LottieAnimationView.addOverlayDynamicColor(colorCallbacks: List<LottieCallback>) {
@@ -236,11 +280,3 @@ private fun LottieAnimationView.addOverlayDynamicColor(colorCallbacks: List<Lott
resumeAnimation()
}
}
-
-fun LottieAnimationView.toggleAnimation() {
- if (isAnimating) {
- pauseAnimation()
- } else {
- resumeAnimation()
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt
index 4e17a2658ee7..27fc1878cc99 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt
@@ -187,10 +187,10 @@ constructor(
}
}
- private val _accessibilityHint = MutableSharedFlow<String>()
+ private val _accessibilityHint = MutableSharedFlow<String?>()
/** Hint for talkback directional guidance */
- val accessibilityHint: Flow<String> = _accessibilityHint.asSharedFlow()
+ val accessibilityHint: Flow<String?> = _accessibilityHint.asSharedFlow()
private val _isAuthenticating: MutableStateFlow<Boolean> = MutableStateFlow(false)
@@ -923,6 +923,19 @@ constructor(
return false
}
+ /** Clears the message used for UDFPS directional guidance */
+ suspend fun onClearUdfpsGuidanceHint(touchExplorationEnabled: Boolean) {
+ if (
+ modalities.first().hasUdfps &&
+ touchExplorationEnabled &&
+ !isAuthenticated.first().isAuthenticated
+ ) {
+ // Add delay to make sure we read the guidance message before clearing it
+ delay(1000)
+ _accessibilityHint.emit(null)
+ }
+ }
+
/**
* Switch to the credential view.
*
diff --git a/packages/SystemUI/src/com/android/systemui/brightness/ui/compose/BrightnessSlider.kt b/packages/SystemUI/src/com/android/systemui/brightness/ui/compose/BrightnessSlider.kt
index 6aeb35b3b158..99f299918969 100644
--- a/packages/SystemUI/src/com/android/systemui/brightness/ui/compose/BrightnessSlider.kt
+++ b/packages/SystemUI/src/com/android/systemui/brightness/ui/compose/BrightnessSlider.kt
@@ -37,6 +37,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Slider
+import androidx.compose.material3.SliderColors
import androidx.compose.material3.SliderDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
@@ -73,6 +74,7 @@ import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.android.app.tracing.coroutines.launchTraced as launch
import com.android.compose.modifiers.padding
+import com.android.compose.theme.LocalAndroidColorScheme
import com.android.compose.ui.graphics.drawInOverlay
import com.android.systemui.Flags
import com.android.systemui.biometrics.Utils.toBitmap
@@ -139,7 +141,7 @@ fun BrightnessSlider(
} else {
null
}
- val colors = SliderDefaults.colors()
+ val colors = colors()
// The value state is recreated every time gammaValue changes, so we recreate this derivedState
// We have to use value as that's the value that changes when the user is dragging (gammaValue
@@ -211,6 +213,7 @@ fun BrightnessSlider(
interactionSource = interactionSource,
enabled = enabled,
thumbSize = DpSize(4.dp, 52.dp),
+ colors = colors,
)
},
track = { sliderState ->
@@ -293,6 +296,7 @@ fun BrightnessSlider(
trackInsideCornerSize = 2.dp,
drawStopIndicator = null,
thumbTrackGapSize = ThumbTrackGapSize,
+ colors = colors,
)
},
)
@@ -441,3 +445,13 @@ object BrightnessSliderMotionTestKeys {
val ActiveIconAlpha = MotionTestValueKey<Float>("activeIconAlpha")
val InactiveIconAlpha = MotionTestValueKey<Float>("inactiveIconAlpha")
}
+
+@Composable
+private fun colors(): SliderColors {
+ return SliderDefaults.colors()
+ .copy(
+ inactiveTrackColor = LocalAndroidColorScheme.current.surfaceEffect2,
+ activeTickColor = MaterialTheme.colorScheme.onPrimary,
+ inactiveTickColor = MaterialTheme.colorScheme.onSurface,
+ )
+}
diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ActionIntentCreator.kt b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ActionIntentCreator.kt
index 8cebe04d4e01..96dbcc5867c1 100644
--- a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ActionIntentCreator.kt
+++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ActionIntentCreator.kt
@@ -21,20 +21,30 @@ import android.content.ClipDescription
import android.content.ComponentName
import android.content.Context
import android.content.Intent
+import android.content.pm.PackageManager
+import android.content.pm.PackageManager.NameNotFoundException
import android.net.Uri
import android.text.TextUtils
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.res.R
import java.util.function.Consumer
import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
@SysUISingleton
class ActionIntentCreator
@Inject
-constructor(@Application private val applicationScope: CoroutineScope) : IntentCreator {
+constructor(
+ private val context: Context,
+ private val packageManager: PackageManager,
+ @Application private val applicationScope: CoroutineScope,
+ @Background private val backgroundDispatcher: CoroutineDispatcher,
+) : IntentCreator {
override fun getTextEditorIntent(context: Context?) =
Intent(context, EditTextActivity::class.java).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
@@ -72,11 +82,9 @@ constructor(@Application private val applicationScope: CoroutineScope) : IntentC
}
suspend fun getImageEditIntent(uri: Uri?, context: Context): Intent {
- val editorPackage = context.getString(R.string.config_screenshotEditor)
return Intent(Intent.ACTION_EDIT).apply {
- if (!TextUtils.isEmpty(editorPackage)) {
- setComponent(ComponentName.unflattenFromString(editorPackage))
- }
+ // Use the preferred editor if it's available, otherwise fall back to the default editor
+ component = preferredEditor() ?: defaultEditor()
setDataAndType(uri, "image/*")
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
@@ -105,6 +113,39 @@ constructor(@Application private val applicationScope: CoroutineScope) : IntentC
}
}
+ private suspend fun preferredEditor(): ComponentName? =
+ runCatching {
+ val preferredEditor = context.getString(R.string.config_preferredScreenshotEditor)
+ val component = ComponentName.unflattenFromString(preferredEditor) ?: return null
+
+ return if (isComponentAvailable(component)) component else null
+ }
+ .getOrNull()
+
+ private suspend fun isComponentAvailable(component: ComponentName): Boolean =
+ withContext(backgroundDispatcher) {
+ try {
+ val info =
+ packageManager.getPackageInfo(
+ component.packageName,
+ PackageManager.GET_ACTIVITIES,
+ )
+ info.activities?.firstOrNull {
+ it.componentName.className == component.className
+ } != null
+ } catch (e: NameNotFoundException) {
+ false
+ }
+ }
+
+ private fun defaultEditor(): ComponentName? =
+ runCatching {
+ context.getString(R.string.config_screenshotEditor).let {
+ ComponentName.unflattenFromString(it)
+ }
+ }
+ .getOrNull()
+
companion object {
private const val EXTRA_EDIT_SOURCE: String = "edit_source"
private const val EDIT_SOURCE_CLIPBOARD: String = "clipboard"
diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/dagger/ClipboardOverlayModule.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/dagger/ClipboardOverlayModule.java
index 6d58443d5c8c..7a60cce63a33 100644
--- a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/dagger/ClipboardOverlayModule.java
+++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/dagger/ClipboardOverlayModule.java
@@ -36,6 +36,7 @@ import com.android.systemui.clipboardoverlay.IntentCreator;
import com.android.systemui.res.R;
import com.android.systemui.settings.DisplayTracker;
import com.android.systemui.settings.UserTracker;
+import com.android.systemui.utils.windowmanager.WindowManagerProvider;
import dagger.Lazy;
import dagger.Module;
@@ -85,8 +86,9 @@ public interface ClipboardOverlayModule {
*/
@Provides
@OverlayWindowContext
- static WindowManager provideWindowManager(@OverlayWindowContext Context context) {
- return context.getSystemService(WindowManager.class);
+ static WindowManager provideWindowManager(@OverlayWindowContext Context context,
+ WindowManagerProvider windowManagerProvider) {
+ return windowManagerProvider.getWindowManager(context);
}
@Provides
diff --git a/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt b/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt
index a31c0bd35453..2875b7e2ae92 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt
@@ -108,6 +108,9 @@ interface CommunalModule {
const val LAUNCHER_PACKAGE = "launcher_package"
const val SWIPE_TO_HUB = "swipe_to_hub"
const val SHOW_UMO = "show_umo"
+ const val TOUCH_NOTIFICATION_RATE_LIMIT = "TOUCH_NOTIFICATION_RATE_LIMIT"
+
+ const val TOUCH_NOTIFIFCATION_RATE_LIMIT_MS = 100
@Provides
@Communal
@@ -159,5 +162,11 @@ interface CommunalModule {
fun provideShowUmo(@Main resources: Resources): Boolean {
return resources.getBoolean(R.bool.config_showUmoOnHub)
}
+
+ @Provides
+ @Named(TOUCH_NOTIFICATION_RATE_LIMIT)
+ fun providesRateLimit(): Int {
+ return TOUCH_NOTIFIFCATION_RATE_LIMIT_MS
+ }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSceneRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSceneRepository.kt
index bf4445ba18db..100e21d34c42 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSceneRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSceneRepository.kt
@@ -16,18 +16,16 @@
package com.android.systemui.communal.data.repository
+import android.content.res.Configuration
import com.android.app.tracing.coroutines.launchTraced as launch
import com.android.compose.animation.scene.ObservableTransitionState
-import com.android.compose.animation.scene.OverlayKey
import com.android.compose.animation.scene.SceneKey
import com.android.compose.animation.scene.TransitionKey
import com.android.systemui.communal.dagger.Communal
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.scene.shared.model.SceneDataSource
-import com.android.systemui.scene.shared.model.SceneDataSourceDelegator
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
@@ -49,31 +47,32 @@ interface CommunalSceneRepository {
/** Exposes the transition state of the communal [SceneTransitionLayout]. */
val transitionState: StateFlow<ObservableTransitionState>
+ /** Current orientation of the communal container. */
+ val communalContainerOrientation: StateFlow<Int>
+
/** Updates the requested scene. */
fun changeScene(toScene: SceneKey, transitionKey: TransitionKey? = null)
/** Immediately snaps to the desired scene. */
fun snapToScene(toScene: SceneKey)
- /** Shows the hub from a power button press. */
- suspend fun showHubFromPowerButton()
-
/**
* Updates the transition state of the hub [SceneTransitionLayout].
*
* Note that you must call is with `null` when the UI is done or risk a memory leak.
*/
fun setTransitionState(transitionState: Flow<ObservableTransitionState>?)
+
+ /** Set the current orientation of the communal container. */
+ fun setCommunalContainerOrientation(orientation: Int)
}
@SysUISingleton
class CommunalSceneRepositoryImpl
@Inject
constructor(
- @Application private val applicationScope: CoroutineScope,
@Background backgroundScope: CoroutineScope,
@Communal private val sceneDataSource: SceneDataSource,
- @Communal private val delegator: SceneDataSourceDelegator,
) : CommunalSceneRepository {
override val currentScene: StateFlow<SceneKey> = sceneDataSource.currentScene
@@ -89,32 +88,21 @@ constructor(
initialValue = defaultTransitionState,
)
+ private val _communalContainerOrientation =
+ MutableStateFlow(Configuration.ORIENTATION_UNDEFINED)
+ override val communalContainerOrientation: StateFlow<Int> =
+ _communalContainerOrientation.asStateFlow()
+
override fun changeScene(toScene: SceneKey, transitionKey: TransitionKey?) {
- applicationScope.launch {
- // SceneTransitionLayout state updates must be triggered on the thread the STL was
- // created on.
- sceneDataSource.changeScene(toScene, transitionKey)
- }
+ sceneDataSource.changeScene(toScene, transitionKey)
}
override fun snapToScene(toScene: SceneKey) {
- applicationScope.launch {
- // SceneTransitionLayout state updates must be triggered on the thread the STL was
- // created on.
- sceneDataSource.snapToScene(toScene)
- }
+ sceneDataSource.snapToScene(toScene)
}
- override suspend fun showHubFromPowerButton() {
- // If keyguard is not showing yet, the hub view is not ready and the
- // [SceneDataSourceDelegator] will still be using the default [NoOpSceneDataSource]
- // and initial key, which is Blank. This means that when the hub container loads, it
- // will default to not showing the hub. Attempting to set the scene in this state
- // is simply ignored by the [NoOpSceneDataSource]. Instead, we temporarily override
- // it with a new one that defaults to Communal. This delegate will be overwritten
- // once the [CommunalContainer] loads.
- // TODO(b/392969914): show the hub first instead of forcing the scene.
- delegator.setDelegate(NoOpSceneDataSource(CommunalScenes.Communal))
+ override fun setCommunalContainerOrientation(orientation: Int) {
+ _communalContainerOrientation.value = orientation
}
/**
@@ -125,33 +113,4 @@ constructor(
override fun setTransitionState(transitionState: Flow<ObservableTransitionState>?) {
_transitionState.value = transitionState
}
-
- /** Noop implementation of a scene data source that always returns the initial [SceneKey]. */
- private class NoOpSceneDataSource(initialSceneKey: SceneKey) : SceneDataSource {
- override val currentScene: StateFlow<SceneKey> =
- MutableStateFlow(initialSceneKey).asStateFlow()
-
- override val currentOverlays: StateFlow<Set<OverlayKey>> =
- MutableStateFlow(emptySet<OverlayKey>()).asStateFlow()
-
- override fun changeScene(toScene: SceneKey, transitionKey: TransitionKey?) = Unit
-
- override fun snapToScene(toScene: SceneKey) = Unit
-
- override fun showOverlay(overlay: OverlayKey, transitionKey: TransitionKey?) = Unit
-
- override fun hideOverlay(overlay: OverlayKey, transitionKey: TransitionKey?) = Unit
-
- override fun replaceOverlay(
- from: OverlayKey,
- to: OverlayKey,
- transitionKey: TransitionKey?,
- ) = Unit
-
- override fun instantlyShowOverlay(overlay: OverlayKey) = Unit
-
- override fun instantlyHideOverlay(overlay: OverlayKey) = Unit
-
- override fun freezeAndAnimateToCurrentState() = Unit
- }
}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSettingsRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSettingsRepository.kt
index 42a345b7deb4..8d599541b184 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSettingsRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSettingsRepository.kt
@@ -206,8 +206,11 @@ constructor(
}
.flowOn(bgDispatcher)
- override fun getWhenToStartHubState(user: UserInfo): Flow<WhenToStartHub> =
- secureSettings
+ override fun getWhenToStartHubState(user: UserInfo): Flow<WhenToStartHub> {
+ if (!getV2FlagEnabled()) {
+ return MutableStateFlow(WhenToStartHub.NEVER)
+ }
+ return secureSettings
.observerFlow(
userId = user.id,
names = arrayOf(Settings.Secure.WHEN_TO_START_GLANCEABLE_HUB),
@@ -225,11 +228,13 @@ constructor(
Settings.Secure.GLANCEABLE_HUB_START_CHARGING -> WhenToStartHub.WHILE_CHARGING
Settings.Secure.GLANCEABLE_HUB_START_CHARGING_UPRIGHT ->
WhenToStartHub.WHILE_CHARGING_AND_POSTURED
+
Settings.Secure.GLANCEABLE_HUB_START_DOCKED -> WhenToStartHub.WHILE_DOCKED
else -> WhenToStartHub.NEVER
}
}
.flowOn(bgDispatcher)
+ }
override fun getAllowedByDevicePolicy(user: UserInfo): Flow<Boolean> =
broadcastDispatcher
diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt
index 684c52ad45f3..272439e68f71 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt
@@ -149,9 +149,18 @@ constructor(
val isCommunalEnabled: StateFlow<Boolean> = communalSettingsInteractor.isCommunalEnabled
/** Whether communal features are enabled and available. */
- val isCommunalAvailable: Flow<Boolean> =
- allOf(communalSettingsInteractor.isCommunalEnabled, keyguardInteractor.isKeyguardShowing)
- .distinctUntilChanged()
+ @Deprecated("Use isCommunalEnabled instead", replaceWith = ReplaceWith("isCommunalEnabled"))
+ val isCommunalAvailable: Flow<Boolean> by lazy {
+ val availableFlow =
+ if (communalSettingsInteractor.isV2FlagEnabled()) {
+ communalSettingsInteractor.isCommunalEnabled
+ } else {
+ allOf(
+ communalSettingsInteractor.isCommunalEnabled,
+ keyguardInteractor.isKeyguardShowing,
+ )
+ }
+ availableFlow
.onEach { available ->
logger.i({ "Communal is ${if (bool1) "" else "un"}available" }) {
bool1 = available
@@ -167,6 +176,7 @@ constructor(
started = SharingStarted.WhileSubscribed(),
replay = 1,
)
+ }
private val _isDisclaimerDismissed = MutableStateFlow(false)
val isDisclaimerDismissed: Flow<Boolean> = _isDisclaimerDismissed.asStateFlow()
@@ -467,6 +477,7 @@ constructor(
size = CommunalContentSize.toSize(widget.spanY),
)
}
+
is CommunalWidgetContentModel.Pending -> {
WidgetContent.PendingWidget(
appWidgetId = widget.appWidgetId,
@@ -493,6 +504,7 @@ constructor(
when (model) {
is CommunalWidgetContentModel.Available ->
model.providerInfo.profile.identifier
+
is CommunalWidgetContentModel.Pending -> model.user.identifier
}
uid != disallowedByDevicePolicyUser.id
@@ -576,6 +588,7 @@ constructor(
when (widget) {
is CommunalWidgetContentModel.Available ->
currentUserIds.contains(widget.providerInfo.profile?.identifier)
+
is CommunalWidgetContentModel.Pending -> true
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneInteractor.kt
index fed99d71fa3b..80222299177b 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneInteractor.kt
@@ -16,6 +16,7 @@
package com.android.systemui.communal.domain.interactor
+import android.content.res.Configuration
import com.android.app.tracing.coroutines.launchTraced as launch
import com.android.compose.animation.scene.ObservableTransitionState
import com.android.compose.animation.scene.SceneKey
@@ -28,13 +29,16 @@ import com.android.systemui.communal.shared.model.CommunalScenes.toSceneContaine
import com.android.systemui.communal.shared.model.EditModeState
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.scene.domain.interactor.SceneInteractor
import com.android.systemui.scene.shared.flag.SceneContainerFlag
import com.android.systemui.scene.shared.model.Scenes
+import com.android.systemui.statusbar.policy.KeyguardStateController
import com.android.systemui.util.kotlin.BooleanFlowOperators.allOf
import com.android.systemui.util.kotlin.pairwiseBy
import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
@@ -55,9 +59,11 @@ class CommunalSceneInteractor
@Inject
constructor(
@Application private val applicationScope: CoroutineScope,
+ @Main private val mainImmediateDispatcher: CoroutineDispatcher,
private val repository: CommunalSceneRepository,
private val logger: CommunalSceneLogger,
private val sceneInteractor: SceneInteractor,
+ private val keyguardStateController: KeyguardStateController,
) {
private val _isLaunchingWidget = MutableStateFlow(false)
@@ -68,6 +74,30 @@ constructor(
_isLaunchingWidget.value = launching
}
+ /**
+ * Whether screen will be rotated to portrait if transitioned out of hub to keyguard screens.
+ */
+ var willRotateToPortrait: Flow<Boolean> =
+ repository.communalContainerOrientation
+ .map {
+ it == Configuration.ORIENTATION_LANDSCAPE &&
+ !keyguardStateController.isKeyguardScreenRotationAllowed()
+ }
+ .distinctUntilChanged()
+
+ /** Whether communal container is rotated to portrait. Emits an initial value of false. */
+ val rotatedToPortrait: StateFlow<Boolean> =
+ repository.communalContainerOrientation
+ .pairwiseBy(initialValue = false) { old, new ->
+ old == Configuration.ORIENTATION_LANDSCAPE &&
+ new == Configuration.ORIENTATION_PORTRAIT
+ }
+ .stateIn(applicationScope, SharingStarted.Eagerly, false)
+
+ fun setCommunalContainerOrientation(orientation: Int) {
+ repository.setCommunalContainerOrientation(orientation)
+ }
+
fun interface OnSceneAboutToChangeListener {
/** Notifies that the scene is about to change to [toScene]. */
fun onSceneAboutToChange(toScene: SceneKey, keyguardState: KeyguardState?)
@@ -86,6 +116,12 @@ constructor(
onSceneAboutToChangeListener.add(processor)
}
+ /** Unregisters a previously registered listener. */
+ fun unregisterSceneStateProcessor(processor: OnSceneAboutToChangeListener) {
+ SceneContainerFlag.assertInLegacyMode()
+ onSceneAboutToChangeListener.remove(processor)
+ }
+
/**
* Asks for an asynchronous scene witch to [newScene], which will use the corresponding
* installed transition or the one specified by [transitionKey], if provided.
@@ -96,7 +132,7 @@ constructor(
transitionKey: TransitionKey? = null,
keyguardState: KeyguardState? = null,
) {
- applicationScope.launch("$TAG#changeScene") {
+ applicationScope.launch("$TAG#changeScene", mainImmediateDispatcher) {
if (SceneContainerFlag.isEnabled) {
sceneInteractor.changeScene(
toScene = newScene.toSceneContainerSceneKey(),
@@ -148,29 +184,6 @@ constructor(
}
}
- fun showHubFromPowerButton() {
- val loggingReason = "showing hub from power button"
- applicationScope.launch("$TAG#showHubFromPowerButton") {
- if (SceneContainerFlag.isEnabled) {
- sceneInteractor.changeScene(
- toScene = CommunalScenes.Communal.toSceneContainerSceneKey(),
- loggingReason = loggingReason,
- )
- return@launch
- }
-
- if (currentScene.value == CommunalScenes.Communal) return@launch
- logger.logSceneChangeRequested(
- from = currentScene.value,
- to = CommunalScenes.Communal,
- reason = loggingReason,
- isInstant = true,
- )
- notifyListeners(CommunalScenes.Communal, null)
- repository.showHubFromPowerButton()
- }
- }
-
private fun notifyListeners(newScene: SceneKey, keyguardState: KeyguardState?) {
onSceneAboutToChangeListener.forEach { it.onSceneAboutToChange(newScene, keyguardState) }
}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractor.kt
index 477b87119563..89d738ef3bcc 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractor.kt
@@ -25,6 +25,7 @@ import com.android.systemui.communal.data.repository.CommunalSceneTransitionRepo
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.Main
import com.android.systemui.keyguard.domain.interactor.InternalKeyguardTransitionInteractor
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
@@ -37,6 +38,7 @@ import com.android.systemui.scene.shared.flag.SceneContainerFlag
import com.android.systemui.util.kotlin.pairwise
import java.util.UUID
import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.SharingStarted
@@ -64,6 +66,7 @@ constructor(
val internalTransitionInteractor: InternalKeyguardTransitionInteractor,
private val settingsInteractor: CommunalSettingsInteractor,
@Application private val applicationScope: CoroutineScope,
+ @Main private val mainImmediateDispatcher: CoroutineDispatcher,
private val sceneInteractor: CommunalSceneInteractor,
private val repository: CommunalSceneTransitionRepository,
private val powerInteractor: PowerInteractor,
@@ -143,7 +146,7 @@ constructor(
/** Monitors [SceneTransitionLayout] state and updates KTF state accordingly. */
private fun listenForSceneTransitionProgress() {
- applicationScope.launch {
+ applicationScope.launch("$TAG#listenForSceneTransitionProgress", mainImmediateDispatcher) {
sceneInteractor.transitionState
.pairwise(ObservableTransitionState.Idle(CommunalScenes.Blank))
.collect { (prevTransition, transition) ->
@@ -256,7 +259,10 @@ constructor(
private fun collectProgress(transition: ObservableTransitionState.Transition) {
progressJob?.cancel()
- progressJob = applicationScope.launch { transition.progress.collect { updateProgress(it) } }
+ progressJob =
+ applicationScope.launch("$TAG#collectProgress", mainImmediateDispatcher) {
+ transition.progress.collect { updateProgress(it) }
+ }
}
private suspend fun startTransitionFromGlanceableHub() {
@@ -300,4 +306,8 @@ constructor(
TransitionState.RUNNING,
)
}
+
+ private companion object {
+ const val TAG = "CommunalSceneTransitionInteractor"
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/posturing/domain/interactor/PosturingInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/posturing/domain/interactor/PosturingInteractor.kt
index e487590d87d7..b9a420c45262 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/posturing/domain/interactor/PosturingInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/posturing/domain/interactor/PosturingInteractor.kt
@@ -18,9 +18,6 @@ package com.android.systemui.communal.posturing.domain.interactor
import android.annotation.SuppressLint
import android.hardware.Sensor
-import android.hardware.TriggerEvent
-import android.hardware.TriggerEventListener
-import android.service.dreams.Flags.allowDreamWhenPostured
import com.android.systemui.communal.posturing.data.model.PositionState
import com.android.systemui.communal.posturing.data.repository.PosturingRepository
import com.android.systemui.communal.posturing.shared.model.PosturedState
@@ -31,20 +28,19 @@ import com.android.systemui.log.LogBuffer
import com.android.systemui.log.core.Logger
import com.android.systemui.log.dagger.CommunalLog
import com.android.systemui.util.kotlin.BooleanFlowOperators.allOf
+import com.android.systemui.util.kotlin.observeTriggerSensor
import com.android.systemui.util.kotlin.slidingWindow
import com.android.systemui.util.sensors.AsyncSensorManager
import com.android.systemui.util.time.SystemClock
-import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
-import java.util.concurrent.atomic.AtomicBoolean
import javax.inject.Inject
import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.CoroutineDispatcher
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.combine
+import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.filterNot
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
@@ -170,49 +166,18 @@ constructor(
* NOTE: Due to smoothing, this signal may be delayed to ensure we have a stable reading before
* being considered postured.
*/
- val postured: Flow<Boolean> by lazy {
- if (allowDreamWhenPostured()) {
- combine(posturedSmoothed, debugPostured) { postured, debugValue ->
- debugValue.asBoolean() ?: postured.asBoolean() ?: false
- }
- } else {
- MutableStateFlow(false)
+ val postured: Flow<Boolean> =
+ combine(posturedSmoothed, debugPostured) { postured, debugValue ->
+ debugValue.asBoolean() ?: postured.asBoolean() ?: false
}
- }
/**
* Helper for observing a trigger sensor, which automatically unregisters itself after it
* executes once.
*/
- private fun observeTriggerSensor(type: Int): Flow<Unit> = conflatedCallbackFlow {
- val sensor = asyncSensorManager.getDefaultSensor(type)
- val isRegistered = AtomicBoolean(false)
-
- fun registerCallbackInternal(callback: TriggerEventListener) {
- if (isRegistered.compareAndSet(false, true)) {
- asyncSensorManager.requestTriggerSensor(callback, sensor)
- }
- }
-
- val callback =
- object : TriggerEventListener() {
- override fun onTrigger(event: TriggerEvent) {
- trySend(Unit)
- if (isRegistered.getAndSet(false)) {
- registerCallbackInternal(this)
- }
- }
- }
-
- if (sensor != null) {
- registerCallbackInternal(callback)
- }
-
- awaitClose {
- if (isRegistered.getAndSet(false)) {
- asyncSensorManager.cancelTriggerSensor(callback, sensor)
- }
- }
+ private fun observeTriggerSensor(type: Int): Flow<Unit> {
+ val sensor = asyncSensorManager.getDefaultSensor(type) ?: return emptyFlow()
+ return asyncSensorManager.observeTriggerSensor(sensor)
}
companion object {
diff --git a/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalTransitionKeys.kt b/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalTransitionKeys.kt
index a84c45732169..49dc59ac0004 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalTransitionKeys.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalTransitionKeys.kt
@@ -33,4 +33,6 @@ object CommunalTransitionKeys {
val FromEditMode = TransitionKey("FromEditMode")
/** Swipes the glanceable hub in/out of view */
val Swipe = TransitionKey("Swipe")
+ /** Swipes out of glanceable hub in landscape orientation */
+ val SwipeInLandscape = TransitionKey("SwipeInLandscape")
}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt
index 5a4b0b0e2d24..a6309d1be03d 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt
@@ -386,6 +386,11 @@ constructor(
}
}
+ val swipeFromHubInLandscape: Flow<Boolean> = communalSceneInteractor.willRotateToPortrait
+
+ fun onOrientationChange(orientation: Int) =
+ communalSceneInteractor.setCommunalContainerOrientation(orientation)
+
companion object {
const val POPUP_AUTO_HIDE_TIMEOUT_MS = 12000L
}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/util/UserTouchActivityNotifier.kt b/packages/SystemUI/src/com/android/systemui/communal/util/UserTouchActivityNotifier.kt
new file mode 100644
index 000000000000..fec98a311fbd
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/communal/util/UserTouchActivityNotifier.kt
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.communal.util
+
+import android.view.MotionEvent
+import com.android.systemui.communal.dagger.CommunalModule.Companion.TOUCH_NOTIFICATION_RATE_LIMIT
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.power.domain.interactor.PowerInteractor
+import javax.inject.Inject
+import javax.inject.Named
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+
+/**
+ * {@link UserTouchActivityNotifier} helps rate limit the user activity notifications sent to {@link
+ * PowerManager} from a single touch source.
+ */
+class UserTouchActivityNotifier
+@Inject
+constructor(
+ @Background private val scope: CoroutineScope,
+ private val powerInteractor: PowerInteractor,
+ @Named(TOUCH_NOTIFICATION_RATE_LIMIT) private val rateLimitMs: Int,
+) {
+ private var lastNotification: Long? = null
+
+ fun notifyActivity(event: MotionEvent) {
+ val metered =
+ when (event.action) {
+ MotionEvent.ACTION_CANCEL -> false
+ MotionEvent.ACTION_UP -> false
+ MotionEvent.ACTION_DOWN -> false
+ else -> true
+ }
+
+ if (metered && lastNotification?.let { event.eventTime - it < rateLimitMs } == true) {
+ return
+ }
+
+ lastNotification = event.eventTime
+
+ scope.launch { powerInteractor.onUserTouch() }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartable.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartable.kt
index 440c3001a2f9..701aa5c8d2c5 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartable.kt
@@ -92,7 +92,17 @@ constructor(
!glanceableHubMultiUserHelper.glanceableHubHsumFlagEnabled ||
!glanceableHubMultiUserHelper.isHeadlessSystemUserMode()
) {
- anyOf(communalInteractor.isCommunalAvailable, communalInteractor.editModeOpen)
+ val isAvailable =
+ if (communalSettingsInteractor.isV2FlagEnabled()) {
+ allOf(
+ communalInteractor.isCommunalEnabled,
+ keyguardInteractor.isKeyguardShowing,
+ )
+ } else {
+ communalInteractor.isCommunalAvailable
+ }
+
+ anyOf(isAvailable, communalInteractor.editModeOpen)
// Only trigger updates on state changes, ignoring the initial false value.
.pairwise(false)
.filter { (previous, new) -> previous != new }
@@ -153,6 +163,7 @@ constructor(
is CommunalWidgetContentModel.Available ->
widget.providerInfo.widgetCategory and
AppWidgetProviderInfo.WIDGET_CATEGORY_NOT_KEYGUARD != 0
+
else -> false
}
}
@@ -171,6 +182,7 @@ constructor(
when (widget) {
is CommunalWidgetContentModel.Available ->
widget.providerInfo.profile?.identifier
+
is CommunalWidgetContentModel.Pending -> widget.user.identifier
}
!currentUserIds.contains(uid)
diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/SystemUIDeviceEntryFaceAuthInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/SystemUIDeviceEntryFaceAuthInteractor.kt
index 0d0105404726..1e50205500f9 100644
--- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/SystemUIDeviceEntryFaceAuthInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/SystemUIDeviceEntryFaceAuthInteractor.kt
@@ -20,6 +20,7 @@ import android.app.trust.TrustManager
import android.content.Context
import android.hardware.biometrics.BiometricFaceConstants
import android.hardware.biometrics.BiometricSourceType
+import android.service.dreams.Flags.dreamsV2
import com.android.keyguard.KeyguardUpdateMonitor
import com.android.systemui.biometrics.data.repository.FacePropertyRepository
import com.android.systemui.biometrics.shared.model.LockoutMode
@@ -40,6 +41,7 @@ import com.android.systemui.keyguard.shared.model.DevicePosture
import com.android.systemui.keyguard.shared.model.Edge
import com.android.systemui.keyguard.shared.model.KeyguardState.AOD
import com.android.systemui.keyguard.shared.model.KeyguardState.DOZING
+import com.android.systemui.keyguard.shared.model.KeyguardState.DREAMING
import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN
import com.android.systemui.keyguard.shared.model.KeyguardState.OFF
import com.android.systemui.keyguard.shared.model.TransitionState
@@ -136,11 +138,18 @@ constructor(
}
.launchIn(applicationScope)
- merge(
- keyguardTransitionInteractor.transition(Edge.create(AOD, LOCKSCREEN)),
- keyguardTransitionInteractor.transition(Edge.create(OFF, LOCKSCREEN)),
- keyguardTransitionInteractor.transition(Edge.create(DOZING, LOCKSCREEN)),
- )
+ val transitionFlows = buildList {
+ add(keyguardTransitionInteractor.transition(Edge.create(AOD, LOCKSCREEN)))
+ add(keyguardTransitionInteractor.transition(Edge.create(OFF, LOCKSCREEN)))
+ add(keyguardTransitionInteractor.transition(Edge.create(DOZING, LOCKSCREEN)))
+
+ if (dreamsV2()) {
+ add(keyguardTransitionInteractor.transition(Edge.create(DREAMING, LOCKSCREEN)))
+ }
+ }
+
+ transitionFlows
+ .merge()
.filter { it.transitionState == TransitionState.STARTED }
.sample(powerInteractor.detailedWakefulness)
.filter { wakefulnessModel ->
diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/ui/binder/UdfpsAccessibilityOverlayBinder.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/ui/binder/UdfpsAccessibilityOverlayBinder.kt
index e2172d0773d3..3abc260fdcbd 100644
--- a/packages/SystemUI/src/com/android/systemui/deviceentry/ui/binder/UdfpsAccessibilityOverlayBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/deviceentry/ui/binder/UdfpsAccessibilityOverlayBinder.kt
@@ -18,27 +18,58 @@
package com.android.systemui.deviceentry.ui.binder
import android.annotation.SuppressLint
+import android.util.Log
+import android.view.MotionEvent
+import android.view.View.IMPORTANT_FOR_ACCESSIBILITY_NO
+import android.view.View.IMPORTANT_FOR_ACCESSIBILITY_YES
import androidx.core.view.isInvisible
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.repeatOnLifecycle
+import com.android.app.tracing.coroutines.launchTraced as launch
import com.android.systemui.deviceentry.ui.view.UdfpsAccessibilityOverlay
import com.android.systemui.deviceentry.ui.viewmodel.UdfpsAccessibilityOverlayViewModel
import com.android.systemui.lifecycle.repeatWhenAttached
object UdfpsAccessibilityOverlayBinder {
+ private const val TAG = "UdfpsAccessibilityOverlayBinder"
/** Forwards hover events to the view model to make guided announcements for accessibility. */
@SuppressLint("ClickableViewAccessibility")
@JvmStatic
- fun bind(
- view: UdfpsAccessibilityOverlay,
- viewModel: UdfpsAccessibilityOverlayViewModel,
- ) {
- view.setOnHoverListener { v, event -> viewModel.onHoverEvent(v, event) }
+ fun bind(view: UdfpsAccessibilityOverlay, viewModel: UdfpsAccessibilityOverlayViewModel) {
view.repeatWhenAttached {
// Repeat on CREATED because we update the visibility of the view
repeatOnLifecycle(Lifecycle.State.CREATED) {
- viewModel.visible.collect { visible -> view.isInvisible = !visible }
+ view.setOnHoverListener { v, event ->
+ if (event.action == MotionEvent.ACTION_HOVER_ENTER) {
+ launch { viewModel.onHoverEvent(v, event) }
+ }
+ false
+ }
+
+ launch { viewModel.visible.collect { visible -> view.isInvisible = !visible } }
+
+ launch {
+ viewModel.contentDescription.collect { contentDescription ->
+ if (contentDescription != null) {
+ view.importantForAccessibility = IMPORTANT_FOR_ACCESSIBILITY_YES
+ view.contentDescription = contentDescription
+ }
+ }
+ }
+
+ launch {
+ viewModel.clearAccessibilityOverlayMessageReason.collect { reason ->
+ Log.d(
+ TAG,
+ "clearing content description of UDFPS accessibility overlay " +
+ "for reason: $reason",
+ )
+ view.importantForAccessibility = IMPORTANT_FOR_ACCESSIBILITY_NO
+ view.contentDescription = null
+ viewModel.setContentDescription(null)
+ }
+ }
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/ui/view/UdfpsAccessibilityOverlay.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/ui/view/UdfpsAccessibilityOverlay.kt
index 9c3b9b273ab5..0a2d10d10a40 100644
--- a/packages/SystemUI/src/com/android/systemui/deviceentry/ui/view/UdfpsAccessibilityOverlay.kt
+++ b/packages/SystemUI/src/com/android/systemui/deviceentry/ui/view/UdfpsAccessibilityOverlay.kt
@@ -23,5 +23,7 @@ import android.view.View
class UdfpsAccessibilityOverlay(context: Context?) : View(context) {
init {
accessibilityLiveRegion = ACCESSIBILITY_LIVE_REGION_ASSERTIVE
+ importantForAccessibility = IMPORTANT_FOR_ACCESSIBILITY_AUTO
+ isClickable = false
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/ui/viewmodel/AlternateBouncerUdfpsAccessibilityOverlayViewModel.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/ui/viewmodel/AlternateBouncerUdfpsAccessibilityOverlayViewModel.kt
index 5c7cd5f55942..22ed6da2e5bf 100644
--- a/packages/SystemUI/src/com/android/systemui/deviceentry/ui/viewmodel/AlternateBouncerUdfpsAccessibilityOverlayViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/deviceentry/ui/viewmodel/AlternateBouncerUdfpsAccessibilityOverlayViewModel.kt
@@ -17,6 +17,7 @@
package com.android.systemui.deviceentry.ui.viewmodel
import com.android.systemui.accessibility.domain.interactor.AccessibilityInteractor
+import com.android.systemui.biometrics.UdfpsUtils
import com.android.systemui.biometrics.domain.interactor.UdfpsOverlayInteractor
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
@@ -26,13 +27,23 @@ import kotlinx.coroutines.flow.flowOf
class AlternateBouncerUdfpsAccessibilityOverlayViewModel
@Inject
constructor(
- udfpsOverlayInteractor: UdfpsOverlayInteractor,
+ private val udfpsOverlayInteractor: UdfpsOverlayInteractor,
accessibilityInteractor: AccessibilityInteractor,
+ udfpsUtils: UdfpsUtils,
) :
UdfpsAccessibilityOverlayViewModel(
udfpsOverlayInteractor,
accessibilityInteractor,
+ udfpsUtils,
) {
/** Overlay is always visible if touch exploration is enabled on the alternate bouncer. */
override fun isVisibleWhenTouchExplorationEnabled(): Flow<Boolean> = flowOf(true)
+
+ /**
+ * Clears the content description to prevent the view from storing stale UDFPS directional
+ * guidance messages for accessibility.
+ */
+ suspend fun clearUdfpsAccessibilityOverlayMessage(reason: String) {
+ udfpsOverlayInteractor.clearUdfpsAccessibilityOverlayMessage(reason)
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/ui/viewmodel/DeviceEntryUdfpsAccessibilityOverlayViewModel.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/ui/viewmodel/DeviceEntryUdfpsAccessibilityOverlayViewModel.kt
index b84d65a2b430..5c86514775de 100644
--- a/packages/SystemUI/src/com/android/systemui/deviceentry/ui/viewmodel/DeviceEntryUdfpsAccessibilityOverlayViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/deviceentry/ui/viewmodel/DeviceEntryUdfpsAccessibilityOverlayViewModel.kt
@@ -17,6 +17,7 @@
package com.android.systemui.deviceentry.ui.viewmodel
import com.android.systemui.accessibility.domain.interactor.AccessibilityInteractor
+import com.android.systemui.biometrics.UdfpsUtils
import com.android.systemui.biometrics.domain.interactor.UdfpsOverlayInteractor
import com.android.systemui.keyguard.ui.view.DeviceEntryIconView
import com.android.systemui.keyguard.ui.viewmodel.DeviceEntryForegroundViewModel
@@ -33,10 +34,12 @@ constructor(
accessibilityInteractor: AccessibilityInteractor,
private val deviceEntryIconViewModel: DeviceEntryIconViewModel,
private val deviceEntryFgIconViewModel: DeviceEntryForegroundViewModel,
+ udfpsUtils: UdfpsUtils,
) :
UdfpsAccessibilityOverlayViewModel(
udfpsOverlayInteractor,
accessibilityInteractor,
+ udfpsUtils,
) {
/** Overlay is only visible if the UDFPS icon is visible on the keyguard. */
override fun isVisibleWhenTouchExplorationEnabled(): Flow<Boolean> =
diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/ui/viewmodel/UdfpsAccessibilityOverlayViewModel.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/ui/viewmodel/UdfpsAccessibilityOverlayViewModel.kt
index 1849bf20abdb..a58f3681555c 100644
--- a/packages/SystemUI/src/com/android/systemui/deviceentry/ui/viewmodel/UdfpsAccessibilityOverlayViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/deviceentry/ui/viewmodel/UdfpsAccessibilityOverlayViewModel.kt
@@ -24,7 +24,10 @@ import com.android.systemui.biometrics.UdfpsUtils
import com.android.systemui.biometrics.domain.interactor.UdfpsOverlayInteractor
import com.android.systemui.biometrics.shared.model.UdfpsOverlayParams
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
@@ -32,8 +35,17 @@ import kotlinx.coroutines.flow.flowOf
abstract class UdfpsAccessibilityOverlayViewModel(
udfpsOverlayInteractor: UdfpsOverlayInteractor,
accessibilityInteractor: AccessibilityInteractor,
+ private val udfpsUtils: UdfpsUtils,
) {
- private val udfpsUtils = UdfpsUtils()
+ /** Indicates the reason for clearing the UDFPS accessibility overlay content description */
+ val clearAccessibilityOverlayMessageReason: SharedFlow<String?> =
+ udfpsOverlayInteractor.clearAccessibilityOverlayMessageReason
+
+ private val _contentDescription: MutableStateFlow<CharSequence?> = MutableStateFlow(null)
+
+ /** Content description of the UDFPS accessibility overlay */
+ val contentDescription: Flow<CharSequence?> = _contentDescription.asStateFlow()
+
private val udfpsOverlayParams: StateFlow<UdfpsOverlayParams> =
udfpsOverlayInteractor.udfpsOverlayParams
@@ -46,6 +58,10 @@ abstract class UdfpsAccessibilityOverlayViewModel(
}
}
+ fun setContentDescription(contentDescription: CharSequence?) {
+ _contentDescription.value = contentDescription
+ }
+
abstract fun isVisibleWhenTouchExplorationEnabled(): Flow<Boolean>
/** Give directional feedback to help the user authenticate with UDFPS. */
@@ -77,8 +93,9 @@ abstract class UdfpsAccessibilityOverlayViewModel(
overlayParams,
/* touchRotatedToPortrait */ false,
)
+
if (announceStr != null) {
- v.contentDescription = announceStr
+ _contentDescription.value = announceStr
}
}
// always let the motion events go through to underlying views
diff --git a/packages/SystemUI/src/com/android/systemui/display/data/repository/DisplayWindowPropertiesRepository.kt b/packages/SystemUI/src/com/android/systemui/display/data/repository/DisplayWindowPropertiesRepository.kt
index 792d3288e96a..aaaaacef001a 100644
--- a/packages/SystemUI/src/com/android/systemui/display/data/repository/DisplayWindowPropertiesRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/display/data/repository/DisplayWindowPropertiesRepository.kt
@@ -31,6 +31,7 @@ import com.android.systemui.display.shared.model.DisplayWindowProperties
import com.android.systemui.res.R
import com.android.systemui.shade.shared.flag.ShadeWindowGoesAround
import com.android.systemui.statusbar.core.StatusBarConnectedDisplays
+import com.android.systemui.utils.windowmanager.WindowManagerUtils
import com.google.common.collect.HashBasedTable
import com.google.common.collect.Table
import java.io.PrintWriter
@@ -110,7 +111,7 @@ constructor(
return null
}
@SuppressLint("NonInjectedService") // Need to manually get the service
- val windowManager = context.getSystemService(WindowManager::class.java)
+ val windowManager = WindowManagerUtils.getWindowManager(context)
val layoutInflater = LayoutInflater.from(context)
DisplayWindowProperties(displayId, windowType, context, windowManager, layoutInflater)
}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayContainerViewController.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayContainerViewController.java
index 599c945db064..c78231f16437 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayContainerViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayContainerViewController.java
@@ -21,6 +21,7 @@ import static android.service.dreams.Flags.dreamHandlesBeingObscured;
import static com.android.keyguard.BouncerPanelExpansionCalculator.aboutToShowBouncerProgress;
import static com.android.keyguard.BouncerPanelExpansionCalculator.getDreamAlphaScaledExpansion;
import static com.android.keyguard.BouncerPanelExpansionCalculator.getDreamYPositionScaledExpansion;
+import static com.android.systemui.Flags.bouncerUiRevamp;
import static com.android.systemui.complication.ComplicationLayoutParams.POSITION_BOTTOM;
import static com.android.systemui.complication.ComplicationLayoutParams.POSITION_TOP;
import static com.android.systemui.doze.util.BurnInHelperKt.getBurnInOffset;
@@ -362,9 +363,11 @@ public class DreamOverlayContainerViewController extends
});
}
- mBlurUtils.applyBlur(mView.getViewRootImpl(),
- (int) mBlurUtils.blurRadiusOfRatio(
- 1 - aboutToShowBouncerProgress(bouncerHideAmount)), false);
+ if (!bouncerUiRevamp()) {
+ mBlurUtils.applyBlur(mView.getViewRootImpl(),
+ (int) mBlurUtils.blurRadiusOfRatio(
+ 1 - aboutToShowBouncerProgress(bouncerHideAmount)), false);
+ }
}
private static float getAlpha(int position, float expansion) {
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java
index fd716eea799a..501883e257ab 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java
@@ -17,6 +17,7 @@
package com.android.systemui.dreams;
import static android.service.dreams.Flags.dreamWakeRedirect;
+import static android.service.dreams.Flags.dreamsV2;
import static com.android.systemui.Flags.glanceableHubAllowKeyguardWhenDreaming;
import static com.android.systemui.dreams.dagger.DreamModule.DREAM_OVERLAY_WINDOW_TITLE;
@@ -29,6 +30,7 @@ import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.graphics.drawable.ColorDrawable;
+import android.os.PowerManager;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
@@ -69,6 +71,7 @@ import com.android.systemui.dreams.dagger.DreamOverlayComponent;
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
import com.android.systemui.navigationbar.gestural.domain.GestureInteractor;
import com.android.systemui.navigationbar.gestural.domain.TaskMatcher;
+import com.android.systemui.power.domain.interactor.PowerInteractor;
import com.android.systemui.scene.domain.interactor.SceneInteractor;
import com.android.systemui.scene.shared.flag.SceneContainerFlag;
import com.android.systemui.scene.shared.model.Overlays;
@@ -77,6 +80,8 @@ import com.android.systemui.shade.ShadeExpansionChangeEvent;
import com.android.systemui.touch.TouchInsetManager;
import com.android.systemui.util.concurrency.DelayableExecutor;
+import kotlin.Unit;
+
import kotlinx.coroutines.Job;
import java.util.ArrayList;
@@ -105,6 +110,7 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ
private final Context mContext;
// The Executor ensures actions and ui updates happen on the same thread.
private final DelayableExecutor mExecutor;
+ private final PowerInteractor mPowerInteractor;
// A controller for the dream overlay container view (which contains both the status bar and the
// content area).
private DreamOverlayContainerViewController mDreamOverlayContainerViewController;
@@ -230,6 +236,15 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ
}
};
+ private final Consumer<Unit> mPickupConsumer = new Consumer<>() {
+ @Override
+ public void accept(Unit unit) {
+ mExecutor.execute(() ->
+ mPowerInteractor.wakeUpIfDreaming("pickupGesture",
+ PowerManager.WAKE_REASON_LIFT));
+ }
+ };
+
/**
* {@link ResetHandler} protects resetting {@link DreamOverlayService} by making sure reset
* requests are processed before subsequent actions proceed. Requests themselves are also
@@ -398,6 +413,8 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ
DreamOverlayCallbackController dreamOverlayCallbackController,
KeyguardInteractor keyguardInteractor,
GestureInteractor gestureInteractor,
+ WakeGestureMonitor wakeGestureMonitor,
+ PowerInteractor powerInteractor,
@Named(DREAM_OVERLAY_WINDOW_TITLE) String windowTitle) {
super(executor);
mContext = context;
@@ -424,6 +441,7 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ
mTouchInsetManager = touchInsetManager;
mLifecycleOwner = lifecycleOwner;
mLifecycleRegistry = lifecycleOwner.getRegistry();
+ mPowerInteractor = powerInteractor;
mExecutor.execute(() -> setLifecycleStateLocked(Lifecycle.State.CREATED));
@@ -438,6 +456,11 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ
mFlows.add(collectFlow(getLifecycle(), keyguardInteractor.primaryBouncerShowing,
mBouncerShowingConsumer));
}
+
+ if (dreamsV2()) {
+ mFlows.add(collectFlow(getLifecycle(), wakeGestureMonitor.getWakeUpDetected(),
+ mPickupConsumer));
+ }
}
@NonNull
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/WakeGestureMonitor.kt b/packages/SystemUI/src/com/android/systemui/dreams/WakeGestureMonitor.kt
new file mode 100644
index 000000000000..1ba170bf656a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dreams/WakeGestureMonitor.kt
@@ -0,0 +1,74 @@
+/*
+ * 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.dreams
+
+import android.hardware.Sensor
+import android.hardware.display.AmbientDisplayConfiguration
+import android.provider.Settings
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.user.domain.interactor.SelectedUserInteractor
+import com.android.systemui.util.kotlin.emitOnStart
+import com.android.systemui.util.kotlin.observeTriggerSensor
+import com.android.systemui.util.sensors.AsyncSensorManager
+import com.android.systemui.util.settings.SecureSettings
+import com.android.systemui.util.settings.SettingsProxyExt.observerFlow
+import com.android.systemui.utils.coroutines.flow.flatMapLatestConflated
+import javax.inject.Inject
+import kotlin.coroutines.CoroutineContext
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.emptyFlow
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
+
+@SysUISingleton
+class WakeGestureMonitor
+@Inject
+constructor(
+ private val ambientDisplayConfiguration: AmbientDisplayConfiguration,
+ private val asyncSensorManager: AsyncSensorManager,
+ @Background bgContext: CoroutineContext,
+ private val secureSettings: SecureSettings,
+ selectedUserInteractor: SelectedUserInteractor,
+) {
+
+ private val pickupSensor by lazy {
+ asyncSensorManager.getDefaultSensor(Sensor.TYPE_PICK_UP_GESTURE)
+ }
+
+ private val pickupGestureEnabled: Flow<Boolean> =
+ selectedUserInteractor.selectedUser.flatMapLatestConflated { userId ->
+ isPickupEnabledForUser(userId)
+ }
+
+ private fun isPickupEnabledForUser(userId: Int): Flow<Boolean> =
+ secureSettings
+ .observerFlow(userId, Settings.Secure.DOZE_PICK_UP_GESTURE)
+ .emitOnStart()
+ .map { ambientDisplayConfiguration.pickupGestureEnabled(userId) }
+
+ val wakeUpDetected: Flow<Unit> =
+ pickupGestureEnabled
+ .flatMapLatestConflated { enabled ->
+ if (enabled && pickupSensor != null) {
+ asyncSensorManager.observeTriggerSensor(pickupSensor!!)
+ } else {
+ emptyFlow()
+ }
+ }
+ .flowOn(bgContext)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/education/data/repository/UserContextualEducationRepository.kt b/packages/SystemUI/src/com/android/systemui/education/data/repository/UserContextualEducationRepository.kt
index 0640351c8149..d9f9a3ea1032 100644
--- a/packages/SystemUI/src/com/android/systemui/education/data/repository/UserContextualEducationRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/education/data/repository/UserContextualEducationRepository.kt
@@ -121,16 +121,8 @@ constructor(
InputManager.KeyGestureEventListener { event ->
// Only store keyboard shortcut time for gestures providing keyboard
// education
- val shortcutType =
- when (event.keyGestureType) {
- KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_ALL_APPS,
- KeyGestureEvent.KEY_GESTURE_TYPE_ALL_APPS -> ALL_APPS
-
- else -> null
- }
-
- if (shortcutType != null) {
- trySendWithFailureLogging(shortcutType, TAG)
+ if (event.keyGestureType == KeyGestureEvent.KEY_GESTURE_TYPE_ALL_APPS) {
+ trySendWithFailureLogging(ALL_APPS, TAG)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
index 84bb23140ae7..9a37439e7486 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
@@ -200,9 +200,6 @@ object Flags {
// TODO(b/266157412): Tracking Bug
val MEDIA_RETAIN_SESSIONS = unreleasedFlag("media_retain_sessions")
- // TODO(b/267007629): Tracking Bug
- val MEDIA_RESUME_PROGRESS = releasedFlag("media_resume_progress")
-
// TODO(b/270437894): Tracking Bug
val MEDIA_REMOTE_RESUME = unreleasedFlag("media_remote_resume")
diff --git a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java
index f2a10cc43fd9..9444ae1065b5 100644
--- a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java
+++ b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java
@@ -58,7 +58,9 @@ import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
+import android.os.PowerManager;
import android.os.RemoteException;
+import android.os.SystemClock;
import android.os.SystemProperties;
import android.os.Trace;
import android.os.UserHandle;
@@ -194,6 +196,7 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene
static final String GLOBAL_ACTION_KEY_EMERGENCY = "emergency";
static final String GLOBAL_ACTION_KEY_SCREENSHOT = "screenshot";
static final String GLOBAL_ACTION_KEY_SYSTEM_UPDATE = "system_update";
+ static final String GLOBAL_ACTION_KEY_STANDBY = "standby";
// See NotificationManagerService#scheduleDurationReachedLocked
private static final long TOAST_FADE_TIME = 333;
@@ -250,6 +253,7 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene
private boolean mHasTelephony;
private boolean mHasVibrator;
private final boolean mShowSilentToggle;
+ private final boolean mIsTv;
private final EmergencyAffordanceManager mEmergencyAffordanceManager;
private final ScreenshotHelper mScreenshotHelper;
private final SysuiColorExtractor mSysuiColorExtractor;
@@ -270,6 +274,7 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene
private final UserLogoutInteractor mLogoutInteractor;
private final GlobalActionsInteractor mInteractor;
private final Lazy<DisplayWindowPropertiesRepository> mDisplayWindowPropertiesRepositoryLazy;
+ private final PowerManager mPowerManager;
private final Handler mHandler;
private final UserTracker.Callback mOnUserSwitched = new UserTracker.Callback() {
@@ -341,7 +346,10 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene
GA_CLOSE_POWER_VOLUP(811),
@UiEvent(doc = "System Update button was pressed.")
- GA_SYSTEM_UPDATE_PRESS(1716);
+ GA_SYSTEM_UPDATE_PRESS(1716),
+
+ @UiEvent(doc = "The global actions standby button was pressed.")
+ GA_STANDBY_PRESS(2210);
private final int mId;
@@ -396,7 +404,8 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene
SelectedUserInteractor selectedUserInteractor,
UserLogoutInteractor logoutInteractor,
GlobalActionsInteractor interactor,
- Lazy<DisplayWindowPropertiesRepository> displayWindowPropertiesRepository) {
+ Lazy<DisplayWindowPropertiesRepository> displayWindowPropertiesRepository,
+ PowerManager powerManager) {
mContext = context;
mWindowManagerFuncs = windowManagerFuncs;
mAudioManager = audioManager;
@@ -434,6 +443,7 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene
mLogoutInteractor = logoutInteractor;
mInteractor = interactor;
mDisplayWindowPropertiesRepositoryLazy = displayWindowPropertiesRepository;
+ mPowerManager = powerManager;
mHandler = new Handler(mMainHandler.getLooper()) {
public void handleMessage(Message msg) {
@@ -466,6 +476,7 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene
mBroadcastDispatcher.registerReceiver(mBroadcastReceiver, filter);
mHasTelephony = packageManager.hasSystemFeature(PackageManager.FEATURE_TELEPHONY);
+ mIsTv = packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK);
// get notified of phone state changes
mTelephonyListenerManager.addServiceStateListener(mPhoneStateListener);
@@ -697,6 +708,8 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene
}
} else if (GLOBAL_ACTION_KEY_SYSTEM_UPDATE.equals(actionKey)) {
addIfShouldShowAction(tempActions, new SystemUpdateAction());
+ } else if (GLOBAL_ACTION_KEY_STANDBY.equals(actionKey)) {
+ addIfShouldShowAction(tempActions, new StandbyAction());
} else {
Log.e(TAG, "Invalid global action key " + actionKey);
}
@@ -850,6 +863,11 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene
}
@VisibleForTesting
+ boolean isTv() {
+ return mIsTv;
+ }
+
+ @VisibleForTesting
protected final class PowerOptionsAction extends SinglePressAction {
private PowerOptionsAction() {
super(com.android.systemui.res.R.drawable.ic_settings_power,
@@ -1245,6 +1263,36 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene
}
}
+ @VisibleForTesting
+ class StandbyAction extends SinglePressAction {
+ StandbyAction() {
+ super(R.drawable.ic_standby, R.string.global_action_standby);
+ }
+
+ @Override
+ public void onPress() {
+ // Add a little delay before executing, to give the dialog a chance to go away before
+ // going to sleep. Otherwise, we see screen flicker randomly.
+ mHandler.postDelayed(() -> {
+ mUiEventLogger.log(GlobalActionsEvent.GA_STANDBY_PRESS);
+ mBackgroundExecutor.execute(() -> {
+ mPowerManager.goToSleep(SystemClock.uptimeMillis(),
+ PowerManager.GO_TO_SLEEP_REASON_POWER_BUTTON, 0);
+ });
+ }, mDialogPressDelay);
+ }
+
+ @Override
+ public boolean showDuringKeyguard() {
+ return true;
+ }
+
+ @Override
+ public boolean showBeforeProvisioning() {
+ return true;
+ }
+ }
+
private Action getSettingsAction() {
return new SinglePressAction(R.drawable.ic_settings,
R.string.global_action_settings) {
@@ -1820,17 +1868,20 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene
* A single press action maintains no state, just responds to a press and takes an action.
*/
- private abstract class SinglePressAction implements Action {
+ @VisibleForTesting
+ abstract class SinglePressAction implements Action {
private final int mIconResId;
private final Drawable mIcon;
private final int mMessageResId;
private final CharSequence mMessage;
+ @VisibleForTesting ImageView mIconView;
protected SinglePressAction(int iconResId, int messageResId) {
mIconResId = iconResId;
mMessageResId = messageResId;
mMessage = null;
mIcon = null;
+ mIconView = null;
}
protected SinglePressAction(int iconResId, Drawable icon, CharSequence message) {
@@ -1881,12 +1932,24 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene
// ConstraintLayout flow needs an ID to reference
v.setId(View.generateViewId());
- ImageView icon = v.findViewById(R.id.icon);
+ mIconView = v.findViewById(R.id.icon);
TextView messageView = v.findViewById(R.id.message);
messageView.setSelected(true); // necessary for marquee to work
- icon.setImageDrawable(getIcon(context));
- icon.setScaleType(ScaleType.CENTER_CROP);
+ mIconView.setImageDrawable(getIcon(context));
+ mIconView.setScaleType(ScaleType.CENTER_CROP);
+ if (com.android.systemui.Flags.tvGlobalActionsFocus()) {
+ if (isTv()) {
+ mIconView.setFocusable(true);
+ mIconView.setClickable(true);
+ mIconView.setBackground(mContext.getDrawable(com.android.systemui.res.R.drawable
+ .global_actions_lite_button_background));
+ mIconView.setOnClickListener(i -> onClick());
+ if (mItems.get(0) == this) {
+ mIconView.requestFocus();
+ }
+ }
+ }
if (mMessage != null) {
messageView.setText(mMessage);
@@ -1896,6 +1959,22 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene
return v;
}
+
+ private void onClick() {
+ if (mDialog != null) {
+ // don't dismiss the dialog if we're opening the power options menu
+ if (!(this instanceof PowerOptionsAction)) {
+ // Usually clicking an item shuts down the phone, locks, or starts an
+ // activity. We don't want to animate back into the power button when that
+ // happens, so we disable the dialog animation before dismissing.
+ mDialogTransitionAnimator.disableAllCurrentDialogsExitAnimations();
+ mDialog.dismiss();
+ }
+ } else {
+ Log.w(TAG, "Action icon clicked while mDialog is null.");
+ }
+ onPress();
+ }
}
protected int getGridItemLayoutResource() {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt
index 74cf7e4f7359..6caff6432cb2 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt
@@ -147,10 +147,8 @@ constructor(
configuration,
occludingAppDeviceEntryMessageViewModel,
chipbarCoordinator,
- screenOffAnimationController,
shadeInteractor,
- clockInteractor,
- keyguardClockViewModel,
+ smartspaceViewModel,
deviceEntryHapticsInteractor,
vibratorHelper,
falsingManager,
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
index 099a7f067482..170966b45618 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
@@ -2433,11 +2433,7 @@ 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)) {
- 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 (!mKeyguardInteractor.showGlanceableHub()) {
// 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.
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/WindowManagerLockscreenVisibilityManager.kt b/packages/SystemUI/src/com/android/systemui/keyguard/WindowManagerLockscreenVisibilityManager.kt
index 51b953ef290c..979c7ceb239b 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/WindowManagerLockscreenVisibilityManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/WindowManagerLockscreenVisibilityManager.kt
@@ -25,6 +25,7 @@ import android.view.WindowManager
import com.android.internal.widget.LockPatternUtils
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.dagger.qualifiers.UiBackground
import com.android.systemui.keyguard.domain.interactor.KeyguardDismissTransitionInteractor
import com.android.systemui.keyguard.domain.interactor.KeyguardShowWhileAwakeInteractor
import com.android.systemui.keyguard.ui.binder.KeyguardSurfaceBehindParamsApplier
@@ -44,6 +45,7 @@ class WindowManagerLockscreenVisibilityManager
@Inject
constructor(
@Main private val executor: Executor,
+ @UiBackground private val uiBgExecutor: Executor,
private val activityTaskManagerService: IActivityTaskManager,
private val keyguardStateController: KeyguardStateController,
private val keyguardSurfaceBehindAnimator: KeyguardSurfaceBehindParamsApplier,
@@ -144,11 +146,15 @@ constructor(
isKeyguardGoingAway = true
return
}
- // Make the surface behind the keyguard visible by calling keyguardGoingAway. The
- // lockscreen is still showing as well, allowing us to animate unlocked.
- Log.d(TAG, "ActivityTaskManagerService#keyguardGoingAway()")
- activityTaskManagerService.keyguardGoingAway(0)
+
isKeyguardGoingAway = true
+ Log.d(TAG, "Enqueuing ATMS#keyguardGoingAway() on uiBgExecutor")
+ uiBgExecutor.execute {
+ // Make the surface behind the keyguard visible by calling keyguardGoingAway. The
+ // lockscreen is still showing as well, allowing us to animate unlocked.
+ Log.d(TAG, "ATMS#keyguardGoingAway()")
+ activityTaskManagerService.keyguardGoingAway(0)
+ }
} else if (isLockscreenShowing == true) {
// Re-show the lockscreen if the surface was visible and we want to make it invisible,
// and the lockscreen is currently showing (this is the usual case of the going away
@@ -273,32 +279,44 @@ constructor(
return
}
- if (this.isLockscreenShowing == lockscreenShowing && this.isAodVisible == aodVisible) {
+ if (
+ this.isLockscreenShowing == lockscreenShowing &&
+ this.isAodVisible == aodVisible &&
+ !this.isKeyguardGoingAway
+ ) {
Log.d(
TAG,
"#setWmLockscreenState: lockscreenShowing=$lockscreenShowing and " +
- "isAodVisible=$aodVisible were both unchanged, not forwarding to ATMS.",
+ "isAodVisible=$aodVisible were both unchanged and we're not going away, not " +
+ "forwarding to ATMS.",
)
return
}
+ this.isLockscreenShowing = lockscreenShowing
+ this.isAodVisible = aodVisible
Log.d(
TAG,
- "ATMS#setLockScreenShown(" +
- "isLockscreenShowing=$lockscreenShowing, " +
- "aodVisible=$aodVisible).",
+ "Enqueuing ATMS#setLockScreenShown($lockscreenShowing, $aodVisible) " +
+ "on uiBgExecutor",
)
- if (enableNewKeyguardShellTransitions) {
- startKeyguardTransition(lockscreenShowing, aodVisible)
- } else {
- try {
- activityTaskManagerService.setLockScreenShown(lockscreenShowing, aodVisible)
- } catch (e: RemoteException) {
- Log.e(TAG, "Remote exception", e)
+ uiBgExecutor.execute {
+ Log.d(
+ TAG,
+ "ATMS#setLockScreenShown(" +
+ "isLockscreenShowing=$lockscreenShowing, " +
+ "aodVisible=$aodVisible).",
+ )
+ if (enableNewKeyguardShellTransitions) {
+ startKeyguardTransition(lockscreenShowing, aodVisible)
+ } else {
+ try {
+ activityTaskManagerService.setLockScreenShown(lockscreenShowing, aodVisible)
+ } catch (e: RemoteException) {
+ Log.e(TAG, "Remote exception", e)
+ }
}
}
- this.isLockscreenShowing = lockscreenShowing
- this.isAodVisible = aodVisible
}
private fun startKeyguardTransition(keyguardShowing: Boolean, aodShowing: Boolean) {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/PrimaryBouncerTransitionModule.kt b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/PrimaryBouncerTransitionModule.kt
index 7c4dbfeba50f..7110c37e88e7 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/PrimaryBouncerTransitionModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/PrimaryBouncerTransitionModule.kt
@@ -20,11 +20,13 @@ import com.android.systemui.keyguard.ui.transitions.PrimaryBouncerTransition
import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerToPrimaryBouncerTransitionViewModel
import com.android.systemui.keyguard.ui.viewmodel.AodToPrimaryBouncerTransitionViewModel
import com.android.systemui.keyguard.ui.viewmodel.DozingToPrimaryBouncerTransitionViewModel
+import com.android.systemui.keyguard.ui.viewmodel.DreamingToPrimaryBouncerTransitionViewModel
import com.android.systemui.keyguard.ui.viewmodel.GlanceableHubToPrimaryBouncerTransitionViewModel
import com.android.systemui.keyguard.ui.viewmodel.LockscreenToPrimaryBouncerTransitionViewModel
import com.android.systemui.keyguard.ui.viewmodel.OccludedToPrimaryBouncerTransitionViewModel
import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToAodTransitionViewModel
import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToDozingTransitionViewModel
+import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToDreamingTransitionViewModel
import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToGlanceableHubTransitionViewModel
import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToGoneTransitionViewModel
import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToLockscreenTransitionViewModel
@@ -81,6 +83,10 @@ interface PrimaryBouncerTransitionImplModule {
@Binds
@IntoSet
+ fun fromDreaming(impl: DreamingToPrimaryBouncerTransitionViewModel): PrimaryBouncerTransition
+
+ @Binds
+ @IntoSet
fun toAod(impl: PrimaryBouncerToAodTransitionViewModel): PrimaryBouncerTransition
@Binds
@@ -103,5 +109,9 @@ interface PrimaryBouncerTransitionImplModule {
@Binds
@IntoSet
+ fun toDreaming(impl: PrimaryBouncerToDreamingTransitionViewModel): PrimaryBouncerTransition
+
+ @Binds
+ @IntoSet
fun toOccluded(impl: PrimaryBouncerToOccludedTransitionViewModel): PrimaryBouncerTransition
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt
index f53421d539fe..4fca453a184c 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt
@@ -93,8 +93,8 @@ constructor(
// transition.
scope.launch("$TAG#listenForAodToAwake") {
powerInteractor.detailedWakefulness
- .filterRelevantKeyguardStateAnd { wakefulness -> wakefulness.isAwake() }
.debounce(50L)
+ .filterRelevantKeyguardStateAnd { wakefulness -> wakefulness.isAwake() }
.sample(
transitionInteractor.startedKeyguardTransitionStep,
wakeToGoneInteractor.canWakeDirectlyToGone,
@@ -140,7 +140,8 @@ constructor(
val shouldTransitionToCommunal =
communalSettingsInteractor.isV2FlagEnabled() &&
autoOpenCommunal &&
- !detailedWakefulness.isAwakeFromMotionOrLift()
+ !detailedWakefulness.isAwakeFromMotionOrLift() &&
+ !isKeyguardOccludedLegacy
if (shouldTransitionToGone) {
// TODO(b/360368320): Adapt for scene framework
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt
index 4aaa1fab4c65..d673f22386b7 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt
@@ -125,7 +125,9 @@ constructor(
wakefulness: WakefulnessModel,
) =
if (communalSettingsInteractor.isV2FlagEnabled()) {
- shouldShowCommunal && !wakefulness.isAwakeFromMotionOrLift()
+ shouldShowCommunal &&
+ !wakefulness.isAwakeFromMotionOrLift() &&
+ !keyguardInteractor.isKeyguardOccluded.value
} else {
isCommunalAvailable && dreamManager.canStartDreaming(false)
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt
index 3b1b6fcc45f2..09bf478a9338 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt
@@ -316,5 +316,6 @@ constructor(
val TO_LOCKSCREEN_DURATION = 1167.milliseconds
val TO_AOD_DURATION = 300.milliseconds
val TO_GONE_DURATION = DEFAULT_DURATION
+ val TO_PRIMARY_BOUNCER_DURATION = DEFAULT_DURATION
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt
index 3ad862b761fc..be0cf62b0526 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt
@@ -251,6 +251,8 @@ constructor(
* Set at 400ms for parity with [FromLockscreenTransitionInteractor]
*/
val DEFAULT_DURATION = 400.milliseconds
+ // To lockscreen duration must be at least 500ms to allow for potential screen rotation
+ // during the transition while the animation begins after 500ms.
val TO_LOCKSCREEN_DURATION = 1.seconds
val TO_BOUNCER_DURATION = 400.milliseconds
val TO_OCCLUDED_DURATION = 450.milliseconds
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt
index ca6a7907a8eb..cc5ec79a1060 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt
@@ -20,6 +20,8 @@ import android.animation.ValueAnimator
import com.android.app.animation.Interpolators
import com.android.app.tracing.coroutines.launchTraced as launch
import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor
+import com.android.systemui.communal.domain.interactor.CommunalSettingsInteractor
+import com.android.systemui.communal.shared.model.CommunalScenes
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.Main
@@ -48,6 +50,7 @@ constructor(
keyguardInteractor: KeyguardInteractor,
powerInteractor: PowerInteractor,
private val communalSceneInteractor: CommunalSceneInteractor,
+ private val communalSettingsInteractor: CommunalSettingsInteractor,
keyguardOcclusionInteractor: KeyguardOcclusionInteractor,
private val keyguardShowWhileAwakeInteractor: KeyguardShowWhileAwakeInteractor,
) :
@@ -77,6 +80,26 @@ constructor(
}
/**
+ * Attempt to show the glanceable hub from the gone state (eg due to power button press).
+ *
+ * This will return whether the hub was successfully shown or not.
+ */
+ fun showGlanceableHub(): Boolean {
+ val isRelevantKeyguardState =
+ transitionInteractor.startedKeyguardTransitionStep.value.to == KeyguardState.GONE
+ val showGlanceableHub =
+ isRelevantKeyguardState &&
+ communalSettingsInteractor.isV2FlagEnabled() &&
+ communalSettingsInteractor.autoOpenEnabled.value &&
+ !keyguardInteractor.isKeyguardOccluded.value
+ if (showGlanceableHub) {
+ communalSceneInteractor.snapToScene(CommunalScenes.Communal, "showGlanceableHub()")
+ return true
+ }
+ return false
+ }
+
+ /**
* A special case supported on foldables, where folding the device may put the device on an
* unlocked lockscreen, but if an occluding app is already showing (like a active phone call),
* then go directly to OCCLUDED.
@@ -100,28 +123,36 @@ constructor(
scope.launch {
keyguardShowWhileAwakeInteractor.showWhileAwakeEvents
.filterRelevantKeyguardState()
- .sample(communalSceneInteractor.isIdleOnCommunalNotEditMode, ::Pair)
- .collect { (lockReason, idleOnCommunal) ->
- val to =
- if (idleOnCommunal) {
- KeyguardState.GLANCEABLE_HUB
- } else {
- KeyguardState.LOCKSCREEN
- }
- startTransitionTo(to, ownerReason = "lockWhileAwake: $lockReason")
+ .sample(communalSettingsInteractor.autoOpenEnabled, ::Pair)
+ .collect { (lockReason, autoOpenHub) ->
+ if (autoOpenHub) {
+ communalSceneInteractor.changeScene(
+ CommunalScenes.Communal,
+ "lockWhileAwake: $lockReason",
+ )
+ } else {
+ startTransitionTo(
+ KeyguardState.LOCKSCREEN,
+ ownerReason = "lockWhileAwake: $lockReason",
+ )
+ }
}
}
} else {
- scope.launch("$TAG#listenForGoneToLockscreenOrHubOrOccluded") {
+ scope.launch("$TAG#listenForGoneToLockscreenOrHubOrOccluded", mainDispatcher) {
keyguardInteractor.isKeyguardShowing
.filterRelevantKeyguardStateAnd { isKeyguardShowing -> isKeyguardShowing }
- .sample(communalSceneInteractor.isIdleOnCommunalNotEditMode, ::Pair)
- .collect { (_, isIdleOnCommunal) ->
+ .sample(communalSettingsInteractor.autoOpenEnabled, ::Pair)
+ .collect { (_, autoOpenHub) ->
val to =
- if (isIdleOnCommunal) {
- KeyguardState.GLANCEABLE_HUB
- } else if (keyguardInteractor.isKeyguardOccluded.value) {
+ if (keyguardInteractor.isKeyguardOccluded.value) {
KeyguardState.OCCLUDED
+ } else if (autoOpenHub) {
+ communalSceneInteractor.changeScene(
+ CommunalScenes.Communal,
+ "keyguard interactor says keyguard is showing",
+ )
+ return@collect
} else {
KeyguardState.LOCKSCREEN
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractor.kt
index 75d6631008ca..77fc804d1e82 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractor.kt
@@ -294,5 +294,6 @@ constructor(
val TO_OCCLUDED_DURATION = 550.milliseconds
val TO_GLANCEABLE_HUB_DURATION = DEFAULT_DURATION
val TO_GONE_SURFACE_BEHIND_VISIBLE_THRESHOLD = 0.1f
+ val TO_DREAMING_DURATION = DEFAULT_DURATION
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractor.kt
index 02e04aa279d8..21b28a24213f 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractor.kt
@@ -35,7 +35,7 @@ import com.android.systemui.scene.shared.flag.SceneContainerFlag
import com.android.systemui.shade.domain.interactor.ShadeModeInteractor
import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor
import com.android.systemui.statusbar.notification.domain.interactor.HeadsUpNotificationInteractor
-import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUiAod
+import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUi
import com.android.systemui.statusbar.notification.promoted.domain.interactor.AODPromotedNotificationInteractor
import com.android.systemui.util.kotlin.combine
import com.android.systemui.utils.coroutines.flow.flatMapLatestConflated
@@ -90,14 +90,14 @@ constructor(
var clock: ClockController? by keyguardClockRepository.clockEventController::clock
private val isAodPromotedNotificationPresent: Flow<Boolean> =
- if (PromotedNotificationUiAod.isEnabled) {
+ if (PromotedNotificationUi.isEnabled) {
aodPromotedNotificationInteractor.isPresent
} else {
flowOf(false)
}
private val areAnyNotificationsPresent: Flow<Boolean> =
- if (PromotedNotificationUiAod.isEnabled) {
+ if (PromotedNotificationUi.isEnabled) {
combine(
activeNotificationsInteractor.areAnyNotificationsPresent,
isAodPromotedNotificationPresent,
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 2d5ff61a5015..e625fd72e159 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
@@ -507,6 +507,11 @@ constructor(
}
/** Temporary shim, until [KeyguardWmStateRefactor] is enabled */
+ fun showGlanceableHub(): Boolean {
+ return fromGoneTransitionInteractor.get().showGlanceableHub()
+ }
+
+ /** Temporary shim, until [KeyguardWmStateRefactor] is enabled */
fun dismissKeyguard() {
when (keyguardTransitionInteractor.transitionState.value.to) {
LOCKSCREEN -> fromLockscreenTransitionInteractor.get().dismissKeyguard()
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardWakeDirectlyToGoneInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardWakeDirectlyToGoneInteractor.kt
index ef1fe49372b2..6249b8006083 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardWakeDirectlyToGoneInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardWakeDirectlyToGoneInteractor.kt
@@ -33,9 +33,11 @@ import com.android.systemui.keyguard.KeyguardViewMediator
import com.android.systemui.keyguard.KeyguardWmStateRefactor
import com.android.systemui.keyguard.data.repository.KeyguardRepository
import com.android.systemui.keyguard.shared.model.BiometricUnlockMode
+import com.android.systemui.keyguard.shared.model.BiometricUnlockModel
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.model.KeyguardState.Companion.deviceIsAsleepInState
import com.android.systemui.keyguard.shared.model.KeyguardState.Companion.deviceIsAwakeInState
+import com.android.systemui.keyguard.shared.model.TransitionStep
import com.android.systemui.power.domain.interactor.PowerInteractor
import com.android.systemui.power.shared.model.WakeSleepReason
import com.android.systemui.scene.shared.model.Scenes
@@ -137,19 +139,20 @@ constructor(
repository.biometricUnlockState,
repository.canIgnoreAuthAndReturnToGone,
transitionInteractor.currentKeyguardState,
- ) {
- keyguardEnabled,
- shouldSuppressKeyguard,
- biometricUnlockState,
- canIgnoreAuthAndReturnToGone,
- currentState ->
+ transitionInteractor.startedKeyguardTransitionStep,
+ ) { values ->
+ val keyguardEnabled = values[0] as Boolean
+ val shouldSuppressKeyguard = values[1] as Boolean
+ val biometricUnlockState = values[2] as BiometricUnlockModel
+ val canIgnoreAuthAndReturnToGone = values[3] as Boolean
+ val currentState = values[4] as KeyguardState
+ val startedStep = values[5] as TransitionStep
(!keyguardEnabled || shouldSuppressKeyguard) ||
BiometricUnlockMode.isWakeAndUnlock(biometricUnlockState.mode) ||
canIgnoreAuthAndReturnToGone ||
(currentState == KeyguardState.DREAMING &&
keyguardInteractor.isKeyguardDismissible.value) ||
- (currentState == KeyguardState.GONE &&
- transitionInteractor.getStartedState() == KeyguardState.GONE)
+ (currentState == KeyguardState.GONE && startedStep.to == KeyguardState.GONE)
}
.distinctUntilChanged()
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/AccessibilityActionsViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/AccessibilityActionsViewBinder.kt
index 824e0228adca..c7c54e95a63b 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/AccessibilityActionsViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/AccessibilityActionsViewBinder.kt
@@ -19,21 +19,19 @@ package com.android.systemui.keyguard.ui.binder
import android.os.Bundle
import android.view.View
+import android.view.accessibility.AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED
import android.view.accessibility.AccessibilityNodeInfo
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.repeatOnLifecycle
+import com.android.app.tracing.coroutines.launchTraced as launch
import com.android.systemui.keyguard.ui.viewmodel.AccessibilityActionsViewModel
import com.android.systemui.lifecycle.repeatWhenAttached
import com.android.systemui.res.R
import kotlinx.coroutines.DisposableHandle
-import com.android.app.tracing.coroutines.launchTraced as launch
/** View binder for accessibility actions placeholder on keyguard. */
object AccessibilityActionsViewBinder {
- fun bind(
- view: View,
- viewModel: AccessibilityActionsViewModel,
- ): DisposableHandle {
+ fun bind(view: View, viewModel: AccessibilityActionsViewModel): DisposableHandle {
val disposableHandle =
view.repeatWhenAttached {
repeatOnLifecycle(Lifecycle.State.STARTED) {
@@ -60,9 +58,10 @@ object AccessibilityActionsViewBinder {
object : View.AccessibilityDelegate() {
override fun onInitializeAccessibilityNodeInfo(
host: View,
- info: AccessibilityNodeInfo
+ info: AccessibilityNodeInfo,
) {
super.onInitializeAccessibilityNodeInfo(host, info)
+
// Add custom actions
if (canOpenGlanceableHub) {
val action =
@@ -80,7 +79,7 @@ object AccessibilityActionsViewBinder {
override fun performAccessibilityAction(
host: View,
action: Int,
- args: Bundle?
+ args: Bundle?,
): Boolean {
return if (
action == R.id.accessibility_action_open_communal_hub
@@ -89,6 +88,20 @@ object AccessibilityActionsViewBinder {
true
} else super.performAccessibilityAction(host, action, args)
}
+
+ override fun sendAccessibilityEvent(
+ host: View,
+ eventType: Int,
+ ) {
+ if (eventType == TYPE_VIEW_ACCESSIBILITY_FOCUSED) {
+ launch {
+ viewModel.clearUdfpsAccessibilityOverlayMessage(
+ "eventType $eventType on view $host"
+ )
+ }
+ }
+ super.sendAccessibilityEvent(host, eventType)
+ }
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerViewBinder.kt
index b8b032719ef8..00d41d0a7aa7 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerViewBinder.kt
@@ -19,8 +19,10 @@ package com.android.systemui.keyguard.ui.binder
import android.util.Log
import android.view.LayoutInflater
import android.view.View
+import android.view.View.IMPORTANT_FOR_ACCESSIBILITY_AUTO
import android.view.ViewGroup
import android.view.WindowManager
+import android.view.accessibility.AccessibilityEvent.TYPE_VIEW_HOVER_EXIT
import android.window.OnBackInvokedCallback
import android.window.OnBackInvokedDispatcher
import androidx.constraintlayout.widget.ConstraintLayout
@@ -47,6 +49,7 @@ import com.android.systemui.scrim.ScrimView
import dagger.Lazy
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
/**
* When necessary, adds the alternate bouncer window above most other windows (including the
@@ -235,6 +238,25 @@ constructor(
udfpsA11yOverlay =
UdfpsAccessibilityOverlay(view.context).apply {
id = udfpsA11yOverlayViewId
+ importantForAccessibility = IMPORTANT_FOR_ACCESSIBILITY_AUTO
+ }
+ udfpsA11yOverlay.accessibilityDelegate =
+ object : View.AccessibilityDelegate() {
+ override fun sendAccessibilityEvent(
+ host: View,
+ eventType: Int,
+ ) {
+ if (eventType == TYPE_VIEW_HOVER_EXIT) {
+ applicationScope.launch {
+ udfpsA11yOverlayViewModel
+ .get()
+ .clearUdfpsAccessibilityOverlayMessage(
+ "$eventType on view $host"
+ )
+ }
+ }
+ super.sendAccessibilityEvent(host, eventType)
+ }
}
view.addView(udfpsA11yOverlay)
UdfpsAccessibilityOverlayBinder.bind(
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBlueprintViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBlueprintViewBinder.kt
index fc5914b02e05..f38a2430b8fc 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBlueprintViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBlueprintViewBinder.kt
@@ -128,13 +128,7 @@ object KeyguardBlueprintViewBinder {
cs: ConstraintSet,
constraintLayout: ConstraintLayout,
) {
- val ids =
- listOf(
- sharedR.id.date_smartspace_view,
- sharedR.id.date_smartspace_view_large,
- sharedR.id.weather_smartspace_view,
- sharedR.id.weather_smartspace_view_large,
- )
+ val ids = listOf(sharedR.id.date_smartspace_view, sharedR.id.date_smartspace_view_large)
for (i in ids) {
constraintLayout.getViewById(i)?.visibility = cs.getVisibility(i)
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 60460bf68c12..d90292517b01 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
@@ -51,13 +51,12 @@ import com.android.systemui.common.ui.view.onLayoutChanged
import com.android.systemui.common.ui.view.onTouchListener
import com.android.systemui.customization.R as customR
import com.android.systemui.deviceentry.domain.interactor.DeviceEntryHapticsInteractor
-import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.ui.view.layout.sections.AodPromotedNotificationSection
import com.android.systemui.keyguard.ui.viewmodel.BurnInParameters
import com.android.systemui.keyguard.ui.viewmodel.KeyguardBlueprintViewModel
-import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel
import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel
+import com.android.systemui.keyguard.ui.viewmodel.KeyguardSmartspaceViewModel
import com.android.systemui.keyguard.ui.viewmodel.OccludingAppDeviceEntryMessageViewModel
import com.android.systemui.keyguard.ui.viewmodel.TransitionData
import com.android.systemui.keyguard.ui.viewmodel.ViewStateAccessor
@@ -72,7 +71,6 @@ import com.android.systemui.shade.domain.interactor.ShadeInteractor
import com.android.systemui.shared.R as sharedR
import com.android.systemui.statusbar.CrossFadeHelper
import com.android.systemui.statusbar.VibratorHelper
-import com.android.systemui.statusbar.phone.ScreenOffAnimationController
import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager
import com.android.systemui.temporarydisplay.ViewPriority
import com.android.systemui.temporarydisplay.chipbar.ChipbarCoordinator
@@ -103,10 +101,8 @@ object KeyguardRootViewBinder {
configuration: ConfigurationState,
occludingAppDeviceEntryMessageViewModel: OccludingAppDeviceEntryMessageViewModel?,
chipbarCoordinator: ChipbarCoordinator?,
- screenOffAnimationController: ScreenOffAnimationController,
shadeInteractor: ShadeInteractor,
- clockInteractor: KeyguardClockInteractor,
- clockViewModel: KeyguardClockViewModel,
+ smartspaceViewModel: KeyguardSmartspaceViewModel,
deviceEntryHapticsInteractor: DeviceEntryHapticsInteractor?,
vibratorHelper: VibratorHelper?,
falsingManager: FalsingManager?,
@@ -193,7 +189,6 @@ object KeyguardRootViewBinder {
childViews[largeClockId]?.translationY = y
if (com.android.systemui.shared.Flags.clockReactiveSmartspaceLayout()) {
childViews[largeClockDateId]?.translationY = y
- childViews[largeClockWeatherId]?.translationY = y
}
childViews[aodPromotedNotificationId]?.translationY = y
childViews[aodNotificationIconContainerId]?.translationY = y
@@ -327,7 +322,7 @@ object KeyguardRootViewBinder {
if (isFullyAnyExpanded) {
INVISIBLE
} else {
- View.VISIBLE
+ VISIBLE
}
}
}
@@ -395,7 +390,7 @@ object KeyguardRootViewBinder {
OnLayoutChange(
viewModel,
blueprintViewModel,
- clockViewModel,
+ smartspaceViewModel,
childViews,
burnInParams,
Logger(blueprintLog, TAG),
@@ -453,7 +448,7 @@ object KeyguardRootViewBinder {
private class OnLayoutChange(
private val viewModel: KeyguardRootViewModel,
private val blueprintViewModel: KeyguardBlueprintViewModel,
- private val clockViewModel: KeyguardClockViewModel,
+ private val smartspaceViewModel: KeyguardSmartspaceViewModel,
private val childViews: Map<Int, View>,
private val burnInParams: MutableStateFlow<BurnInParameters>,
private val logger: Logger,
@@ -471,12 +466,16 @@ object KeyguardRootViewBinder {
oldRight: Int,
oldBottom: Int,
) {
+ val prevSmartspaceVisibility = smartspaceViewModel.bcSmartspaceVisibility.value
+ val smartspaceVisibility = childViews[bcSmartspaceId]?.visibility ?: GONE
+ val smartspaceVisibilityChanged = prevSmartspaceVisibility != smartspaceVisibility
+
// After layout, ensure the notifications are positioned correctly
childViews[nsslPlaceholderId]?.let { notificationListPlaceholder ->
// Do not update a second time while a blueprint transition is running
val transition = blueprintViewModel.currentTransition.value
val shouldAnimate = transition != null && transition.config.type.animateNotifChanges
- if (prevTransition == transition && shouldAnimate) {
+ if (prevTransition == transition && shouldAnimate && !smartspaceVisibilityChanged) {
logger.w("Skipping onNotificationContainerBoundsChanged during transition")
return
}
@@ -485,7 +484,7 @@ object KeyguardRootViewBinder {
viewModel.onNotificationContainerBoundsChanged(
notificationListPlaceholder.top.toFloat(),
notificationListPlaceholder.bottom.toFloat(),
- animate = shouldAnimate,
+ animate = (shouldAnimate || smartspaceVisibilityChanged),
)
}
@@ -585,6 +584,7 @@ object KeyguardRootViewBinder {
private val largeClockId = customR.id.lockscreen_clock_view_large
private val largeClockDateId = sharedR.id.date_smartspace_view_large
private val largeClockWeatherId = sharedR.id.weather_smartspace_view_large
+ private val bcSmartspaceId = sharedR.id.bc_smartspace_view
private val smallClockId = customR.id.lockscreen_clock_view
private val indicationArea = R.id.keyguard_indication_area
private val startButton = R.id.start_button
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSmartspaceViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSmartspaceViewBinder.kt
index 5ef2d6fd3256..39fe588d8b6b 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSmartspaceViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSmartspaceViewBinder.kt
@@ -91,14 +91,9 @@ object KeyguardSmartspaceViewBinder {
R.dimen.smartspace_padding_vertical
)
- val smallViewIds =
- listOf(sharedR.id.date_smartspace_view, sharedR.id.weather_smartspace_view)
+ val smallViewId = sharedR.id.date_smartspace_view
- val largeViewIds =
- listOf(
- sharedR.id.date_smartspace_view_large,
- sharedR.id.weather_smartspace_view_large,
- )
+ val largeViewId = sharedR.id.date_smartspace_view_large
launch("$TAG#smartspaceViewModel.burnInLayerVisibility") {
combine(
@@ -109,10 +104,8 @@ object KeyguardSmartspaceViewBinder {
.collect { (visibility, isLargeClock) ->
if (isLargeClock) {
// hide small clock date/weather
- for (viewId in smallViewIds) {
- keyguardRootView.findViewById<View>(viewId)?.let {
- it.visibility = View.GONE
- }
+ keyguardRootView.findViewById<View>(smallViewId)?.let {
+ it.visibility = View.GONE
}
}
}
@@ -130,10 +123,9 @@ object KeyguardSmartspaceViewBinder {
::Pair,
)
.collect { (isLargeClock, clockBounds) ->
- for (id in (if (isLargeClock) smallViewIds else largeViewIds)) {
- keyguardRootView.findViewById<View>(id)?.let {
- it.visibility = View.GONE
- }
+ val viewId = if (isLargeClock) smallViewId else largeViewId
+ keyguardRootView.findViewById<View>(viewId)?.let {
+ it.visibility = View.GONE
}
if (clockBounds == VRectF.ZERO) return@collect
@@ -144,26 +136,26 @@ object KeyguardSmartspaceViewBinder {
sharedR.id.date_smartspace_view_large
)
?.height ?: 0
- for (id in largeViewIds) {
- keyguardRootView.findViewById<View>(id)?.let { view ->
- val viewHeight = view.height
- val offset = (largeDateHeight - viewHeight) / 2
- view.top =
- (clockBounds.bottom + yBuffer + offset).toInt()
- view.bottom = view.top + viewHeight
- }
+
+ keyguardRootView.findViewById<View>(largeViewId)?.let { view ->
+ val viewHeight = view.height
+ val offset = (largeDateHeight - viewHeight) / 2
+ view.top = (clockBounds.bottom + yBuffer + offset).toInt()
+ view.bottom = view.top + viewHeight
}
- } else {
- for (id in smallViewIds) {
- keyguardRootView.findViewById<View>(id)?.let { view ->
- val viewWidth = view.width
- if (view.isLayoutRtl()) {
- view.right = (clockBounds.left - xBuffer).toInt()
- view.left = view.right - viewWidth
- } else {
- view.left = (clockBounds.right + xBuffer).toInt()
- view.right = view.left + viewWidth
- }
+ } else if (
+ !KeyguardSmartspaceViewModel.dateWeatherBelowSmallClock(
+ keyguardRootView.resources.configuration
+ )
+ ) {
+ keyguardRootView.findViewById<View>(smallViewId)?.let { view ->
+ val viewWidth = view.width
+ if (view.isLayoutRtl()) {
+ view.right = (clockBounds.left - xBuffer).toInt()
+ view.left = view.right - viewWidth
+ } else {
+ view.left = (clockBounds.right + xBuffer).toInt()
+ view.right = view.left + viewWidth
}
}
}
@@ -218,11 +210,6 @@ object KeyguardSmartspaceViewBinder {
val dateView =
constraintLayout.requireViewById<View>(sharedR.id.date_smartspace_view)
addView(dateView)
- if (com.android.systemui.shared.Flags.clockReactiveSmartspaceLayout()) {
- val weatherView =
- constraintLayout.requireViewById<View>(sharedR.id.weather_smartspace_view)
- addView(weatherView)
- }
}
}
}
@@ -240,11 +227,6 @@ object KeyguardSmartspaceViewBinder {
val dateView =
constraintLayout.requireViewById<View>(sharedR.id.date_smartspace_view)
removeView(dateView)
- if (com.android.systemui.shared.Flags.clockReactiveSmartspaceLayout()) {
- val weatherView =
- constraintLayout.requireViewById<View>(sharedR.id.weather_smartspace_view)
- removeView(weatherView)
- }
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodNotificationIconsSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodNotificationIconsSection.kt
index f717431f6a40..bca0bedc7350 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodNotificationIconsSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodNotificationIconsSection.kt
@@ -39,7 +39,7 @@ import com.android.systemui.statusbar.notification.icon.ui.viewbinder.AlwaysOnDi
import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerViewBinder
import com.android.systemui.statusbar.notification.icon.ui.viewbinder.StatusBarIconViewBindingFailureTracker
import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerAlwaysOnDisplayViewModel
-import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUiAod
+import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUi
import com.android.systemui.statusbar.phone.NotificationIconContainer
import com.android.systemui.statusbar.ui.SystemBarUtilsState
import com.android.systemui.util.ui.value
@@ -102,7 +102,7 @@ constructor(
val isShadeLayoutWide = shadeModeInteractor.isShadeLayoutWide.value
constraintSet.apply {
- if (PromotedNotificationUiAod.isEnabled) {
+ if (PromotedNotificationUi.isEnabled) {
connect(nicId, TOP, AodPromotedNotificationSection.viewId, BOTTOM, bottomMargin)
} else {
connect(nicId, TOP, R.id.smart_space_barrier_bottom, BOTTOM, bottomMargin)
@@ -111,7 +111,7 @@ constructor(
setGoneMargin(nicId, BOTTOM, bottomMargin)
setVisibility(nicId, if (isVisible.value) VISIBLE else GONE)
- if (PromotedNotificationUiAod.isEnabled && isShadeLayoutWide) {
+ if (PromotedNotificationUi.isEnabled && isShadeLayoutWide) {
// Don't create a start constraint, so the icons can hopefully right-align.
} else {
connect(nicId, START, PARENT_ID, START, horizontalMargin)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodPromotedNotificationSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodPromotedNotificationSection.kt
index efdc5abf1f67..f75b53017500 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodPromotedNotificationSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodPromotedNotificationSection.kt
@@ -31,7 +31,7 @@ import com.android.systemui.shade.ShadeDisplayAware
import com.android.systemui.shade.domain.interactor.ShadeModeInteractor
import com.android.systemui.statusbar.notification.promoted.AODPromotedNotification
import com.android.systemui.statusbar.notification.promoted.PromotedNotificationLogger
-import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUiAod
+import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUi
import com.android.systemui.statusbar.notification.promoted.ui.viewmodel.AODPromotedNotificationViewModel
import javax.inject.Inject
@@ -50,7 +50,7 @@ constructor(
}
override fun addViews(constraintLayout: ConstraintLayout) {
- if (!PromotedNotificationUiAod.isEnabled) {
+ if (!PromotedNotificationUi.isEnabled) {
return
}
@@ -67,7 +67,7 @@ constructor(
}
override fun bindData(constraintLayout: ConstraintLayout) {
- if (!PromotedNotificationUiAod.isEnabled) {
+ if (!PromotedNotificationUi.isEnabled) {
return
}
@@ -79,7 +79,7 @@ constructor(
}
override fun applyConstraints(constraintSet: ConstraintSet) {
- if (!PromotedNotificationUiAod.isEnabled) {
+ if (!PromotedNotificationUi.isEnabled) {
return
}
@@ -119,7 +119,7 @@ constructor(
}
override fun removeViews(constraintLayout: ConstraintLayout) {
- if (!PromotedNotificationUiAod.isEnabled) {
+ if (!PromotedNotificationUi.isEnabled) {
return
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt
index 8a33c6471326..9c6f46570b1d 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt
@@ -121,18 +121,22 @@ constructor(
setAlpha(getNonTargetClockFace(clock).views, 0F)
if (!keyguardClockViewModel.isLargeClockVisible.value) {
- if (com.android.systemui.shared.Flags.clockReactiveSmartspaceLayout()) {
+ if (
+ KeyguardSmartspaceViewModel.dateWeatherBelowSmallClock(
+ context.resources.configuration
+ )
+ ) {
connect(
sharedR.id.bc_smartspace_view,
TOP,
- customR.id.lockscreen_clock_view,
+ sharedR.id.date_smartspace_view,
BOTTOM,
)
} else {
connect(
sharedR.id.bc_smartspace_view,
TOP,
- sharedR.id.date_smartspace_view,
+ customR.id.lockscreen_clock_view,
BOTTOM,
)
}
@@ -187,6 +191,8 @@ constructor(
val guideline =
if (keyguardClockViewModel.clockShouldBeCentered.value) PARENT_ID
else R.id.split_shade_guideline
+ val dateWeatherBelowSmallClock =
+ KeyguardSmartspaceViewModel.dateWeatherBelowSmallClock(context.resources.configuration)
constraints.apply {
connect(customR.id.lockscreen_clock_view_large, START, PARENT_ID, START)
connect(customR.id.lockscreen_clock_view_large, END, guideline, END)
@@ -254,11 +260,7 @@ constructor(
0
}
- if (com.android.systemui.shared.Flags.clockReactiveSmartspaceLayout()) {
- clockInteractor.setNotificationStackDefaultTop(
- (smallClockBottom + marginBetweenSmartspaceAndNotification).toFloat()
- )
- } else {
+ if (dateWeatherBelowSmallClock) {
val dateWeatherSmartspaceHeight =
getDimen(context, DATE_WEATHER_VIEW_HEIGHT).toFloat()
clockInteractor.setNotificationStackDefaultTop(
@@ -266,6 +268,10 @@ constructor(
dateWeatherSmartspaceHeight +
marginBetweenSmartspaceAndNotification
)
+ } else {
+ clockInteractor.setNotificationStackDefaultTop(
+ (smallClockBottom + marginBetweenSmartspaceAndNotification).toFloat()
+ )
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSection.kt
index d0b5f743c277..d9652b590678 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSection.kt
@@ -20,6 +20,7 @@ import android.content.Context
import android.view.View
import android.view.ViewGroup
import android.view.ViewTreeObserver.OnGlobalLayoutListener
+import android.widget.LinearLayout
import androidx.constraintlayout.widget.Barrier
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.constraintlayout.widget.ConstraintSet
@@ -57,10 +58,8 @@ constructor(
private val keyguardRootViewModel: KeyguardRootViewModel,
) : KeyguardSection() {
private var smartspaceView: View? = null
- private var weatherView: View? = null
private var dateView: ViewGroup? = null
- private var weatherViewLargeClock: View? = null
- private var dateViewLargeClock: View? = null
+ private var dateViewLargeClock: ViewGroup? = null
private var smartspaceVisibilityListener: OnGlobalLayoutListener? = null
private var pastVisibility: Int = -1
@@ -77,34 +76,47 @@ constructor(
override fun addViews(constraintLayout: ConstraintLayout) {
if (!keyguardSmartspaceViewModel.isSmartspaceEnabled) return
smartspaceView = smartspaceController.buildAndConnectView(constraintLayout)
- weatherView = smartspaceController.buildAndConnectWeatherView(constraintLayout, false)
dateView =
smartspaceController.buildAndConnectDateView(constraintLayout, false) as? ViewGroup
+ var weatherViewLargeClock: View? = null
+ val weatherView: View? =
+ smartspaceController.buildAndConnectWeatherView(constraintLayout, false)
if (com.android.systemui.shared.Flags.clockReactiveSmartspaceLayout()) {
weatherViewLargeClock =
smartspaceController.buildAndConnectWeatherView(constraintLayout, true)
dateViewLargeClock =
- smartspaceController.buildAndConnectDateView(constraintLayout, true)
+ smartspaceController.buildAndConnectDateView(constraintLayout, true) as? ViewGroup
}
pastVisibility = smartspaceView?.visibility ?: View.GONE
constraintLayout.addView(smartspaceView)
if (com.android.systemui.shared.Flags.clockReactiveSmartspaceLayout()) {
dateView?.visibility = View.GONE
- weatherView?.visibility = View.GONE
dateViewLargeClock?.visibility = View.GONE
- weatherViewLargeClock?.visibility = View.GONE
- constraintLayout.addView(dateView)
- constraintLayout.addView(weatherView)
- constraintLayout.addView(weatherViewLargeClock)
constraintLayout.addView(dateViewLargeClock)
- } else {
if (keyguardSmartspaceViewModel.isDateWeatherDecoupled) {
- constraintLayout.addView(dateView)
// Place weather right after the date, before the extras (alarm and dnd)
- val index = if (dateView?.childCount == 0) 0 else 1
- dateView?.addView(weatherView, index)
+ val index = if (dateViewLargeClock?.childCount == 0) 0 else 1
+ dateViewLargeClock?.addView(weatherViewLargeClock, index)
+ }
+
+ if (
+ KeyguardSmartspaceViewModel.dateWeatherBelowSmallClock(
+ context.resources.configuration,
+ keyguardClockViewModel.hasCustomWeatherDataDisplay.value,
+ )
+ ) {
+ (dateView as? LinearLayout)?.orientation = LinearLayout.HORIZONTAL
+ } else {
+ (dateView as? LinearLayout)?.orientation = LinearLayout.VERTICAL
}
}
+
+ if (keyguardSmartspaceViewModel.isDateWeatherDecoupled) {
+ constraintLayout.addView(dateView)
+ // Place weather right after the date, before the extras (alarm and dnd)
+ val index = if (dateView?.childCount == 0) 0 else 1
+ dateView?.addView(weatherView, index)
+ }
keyguardUnlockAnimationController.lockscreenSmartspace = smartspaceView
smartspaceVisibilityListener = OnGlobalLayoutListener {
smartspaceView?.let {
@@ -136,10 +148,15 @@ constructor(
val dateWeatherPaddingStart = KeyguardSmartspaceViewModel.getDateWeatherStartMargin(context)
val smartspaceHorizontalPadding =
KeyguardSmartspaceViewModel.getSmartspaceHorizontalMargin(context)
+ val dateWeatherBelowSmallClock =
+ KeyguardSmartspaceViewModel.dateWeatherBelowSmallClock(
+ context.resources.configuration,
+ keyguardClockViewModel.hasCustomWeatherDataDisplay.value,
+ )
constraintSet.apply {
constrainHeight(sharedR.id.date_smartspace_view, ConstraintSet.WRAP_CONTENT)
constrainWidth(sharedR.id.date_smartspace_view, ConstraintSet.WRAP_CONTENT)
- if (!com.android.systemui.shared.Flags.clockReactiveSmartspaceLayout()) {
+ if (dateWeatherBelowSmallClock) {
connect(
sharedR.id.date_smartspace_view,
ConstraintSet.START,
@@ -167,7 +184,7 @@ constructor(
smartspaceHorizontalPadding,
)
if (keyguardClockViewModel.hasCustomWeatherDataDisplay.value) {
- if (!com.android.systemui.shared.Flags.clockReactiveSmartspaceLayout()) {
+ if (dateWeatherBelowSmallClock) {
clear(sharedR.id.date_smartspace_view, ConstraintSet.TOP)
connect(
sharedR.id.date_smartspace_view,
@@ -179,12 +196,27 @@ constructor(
} else {
clear(sharedR.id.date_smartspace_view, ConstraintSet.BOTTOM)
if (com.android.systemui.shared.Flags.clockReactiveSmartspaceLayout()) {
- connect(
- sharedR.id.bc_smartspace_view,
- ConstraintSet.TOP,
- customR.id.lockscreen_clock_view,
- ConstraintSet.BOTTOM,
- )
+ if (dateWeatherBelowSmallClock) {
+ connect(
+ sharedR.id.date_smartspace_view,
+ ConstraintSet.TOP,
+ customR.id.lockscreen_clock_view,
+ ConstraintSet.BOTTOM,
+ )
+ connect(
+ sharedR.id.bc_smartspace_view,
+ ConstraintSet.TOP,
+ sharedR.id.date_smartspace_view,
+ ConstraintSet.BOTTOM,
+ )
+ } else {
+ connect(
+ sharedR.id.bc_smartspace_view,
+ ConstraintSet.TOP,
+ customR.id.lockscreen_clock_view,
+ ConstraintSet.BOTTOM,
+ )
+ }
} else {
connect(
sharedR.id.date_smartspace_view,
@@ -203,7 +235,6 @@ constructor(
if (com.android.systemui.shared.Flags.clockReactiveSmartspaceLayout()) {
if (keyguardClockViewModel.isLargeClockVisible.value) {
- setVisibility(sharedR.id.weather_smartspace_view, GONE)
setVisibility(sharedR.id.date_smartspace_view, GONE)
constrainHeight(
sharedR.id.date_smartspace_view_large,
@@ -238,118 +269,79 @@ constructor(
connect(
sharedR.id.date_smartspace_view_large,
ConstraintSet.END,
- sharedR.id.weather_smartspace_view_large,
- ConstraintSet.START,
- )
-
- connect(
- sharedR.id.weather_smartspace_view_large,
- ConstraintSet.BOTTOM,
- sharedR.id.date_smartspace_view_large,
- ConstraintSet.BOTTOM,
- )
-
- connect(
- sharedR.id.weather_smartspace_view_large,
- ConstraintSet.TOP,
- sharedR.id.date_smartspace_view_large,
- ConstraintSet.TOP,
- )
-
- connect(
- sharedR.id.weather_smartspace_view_large,
- ConstraintSet.START,
- sharedR.id.date_smartspace_view_large,
- ConstraintSet.END,
- )
-
- connect(
- sharedR.id.weather_smartspace_view_large,
- ConstraintSet.END,
customR.id.lockscreen_clock_view_large,
ConstraintSet.END,
)
-
- setHorizontalChainStyle(
- sharedR.id.weather_smartspace_view_large,
- ConstraintSet.CHAIN_PACKED,
- )
setHorizontalChainStyle(
sharedR.id.date_smartspace_view_large,
ConstraintSet.CHAIN_PACKED,
)
} else {
- setVisibility(sharedR.id.weather_smartspace_view_large, GONE)
- setVisibility(sharedR.id.date_smartspace_view_large, GONE)
- constrainHeight(sharedR.id.date_smartspace_view, ConstraintSet.WRAP_CONTENT)
- constrainWidth(sharedR.id.date_smartspace_view, ConstraintSet.WRAP_CONTENT)
- constrainHeight(sharedR.id.weather_smartspace_view, ConstraintSet.WRAP_CONTENT)
- constrainWidth(sharedR.id.weather_smartspace_view, ConstraintSet.WRAP_CONTENT)
+ if (dateWeatherBelowSmallClock) {
+ connect(
+ sharedR.id.date_smartspace_view,
+ ConstraintSet.START,
+ ConstraintSet.PARENT_ID,
+ ConstraintSet.START,
+ dateWeatherPaddingStart,
+ )
+ } else {
+ setVisibility(sharedR.id.date_smartspace_view_large, GONE)
+ constrainHeight(sharedR.id.date_smartspace_view, ConstraintSet.WRAP_CONTENT)
+ constrainWidth(sharedR.id.date_smartspace_view, ConstraintSet.WRAP_CONTENT)
+ connect(
+ sharedR.id.date_smartspace_view,
+ ConstraintSet.START,
+ customR.id.lockscreen_clock_view,
+ ConstraintSet.END,
+ context.resources.getDimensionPixelSize(
+ R.dimen.smartspace_padding_horizontal
+ ),
+ )
+ connect(
+ sharedR.id.date_smartspace_view,
+ ConstraintSet.TOP,
+ customR.id.lockscreen_clock_view,
+ ConstraintSet.TOP,
+ )
+ connect(
+ sharedR.id.date_smartspace_view,
+ ConstraintSet.BOTTOM,
+ customR.id.lockscreen_clock_view,
+ ConstraintSet.BOTTOM,
+ )
+ }
+ }
+ }
- connect(
- sharedR.id.date_smartspace_view,
- ConstraintSet.START,
- customR.id.lockscreen_clock_view,
- ConstraintSet.END,
- context.resources.getDimensionPixelSize(
- R.dimen.smartspace_padding_horizontal
- ),
- )
- connect(
- sharedR.id.date_smartspace_view,
- ConstraintSet.TOP,
- customR.id.lockscreen_clock_view,
- ConstraintSet.TOP,
- )
- connect(
- sharedR.id.date_smartspace_view,
- ConstraintSet.BOTTOM,
- sharedR.id.weather_smartspace_view,
- ConstraintSet.TOP,
- )
- connect(
- sharedR.id.weather_smartspace_view,
- ConstraintSet.START,
- sharedR.id.date_smartspace_view,
- ConstraintSet.START,
- )
- connect(
- sharedR.id.weather_smartspace_view,
- ConstraintSet.TOP,
- sharedR.id.date_smartspace_view,
- ConstraintSet.BOTTOM,
+ if (com.android.systemui.shared.Flags.clockReactiveSmartspaceLayout()) {
+ if (dateWeatherBelowSmallClock) {
+ createBarrier(
+ R.id.smart_space_barrier_bottom,
+ Barrier.BOTTOM,
+ 0,
+ *intArrayOf(sharedR.id.bc_smartspace_view, sharedR.id.date_smartspace_view),
)
- connect(
- sharedR.id.weather_smartspace_view,
- ConstraintSet.BOTTOM,
- customR.id.lockscreen_clock_view,
- ConstraintSet.BOTTOM,
+ createBarrier(
+ R.id.smart_space_barrier_top,
+ Barrier.TOP,
+ 0,
+ *intArrayOf(sharedR.id.bc_smartspace_view, sharedR.id.date_smartspace_view),
)
-
- setVerticalChainStyle(
- sharedR.id.weather_smartspace_view,
- ConstraintSet.CHAIN_PACKED,
+ } else {
+ createBarrier(
+ R.id.smart_space_barrier_bottom,
+ Barrier.BOTTOM,
+ 0,
+ sharedR.id.bc_smartspace_view,
)
- setVerticalChainStyle(
- sharedR.id.date_smartspace_view,
- ConstraintSet.CHAIN_PACKED,
+ createBarrier(
+ R.id.smart_space_barrier_top,
+ Barrier.TOP,
+ 0,
+ sharedR.id.bc_smartspace_view,
)
}
- }
-
- if (com.android.systemui.shared.Flags.clockReactiveSmartspaceLayout()) {
- createBarrier(
- R.id.smart_space_barrier_bottom,
- Barrier.BOTTOM,
- 0,
- sharedR.id.bc_smartspace_view,
- )
- createBarrier(
- R.id.smart_space_barrier_top,
- Barrier.TOP,
- 0,
- sharedR.id.bc_smartspace_view,
- )
} else {
createBarrier(
R.id.smart_space_barrier_bottom,
@@ -373,13 +365,7 @@ constructor(
val list =
if (com.android.systemui.shared.Flags.clockReactiveSmartspaceLayout()) {
- listOf(
- smartspaceView,
- dateView,
- weatherView,
- weatherViewLargeClock,
- dateViewLargeClock,
- )
+ listOf(smartspaceView, dateView, dateViewLargeClock)
} else {
listOf(smartspaceView, dateView)
}
@@ -424,10 +410,8 @@ constructor(
if (com.android.systemui.shared.Flags.clockReactiveSmartspaceLayout()) {
if (keyguardClockViewModel.isLargeClockVisible.value) {
- setVisibility(sharedR.id.weather_smartspace_view, GONE)
setVisibility(sharedR.id.date_smartspace_view, GONE)
} else {
- setVisibility(sharedR.id.weather_smartspace_view_large, GONE)
setVisibility(sharedR.id.date_smartspace_view_large, GONE)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/transitions/ClockSizeTransition.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/transitions/ClockSizeTransition.kt
index 434d7eadd742..d830a8456d66 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/transitions/ClockSizeTransition.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/transitions/ClockSizeTransition.kt
@@ -299,14 +299,12 @@ class ClockSizeTransition(
}
if (com.android.systemui.shared.Flags.clockReactiveSmartspaceLayout()) {
addTarget(sharedR.id.date_smartspace_view_large)
- addTarget(sharedR.id.weather_smartspace_view_large)
}
} else {
logger.i("Adding small clock")
addTarget(customR.id.lockscreen_clock_view)
- if (com.android.systemui.shared.Flags.clockReactiveSmartspaceLayout()) {
+ if (!viewModel.dateWeatherBelowSmallClock()) {
addTarget(sharedR.id.date_smartspace_view)
- addTarget(sharedR.id.weather_smartspace_view)
}
}
}
@@ -386,7 +384,7 @@ class ClockSizeTransition(
duration =
if (isLargeClock) STATUS_AREA_MOVE_UP_MILLIS else STATUS_AREA_MOVE_DOWN_MILLIS
interpolator = Interpolators.EMPHASIZED
- if (!com.android.systemui.shared.Flags.clockReactiveSmartspaceLayout()) {
+ if (viewModel.dateWeatherBelowSmallClock()) {
addTarget(sharedR.id.date_smartspace_view)
}
addTarget(sharedR.id.bc_smartspace_view)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/transitions/DefaultClockSteppingTransition.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/transitions/DefaultClockSteppingTransition.kt
index 0874b6da180e..9faca7567279 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/transitions/DefaultClockSteppingTransition.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/transitions/DefaultClockSteppingTransition.kt
@@ -32,7 +32,6 @@ class DefaultClockSteppingTransition(private val clock: ClockController) : Trans
addTarget(clock.largeClock.view)
if (com.android.systemui.shared.Flags.clockReactiveSmartspaceLayout()) {
addTarget(sharedR.id.date_smartspace_view_large)
- addTarget(sharedR.id.weather_smartspace_view_large)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AccessibilityActionsViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AccessibilityActionsViewModel.kt
index 38f5d3e76c7c..678872d0d64d 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AccessibilityActionsViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AccessibilityActionsViewModel.kt
@@ -16,6 +16,7 @@
package com.android.systemui.keyguard.ui.viewmodel
+import com.android.systemui.biometrics.domain.interactor.UdfpsOverlayInteractor
import com.android.systemui.communal.domain.interactor.CommunalInteractor
import com.android.systemui.communal.shared.model.CommunalScenes
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
@@ -33,7 +34,8 @@ class AccessibilityActionsViewModel
constructor(
private val communalInteractor: CommunalInteractor,
keyguardInteractor: KeyguardInteractor,
- keyguardTransitionInteractor: KeyguardTransitionInteractor,
+ val keyguardTransitionInteractor: KeyguardTransitionInteractor,
+ private val udfpsOverlayInteractor: UdfpsOverlayInteractor,
) {
val isCommunalAvailable = communalInteractor.isCommunalAvailable
@@ -44,7 +46,7 @@ constructor(
keyguardTransitionInteractor.transitionValue(KeyguardState.LOCKSCREEN).map {
it == 1f
},
- keyguardInteractor.statusBarState
+ keyguardInteractor.statusBarState,
) { transitionFinishedOnLockscreen, statusBarState ->
transitionFinishedOnLockscreen && statusBarState == StatusBarState.KEYGUARD
}
@@ -55,4 +57,12 @@ constructor(
newScene = CommunalScenes.Communal,
loggingReason = "accessibility",
)
+
+ /**
+ * Clears the content description to prevent the view from storing stale UDFPS directional
+ * guidance messages for accessibility.
+ */
+ suspend fun clearUdfpsAccessibilityOverlayMessage(reason: String) {
+ udfpsOverlayInteractor.clearUdfpsAccessibilityOverlayMessage(reason)
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToPrimaryBouncerTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToPrimaryBouncerTransitionViewModel.kt
new file mode 100644
index 000000000000..8771f02326fa
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToPrimaryBouncerTransitionViewModel.kt
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.ui.viewmodel
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyguard.domain.interactor.FromDreamingTransitionInteractor
+import com.android.systemui.keyguard.shared.model.Edge
+import com.android.systemui.keyguard.shared.model.KeyguardState.DREAMING
+import com.android.systemui.keyguard.shared.model.KeyguardState.PRIMARY_BOUNCER
+import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
+import com.android.systemui.keyguard.ui.transitions.BlurConfig
+import com.android.systemui.keyguard.ui.transitions.PrimaryBouncerTransition
+import com.android.systemui.scene.shared.model.Overlays
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.emptyFlow
+
+@SysUISingleton
+class DreamingToPrimaryBouncerTransitionViewModel
+@Inject
+constructor(blurConfig: BlurConfig, animationFlow: KeyguardTransitionAnimationFlow) :
+ PrimaryBouncerTransition {
+ private val transitionAnimation =
+ animationFlow
+ .setup(
+ duration = FromDreamingTransitionInteractor.TO_PRIMARY_BOUNCER_DURATION,
+ edge = Edge.create(from = DREAMING, to = Overlays.Bouncer),
+ )
+ .setupWithoutSceneContainer(edge = Edge.create(from = DREAMING, to = PRIMARY_BOUNCER))
+
+ override val windowBlurRadius: Flow<Float> =
+ transitionAnimation.immediatelyTransitionTo(blurConfig.maxBlurRadiusPx)
+
+ override val notificationBlurRadius: Flow<Float> = emptyFlow()
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModel.kt
index bcbe66642d11..fd5783ef7f8e 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModel.kt
@@ -19,7 +19,10 @@ package com.android.systemui.keyguard.ui.viewmodel
import android.util.LayoutDirection
import com.android.app.animation.Interpolators.EMPHASIZED
import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor
+import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor
+import com.android.systemui.communal.domain.interactor.CommunalSettingsInteractor
import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.keyguard.dagger.GlanceableHubBlurComponent
import com.android.systemui.keyguard.domain.interactor.FromGlanceableHubTransitionInteractor.Companion.TO_LOCKSCREEN_DURATION
import com.android.systemui.keyguard.shared.model.Edge
@@ -34,21 +37,32 @@ import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.shade.ShadeDisplayAware
import javax.inject.Inject
import kotlin.time.Duration.Companion.milliseconds
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.combineTransform
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.stateIn
/**
* Breaks down GLANCEABLE_HUB->LOCKSCREEN transition into discrete steps for corresponding views to
* consume.
*/
+@OptIn(ExperimentalCoroutinesApi::class)
@SysUISingleton
class GlanceableHubToLockscreenTransitionViewModel
@Inject
constructor(
+ @Application applicationScope: CoroutineScope,
@ShadeDisplayAware configurationInteractor: ConfigurationInteractor,
animationFlow: KeyguardTransitionAnimationFlow,
+ communalSceneInteractor: CommunalSceneInteractor,
+ communalSettingsInteractor: CommunalSettingsInteractor,
private val blurFactory: GlanceableHubBlurComponent.Factory,
) : GlanceableHubTransition, DeviceEntryIconTransition {
private val transitionAnimation =
@@ -59,18 +73,45 @@ constructor(
)
.setupWithoutSceneContainer(edge = Edge.create(from = GLANCEABLE_HUB, to = LOCKSCREEN))
+ // Whether screen rotation will happen with the transition. Only emit when idle so ongoing
+ // animation won't be interrupted when orientation is updated during the transition.
+ private val willRotateToPortraitInTransition: StateFlow<Boolean> =
+ if (!communalSettingsInteractor.isV2FlagEnabled()) {
+ flowOf(false)
+ } else {
+ communalSceneInteractor.isIdleOnCommunal.combineTransform(
+ communalSceneInteractor.willRotateToPortrait
+ ) { isIdle, willRotate ->
+ if (isIdle) emit(willRotate)
+ }
+ }
+ .stateIn(applicationScope, SharingStarted.Eagerly, false)
+
override val windowBlurRadius: Flow<Float> =
blurFactory.create(transitionAnimation).getBlurProvider().exitBlurRadius
val keyguardAlpha: Flow<Float> =
- transitionAnimation.sharedFlow(
- duration = 167.milliseconds,
- startTime = 167.milliseconds,
- onStep = { it },
- onFinish = { 1f },
- onCancel = { 0f },
- name = "GLANCEABLE_HUB->LOCKSCREEN: keyguardAlpha",
- )
+ willRotateToPortraitInTransition.flatMapLatest { willRotate ->
+ transitionAnimation.sharedFlow(
+ duration = 167.milliseconds,
+ // If will rotate, start later to leave time for screen rotation.
+ startTime = if (willRotate) 500.milliseconds else 167.milliseconds,
+ onStep = { step ->
+ if (willRotate) {
+ if (!communalSceneInteractor.rotatedToPortrait.value) {
+ 0f
+ } else {
+ 1f
+ }
+ } else {
+ step
+ }
+ },
+ onFinish = { 1f },
+ onCancel = { 0f },
+ name = "GLANCEABLE_HUB->LOCKSCREEN: keyguardAlpha",
+ )
+ }
// Show UMO as long as keyguard is not visible.
val showUmo: Flow<Boolean> = keyguardAlpha.map { alpha -> alpha == 0f }
@@ -84,7 +125,14 @@ constructor(
.flatMapLatest { translatePx: Int ->
transitionAnimation.sharedFlowWithState(
duration = TO_LOCKSCREEN_DURATION,
- onStep = { value -> -translatePx + value * translatePx },
+ onStep = { value ->
+ // do not animate translation-x if screen rotation will happen
+ if (willRotateToPortraitInTransition.value) {
+ 0f
+ } else {
+ -translatePx + value * translatePx
+ }
+ },
interpolator = EMPHASIZED,
// Move notifications back to their original position since they can be
// accessed from the shade, and also keyguard elements in case the animation
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToGlanceableHubTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToGlanceableHubTransitionViewModel.kt
index 6d95ade59211..4bc722bc6695 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToGlanceableHubTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToGlanceableHubTransitionViewModel.kt
@@ -30,15 +30,15 @@ import kotlinx.coroutines.flow.Flow
@SysUISingleton
class GoneToGlanceableHubTransitionViewModel
@Inject
-constructor(
- animationFlow: KeyguardTransitionAnimationFlow,
-) : DeviceEntryIconTransition {
+constructor(animationFlow: KeyguardTransitionAnimationFlow) : DeviceEntryIconTransition {
private val transitionAnimation =
animationFlow
.setup(duration = TO_GLANCEABLE_HUB_DURATION, edge = Edge.INVALID)
.setupWithoutSceneContainer(edge = Edge.create(GONE, GLANCEABLE_HUB))
+ val keyguardAlpha = transitionAnimation.immediatelyTransitionTo(0f)
+
override val deviceEntryParentViewAlpha: Flow<Float> =
transitionAnimation.sharedFlow(
duration = 167.milliseconds,
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt
index dcbf7b5a9335..cf6845354f44 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt
@@ -180,6 +180,9 @@ constructor(
val largeClockTextSize: Flow<Int> =
configurationInteractor.dimensionPixelSize(customR.dimen.large_clock_text_size)
+ fun dateWeatherBelowSmallClock() =
+ KeyguardSmartspaceViewModel.dateWeatherBelowSmallClock(context.resources.configuration)
+
enum class ClockLayout {
LARGE_CLOCK,
SMALL_CLOCK,
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt
index 830afeac7b96..a5051657c2f0 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt
@@ -116,6 +116,7 @@ constructor(
private val goneToDozingTransitionViewModel: GoneToDozingTransitionViewModel,
private val goneToDreamingTransitionViewModel: GoneToDreamingTransitionViewModel,
private val goneToLockscreenTransitionViewModel: GoneToLockscreenTransitionViewModel,
+ private val goneToGlanceableHubTransitionViewModel: GoneToGlanceableHubTransitionViewModel,
private val lockscreenToAodTransitionViewModel: LockscreenToAodTransitionViewModel,
private val lockscreenToDozingTransitionViewModel: LockscreenToDozingTransitionViewModel,
private val lockscreenToDreamingTransitionViewModel: LockscreenToDreamingTransitionViewModel,
@@ -277,6 +278,7 @@ constructor(
primaryBouncerToAodTransitionViewModel.lockscreenAlpha,
primaryBouncerToGoneTransitionViewModel.lockscreenAlpha,
primaryBouncerToLockscreenTransitionViewModel.lockscreenAlpha(viewState),
+ goneToGlanceableHubTransitionViewModel.keyguardAlpha,
)
.onStart { emit(0f) },
) { hideKeyguard, alpha ->
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardSmartspaceViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardSmartspaceViewModel.kt
index 5cc34e749b46..a00d0ced2c07 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardSmartspaceViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardSmartspaceViewModel.kt
@@ -17,6 +17,8 @@
package com.android.systemui.keyguard.ui.viewmodel
import android.content.Context
+import android.content.res.Configuration
+import android.util.Log
import com.android.systemui.customization.R as customR
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
@@ -94,6 +96,43 @@ constructor(
val isShadeLayoutWide: StateFlow<Boolean> = shadeModeInteractor.isShadeLayoutWide
companion object {
+ private const val TAG = "KeyguardSmartspaceVM"
+
+ fun dateWeatherBelowSmallClock(
+ configuration: Configuration,
+ customDateWeather: Boolean = false,
+ ): Boolean {
+ return if (
+ com.android.systemui.shared.Flags.clockReactiveSmartspaceLayout() &&
+ !customDateWeather
+ ) {
+ // font size to display size
+ // These values come from changing the font size and display size on a non-foldable.
+ // Visually looked at which configs cause the date/weather to push off of the screen
+ val breakingPairs =
+ listOf(
+ 0.85f to 320, // tiny font size but large display size
+ 1f to 346,
+ 1.15f to 346,
+ 1.5f to 376,
+ 1.8f to 411, // large font size but tiny display size
+ )
+ val screenWidthDp = configuration.screenWidthDp
+ val fontScale = configuration.fontScale
+ var fallBelow = false
+ for ((font, width) in breakingPairs) {
+ if (fontScale >= font && screenWidthDp <= width) {
+ fallBelow = true
+ break
+ }
+ }
+ Log.d(TAG, "Width: $screenWidthDp, Font: $fontScale, BelowClock: $fallBelow")
+ return fallBelow
+ } else {
+ true
+ }
+ }
+
fun getDateWeatherStartMargin(context: Context): Int {
return context.resources.getDimensionPixelSize(R.dimen.below_clock_padding_start) +
context.resources.getDimensionPixelSize(customR.dimen.status_view_margin_horizontal)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToDreamingTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToDreamingTransitionViewModel.kt
new file mode 100644
index 000000000000..9de25fcac64a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToDreamingTransitionViewModel.kt
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.ui.viewmodel
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyguard.domain.interactor.FromPrimaryBouncerTransitionInteractor
+import com.android.systemui.keyguard.shared.model.Edge
+import com.android.systemui.keyguard.shared.model.KeyguardState.DREAMING
+import com.android.systemui.keyguard.shared.model.KeyguardState.PRIMARY_BOUNCER
+import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
+import com.android.systemui.keyguard.ui.transitions.BlurConfig
+import com.android.systemui.keyguard.ui.transitions.PrimaryBouncerTransition
+import com.android.systemui.scene.shared.model.Overlays
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.emptyFlow
+
+@SysUISingleton
+class PrimaryBouncerToDreamingTransitionViewModel
+@Inject
+constructor(blurConfig: BlurConfig, animationFlow: KeyguardTransitionAnimationFlow) :
+ PrimaryBouncerTransition {
+ private val transitionAnimation =
+ animationFlow
+ .setup(
+ duration = FromPrimaryBouncerTransitionInteractor.TO_DREAMING_DURATION,
+ edge = Edge.create(from = Overlays.Bouncer, to = DREAMING),
+ )
+ .setupWithoutSceneContainer(edge = Edge.create(from = PRIMARY_BOUNCER, to = DREAMING))
+
+ override val windowBlurRadius: Flow<Float> =
+ transitionAnimation.sharedFlow(
+ onStart = { blurConfig.maxBlurRadiusPx },
+ onStep = {
+ transitionProgressToBlurRadius(
+ blurConfig.maxBlurRadiusPx,
+ endBlurRadius = blurConfig.minBlurRadiusPx,
+ transitionProgress = it,
+ )
+ },
+ onFinish = { blurConfig.minBlurRadiusPx },
+ )
+
+ override val notificationBlurRadius: Flow<Float> = emptyFlow()
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModel.kt
index 9968bc95a5ba..751674afa745 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModel.kt
@@ -17,6 +17,7 @@
package com.android.systemui.keyguard.ui.viewmodel
import android.util.MathUtils
+import com.android.systemui.Flags
import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor
import com.android.systemui.bouncer.shared.flag.ComposeBouncerFlags
import com.android.systemui.dagger.SysUISingleton
@@ -109,15 +110,14 @@ constructor(
)
}
- private fun createBouncerWindowBlurFlow(
- willRunAnimationOnKeyguard: () -> Boolean
- ): Flow<Float> {
+ private fun createBouncerWindowBlurFlow(): Flow<Float> {
return transitionAnimation.sharedFlow(
duration = TO_GONE_SHORT_DURATION,
- onStart = { willRunDismissFromKeyguard = willRunAnimationOnKeyguard() },
+ onStart = { leaveShadeOpen = statusBarStateController.leaveOpenOnKeyguardHide() },
onStep = {
- if (willRunDismissFromKeyguard) {
- blurConfig.minBlurRadiusPx
+ if (leaveShadeOpen && Flags.notificationShadeBlur()) {
+ // Going back to shade from bouncer after keyguard dismissal
+ blurConfig.maxBlurRadiusPx
} else {
transitionProgressToBlurRadius(
starBlurRadius = blurConfig.maxBlurRadiusPx,
@@ -158,15 +158,7 @@ constructor(
)
}
- override val windowBlurRadius: Flow<Float> =
- if (ComposeBouncerFlags.isEnabled) {
- keyguardDismissActionInteractor
- .get()
- .willAnimateDismissActionOnLockscreen
- .flatMapLatest { createBouncerWindowBlurFlow { it } }
- } else {
- createBouncerWindowBlurFlow(primaryBouncerInteractor::willRunDismissFromKeyguard)
- }
+ override val windowBlurRadius: Flow<Float> = createBouncerWindowBlurFlow()
override val notificationBlurRadius: Flow<Float> =
transitionAnimation.immediatelyTransitionTo(0.0f)
diff --git a/packages/SystemUI/src/com/android/systemui/lowlightclock/AmbientLightModeMonitor.kt b/packages/SystemUI/src/com/android/systemui/lowlightclock/AmbientLightModeMonitor.kt
index c1658e1f1694..5e9e930812a6 100644
--- a/packages/SystemUI/src/com/android/systemui/lowlightclock/AmbientLightModeMonitor.kt
+++ b/packages/SystemUI/src/com/android/systemui/lowlightclock/AmbientLightModeMonitor.kt
@@ -23,7 +23,7 @@ import android.hardware.SensorEventListener
import android.hardware.SensorManager
import android.util.Log
import com.android.systemui.Dumpable
-import com.android.systemui.lowlightclock.dagger.LowLightModule.LIGHT_SENSOR
+import com.android.systemui.lowlightclock.dagger.LowLightModule.Companion.LIGHT_SENSOR
import com.android.systemui.util.sensors.AsyncSensorManager
import java.io.PrintWriter
import java.util.Optional
diff --git a/packages/SystemUI/src/com/android/systemui/lowlightclock/LowLightMonitor.java b/packages/SystemUI/src/com/android/systemui/lowlightclock/LowLightMonitor.java
deleted file mode 100644
index e5eec64ac615..000000000000
--- a/packages/SystemUI/src/com/android/systemui/lowlightclock/LowLightMonitor.java
+++ /dev/null
@@ -1,148 +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.lowlightclock;
-
-import static com.android.dream.lowlight.LowLightDreamManager.AMBIENT_LIGHT_MODE_LOW_LIGHT;
-import static com.android.dream.lowlight.LowLightDreamManager.AMBIENT_LIGHT_MODE_REGULAR;
-import static com.android.systemui.dreams.dagger.DreamModule.LOW_LIGHT_DREAM_SERVICE;
-import static com.android.systemui.keyguard.ScreenLifecycle.SCREEN_ON;
-import static com.android.systemui.lowlightclock.dagger.LowLightModule.LOW_LIGHT_PRECONDITIONS;
-
-import android.content.ComponentName;
-import android.content.pm.PackageManager;
-
-import androidx.annotation.Nullable;
-
-import com.android.dream.lowlight.LowLightDreamManager;
-import com.android.systemui.dagger.qualifiers.Background;
-import com.android.systemui.dagger.qualifiers.SystemUser;
-import com.android.systemui.keyguard.ScreenLifecycle;
-import com.android.systemui.shared.condition.Condition;
-import com.android.systemui.shared.condition.Monitor;
-import com.android.systemui.util.condition.ConditionalCoreStartable;
-
-import dagger.Lazy;
-
-import java.util.Set;
-import java.util.concurrent.Executor;
-
-import javax.inject.Inject;
-import javax.inject.Named;
-
-/**
- * Tracks environment (low-light or not) in order to correctly show or hide a low-light clock while
- * dreaming.
- */
-public class LowLightMonitor extends ConditionalCoreStartable implements Monitor.Callback,
- ScreenLifecycle.Observer {
- private static final String TAG = "LowLightMonitor";
-
- private final Lazy<LowLightDreamManager> mLowLightDreamManager;
- private final Monitor mConditionsMonitor;
- private final Lazy<Set<Condition>> mLowLightConditions;
- private Monitor.Subscription.Token mSubscriptionToken;
- private ScreenLifecycle mScreenLifecycle;
- private final LowLightLogger mLogger;
-
- private final ComponentName mLowLightDreamService;
-
- private final PackageManager mPackageManager;
-
- private final Executor mExecutor;
-
- @Inject
- public LowLightMonitor(Lazy<LowLightDreamManager> lowLightDreamManager,
- @SystemUser Monitor conditionsMonitor,
- @Named(LOW_LIGHT_PRECONDITIONS) Lazy<Set<Condition>> lowLightConditions,
- ScreenLifecycle screenLifecycle,
- LowLightLogger lowLightLogger,
- @Nullable @Named(LOW_LIGHT_DREAM_SERVICE) ComponentName lowLightDreamService,
- PackageManager packageManager,
- @Background Executor backgroundExecutor) {
- super(conditionsMonitor);
- mLowLightDreamManager = lowLightDreamManager;
- mConditionsMonitor = conditionsMonitor;
- mLowLightConditions = lowLightConditions;
- mScreenLifecycle = screenLifecycle;
- mLogger = lowLightLogger;
- mLowLightDreamService = lowLightDreamService;
- mPackageManager = packageManager;
- mExecutor = backgroundExecutor;
- }
-
- @Override
- public void onConditionsChanged(boolean allConditionsMet) {
- mExecutor.execute(() -> {
- mLogger.d(TAG, "Low light enabled: " + allConditionsMet);
-
- mLowLightDreamManager.get().setAmbientLightMode(allConditionsMet
- ? AMBIENT_LIGHT_MODE_LOW_LIGHT : AMBIENT_LIGHT_MODE_REGULAR);
- });
- }
-
- @Override
- public void onScreenTurnedOn() {
- mExecutor.execute(() -> {
- if (mSubscriptionToken == null) {
- mLogger.d(TAG, "Screen turned on. Subscribing to low light conditions.");
-
- mSubscriptionToken = mConditionsMonitor.addSubscription(
- new Monitor.Subscription.Builder(this)
- .addConditions(mLowLightConditions.get())
- .build());
- }
- });
- }
-
-
- @Override
- public void onScreenTurnedOff() {
- mExecutor.execute(() -> {
- if (mSubscriptionToken != null) {
- mLogger.d(TAG, "Screen turned off. Removing subscription to low light conditions.");
-
- mConditionsMonitor.removeSubscription(mSubscriptionToken);
- mSubscriptionToken = null;
- }
- });
- }
-
- @Override
- protected void onStart() {
- mExecutor.execute(() -> {
- if (mLowLightDreamService != null) {
- // Note that the dream service is disabled by default. This prevents the dream from
- // appearing in settings on devices that don't have it explicitly excluded (done in
- // the settings overlay). Therefore, the component is enabled if it is to be used
- // here.
- mPackageManager.setComponentEnabledSetting(
- mLowLightDreamService,
- PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
- PackageManager.DONT_KILL_APP
- );
- } else {
- // If there is no low light dream service, do not observe conditions.
- return;
- }
-
- mScreenLifecycle.addObserver(this);
- if (mScreenLifecycle.getScreenState() == SCREEN_ON) {
- onScreenTurnedOn();
- }
- });
-
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/lowlightclock/LowLightMonitor.kt b/packages/SystemUI/src/com/android/systemui/lowlightclock/LowLightMonitor.kt
new file mode 100644
index 000000000000..137226332e38
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/lowlightclock/LowLightMonitor.kt
@@ -0,0 +1,116 @@
+/*
+ * 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.lowlightclock
+
+import android.content.ComponentName
+import android.content.pm.PackageManager
+import com.android.dream.lowlight.LowLightDreamManager
+import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractor
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.dagger.qualifiers.SystemUser
+import com.android.systemui.dreams.dagger.DreamModule
+import com.android.systemui.lowlightclock.dagger.LowLightModule
+import com.android.systemui.shared.condition.Condition
+import com.android.systemui.shared.condition.Monitor
+import com.android.systemui.util.condition.ConditionalCoreStartable
+import com.android.systemui.util.kotlin.BooleanFlowOperators.not
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
+import dagger.Lazy
+import javax.inject.Inject
+import javax.inject.Named
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.launch
+
+/**
+ * Tracks environment (low-light or not) in order to correctly show or hide a low-light clock while
+ * dreaming.
+ */
+class LowLightMonitor
+@Inject
+constructor(
+ private val lowLightDreamManager: Lazy<LowLightDreamManager>,
+ @param:SystemUser private val conditionsMonitor: Monitor,
+ @param:Named(LowLightModule.LOW_LIGHT_PRECONDITIONS)
+ private val lowLightConditions: Lazy<Set<Condition>>,
+ displayStateInteractor: DisplayStateInteractor,
+ private val logger: LowLightLogger,
+ @param:Named(DreamModule.LOW_LIGHT_DREAM_SERVICE)
+ private val lowLightDreamService: ComponentName?,
+ private val packageManager: PackageManager,
+ @Background private val scope: CoroutineScope,
+) : ConditionalCoreStartable(conditionsMonitor) {
+ private val isScreenOn = not(displayStateInteractor.isDefaultDisplayOff).distinctUntilChanged()
+
+ private val isLowLight = conflatedCallbackFlow {
+ val token =
+ conditionsMonitor.addSubscription(
+ Monitor.Subscription.Builder { trySend(it) }
+ .addConditions(lowLightConditions.get())
+ .build()
+ )
+
+ awaitClose { conditionsMonitor.removeSubscription(token) }
+ }
+
+ @OptIn(ExperimentalCoroutinesApi::class)
+ override fun onStart() {
+ scope.launch {
+ if (lowLightDreamService != null) {
+ // Note that the dream service is disabled by default. This prevents the dream from
+ // appearing in settings on devices that don't have it explicitly excluded (done in
+ // the settings overlay). Therefore, the component is enabled if it is to be used
+ // here.
+ packageManager.setComponentEnabledSetting(
+ lowLightDreamService,
+ PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
+ PackageManager.DONT_KILL_APP,
+ )
+ } else {
+ // If there is no low light dream service, do not observe conditions.
+ return@launch
+ }
+
+ isScreenOn
+ .flatMapLatest {
+ if (it) {
+ isLowLight
+ } else {
+ flowOf(false)
+ }
+ }
+ .distinctUntilChanged()
+ .collect {
+ logger.d(TAG, "Low light enabled: $it")
+ lowLightDreamManager
+ .get()
+ .setAmbientLightMode(
+ if (it) LowLightDreamManager.AMBIENT_LIGHT_MODE_LOW_LIGHT
+ else LowLightDreamManager.AMBIENT_LIGHT_MODE_REGULAR
+ )
+ }
+ }
+ }
+
+ companion object {
+ private const val TAG = "LowLightMonitor"
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/lowlightclock/dagger/LowLightModule.java b/packages/SystemUI/src/com/android/systemui/lowlightclock/dagger/LowLightModule.java
deleted file mode 100644
index f8072f2f79b4..000000000000
--- a/packages/SystemUI/src/com/android/systemui/lowlightclock/dagger/LowLightModule.java
+++ /dev/null
@@ -1,152 +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.lowlightclock.dagger;
-
-import android.annotation.Nullable;
-import android.content.res.Resources;
-import android.hardware.Sensor;
-
-import com.android.dream.lowlight.dagger.LowLightDreamModule;
-import com.android.systemui.CoreStartable;
-import com.android.systemui.communal.DeviceInactiveCondition;
-import com.android.systemui.dagger.SysUISingleton;
-import com.android.systemui.dagger.qualifiers.Main;
-import com.android.systemui.log.LogBuffer;
-import com.android.systemui.log.LogBufferFactory;
-import com.android.systemui.lowlightclock.AmbientLightModeMonitor;
-import com.android.systemui.lowlightclock.DirectBootCondition;
-import com.android.systemui.lowlightclock.ForceLowLightCondition;
-import com.android.systemui.lowlightclock.LowLightCondition;
-import com.android.systemui.lowlightclock.LowLightDisplayController;
-import com.android.systemui.lowlightclock.LowLightMonitor;
-import com.android.systemui.lowlightclock.ScreenSaverEnabledCondition;
-import com.android.systemui.res.R;
-import com.android.systemui.shared.condition.Condition;
-
-import dagger.Binds;
-import dagger.BindsOptionalOf;
-import dagger.Module;
-import dagger.Provides;
-import dagger.multibindings.ClassKey;
-import dagger.multibindings.IntoMap;
-import dagger.multibindings.IntoSet;
-
-import javax.inject.Named;
-
-@Module(includes = LowLightDreamModule.class)
-public abstract class LowLightModule {
- public static final String Y_TRANSLATION_ANIMATION_OFFSET =
- "y_translation_animation_offset";
- public static final String Y_TRANSLATION_ANIMATION_DURATION_MILLIS =
- "y_translation_animation_duration_millis";
- public static final String ALPHA_ANIMATION_IN_START_DELAY_MILLIS =
- "alpha_animation_in_start_delay_millis";
- public static final String ALPHA_ANIMATION_DURATION_MILLIS =
- "alpha_animation_duration_millis";
- public static final String LOW_LIGHT_PRECONDITIONS = "low_light_preconditions";
- public static final String LIGHT_SENSOR = "low_light_monitor_light_sensor";
-
-
- /**
- * Provides a {@link LogBuffer} for logs related to low-light features.
- */
- @Provides
- @SysUISingleton
- @LowLightLog
- public static LogBuffer provideLowLightLogBuffer(LogBufferFactory factory) {
- return factory.create("LowLightLog", 250);
- }
-
- @Binds
- @IntoSet
- @Named(LOW_LIGHT_PRECONDITIONS)
- abstract Condition bindScreenSaverEnabledCondition(ScreenSaverEnabledCondition condition);
-
- @Provides
- @IntoSet
- @Named(LOW_LIGHT_PRECONDITIONS)
- static Condition provideLowLightCondition(LowLightCondition lowLightCondition,
- DirectBootCondition directBootCondition) {
- // Start lowlight if we are either in lowlight or in direct boot. The ordering of the
- // conditions matters here since we don't want to start the lowlight condition if
- // we are in direct boot mode.
- return directBootCondition.or(lowLightCondition);
- }
-
- @Binds
- @IntoSet
- @Named(LOW_LIGHT_PRECONDITIONS)
- abstract Condition bindForceLowLightCondition(ForceLowLightCondition condition);
-
- @Binds
- @IntoSet
- @Named(LOW_LIGHT_PRECONDITIONS)
- abstract Condition bindDeviceInactiveCondition(DeviceInactiveCondition condition);
-
- @BindsOptionalOf
- abstract LowLightDisplayController bindsLowLightDisplayController();
-
- @BindsOptionalOf
- @Nullable
- @Named(LIGHT_SENSOR)
- abstract Sensor bindsLightSensor();
-
- @BindsOptionalOf
- abstract AmbientLightModeMonitor.DebounceAlgorithm bindsDebounceAlgorithm();
-
- /**
- *
- */
- @Provides
- @Named(Y_TRANSLATION_ANIMATION_OFFSET)
- static int providesAnimationInOffset(@Main Resources resources) {
- return resources.getDimensionPixelOffset(
- R.dimen.low_light_clock_translate_animation_offset);
- }
-
- /**
- *
- */
- @Provides
- @Named(Y_TRANSLATION_ANIMATION_DURATION_MILLIS)
- static long providesAnimationDurationMillis(@Main Resources resources) {
- return resources.getInteger(R.integer.low_light_clock_translate_animation_duration_ms);
- }
-
- /**
- *
- */
- @Provides
- @Named(ALPHA_ANIMATION_IN_START_DELAY_MILLIS)
- static long providesAlphaAnimationInStartDelayMillis(@Main Resources resources) {
- return resources.getInteger(R.integer.low_light_clock_alpha_animation_in_start_delay_ms);
- }
-
- /**
- *
- */
- @Provides
- @Named(ALPHA_ANIMATION_DURATION_MILLIS)
- static long providesAlphaAnimationDurationMillis(@Main Resources resources) {
- return resources.getInteger(R.integer.low_light_clock_alpha_animation_duration_ms);
- }
- /** Inject into LowLightMonitor. */
- @Binds
- @IntoMap
- @ClassKey(LowLightMonitor.class)
- abstract CoreStartable bindLowLightMonitor(LowLightMonitor lowLightMonitor);
-}
diff --git a/packages/SystemUI/src/com/android/systemui/lowlightclock/dagger/LowLightModule.kt b/packages/SystemUI/src/com/android/systemui/lowlightclock/dagger/LowLightModule.kt
new file mode 100644
index 000000000000..6b3254e928ec
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/lowlightclock/dagger/LowLightModule.kt
@@ -0,0 +1,146 @@
+/*
+ * 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.lowlightclock.dagger
+
+import android.content.res.Resources
+import android.hardware.Sensor
+import com.android.dream.lowlight.dagger.LowLightDreamModule
+import com.android.systemui.CoreStartable
+import com.android.systemui.communal.DeviceInactiveCondition
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.LogBufferFactory
+import com.android.systemui.lowlightclock.AmbientLightModeMonitor.DebounceAlgorithm
+import com.android.systemui.lowlightclock.DirectBootCondition
+import com.android.systemui.lowlightclock.ForceLowLightCondition
+import com.android.systemui.lowlightclock.LowLightCondition
+import com.android.systemui.lowlightclock.LowLightDisplayController
+import com.android.systemui.lowlightclock.LowLightMonitor
+import com.android.systemui.lowlightclock.ScreenSaverEnabledCondition
+import com.android.systemui.res.R
+import com.android.systemui.shared.condition.Condition
+import dagger.Binds
+import dagger.BindsOptionalOf
+import dagger.Module
+import dagger.Provides
+import dagger.multibindings.ClassKey
+import dagger.multibindings.IntoMap
+import dagger.multibindings.IntoSet
+import javax.inject.Named
+
+@Module(includes = [LowLightDreamModule::class])
+abstract class LowLightModule {
+ @Binds
+ @IntoSet
+ @Named(LOW_LIGHT_PRECONDITIONS)
+ abstract fun bindScreenSaverEnabledCondition(condition: ScreenSaverEnabledCondition): Condition
+
+ @Binds
+ @IntoSet
+ @Named(LOW_LIGHT_PRECONDITIONS)
+ abstract fun bindForceLowLightCondition(condition: ForceLowLightCondition): Condition
+
+ @Binds
+ @IntoSet
+ @Named(LOW_LIGHT_PRECONDITIONS)
+ abstract fun bindDeviceInactiveCondition(condition: DeviceInactiveCondition): Condition
+
+ @BindsOptionalOf abstract fun bindsLowLightDisplayController(): LowLightDisplayController
+
+ @BindsOptionalOf @Named(LIGHT_SENSOR) abstract fun bindsLightSensor(): Sensor
+
+ @BindsOptionalOf abstract fun bindsDebounceAlgorithm(): DebounceAlgorithm
+
+ /** Inject into LowLightMonitor. */
+ @Binds
+ @IntoMap
+ @ClassKey(LowLightMonitor::class)
+ abstract fun bindLowLightMonitor(lowLightMonitor: LowLightMonitor): CoreStartable
+
+ companion object {
+ const val Y_TRANSLATION_ANIMATION_OFFSET: String = "y_translation_animation_offset"
+ const val Y_TRANSLATION_ANIMATION_DURATION_MILLIS: String =
+ "y_translation_animation_duration_millis"
+ const val ALPHA_ANIMATION_IN_START_DELAY_MILLIS: String =
+ "alpha_animation_in_start_delay_millis"
+ const val ALPHA_ANIMATION_DURATION_MILLIS: String = "alpha_animation_duration_millis"
+ const val LOW_LIGHT_PRECONDITIONS: String = "low_light_preconditions"
+ const val LIGHT_SENSOR: String = "low_light_monitor_light_sensor"
+
+ /** Provides a [LogBuffer] for logs related to low-light features. */
+ @JvmStatic
+ @Provides
+ @SysUISingleton
+ @LowLightLog
+ fun provideLowLightLogBuffer(factory: LogBufferFactory): LogBuffer {
+ return factory.create("LowLightLog", 250)
+ }
+
+ @Provides
+ @IntoSet
+ @Named(LOW_LIGHT_PRECONDITIONS)
+ fun provideLowLightCondition(
+ lowLightCondition: LowLightCondition,
+ directBootCondition: DirectBootCondition,
+ ): Condition {
+ // Start lowlight if we are either in lowlight or in direct boot. The ordering of the
+ // conditions matters here since we don't want to start the lowlight condition if
+ // we are in direct boot mode.
+ return directBootCondition.or(lowLightCondition)
+ }
+
+ /** */
+ @JvmStatic
+ @Provides
+ @Named(Y_TRANSLATION_ANIMATION_OFFSET)
+ fun providesAnimationInOffset(@Main resources: Resources): Int {
+ return resources.getDimensionPixelOffset(
+ R.dimen.low_light_clock_translate_animation_offset
+ )
+ }
+
+ /** */
+ @JvmStatic
+ @Provides
+ @Named(Y_TRANSLATION_ANIMATION_DURATION_MILLIS)
+ fun providesAnimationDurationMillis(@Main resources: Resources): Long {
+ return resources
+ .getInteger(R.integer.low_light_clock_translate_animation_duration_ms)
+ .toLong()
+ }
+
+ /** */
+ @JvmStatic
+ @Provides
+ @Named(ALPHA_ANIMATION_IN_START_DELAY_MILLIS)
+ fun providesAlphaAnimationInStartDelayMillis(@Main resources: Resources): Long {
+ return resources
+ .getInteger(R.integer.low_light_clock_alpha_animation_in_start_delay_ms)
+ .toLong()
+ }
+
+ /** */
+ @JvmStatic
+ @Provides
+ @Named(ALPHA_ANIMATION_DURATION_MILLIS)
+ fun providesAlphaAnimationDurationMillis(@Main resources: Resources): Long {
+ return resources
+ .getInteger(R.integer.low_light_clock_alpha_animation_duration_ms)
+ .toLong()
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/data/model/MediaSortKeyModel.kt b/packages/SystemUI/src/com/android/systemui/media/controls/data/model/MediaSortKeyModel.kt
index cfe5cde79ec7..e2c07cac060e 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/data/model/MediaSortKeyModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/data/model/MediaSortKeyModel.kt
@@ -20,8 +20,6 @@ import com.android.internal.logging.InstanceId
import com.android.systemui.media.controls.shared.model.MediaData.Companion.PLAYBACK_LOCAL
data class MediaSortKeyModel(
- /** Whether the item represents a Smartspace media recommendation that should be prioritized. */
- val isPrioritizedRec: Boolean = false,
val isPlaying: Boolean? = null,
val playbackLocation: Int = PLAYBACK_LOCAL,
val active: Boolean = true,
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/data/repository/MediaDataRepository.kt b/packages/SystemUI/src/com/android/systemui/media/controls/data/repository/MediaDataRepository.kt
index 8e773a5de194..80aef67e8789 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/data/repository/MediaDataRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/data/repository/MediaDataRepository.kt
@@ -16,13 +16,10 @@
package com.android.systemui.media.controls.data.repository
-import android.util.Log
import com.android.systemui.Dumpable
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dump.DumpManager
import com.android.systemui.media.controls.shared.model.MediaData
-import com.android.systemui.media.controls.shared.model.SmartspaceMediaData
-import com.android.systemui.media.controls.util.MediaFlags
import java.io.PrintWriter
import javax.inject.Inject
import kotlinx.coroutines.flow.MutableStateFlow
@@ -30,52 +27,26 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
private const val TAG = "MediaDataRepository"
-private const val DEBUG = true
/** A repository that holds the state of all media controls in carousel. */
@SysUISingleton
-class MediaDataRepository
-@Inject
-constructor(private val mediaFlags: MediaFlags, dumpManager: DumpManager) : Dumpable {
+class MediaDataRepository @Inject constructor(dumpManager: DumpManager) : Dumpable {
private val _mediaEntries: MutableStateFlow<Map<String, MediaData>> =
MutableStateFlow(LinkedHashMap())
val mediaEntries: StateFlow<Map<String, MediaData>> = _mediaEntries.asStateFlow()
- private val _smartspaceMediaData: MutableStateFlow<SmartspaceMediaData> =
- MutableStateFlow(SmartspaceMediaData())
- val smartspaceMediaData: StateFlow<SmartspaceMediaData> = _smartspaceMediaData.asStateFlow()
-
init {
dumpManager.registerNormalDumpable(TAG, this)
}
- /** Updates the recommendation data with a new smartspace media data. */
- fun setRecommendation(recommendation: SmartspaceMediaData) {
- _smartspaceMediaData.value = recommendation
- }
-
/**
* Marks the recommendation data as dismissed.
*
* @return true if the recommendation was dismissed or already inactive, false otherwise.
*/
fun dismissSmartspaceRecommendation(key: String): Boolean {
- val data = smartspaceMediaData.value
- if (data.targetId != key || !data.isValid()) {
- // If this doesn't match, or we've already invalidated the data, no action needed
- return false
- }
-
- if (DEBUG) Log.d(TAG, "Dismissing Smartspace media target")
- if (data.isActive) {
- setRecommendation(
- SmartspaceMediaData(
- targetId = smartspaceMediaData.value.targetId,
- instanceId = smartspaceMediaData.value.instanceId,
- )
- )
- }
+ // TODO(b/382680767): remove
return true
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/data/repository/MediaFilterRepository.kt b/packages/SystemUI/src/com/android/systemui/media/controls/data/repository/MediaFilterRepository.kt
index 8ce901aa32d8..099a83747210 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/data/repository/MediaFilterRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/data/repository/MediaFilterRepository.kt
@@ -22,8 +22,6 @@ import com.android.systemui.media.controls.data.model.MediaSortKeyModel
import com.android.systemui.media.controls.shared.model.MediaCommonModel
import com.android.systemui.media.controls.shared.model.MediaData
import com.android.systemui.media.controls.shared.model.MediaDataLoadingModel
-import com.android.systemui.media.controls.shared.model.SmartspaceMediaData
-import com.android.systemui.media.controls.shared.model.SmartspaceMediaLoadingModel
import com.android.systemui.util.time.SystemClock
import java.util.TreeMap
import javax.inject.Inject
@@ -35,14 +33,6 @@ import kotlinx.coroutines.flow.asStateFlow
@SysUISingleton
class MediaFilterRepository @Inject constructor(private val systemClock: SystemClock) {
- /** Instance id of media control that recommendations card reactivated. */
- private val _reactivatedId: MutableStateFlow<InstanceId?> = MutableStateFlow(null)
- val reactivatedId: StateFlow<InstanceId?> = _reactivatedId.asStateFlow()
-
- private val _smartspaceMediaData: MutableStateFlow<SmartspaceMediaData> =
- MutableStateFlow(SmartspaceMediaData())
- val smartspaceMediaData: StateFlow<SmartspaceMediaData> = _smartspaceMediaData.asStateFlow()
-
private val _selectedUserEntries: MutableStateFlow<Map<InstanceId, MediaData>> =
MutableStateFlow(LinkedHashMap())
val selectedUserEntries: StateFlow<Map<InstanceId, MediaData>> =
@@ -60,7 +50,6 @@ class MediaFilterRepository @Inject constructor(private val systemClock: SystemC
it.isPlaying == true && it.playbackLocation == MediaData.PLAYBACK_CAST_LOCAL
}
.thenByDescending { it.active }
- .thenByDescending { it.isPrioritizedRec }
.thenByDescending { !it.isResume }
.thenByDescending { it.playbackLocation != MediaData.PLAYBACK_CAST_REMOTE }
.thenByDescending { it.lastActive }
@@ -72,7 +61,6 @@ class MediaFilterRepository @Inject constructor(private val systemClock: SystemC
val currentMedia = _currentMedia.asStateFlow()
private var sortedMedia = TreeMap<MediaSortKeyModel, MediaCommonModel>(comparator)
- private var mediaFromRecPackageName: String? = null
fun addMediaEntry(key: String, data: MediaData) {
val entries = LinkedHashMap<String, MediaData>(_allUserEntries.value)
@@ -132,16 +120,6 @@ class MediaFilterRepository @Inject constructor(private val systemClock: SystemC
_selectedUserEntries.value = LinkedHashMap()
}
- /** Updates recommendation data with a new smartspace media data. */
- fun setRecommendation(smartspaceMediaData: SmartspaceMediaData) {
- _smartspaceMediaData.value = smartspaceMediaData
- }
-
- /** Updates media control key that recommendations card reactivated. */
- fun setReactivatedId(instanceId: InstanceId?) {
- _reactivatedId.value = instanceId
- }
-
fun addMediaDataLoadingState(
mediaDataLoadingModel: MediaDataLoadingModel,
isUpdate: Boolean = true,
@@ -149,15 +127,13 @@ class MediaFilterRepository @Inject constructor(private val systemClock: SystemC
val sortedMap = TreeMap<MediaSortKeyModel, MediaCommonModel>(comparator)
sortedMap.putAll(
sortedMedia.filter { (_, commonModel) ->
- commonModel !is MediaCommonModel.MediaControl ||
- commonModel.mediaLoadedModel.instanceId != mediaDataLoadingModel.instanceId
+ commonModel.mediaLoadedModel.instanceId != mediaDataLoadingModel.instanceId
}
)
_selectedUserEntries.value[mediaDataLoadingModel.instanceId]?.let {
val sortKey =
MediaSortKeyModel(
- isPrioritizedRec = false,
it.isPlaying,
it.playbackLocation,
it.active,
@@ -170,43 +146,33 @@ class MediaFilterRepository @Inject constructor(private val systemClock: SystemC
if (mediaDataLoadingModel is MediaDataLoadingModel.Loaded) {
val newCommonModel =
- MediaCommonModel.MediaControl(
+ MediaCommonModel(
mediaDataLoadingModel,
canBeRemoved(it),
- isMediaFromRec(it),
if (isUpdate) systemClock.currentTimeMillis() else 0,
)
sortedMap[sortKey] = newCommonModel
- // On Addition or tapping on recommendations, we should show the new order of media.
- if (mediaFromRecPackageName == it.packageName) {
- if (it.isPlaying == true) {
- mediaFromRecPackageName = null
- _currentMedia.value = sortedMap.values.toList()
- }
- } else {
- var isNewToCurrentMedia = true
- val currentList =
- mutableListOf<MediaCommonModel>().apply { addAll(_currentMedia.value) }
- currentList.forEachIndexed { index, mediaCommonModel ->
- if (
- mediaCommonModel is MediaCommonModel.MediaControl &&
- mediaCommonModel.mediaLoadedModel.instanceId ==
- mediaDataLoadingModel.instanceId
- ) {
- // When loading an update for an existing media control.
- isNewToCurrentMedia = false
- if (mediaCommonModel != newCommonModel) {
- // Update media model if changed.
- currentList[index] = newCommonModel
- }
+ var isNewToCurrentMedia = true
+ val currentList =
+ mutableListOf<MediaCommonModel>().apply { addAll(_currentMedia.value) }
+ currentList.forEachIndexed { index, mediaCommonModel ->
+ if (
+ mediaCommonModel.mediaLoadedModel.instanceId ==
+ mediaDataLoadingModel.instanceId
+ ) {
+ // When loading an update for an existing media control.
+ isNewToCurrentMedia = false
+ if (mediaCommonModel != newCommonModel) {
+ // Update media model if changed.
+ currentList[index] = newCommonModel
}
}
- if (isNewToCurrentMedia && it.active) {
- _currentMedia.value = sortedMap.values.toList()
- } else {
- _currentMedia.value = currentList
- }
+ }
+ if (isNewToCurrentMedia && it.active) {
+ _currentMedia.value = sortedMap.values.toList()
+ } else {
+ _currentMedia.value = currentList
}
sortedMedia = sortedMap
@@ -217,57 +183,16 @@ class MediaFilterRepository @Inject constructor(private val systemClock: SystemC
if (mediaDataLoadingModel is MediaDataLoadingModel.Removed) {
_currentMedia.value =
_currentMedia.value.filter { commonModel ->
- commonModel !is MediaCommonModel.MediaControl ||
- mediaDataLoadingModel.instanceId != commonModel.mediaLoadedModel.instanceId
+ mediaDataLoadingModel.instanceId != commonModel.mediaLoadedModel.instanceId
}
sortedMedia = sortedMap
}
}
- fun setRecommendationsLoadingState(smartspaceMediaLoadingModel: SmartspaceMediaLoadingModel) {
- val isPrioritized =
- when (smartspaceMediaLoadingModel) {
- is SmartspaceMediaLoadingModel.Loaded -> smartspaceMediaLoadingModel.isPrioritized
- else -> false
- }
- val sortedMap = TreeMap<MediaSortKeyModel, MediaCommonModel>(comparator)
- sortedMap.putAll(
- sortedMedia.filter { (_, commonModel) ->
- commonModel !is MediaCommonModel.MediaRecommendations
- }
- )
-
- val sortKey =
- MediaSortKeyModel(
- isPrioritizedRec = isPrioritized,
- isPlaying = false,
- active = _smartspaceMediaData.value.isActive,
- )
- val newCommonModel = MediaCommonModel.MediaRecommendations(smartspaceMediaLoadingModel)
- when (smartspaceMediaLoadingModel) {
- is SmartspaceMediaLoadingModel.Loaded -> {
- sortedMap[sortKey] = newCommonModel
- _currentMedia.value = sortedMap.values.toList()
- sortedMedia = sortedMap
- }
- is SmartspaceMediaLoadingModel.Removed -> {
- _currentMedia.value =
- _currentMedia.value.filter { commonModel ->
- commonModel !is MediaCommonModel.MediaRecommendations
- }
- sortedMedia = sortedMap
- }
- }
- }
-
fun setOrderedMedia() {
_currentMedia.value = sortedMedia.values.toList()
}
- fun setMediaFromRecPackageName(packageName: String) {
- mediaFromRecPackageName = packageName
- }
-
fun hasActiveMedia(): Boolean {
return _selectedUserEntries.value.any { it.value.active }
}
@@ -276,21 +201,7 @@ class MediaFilterRepository @Inject constructor(private val systemClock: SystemC
return _selectedUserEntries.value.entries.isNotEmpty()
}
- fun hasActiveMediaOrRecommendation(): Boolean {
- return _selectedUserEntries.value.any { it.value.active } ||
- (isRecommendationActive() &&
- (_smartspaceMediaData.value.isValid() || _reactivatedId.value != null))
- }
-
- fun isRecommendationActive(): Boolean {
- return _smartspaceMediaData.value.isActive
- }
-
private fun canBeRemoved(data: MediaData): Boolean {
return data.isPlaying?.let { !it } ?: data.isClearable && !data.active
}
-
- private fun isMediaFromRec(data: MediaData): Boolean {
- return data.isPlaying == true && mediaFromRecPackageName == data.packageName
- }
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataFilterImpl.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataFilterImpl.kt
index 6ea161c54aaf..16378a3bc50b 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataFilterImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataFilterImpl.kt
@@ -18,40 +18,22 @@ package com.android.systemui.media.controls.domain.pipeline
import android.content.Context
import android.content.pm.UserInfo
-import android.os.SystemProperties
import android.util.Log
import com.android.internal.annotations.KeepForWeakReference
import com.android.internal.annotations.VisibleForTesting
-import com.android.systemui.broadcast.BroadcastSender
import com.android.systemui.dagger.qualifiers.Main
-import com.android.systemui.media.controls.shared.model.EXTRA_KEY_TRIGGER_RESUME
import com.android.systemui.media.controls.shared.model.MediaData
import com.android.systemui.media.controls.shared.model.SmartspaceMediaData
-import com.android.systemui.media.controls.util.MediaFlags
-import com.android.systemui.media.controls.util.MediaUiEventLogger
import com.android.systemui.settings.UserTracker
import com.android.systemui.statusbar.NotificationLockscreenUserManager
import com.android.systemui.util.time.SystemClock
import java.util.SortedMap
import java.util.concurrent.Executor
-import java.util.concurrent.TimeUnit
import javax.inject.Inject
import kotlin.collections.LinkedHashMap
private const val TAG = "MediaDataFilter"
private const val DEBUG = true
-private const val EXPORTED_SMARTSPACE_TRAMPOLINE_ACTIVITY_NAME =
- ("com.google" +
- ".android.apps.gsa.staticplugins.opa.smartspace.ExportedSmartspaceTrampolineActivity")
-private const val RESUMABLE_MEDIA_MAX_AGE_SECONDS_KEY = "resumable_media_max_age_seconds"
-
-/**
- * Maximum age of a media control to re-activate on smartspace signal. If there is no media control
- * available within this time window, smartspace recommendations will be shown instead.
- */
-@VisibleForTesting
-internal val SMARTSPACE_MAX_AGE =
- SystemProperties.getLong("debug.sysui.smartspace_max_age", TimeUnit.MINUTES.toMillis(30))
/**
* Filters data updates from [MediaDataCombineLatest] based on the current user ID, and handles user
@@ -64,14 +46,10 @@ internal val SMARTSPACE_MAX_AGE =
class LegacyMediaDataFilterImpl
@Inject
constructor(
- private val context: Context,
private val userTracker: UserTracker,
- private val broadcastSender: BroadcastSender,
private val lockscreenUserManager: NotificationLockscreenUserManager,
@Main private val executor: Executor,
private val systemClock: SystemClock,
- private val logger: MediaUiEventLogger,
- private val mediaFlags: MediaFlags,
) : MediaDataManager.Listener {
private val _listeners: MutableSet<MediaDataManager.Listener> = mutableSetOf()
val listeners: Set<MediaDataManager.Listener>
@@ -82,8 +60,6 @@ constructor(
private val allEntries: LinkedHashMap<String, MediaData> = LinkedHashMap()
// The filtered userEntries, which will be a subset of all userEntries in MediaDataManager
private val userEntries: LinkedHashMap<String, MediaData> = LinkedHashMap()
- private var smartspaceMediaData: SmartspaceMediaData = EMPTY_SMARTSPACE_MEDIA_DATA
- private var reactivatedKey: String? = null
// Ensure the field (and associated reference) isn't removed during optimization.
@KeepForWeakReference
@@ -136,73 +112,7 @@ constructor(
data: SmartspaceMediaData,
shouldPrioritize: Boolean,
) {
- if (!data.isActive) {
- Log.d(TAG, "Inactive recommendation data. Skip triggering.")
- return
- }
-
- // Override the pass-in value here, as the order of Smartspace card is only determined here.
- var shouldPrioritizeMutable = false
- smartspaceMediaData = data
-
- // Before forwarding the smartspace target, first check if we have recently inactive media
- val sorted = userEntries.toSortedMap(compareBy { userEntries.get(it)?.lastActive ?: -1 })
- val timeSinceActive = timeSinceActiveForMostRecentMedia(sorted)
- var smartspaceMaxAgeMillis = SMARTSPACE_MAX_AGE
- data.cardAction?.extras?.let {
- val smartspaceMaxAgeSeconds = it.getLong(RESUMABLE_MEDIA_MAX_AGE_SECONDS_KEY, 0)
- if (smartspaceMaxAgeSeconds > 0) {
- smartspaceMaxAgeMillis = TimeUnit.SECONDS.toMillis(smartspaceMaxAgeSeconds)
- }
- }
-
- // Check if smartspace has explicitly specified whether to re-activate resumable media.
- // The default behavior is to trigger if the smartspace data is active.
- val shouldTriggerResume =
- data.cardAction?.extras?.getBoolean(EXTRA_KEY_TRIGGER_RESUME, true) ?: true
- val shouldReactivate =
- shouldTriggerResume && !hasActiveMedia() && hasAnyMedia() && data.isActive
-
- if (timeSinceActive < smartspaceMaxAgeMillis) {
- // It could happen there are existing active media resume cards, then we don't need to
- // reactivate.
- if (shouldReactivate) {
- val lastActiveKey = sorted.lastKey() // most recently active
- // Notify listeners to consider this media active
- Log.d(TAG, "reactivating $lastActiveKey instead of smartspace")
- reactivatedKey = lastActiveKey
- val mediaData = sorted.get(lastActiveKey)!!.copy(active = true)
- logger.logRecommendationActivated(
- mediaData.appUid,
- mediaData.packageName,
- mediaData.instanceId,
- )
- listeners.forEach {
- it.onMediaDataLoaded(
- lastActiveKey,
- lastActiveKey,
- mediaData,
- receivedSmartspaceCardLatency =
- (systemClock.currentTimeMillis() - data.headphoneConnectionTimeMillis)
- .toInt(),
- isSsReactivated = true,
- )
- }
- }
- } else if (data.isActive) {
- // Mark to prioritize Smartspace card if no recent media.
- shouldPrioritizeMutable = true
- }
-
- if (!data.isValid()) {
- Log.d(TAG, "Invalid recommendation data. Skip showing the rec card")
- return
- }
- logger.logRecommendationAdded(
- smartspaceMediaData.packageName,
- smartspaceMediaData.instanceId,
- )
- listeners.forEach { it.onSmartspaceMediaDataLoaded(key, data, shouldPrioritizeMutable) }
+ // TODO(b/382680767): remove
}
override fun onMediaDataRemoved(key: String, userInitiated: Boolean) {
@@ -214,27 +124,7 @@ constructor(
}
override fun onSmartspaceMediaDataRemoved(key: String, immediately: Boolean) {
- // First check if we had reactivated media instead of forwarding smartspace
- reactivatedKey?.let {
- val lastActiveKey = it
- reactivatedKey = null
- Log.d(TAG, "expiring reactivated key $lastActiveKey")
- // Notify listeners to update with actual active value
- userEntries.get(lastActiveKey)?.let { mediaData ->
- listeners.forEach {
- it.onMediaDataLoaded(lastActiveKey, lastActiveKey, mediaData, immediately)
- }
- }
- }
-
- if (smartspaceMediaData.isActive) {
- smartspaceMediaData =
- EMPTY_SMARTSPACE_MEDIA_DATA.copy(
- targetId = smartspaceMediaData.targetId,
- instanceId = smartspaceMediaData.instanceId,
- )
- }
- listeners.forEach { it.onSmartspaceMediaDataRemoved(key, immediately) }
+ // TODO(b/382680767): remove
}
@VisibleForTesting
@@ -280,50 +170,12 @@ constructor(
// Force updates to listeners, needed for re-activated card
mediaDataManager.setInactive(it, timedOut = true, forceUpdate = true)
}
- if (smartspaceMediaData.isActive) {
- val dismissIntent = smartspaceMediaData.dismissIntent
- if (dismissIntent == null) {
- Log.w(
- TAG,
- "Cannot create dismiss action click action: extras missing dismiss_intent.",
- )
- } else if (
- dismissIntent.component?.className == EXPORTED_SMARTSPACE_TRAMPOLINE_ACTIVITY_NAME
- ) {
- // Dismiss the card Smartspace data through Smartspace trampoline activity.
- context.startActivity(dismissIntent)
- } else {
- broadcastSender.sendBroadcast(dismissIntent)
- }
-
- smartspaceMediaData =
- EMPTY_SMARTSPACE_MEDIA_DATA.copy(
- targetId = smartspaceMediaData.targetId,
- instanceId = smartspaceMediaData.instanceId,
- )
- mediaDataManager.dismissSmartspaceRecommendation(
- smartspaceMediaData.targetId,
- delay = 0L,
- )
- }
}
- /** Are there any active media entries, including the recommendation? */
- fun hasActiveMediaOrRecommendation() =
- userEntries.any { it.value.active } ||
- (smartspaceMediaData.isActive &&
- (smartspaceMediaData.isValid() || reactivatedKey != null))
-
- /** Are there any media entries we should display? */
- fun hasAnyMediaOrRecommendation(): Boolean {
- val hasSmartspace = smartspaceMediaData.isActive && smartspaceMediaData.isValid()
- return userEntries.isNotEmpty() || hasSmartspace
- }
-
- /** Are there any media notifications active (excluding the recommendation)? */
+ /** Are there any media notifications active? */
fun hasActiveMedia() = userEntries.any { it.value.active }
- /** Are there any media entries we should display (excluding the recommendation)? */
+ /** Are there any media entries we should display? */
fun hasAnyMedia() = userEntries.isNotEmpty()
/** Add a listener for filtered [MediaData] changes */
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImpl.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImpl.kt
index c2efc7559487..1c69d41092a4 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImpl.kt
@@ -23,11 +23,6 @@ import android.app.Notification.EXTRA_SUBSTITUTE_APP_NAME
import android.app.PendingIntent
import android.app.StatusBarManager
import android.app.UriGrantsManager
-import android.app.smartspace.SmartspaceAction
-import android.app.smartspace.SmartspaceConfig
-import android.app.smartspace.SmartspaceManager
-import android.app.smartspace.SmartspaceSession
-import android.app.smartspace.SmartspaceTarget
import android.content.BroadcastReceiver
import android.content.ContentProvider
import android.content.ContentResolver
@@ -55,7 +50,6 @@ import android.util.Pair as APair
import androidx.media.utils.MediaConstants
import com.android.app.tracing.coroutines.launchTraced as launch
import com.android.app.tracing.traceSection
-import com.android.internal.annotations.Keep
import com.android.internal.logging.InstanceId
import com.android.keyguard.KeyguardUpdateMonitor
import com.android.systemui.Dumpable
@@ -76,8 +70,6 @@ import com.android.systemui.media.controls.shared.model.MediaButton
import com.android.systemui.media.controls.shared.model.MediaData
import com.android.systemui.media.controls.shared.model.MediaDeviceData
import com.android.systemui.media.controls.shared.model.MediaNotificationAction
-import com.android.systemui.media.controls.shared.model.SmartspaceMediaData
-import com.android.systemui.media.controls.shared.model.SmartspaceMediaDataProvider
import com.android.systemui.media.controls.ui.view.MediaViewHolder
import com.android.systemui.media.controls.util.MediaControllerFactory
import com.android.systemui.media.controls.util.MediaDataUtils
@@ -109,7 +101,6 @@ private val ART_URIS =
private const val TAG = "MediaDataManager"
private const val DEBUG = true
-private const val EXTRAS_SMARTSPACE_DISMISS_INTENT_KEY = "dismiss_intent"
private val LOADING =
MediaData(
@@ -132,26 +123,12 @@ private val LOADING =
appUid = Process.INVALID_UID,
)
-internal val EMPTY_SMARTSPACE_MEDIA_DATA =
- SmartspaceMediaData(
- targetId = "INVALID",
- isActive = false,
- packageName = "INVALID",
- cardAction = null,
- recommendations = emptyList(),
- dismissIntent = null,
- headphoneConnectionTimeMillis = 0,
- instanceId = InstanceId.fakeInstanceId(-1),
- expiryTimeMs = 0,
- )
-
/** A class that facilitates management and loading of Media Data, ready for binding. */
@SysUISingleton
class LegacyMediaDataManagerImpl(
private val context: Context,
@Background private val backgroundExecutor: Executor,
@Background private val backgroundDispatcher: CoroutineDispatcher,
- @Main private val uiExecutor: Executor,
@Main private val foregroundExecutor: DelayableExecutor,
@Main private val mainDispatcher: CoroutineDispatcher,
@Application private val applicationScope: CoroutineScope,
@@ -164,25 +141,17 @@ class LegacyMediaDataManagerImpl(
private val mediaDeviceManager: MediaDeviceManager,
mediaDataCombineLatest: MediaDataCombineLatest,
private val mediaDataFilter: LegacyMediaDataFilterImpl,
- private val smartspaceMediaDataProvider: SmartspaceMediaDataProvider,
private var useMediaResumption: Boolean,
private val useQsMediaPlayer: Boolean,
private val systemClock: SystemClock,
private val mediaFlags: MediaFlags,
private val logger: MediaUiEventLogger,
- private val smartspaceManager: SmartspaceManager?,
private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
private val mediaDataLoader: dagger.Lazy<MediaDataLoader>,
private val mediaLogger: MediaLogger,
) : Dumpable, MediaDataManager {
companion object {
- // UI surface label for subscribing Smartspace updates.
- @JvmField val SMARTSPACE_UI_SURFACE_LABEL = "media_data_manager"
-
- // Smartspace package name's extra key.
- @JvmField val EXTRAS_MEDIA_SOURCE_PACKAGE_NAME = "package_name"
-
// Maximum number of actions allowed in compact view
@JvmField val MAX_COMPACT_ACTIONS = 3
@@ -211,9 +180,6 @@ class LegacyMediaDataManagerImpl(
} else {
LinkedHashMap()
}
- // There should ONLY be at most one Smartspace media recommendation.
- var smartspaceMediaData: SmartspaceMediaData = EMPTY_SMARTSPACE_MEDIA_DATA
- @Keep private var smartspaceSession: SmartspaceSession? = null
private val artworkWidth =
context.resources.getDimensionPixelSize(
@@ -236,7 +202,6 @@ class LegacyMediaDataManagerImpl(
context: Context,
threadFactory: ThreadFactory,
@Background backgroundDispatcher: CoroutineDispatcher,
- @Main uiExecutor: Executor,
@Main foregroundExecutor: DelayableExecutor,
@Main mainDispatcher: CoroutineDispatcher,
@Application applicationScope: CoroutineScope,
@@ -249,11 +214,9 @@ class LegacyMediaDataManagerImpl(
mediaDeviceManager: MediaDeviceManager,
mediaDataCombineLatest: MediaDataCombineLatest,
mediaDataFilter: LegacyMediaDataFilterImpl,
- smartspaceMediaDataProvider: SmartspaceMediaDataProvider,
clock: SystemClock,
mediaFlags: MediaFlags,
logger: MediaUiEventLogger,
- smartspaceManager: SmartspaceManager?,
keyguardUpdateMonitor: KeyguardUpdateMonitor,
mediaDataLoader: dagger.Lazy<MediaDataLoader>,
mediaLogger: MediaLogger,
@@ -263,7 +226,6 @@ class LegacyMediaDataManagerImpl(
// background thread. Use a custom thread for media.
threadFactory.buildExecutorOnNewThread(TAG),
backgroundDispatcher,
- uiExecutor,
foregroundExecutor,
mainDispatcher,
applicationScope,
@@ -276,13 +238,11 @@ class LegacyMediaDataManagerImpl(
mediaDeviceManager,
mediaDataCombineLatest,
mediaDataFilter,
- smartspaceMediaDataProvider,
Utils.useMediaResumption(context),
Utils.useQsMediaPlayer(context),
clock,
mediaFlags,
logger,
- smartspaceManager,
keyguardUpdateMonitor,
mediaDataLoader,
mediaLogger,
@@ -343,32 +303,9 @@ class LegacyMediaDataManagerImpl(
}
// BroadcastDispatcher does not allow filters with data schemes
context.registerReceiver(appChangeReceiver, uninstallFilter)
-
- // Register for Smartspace data updates.
- // TODO(b/382680767): remove
- smartspaceSession =
- smartspaceManager?.createSmartspaceSession(
- SmartspaceConfig.Builder(context, SMARTSPACE_UI_SURFACE_LABEL).build()
- )
- smartspaceSession?.let {
- it.addOnTargetsAvailableListener(
- // Use a main uiExecutor thread listening to Smartspace updates instead of using
- // the existing background executor.
- // SmartspaceSession has scheduled routine updates which can be unpredictable on
- // test simulators, using the backgroundExecutor makes it's hard to test the threads
- // numbers.
- uiExecutor,
- SmartspaceSession.OnTargetsAvailableListener { targets ->
- smartspaceMediaDataProvider.onTargetsAvailable(targets)
- },
- )
- }
- smartspaceSession?.let { it.requestSmartspaceUpdate() }
}
override fun destroy() {
- smartspaceSession?.close()
- smartspaceSession = null
context.unregisterReceiver(appChangeReceiver)
}
@@ -614,16 +551,6 @@ class LegacyMediaDataManagerImpl(
}
/**
- * Notify internal listeners of Smartspace media loaded event.
- *
- * External listeners registered with [addListener] will be notified after the event propagates
- * through the internal listener pipeline.
- */
- private fun notifySmartspaceMediaDataLoaded(key: String, info: SmartspaceMediaData) {
- internalListeners.forEach { it.onSmartspaceMediaDataLoaded(key, info) }
- }
-
- /**
* Notify internal listeners of media removed event.
*
* External listeners registered with [addListener] will be notified after the event propagates
@@ -634,20 +561,6 @@ class LegacyMediaDataManagerImpl(
}
/**
- * Notify internal listeners of Smartspace media removed event.
- *
- * External listeners registered with [addListener] will be notified after the event propagates
- * through the internal listener pipeline.
- *
- * @param immediately indicates should apply the UI changes immediately, otherwise wait until
- * the next refresh-round before UI becomes visible. Should only be true if the update is
- * initiated by user's interaction.
- */
- private fun notifySmartspaceMediaDataRemoved(key: String, immediately: Boolean) {
- internalListeners.forEach { it.onSmartspaceMediaDataRemoved(key, immediately) }
- }
-
- /**
* Called whenever the player has been paused or stopped for a while, or swiped from QQS. This
* will make the player not active anymore, hiding it from QQS and Keyguard.
*
@@ -674,11 +587,6 @@ class LegacyMediaDataManagerImpl(
if (DEBUG) Log.d(TAG, "Updating $key timedOut: $timedOut")
onMediaDataLoaded(key, key, it)
}
-
- if (key == smartspaceMediaData.targetId) {
- if (DEBUG) Log.d(TAG, "smartspace card expired")
- dismissSmartspaceRecommendation(key, delay = 0L)
- }
}
/** Called when the player's [PlaybackState] has been updated with new actions and/or state */
@@ -747,23 +655,7 @@ class LegacyMediaDataManagerImpl(
* the recommendation card entirely from the carousel.
*/
override fun dismissSmartspaceRecommendation(key: String, delay: Long) {
- if (smartspaceMediaData.targetId != key || !smartspaceMediaData.isValid()) {
- // If this doesn't match, or we've already invalidated the data, no action needed
- return
- }
-
- if (DEBUG) Log.d(TAG, "Dismissing Smartspace media target")
- if (smartspaceMediaData.isActive) {
- smartspaceMediaData =
- EMPTY_SMARTSPACE_MEDIA_DATA.copy(
- targetId = smartspaceMediaData.targetId,
- instanceId = smartspaceMediaData.instanceId,
- )
- }
- foregroundExecutor.executeDelayed(
- { notifySmartspaceMediaDataRemoved(smartspaceMediaData.targetId, immediately = true) },
- delay,
- )
+ // TODO(b/382680767): remove
}
private suspend fun loadMediaDataForResumption(
@@ -875,11 +767,7 @@ class LegacyMediaDataManagerImpl(
desc.extras?.getLong(MediaConstants.METADATA_KEY_IS_EXPLICIT) ==
MediaConstants.METADATA_VALUE_ATTRIBUTE_PRESENT
- val progress =
- if (mediaFlags.isResumeProgressEnabled()) {
- MediaDataUtils.getDescriptionProgress(desc.extras)
- } else null
-
+ val progress = MediaDataUtils.getDescriptionProgress(desc.extras)
val mediaAction = getResumeMediaAction(resumeAction)
val lastActive = systemClock.elapsedRealtime()
val createdTimestampMillis = currentEntry?.createdTimestampMillis ?: 0L
@@ -1445,14 +1333,14 @@ class LegacyMediaDataManagerImpl(
override fun onSwipeToDismiss() = mediaDataFilter.onSwipeToDismiss()
/** Are there any media notifications active, including the recommendations? */
- override fun hasActiveMediaOrRecommendation() = mediaDataFilter.hasActiveMediaOrRecommendation()
+ override fun hasActiveMediaOrRecommendation() = mediaDataFilter.hasActiveMedia()
/**
* Are there any media entries we should display, including the recommendations?
* - If resumption is enabled, this will include inactive players
* - If resumption is disabled, we only want to show active players
*/
- override fun hasAnyMediaOrRecommendation() = mediaDataFilter.hasAnyMediaOrRecommendation()
+ override fun hasAnyMediaOrRecommendation() = mediaDataFilter.hasAnyMedia()
/** Are there any resume media notifications active, excluding the recommendations? */
override fun hasActiveMedia() = mediaDataFilter.hasActiveMedia()
@@ -1464,61 +1352,7 @@ class LegacyMediaDataManagerImpl(
*/
override fun hasAnyMedia() = mediaDataFilter.hasAnyMedia()
- override fun isRecommendationActive() = smartspaceMediaData.isActive
-
- /**
- * Converts the pass-in SmartspaceTarget to SmartspaceMediaData
- *
- * @return An empty SmartspaceMediaData with the valid target Id is returned if the
- * SmartspaceTarget's data is invalid.
- */
- private fun toSmartspaceMediaData(target: SmartspaceTarget): SmartspaceMediaData {
- val baseAction: SmartspaceAction? = target.baseAction
- val dismissIntent =
- baseAction?.extras?.getParcelable(EXTRAS_SMARTSPACE_DISMISS_INTENT_KEY) as Intent?
-
- val isActive = true
-
- packageName(target)?.let {
- return SmartspaceMediaData(
- targetId = target.smartspaceTargetId,
- isActive = isActive,
- packageName = it,
- cardAction = target.baseAction,
- recommendations = target.iconGrid,
- dismissIntent = dismissIntent,
- headphoneConnectionTimeMillis = target.creationTimeMillis,
- instanceId = logger.getNewInstanceId(),
- expiryTimeMs = target.expiryTimeMillis,
- )
- }
- return EMPTY_SMARTSPACE_MEDIA_DATA.copy(
- targetId = target.smartspaceTargetId,
- isActive = isActive,
- dismissIntent = dismissIntent,
- headphoneConnectionTimeMillis = target.creationTimeMillis,
- instanceId = logger.getNewInstanceId(),
- expiryTimeMs = target.expiryTimeMillis,
- )
- }
-
- private fun packageName(target: SmartspaceTarget): String? {
- val recommendationList = target.iconGrid
- if (recommendationList == null || recommendationList.isEmpty()) {
- Log.w(TAG, "Empty or null media recommendation list.")
- return null
- }
- for (recommendation in recommendationList) {
- val extras = recommendation.extras
- extras?.let {
- it.getString(EXTRAS_MEDIA_SOURCE_PACKAGE_NAME)?.let { packageName ->
- return packageName
- }
- }
- }
- Log.w(TAG, "No valid package name is provided.")
- return null
- }
+ override fun isRecommendationActive() = false
override fun dump(pw: PrintWriter, args: Array<out String>) {
pw.apply {
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterImpl.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterImpl.kt
index d053dd2b54d1..d4daa3d12708 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterImpl.kt
@@ -18,41 +18,29 @@ package com.android.systemui.media.controls.domain.pipeline
import android.content.Context
import android.content.pm.UserInfo
-import android.os.SystemProperties
import android.util.Log
import com.android.internal.annotations.KeepForWeakReference
import com.android.internal.annotations.VisibleForTesting
import com.android.internal.logging.InstanceId
-import com.android.systemui.broadcast.BroadcastSender
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.media.controls.data.repository.MediaFilterRepository
import com.android.systemui.media.controls.shared.MediaLogger
-import com.android.systemui.media.controls.shared.model.EXTRA_KEY_TRIGGER_RESUME
import com.android.systemui.media.controls.shared.model.MediaData
import com.android.systemui.media.controls.shared.model.MediaDataLoadingModel
-import com.android.systemui.media.controls.shared.model.SmartspaceMediaData
-import com.android.systemui.media.controls.shared.model.SmartspaceMediaLoadingModel
-import com.android.systemui.media.controls.util.MediaUiEventLogger
import com.android.systemui.settings.UserTracker
import com.android.systemui.statusbar.NotificationLockscreenUserManager
import com.android.systemui.util.time.SystemClock
import java.util.SortedMap
import java.util.concurrent.Executor
-import java.util.concurrent.TimeUnit
import javax.inject.Inject
private const val TAG = "MediaDataFilter"
private const val DEBUG = true
-private const val EXPORTED_SMARTSPACE_TRAMPOLINE_ACTIVITY_NAME =
- ("com.google" +
- ".android.apps.gsa.staticplugins.opa.smartspace.ExportedSmartspaceTrampolineActivity")
-private const val RESUMABLE_MEDIA_MAX_AGE_SECONDS_KEY = "resumable_media_max_age_seconds"
/**
* Filters data updates from [MediaDataCombineLatest] based on the current user ID, and handles user
- * switches (removing entries for the previous user, adding back entries for the current user). Also
- * filters out smartspace updates in favor of local recent media, when avaialble.
+ * switches (removing entries for the previous user, adding back entries for the current user).
*
* This is added at the end of the pipeline since we may still need to handle callbacks from
* background users (e.g. timeouts).
@@ -61,13 +49,10 @@ private const val RESUMABLE_MEDIA_MAX_AGE_SECONDS_KEY = "resumable_media_max_age
class MediaDataFilterImpl
@Inject
constructor(
- private val context: Context,
userTracker: UserTracker,
- private val broadcastSender: BroadcastSender,
private val lockscreenUserManager: NotificationLockscreenUserManager,
@Main private val executor: Executor,
private val systemClock: SystemClock,
- private val logger: MediaUiEventLogger,
private val mediaFilterRepository: MediaFilterRepository,
private val mediaLogger: MediaLogger,
) : MediaDataManager.Listener {
@@ -127,106 +112,6 @@ constructor(
listeners.forEach { it.onMediaDataLoaded(key, oldKey, data) }
}
- override fun onSmartspaceMediaDataLoaded(
- key: String,
- data: SmartspaceMediaData,
- shouldPrioritize: Boolean,
- ) {
- if (!data.isActive) {
- Log.d(TAG, "Inactive recommendation data. Skip triggering.")
- return
- }
-
- // Override the pass-in value here, as the order of Smartspace card is only determined here.
- var shouldPrioritizeMutable = false
- mediaFilterRepository.setRecommendation(data)
-
- // Before forwarding the smartspace target, first check if we have recently inactive media
- val selectedUserEntries = mediaFilterRepository.selectedUserEntries.value
- val sorted =
- selectedUserEntries.toSortedMap(compareBy { selectedUserEntries[it]?.lastActive ?: -1 })
- val timeSinceActive = timeSinceActiveForMostRecentMedia(sorted)
- var smartspaceMaxAgeMillis = SMARTSPACE_MAX_AGE
- data.cardAction?.extras?.let {
- val smartspaceMaxAgeSeconds = it.getLong(RESUMABLE_MEDIA_MAX_AGE_SECONDS_KEY, 0)
- if (smartspaceMaxAgeSeconds > 0) {
- smartspaceMaxAgeMillis = TimeUnit.SECONDS.toMillis(smartspaceMaxAgeSeconds)
- }
- }
-
- // Check if smartspace has explicitly specified whether to re-activate resumable media.
- // The default behavior is to trigger if the smartspace data is active.
- val shouldTriggerResume =
- data.cardAction?.extras?.getBoolean(EXTRA_KEY_TRIGGER_RESUME, true) ?: true
- val shouldReactivate =
- shouldTriggerResume &&
- !selectedUserEntries.any { it.value.active } &&
- selectedUserEntries.isNotEmpty() &&
- data.isActive
-
- if (timeSinceActive < smartspaceMaxAgeMillis) {
- // It could happen there are existing active media resume cards, then we don't need to
- // reactivate.
- if (shouldReactivate) {
- val lastActiveId = sorted.lastKey() // most recently active id
- // Update loading state to consider this media active
- mediaFilterRepository.setReactivatedId(lastActiveId)
- val mediaData = sorted[lastActiveId]!!.copy(active = true)
- logger.logRecommendationActivated(
- mediaData.appUid,
- mediaData.packageName,
- mediaData.instanceId,
- )
- mediaFilterRepository.addMediaDataLoadingState(
- MediaDataLoadingModel.Loaded(
- lastActiveId,
- receivedSmartspaceCardLatency =
- (systemClock.currentTimeMillis() - data.headphoneConnectionTimeMillis)
- .toInt(),
- isSsReactivated = true,
- )
- )
- mediaLogger.logMediaLoaded(
- mediaData.instanceId,
- mediaData.active,
- "reactivating media instead of smartspace",
- )
- listeners.forEach { listener ->
- getKey(lastActiveId)?.let { lastActiveKey ->
- listener.onMediaDataLoaded(
- lastActiveKey,
- lastActiveKey,
- mediaData,
- receivedSmartspaceCardLatency =
- (systemClock.currentTimeMillis() -
- data.headphoneConnectionTimeMillis)
- .toInt(),
- isSsReactivated = true,
- )
- }
- }
- }
- } else if (data.isActive) {
- // Mark to prioritize Smartspace card if no recent media.
- shouldPrioritizeMutable = true
- }
-
- if (!data.isValid()) {
- Log.d(TAG, "Invalid recommendation data. Skip showing the rec card")
- return
- }
- val smartspaceMediaData = mediaFilterRepository.smartspaceMediaData.value
- logger.logRecommendationAdded(
- smartspaceMediaData.packageName,
- smartspaceMediaData.instanceId,
- )
- mediaFilterRepository.setRecommendationsLoadingState(
- SmartspaceMediaLoadingModel.Loaded(key, shouldPrioritizeMutable)
- )
- mediaLogger.logRecommendationLoaded(key, data.isActive, "loading recommendations")
- listeners.forEach { it.onSmartspaceMediaDataLoaded(key, data, shouldPrioritizeMutable) }
- }
-
override fun onMediaDataRemoved(key: String, userInitiated: Boolean) {
mediaFilterRepository.removeMediaEntry(key)?.let { mediaData ->
val instanceId = mediaData.instanceId
@@ -241,40 +126,6 @@ constructor(
}
}
- override fun onSmartspaceMediaDataRemoved(key: String, immediately: Boolean) {
- // First check if we had reactivated media instead of forwarding smartspace
- mediaFilterRepository.reactivatedId.value?.let { lastActiveId ->
- mediaFilterRepository.setReactivatedId(null)
- // Update loading state with actual active value
- mediaFilterRepository.selectedUserEntries.value[lastActiveId]?.let {
- mediaFilterRepository.addMediaDataLoadingState(
- MediaDataLoadingModel.Loaded(lastActiveId)
- )
- mediaLogger.logMediaLoaded(lastActiveId, it.active, "expiring reactivated id")
- listeners.forEach { listener ->
- getKey(lastActiveId)?.let { lastActiveKey ->
- listener.onMediaDataLoaded(lastActiveKey, lastActiveKey, it, immediately)
- }
- }
- }
- }
-
- val smartspaceMediaData = mediaFilterRepository.smartspaceMediaData.value
- if (smartspaceMediaData.isActive) {
- mediaFilterRepository.setRecommendation(
- EMPTY_SMARTSPACE_MEDIA_DATA.copy(
- targetId = smartspaceMediaData.targetId,
- instanceId = smartspaceMediaData.instanceId,
- )
- )
- }
- mediaFilterRepository.setRecommendationsLoadingState(
- SmartspaceMediaLoadingModel.Removed(key, immediately)
- )
- mediaLogger.logRecommendationRemoved(key, immediately, "removing recommendations card")
- listeners.forEach { it.onSmartspaceMediaDataRemoved(key, immediately) }
- }
-
@VisibleForTesting
internal fun handleProfileChanged() {
// TODO(b/317221348) re-add media removed when profile is available.
@@ -335,34 +186,6 @@ constructor(
mediaDataProcessor.setInactive(key, timedOut = true, forceUpdate = true)
}
}
- val smartspaceMediaData = mediaFilterRepository.smartspaceMediaData.value
- if (smartspaceMediaData.isActive) {
- val dismissIntent = smartspaceMediaData.dismissIntent
- if (dismissIntent == null) {
- Log.w(
- TAG,
- "Cannot create dismiss action click action: extras missing dismiss_intent.",
- )
- } else if (
- dismissIntent.component?.className == EXPORTED_SMARTSPACE_TRAMPOLINE_ACTIVITY_NAME
- ) {
- // Dismiss the card Smartspace data through Smartspace trampoline activity.
- context.startActivity(dismissIntent)
- } else {
- broadcastSender.sendBroadcast(dismissIntent)
- }
-
- mediaFilterRepository.setRecommendation(
- EMPTY_SMARTSPACE_MEDIA_DATA.copy(
- targetId = smartspaceMediaData.targetId,
- instanceId = smartspaceMediaData.instanceId,
- )
- )
- mediaDataProcessor.dismissSmartspaceRecommendation(
- smartspaceMediaData.targetId,
- delay = 0L,
- )
- }
}
/** Add a listener for filtered [MediaData] changes */
@@ -399,19 +222,4 @@ constructor(
null
}
}
-
- companion object {
- /**
- * Maximum age of a media control to re-activate on smartspace signal. If there is no media
- * control available within this time window, smartspace recommendations will be shown
- * instead.
- */
- @VisibleForTesting
- internal val SMARTSPACE_MAX_AGE: Long
- get() =
- SystemProperties.getLong(
- "debug.sysui.smartspace_max_age",
- TimeUnit.MINUTES.toMillis(30),
- )
- }
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoader.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoader.kt
index a7c5a36b804a..1a4687b59dbd 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoader.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoader.kt
@@ -339,11 +339,7 @@ constructor(
desc.extras?.getLong(MediaConstants.METADATA_KEY_IS_EXPLICIT) ==
MediaConstants.METADATA_VALUE_ATTRIBUTE_PRESENT
- val progress =
- if (mediaFlags.isResumeProgressEnabled()) {
- MediaDataUtils.getDescriptionProgress(desc.extras)
- } else null
-
+ val progress = MediaDataUtils.getDescriptionProgress(desc.extras)
val mediaAction = getResumeMediaAction(resumeAction)
return MediaDataLoaderResult(
appName = appName,
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt
index ca4a65953cba..7e101582dafa 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt
@@ -23,11 +23,6 @@ import android.app.Notification.EXTRA_SUBSTITUTE_APP_NAME
import android.app.PendingIntent
import android.app.StatusBarManager
import android.app.UriGrantsManager
-import android.app.smartspace.SmartspaceAction
-import android.app.smartspace.SmartspaceConfig
-import android.app.smartspace.SmartspaceManager
-import android.app.smartspace.SmartspaceSession
-import android.app.smartspace.SmartspaceTarget
import android.content.BroadcastReceiver
import android.content.ContentProvider
import android.content.ContentResolver
@@ -55,7 +50,6 @@ import android.util.Pair as APair
import androidx.media.utils.MediaConstants
import com.android.app.tracing.coroutines.launchTraced as launch
import com.android.app.tracing.traceSection
-import com.android.internal.annotations.Keep
import com.android.internal.logging.InstanceId
import com.android.keyguard.KeyguardUpdateMonitor
import com.android.systemui.CoreStartable
@@ -78,7 +72,6 @@ import com.android.systemui.media.controls.shared.model.MediaData
import com.android.systemui.media.controls.shared.model.MediaDeviceData
import com.android.systemui.media.controls.shared.model.MediaNotificationAction
import com.android.systemui.media.controls.shared.model.SmartspaceMediaData
-import com.android.systemui.media.controls.shared.model.SmartspaceMediaDataProvider
import com.android.systemui.media.controls.ui.view.MediaViewHolder
import com.android.systemui.media.controls.util.MediaControllerFactory
import com.android.systemui.media.controls.util.MediaDataUtils
@@ -110,7 +103,6 @@ private val ART_URIS =
private const val TAG = "MediaDataProcessor"
private const val DEBUG = true
-private const val EXTRAS_SMARTSPACE_DISMISS_INTENT_KEY = "dismiss_intent"
/** Processes all media data fields and encapsulates logic for managing media data entries. */
@SysUISingleton
@@ -125,13 +117,11 @@ class MediaDataProcessor(
private val mediaControllerFactory: MediaControllerFactory,
private val broadcastDispatcher: BroadcastDispatcher,
private val dumpManager: DumpManager,
- private val smartspaceMediaDataProvider: SmartspaceMediaDataProvider,
private var useMediaResumption: Boolean,
private val useQsMediaPlayer: Boolean,
private val systemClock: SystemClock,
private val mediaFlags: MediaFlags,
private val logger: MediaUiEventLogger,
- private val smartspaceManager: SmartspaceManager?,
private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
private val mediaDataRepository: MediaDataRepository,
private val mediaDataLoader: dagger.Lazy<MediaDataLoader>,
@@ -139,15 +129,6 @@ class MediaDataProcessor(
) : CoreStartable {
companion object {
- /**
- * UI surface label for subscribing Smartspace updates. String must match with
- * [BcSmartspaceDataPlugin.UI_SURFACE_MEDIA]
- */
- @JvmField val SMARTSPACE_UI_SURFACE_LABEL = "media_data_manager"
-
- // Smartspace package name's extra key.
- @JvmField val EXTRAS_MEDIA_SOURCE_PACKAGE_NAME = "package_name"
-
// Maximum number of actions allowed in compact view
@JvmField val MAX_COMPACT_ACTIONS = 3
@@ -173,9 +154,6 @@ class MediaDataProcessor(
// listeners are listeners that depend on MediaDataProcessor.
private val internalListeners: MutableSet<Listener> = mutableSetOf()
- // There should ONLY be at most one Smartspace media recommendation.
- @Keep private var smartspaceSession: SmartspaceSession? = null
-
private val artworkWidth =
context.resources.getDimensionPixelSize(
com.android.internal.R.dimen.config_mediaMetadataBitmapMaxSize
@@ -204,11 +182,9 @@ class MediaDataProcessor(
mediaControllerFactory: MediaControllerFactory,
dumpManager: DumpManager,
broadcastDispatcher: BroadcastDispatcher,
- smartspaceMediaDataProvider: SmartspaceMediaDataProvider,
clock: SystemClock,
mediaFlags: MediaFlags,
logger: MediaUiEventLogger,
- smartspaceManager: SmartspaceManager?,
keyguardUpdateMonitor: KeyguardUpdateMonitor,
mediaDataRepository: MediaDataRepository,
mediaDataLoader: dagger.Lazy<MediaDataLoader>,
@@ -226,13 +202,11 @@ class MediaDataProcessor(
mediaControllerFactory,
broadcastDispatcher,
dumpManager,
- smartspaceMediaDataProvider,
Utils.useMediaResumption(context),
Utils.useQsMediaPlayer(context),
clock,
mediaFlags,
logger,
- smartspaceManager,
keyguardUpdateMonitor,
mediaDataRepository,
mediaDataLoader,
@@ -273,31 +247,9 @@ class MediaDataProcessor(
}
// BroadcastDispatcher does not allow filters with data schemes
context.registerReceiver(appChangeReceiver, uninstallFilter)
-
- // Register for Smartspace data updates.
- // TODO(b/382680767): remove
- smartspaceSession =
- smartspaceManager?.createSmartspaceSession(
- SmartspaceConfig.Builder(context, SMARTSPACE_UI_SURFACE_LABEL).build()
- )
- smartspaceSession?.let {
- it.addOnTargetsAvailableListener(
- // Use a main uiExecutor thread listening to Smartspace updates instead of using
- // the existing background executor.
- // SmartspaceSession has scheduled routine updates which can be unpredictable on
- // test simulators, using the backgroundExecutor makes it's hard to test the threads
- // numbers.
- uiExecutor
- ) { targets ->
- smartspaceMediaDataProvider.onTargetsAvailable(targets)
- }
- }
- smartspaceSession?.requestSmartspaceUpdate()
}
fun destroy() {
- smartspaceSession?.close()
- smartspaceSession = null
context.unregisterReceiver(appChangeReceiver)
internalListeners.clear()
}
@@ -453,16 +405,6 @@ class MediaDataProcessor(
}
/**
- * Notify internal listeners of Smartspace media loaded event.
- *
- * External listeners registered with [MediaCarouselInteractor.addListener] will be notified
- * after the event propagates through the internal listener pipeline.
- */
- private fun notifySmartspaceMediaDataLoaded(key: String, info: SmartspaceMediaData) {
- internalListeners.forEach { it.onSmartspaceMediaDataLoaded(key, info) }
- }
-
- /**
* Notify internal listeners of media removed event.
*
* External listeners registered with [MediaCarouselInteractor.addListener] will be notified
@@ -473,20 +415,6 @@ class MediaDataProcessor(
}
/**
- * Notify internal listeners of Smartspace media removed event.
- *
- * External listeners registered with [MediaCarouselInteractor.addListener] will be notified
- * after the event propagates through the internal listener pipeline.
- *
- * @param immediately indicates should apply the UI changes immediately, otherwise wait until
- * the next refresh-round before UI becomes visible. Should only be true if the update is
- * initiated by user's interaction.
- */
- private fun notifySmartspaceMediaDataRemoved(key: String, immediately: Boolean) {
- internalListeners.forEach { it.onSmartspaceMediaDataRemoved(key, immediately) }
- }
-
- /**
* Called whenever the player has been paused or stopped for a while, or swiped from QQS. This
* will make the player not active anymore, hiding it from QQS and Keyguard.
*
@@ -513,11 +441,6 @@ class MediaDataProcessor(
if (DEBUG) Log.d(TAG, "Updating $key timedOut: $timedOut")
onMediaDataLoaded(key, key, it)
}
-
- if (key == mediaDataRepository.smartspaceMediaData.value.targetId) {
- if (DEBUG) Log.d(TAG, "smartspace card expired")
- dismissSmartspaceRecommendation(key, delay = 0L)
- }
}
/** Called when the player's [PlaybackState] has been updated with new actions and/or state */
@@ -597,19 +520,6 @@ class MediaDataProcessor(
}
}
- /**
- * Called whenever the recommendation has been expired or removed by the user. This will remove
- * the recommendation card entirely from the carousel.
- */
- fun dismissSmartspaceRecommendation(key: String, delay: Long) {
- if (mediaDataRepository.dismissSmartspaceRecommendation(key)) {
- foregroundExecutor.executeDelayed(
- { notifySmartspaceMediaDataRemoved(key, immediately = true) },
- delay,
- )
- }
- }
-
private suspend fun loadMediaDataForResumption(
userId: Int,
desc: MediaDescription,
@@ -719,11 +629,7 @@ class MediaDataProcessor(
desc.extras?.getLong(MediaConstants.METADATA_KEY_IS_EXPLICIT) ==
MediaConstants.METADATA_VALUE_ATTRIBUTE_PRESENT
- val progress =
- if (mediaFlags.isResumeProgressEnabled()) {
- MediaDataUtils.getDescriptionProgress(desc.extras)
- } else null
-
+ val progress = MediaDataUtils.getDescriptionProgress(desc.extras)
val mediaAction = getResumeMediaAction(resumeAction)
val lastActive = systemClock.elapsedRealtime()
val createdTimestampMillis = currentEntry?.createdTimestampMillis ?: 0L
@@ -1414,6 +1320,7 @@ class MediaDataProcessor(
* it will be prioritized as the first card. Otherwise, it will show up as the last card
* as default.
*/
+ // TODO(b/382680767): remove
fun onSmartspaceMediaDataLoaded(
key: String,
data: SmartspaceMediaData,
@@ -1430,68 +1337,13 @@ class MediaDataProcessor(
* until the next refresh-round before UI becomes visible. True by default to take in
* place immediately.
*/
+ // TODO(b/382680767): remove
fun onSmartspaceMediaDataRemoved(key: String, immediately: Boolean = true) {}
/** Called whenever the current active media notification changes */
fun onCurrentActiveMediaChanged(key: String?, data: MediaData?) {}
}
- /**
- * Converts the pass-in SmartspaceTarget to SmartspaceMediaData
- *
- * @return An empty SmartspaceMediaData with the valid target Id is returned if the
- * SmartspaceTarget's data is invalid.
- */
- private fun toSmartspaceMediaData(target: SmartspaceTarget): SmartspaceMediaData {
- val baseAction: SmartspaceAction? = target.baseAction
- val dismissIntent =
- baseAction
- ?.extras
- ?.getParcelable(EXTRAS_SMARTSPACE_DISMISS_INTENT_KEY, Intent::class.java)
-
- val isActive = true
-
- packageName(target)?.let {
- return SmartspaceMediaData(
- targetId = target.smartspaceTargetId,
- isActive = isActive,
- packageName = it,
- cardAction = target.baseAction,
- recommendations = target.iconGrid,
- dismissIntent = dismissIntent,
- headphoneConnectionTimeMillis = target.creationTimeMillis,
- instanceId = logger.getNewInstanceId(),
- expiryTimeMs = target.expiryTimeMillis,
- )
- }
- return SmartspaceMediaData(
- targetId = target.smartspaceTargetId,
- isActive = isActive,
- dismissIntent = dismissIntent,
- headphoneConnectionTimeMillis = target.creationTimeMillis,
- instanceId = logger.getNewInstanceId(),
- expiryTimeMs = target.expiryTimeMillis,
- )
- }
-
- private fun packageName(target: SmartspaceTarget): String? {
- val recommendationList: MutableList<SmartspaceAction> = target.iconGrid
- if (recommendationList.isEmpty()) {
- Log.w(TAG, "Empty or null media recommendation list.")
- return null
- }
- for (recommendation in recommendationList) {
- val extras = recommendation.extras
- extras?.let {
- it.getString(EXTRAS_MEDIA_SOURCE_PACKAGE_NAME)?.let { packageName ->
- return packageName
- }
- }
- }
- Log.w(TAG, "No valid package name is provided.")
- return null
- }
-
override fun dump(pw: PrintWriter, args: Array<out String>) {
pw.apply {
println("internalListeners: $internalListeners")
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaProcessingHelper.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaProcessingHelper.kt
index df0e1adee968..2e0e3a78c5b7 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaProcessingHelper.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaProcessingHelper.kt
@@ -24,7 +24,6 @@ import android.media.session.MediaController
import android.media.session.PlaybackState
import android.os.BadParcelableException
import android.util.Log
-import com.android.systemui.Flags.mediaControlsPostsOptimization
import com.android.systemui.biometrics.Utils.toBitmap
import com.android.systemui.media.controls.shared.model.MediaData
@@ -45,7 +44,7 @@ fun isSameMediaData(
new: MediaData,
old: MediaData?,
): Boolean {
- if (old == null || !mediaControlsPostsOptimization()) return false
+ if (old == null) return false
return new.userId == old.userId &&
new.app == old.app &&
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaSessionBasedFilter.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaSessionBasedFilter.kt
index b178d84c5d18..e500f27b982c 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaSessionBasedFilter.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaSessionBasedFilter.kt
@@ -26,7 +26,6 @@ import android.util.Log
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.media.controls.shared.model.MediaData
-import com.android.systemui.media.controls.shared.model.SmartspaceMediaData
import com.android.systemui.statusbar.phone.NotificationListenerWithPlugins
import java.util.concurrent.Executor
import javax.inject.Inject
@@ -46,7 +45,7 @@ constructor(
context: Context,
private val sessionManager: MediaSessionManager,
@Main private val foregroundExecutor: Executor,
- @Background private val backgroundExecutor: Executor
+ @Background private val backgroundExecutor: Executor,
) : MediaDataManager.Listener {
private val listeners: MutableSet<MediaDataManager.Listener> = mutableSetOf()
@@ -98,7 +97,7 @@ constructor(
data: MediaData,
immediately: Boolean,
receivedSmartspaceCardLatency: Int,
- isSsReactivated: Boolean
+ isSsReactivated: Boolean,
) {
backgroundExecutor.execute {
data.token?.let { tokensWithNotifications.add(TokenId(it)) }
@@ -143,14 +142,6 @@ constructor(
}
}
- override fun onSmartspaceMediaDataLoaded(
- key: String,
- data: SmartspaceMediaData,
- shouldPrioritize: Boolean
- ) {
- backgroundExecutor.execute { dispatchSmartspaceMediaDataLoaded(key, data) }
- }
-
override fun onMediaDataRemoved(key: String, userInitiated: Boolean) {
// Queue on background thread to ensure ordering of loaded and removed events is maintained.
backgroundExecutor.execute {
@@ -159,15 +150,11 @@ constructor(
}
}
- override fun onSmartspaceMediaDataRemoved(key: String, immediately: Boolean) {
- backgroundExecutor.execute { dispatchSmartspaceMediaDataRemoved(key, immediately) }
- }
-
private fun dispatchMediaDataLoaded(
key: String,
oldKey: String?,
info: MediaData,
- immediately: Boolean
+ immediately: Boolean,
) {
foregroundExecutor.execute {
listeners.toSet().forEach { it.onMediaDataLoaded(key, oldKey, info, immediately) }
@@ -180,18 +167,6 @@ constructor(
}
}
- private fun dispatchSmartspaceMediaDataLoaded(key: String, info: SmartspaceMediaData) {
- foregroundExecutor.execute {
- listeners.toSet().forEach { it.onSmartspaceMediaDataLoaded(key, info) }
- }
- }
-
- private fun dispatchSmartspaceMediaDataRemoved(key: String, immediately: Boolean) {
- foregroundExecutor.execute {
- listeners.toSet().forEach { it.onSmartspaceMediaDataRemoved(key, immediately) }
- }
- }
-
private fun handleControllersChanged(controllers: List<MediaController>?) {
packageControllers.clear()
controllers?.forEach { controller ->
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListener.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListener.kt
index be4e6cc59f76..8e364b81cb36 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListener.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListener.kt
@@ -27,9 +27,7 @@ import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.media.NotificationMediaManager.isPlayingState
import com.android.systemui.media.controls.shared.model.MediaData
-import com.android.systemui.media.controls.shared.model.SmartspaceMediaData
import com.android.systemui.media.controls.util.MediaControllerFactory
-import com.android.systemui.media.controls.util.MediaFlags
import com.android.systemui.plugins.statusbar.StatusBarStateController
import com.android.systemui.statusbar.SysuiStatusBarStateController
import com.android.systemui.util.concurrency.DelayableExecutor
@@ -58,11 +56,9 @@ constructor(
private val logger: MediaTimeoutLogger,
statusBarStateController: SysuiStatusBarStateController,
private val systemClock: SystemClock,
- private val mediaFlags: MediaFlags,
) : MediaDataManager.Listener {
private val mediaListeners: MutableMap<String, PlaybackStateListener> = mutableMapOf()
- private val recommendationListeners: MutableMap<String, RecommendationListener> = mutableMapOf()
/**
* Callback representing that a media object is now expired:
@@ -106,16 +102,6 @@ constructor(
listener.doTimeout()
}
}
-
- recommendationListeners.forEach { (key, listener) ->
- if (
- listener.cancellation != null &&
- listener.expiration <= systemClock.currentTimeMillis()
- ) {
- logger.logTimeoutCancelled(key, "Timed out while dozing")
- listener.doTimeout()
- }
- }
}
}
}
@@ -324,54 +310,4 @@ constructor(
cancellation = null
}
}
-
- /** Listens to changes in recommendation card data and schedules a timeout for its expiration */
- private inner class RecommendationListener(var key: String, data: SmartspaceMediaData) {
- private var timedOut = false
- var destroyed = false
- var expiration = Long.MAX_VALUE
- private set
-
- var cancellation: Runnable? = null
- private set
-
- var recommendationData: SmartspaceMediaData = data
- set(value) {
- destroyed = false
- field = value
- processUpdate()
- }
-
- init {
- recommendationData = data
- }
-
- fun destroy() {
- cancellation?.run()
- cancellation = null
- destroyed = true
- }
-
- private fun processUpdate() {
- if (recommendationData.expiryTimeMs != expiration) {
- // The expiry time changed - cancel and reschedule
- val timeout =
- recommendationData.expiryTimeMs -
- recommendationData.headphoneConnectionTimeMillis
- logger.logRecommendationTimeoutScheduled(key, timeout)
- cancellation?.run()
- cancellation = mainExecutor.executeDelayed({ doTimeout() }, timeout)
- expiration = recommendationData.expiryTimeMs
- }
- }
-
- fun doTimeout() {
- cancellation?.run()
- cancellation = null
- logger.logTimeout(key)
- timedOut = true
- expiration = Long.MAX_VALUE
- timeoutCallback(key, timedOut)
- }
- }
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaCarouselInteractor.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaCarouselInteractor.kt
index cd51a4bf0df9..bc058b6eb796 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaCarouselInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaCarouselInteractor.kt
@@ -41,7 +41,7 @@ import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
/** Encapsulates business logic for media pipeline. */
@@ -61,16 +61,10 @@ constructor(
) : MediaDataManager, CoreStartable {
/** Are there any media notifications active, including the recommendations? */
+ // TODO(b/382680767): rename
val hasActiveMediaOrRecommendation: StateFlow<Boolean> =
- combine(
- mediaFilterRepository.selectedUserEntries,
- mediaFilterRepository.smartspaceMediaData,
- mediaFilterRepository.reactivatedId,
- ) { entries, smartspaceMediaData, reactivatedKey ->
- entries.any { it.value.active } ||
- (smartspaceMediaData.isActive &&
- (smartspaceMediaData.isValid() || reactivatedKey != null))
- }
+ mediaFilterRepository.selectedUserEntries
+ .map { entries -> entries.any { it.value.active } }
.stateIn(
scope = applicationScope,
started = SharingStarted.WhileSubscribed(),
@@ -78,14 +72,10 @@ constructor(
)
/** Are there any media entries we should display, including the recommendations? */
+ // TODO(b/382680767): rename
val hasAnyMediaOrRecommendation: StateFlow<Boolean> =
- combine(
- mediaFilterRepository.selectedUserEntries,
- mediaFilterRepository.smartspaceMediaData,
- ) { entries, smartspaceMediaData ->
- entries.isNotEmpty() ||
- (smartspaceMediaData.isActive && smartspaceMediaData.isValid())
- }
+ mediaFilterRepository.selectedUserEntries
+ .map { entries -> entries.isNotEmpty() }
.stateIn(
scope = applicationScope,
started = SharingStarted.WhileSubscribed(),
@@ -182,9 +172,7 @@ constructor(
mediaDataProcessor.dismissMediaData(instanceId, delay, userInitiated = false)
}
- override fun dismissSmartspaceRecommendation(key: String, delay: Long) {
- return mediaDataProcessor.dismissSmartspaceRecommendation(key, delay)
- }
+ override fun dismissSmartspaceRecommendation(key: String, delay: Long) {}
override fun onNotificationRemoved(key: String) {
mediaDataProcessor.onNotificationRemoved(key)
@@ -198,16 +186,15 @@ constructor(
mediaDataFilter.onSwipeToDismiss()
}
- override fun hasActiveMediaOrRecommendation() =
- mediaFilterRepository.hasActiveMediaOrRecommendation()
+ override fun hasActiveMediaOrRecommendation() = mediaFilterRepository.hasActiveMedia()
- override fun hasAnyMediaOrRecommendation() = hasAnyMediaOrRecommendation.value
+ override fun hasAnyMediaOrRecommendation() = mediaFilterRepository.hasAnyMedia()
override fun hasActiveMedia() = mediaFilterRepository.hasActiveMedia()
override fun hasAnyMedia() = mediaFilterRepository.hasAnyMedia()
- override fun isRecommendationActive() = mediaFilterRepository.isRecommendationActive()
+ override fun isRecommendationActive() = false
fun reorderMedia() {
mediaFilterRepository.setOrderedMedia()
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/shared/model/MediaCommonModel.kt b/packages/SystemUI/src/com/android/systemui/media/controls/shared/model/MediaCommonModel.kt
index 3d5d47b30860..7ba065d802fc 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/shared/model/MediaCommonModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/shared/model/MediaCommonModel.kt
@@ -17,14 +17,8 @@
package com.android.systemui.media.controls.shared.model
/** Models any type of media. */
-sealed class MediaCommonModel {
- data class MediaControl(
- val mediaLoadedModel: MediaDataLoadingModel.Loaded,
- val canBeRemoved: Boolean = false,
- val isMediaFromRec: Boolean = false,
- val updateTime: Long = 0L,
- ) : MediaCommonModel()
-
- data class MediaRecommendations(val recsLoadingModel: SmartspaceMediaLoadingModel) :
- MediaCommonModel()
-}
+data class MediaCommonModel(
+ val mediaLoadedModel: MediaDataLoadingModel.Loaded,
+ val canBeRemoved: Boolean = false,
+ val updateTime: Long = 0L,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/shared/model/MediaDataLoadingModel.kt b/packages/SystemUI/src/com/android/systemui/media/controls/shared/model/MediaDataLoadingModel.kt
index 323e5cb76aba..6a939adb4637 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/shared/model/MediaDataLoadingModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/shared/model/MediaDataLoadingModel.kt
@@ -24,11 +24,7 @@ sealed class MediaDataLoadingModel {
abstract val instanceId: InstanceId
/** Media data has been loaded. */
- data class Loaded(
- override val instanceId: InstanceId,
- val receivedSmartspaceCardLatency: Int = 0,
- val isSsReactivated: Boolean = false,
- ) : MediaDataLoadingModel()
+ data class Loaded(override val instanceId: InstanceId) : MediaDataLoadingModel()
/** Media data has been removed. */
data class Removed(override val instanceId: InstanceId) : MediaDataLoadingModel()
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/shared/model/MediaRecommendationsModel.kt b/packages/SystemUI/src/com/android/systemui/media/controls/shared/model/MediaRecommendationsModel.kt
deleted file mode 100644
index 43bd32d90c55..000000000000
--- a/packages/SystemUI/src/com/android/systemui/media/controls/shared/model/MediaRecommendationsModel.kt
+++ /dev/null
@@ -1,44 +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.media.controls.shared.model
-
-import android.content.Intent
-import android.graphics.drawable.Icon
-import android.os.Bundle
-import android.os.Process
-import com.android.internal.logging.InstanceId
-
-data class MediaRecommendationsModel(
- val key: String,
- val uid: Int = Process.INVALID_UID,
- val packageName: String,
- val instanceId: InstanceId? = null,
- val appName: CharSequence? = null,
- val dismissIntent: Intent? = null,
- /** Whether the model contains enough number of valid recommendations. */
- val areRecommendationsValid: Boolean = false,
- val mediaRecs: List<MediaRecModel>,
-)
-
-/** Represents smartspace media recommendation action */
-data class MediaRecModel(
- val intent: Intent? = null,
- val title: CharSequence? = null,
- val subtitle: CharSequence? = null,
- val icon: Icon? = null,
- val extras: Bundle? = null,
-)
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/shared/model/SmartspaceMediaDataProvider.kt b/packages/SystemUI/src/com/android/systemui/media/controls/shared/model/SmartspaceMediaDataProvider.kt
deleted file mode 100644
index 8726d8193800..000000000000
--- a/packages/SystemUI/src/com/android/systemui/media/controls/shared/model/SmartspaceMediaDataProvider.kt
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open 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.controls.shared.model
-
-import android.app.smartspace.SmartspaceTarget
-import android.util.Log
-import com.android.systemui.plugins.BcSmartspaceDataPlugin
-import com.android.systemui.plugins.BcSmartspaceDataPlugin.SmartspaceTargetListener
-import javax.inject.Inject
-
-private const val TAG = "SsMediaDataProvider"
-
-/** Provides SmartspaceTargets of media types for SystemUI media control. */
-class SmartspaceMediaDataProvider @Inject constructor() : BcSmartspaceDataPlugin {
-
- private val smartspaceMediaTargetListeners: MutableList<SmartspaceTargetListener> =
- mutableListOf()
-
- override fun registerListener(smartspaceTargetListener: SmartspaceTargetListener) {
- smartspaceMediaTargetListeners.add(smartspaceTargetListener)
- }
-
- override fun unregisterListener(smartspaceTargetListener: SmartspaceTargetListener?) {
- smartspaceMediaTargetListeners.remove(smartspaceTargetListener)
- }
-
- /** Updates Smartspace data and propagates it to any listeners. */
- override fun onTargetsAvailable(targets: List<SmartspaceTarget>) {
- Log.d(TAG, "Forwarding Smartspace updates $targets")
- smartspaceMediaTargetListeners.forEach { it.onSmartspaceTargetsUpdated(targets) }
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt
index 800220ee962a..13a9f345c04b 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt
@@ -59,7 +59,6 @@ import com.android.systemui.keyguard.shared.model.TransitionState
import com.android.systemui.lifecycle.repeatWhenAttached
import com.android.systemui.media.controls.domain.pipeline.MediaDataManager
import com.android.systemui.media.controls.shared.model.MediaData
-import com.android.systemui.media.controls.shared.model.SmartspaceMediaData
import com.android.systemui.media.controls.ui.binder.MediaControlViewBinder
import com.android.systemui.media.controls.ui.util.MediaViewModelCallback
import com.android.systemui.media.controls.ui.util.MediaViewModelListUpdateCallback
@@ -204,7 +203,6 @@ constructor(
private var needsReordering: Boolean = false
private var isUserInitiatedRemovalQueued: Boolean = false
private var keysNeedRemoval = mutableSetOf<String>()
- var shouldScrollToKey: Boolean = false
private var isRtl: Boolean = false
set(value) {
if (value != field) {
@@ -458,7 +456,7 @@ constructor(
} else {
null
}
- addOrUpdatePlayer(key, oldKey, data, isSsReactivated, onUiExecutionEnd)
+ addOrUpdatePlayer(key, oldKey, data, onUiExecutionEnd)
val canRemove = data.isPlaying?.let { !it } ?: data.isClearable && !data.active
if (canRemove && !Utils.useMediaResumption(context)) {
@@ -721,18 +719,7 @@ constructor(
MediaPlayerData.getFirstActiveMediaData()
)
MediaPlayerData.updateVisibleMediaPlayers()
- // Automatically scroll to the active player if needed
- if (shouldScrollToKey) {
- shouldScrollToKey = false
- val mediaIndex = key?.let { MediaPlayerData.getMediaPlayerIndex(it) } ?: -1
- if (mediaIndex != -1) {
- previousVisiblePlayerKey?.let {
- val previousVisibleIndex =
- MediaPlayerData.playerKeys().indexOfFirst { key -> it == key }
- mediaCarouselScrollHandler.scrollToPlayer(previousVisibleIndex, mediaIndex)
- } ?: mediaCarouselScrollHandler.scrollToPlayer(destIndex = mediaIndex)
- }
- } else if (isRtl && mediaContent.childCount > 0) {
+ if (isRtl && mediaContent.childCount > 0) {
// In RTL, Scroll to the first player as it is the rightmost player in media carousel.
mediaCarouselScrollHandler.scrollToPlayer(destIndex = 0)
}
@@ -753,7 +740,6 @@ constructor(
key: String,
oldKey: String?,
data: MediaData,
- isSsReactivated: Boolean,
onUiExecutionEnd: Runnable? = null,
): Boolean =
traceSection("MediaCarouselController#addOrUpdatePlayer") {
@@ -768,13 +754,7 @@ constructor(
val mediaViewHolder = createMediaViewHolderInBg()
// Add the new player in the main thread.
uiExecutor.execute {
- setupNewPlayer(
- key,
- data,
- isSsReactivated,
- curVisibleMediaKey,
- mediaViewHolder,
- )
+ setupNewPlayer(key, data, curVisibleMediaKey, mediaViewHolder)
updatePageIndicator()
mediaCarouselScrollHandler.onPlayersChanged()
mediaControlChipInteractor.updateMediaControlChipModelLegacy(
@@ -785,7 +765,7 @@ constructor(
}
}
} else {
- updatePlayer(key, data, isSsReactivated, curVisibleMediaKey, existingPlayer)
+ updatePlayer(key, data, curVisibleMediaKey, existingPlayer)
updatePageIndicator()
mediaCarouselScrollHandler.onPlayersChanged()
mediaControlChipInteractor.updateMediaControlChipModelLegacy(
@@ -798,9 +778,9 @@ constructor(
if (existingPlayer == null) {
val mediaViewHolder =
MediaViewHolder.create(LayoutInflater.from(context), mediaContent)
- setupNewPlayer(key, data, isSsReactivated, curVisibleMediaKey, mediaViewHolder)
+ setupNewPlayer(key, data, curVisibleMediaKey, mediaViewHolder)
} else {
- updatePlayer(key, data, isSsReactivated, curVisibleMediaKey, existingPlayer)
+ updatePlayer(key, data, curVisibleMediaKey, existingPlayer)
}
updatePageIndicator()
mediaCarouselScrollHandler.onPlayersChanged()
@@ -816,27 +796,12 @@ constructor(
private fun updatePlayer(
key: String,
data: MediaData,
- isSsReactivated: Boolean,
curVisibleMediaKey: MediaPlayerData.MediaSortKey?,
existingPlayer: MediaControlPanel,
) {
existingPlayer.bindPlayer(data, key)
- MediaPlayerData.addMediaPlayer(
- key,
- data,
- existingPlayer,
- systemClock,
- isSsReactivated,
- debugLogger,
- )
- val packageName = MediaPlayerData.smartspaceMediaData?.packageName ?: String()
- // In case of recommendations hits.
- // Check the playing status of media player and the package name.
- // To make sure we scroll to the right app's media player.
- if (
- isReorderingAllowed ||
- shouldScrollToKey && data.isPlaying == true && packageName == data.packageName
- ) {
+ MediaPlayerData.addMediaPlayer(key, data, existingPlayer, systemClock, debugLogger)
+ if (isReorderingAllowed) {
reorderAllPlayers(curVisibleMediaKey, key)
} else {
needsReordering = true
@@ -846,7 +811,6 @@ constructor(
private fun setupNewPlayer(
key: String,
data: MediaData,
- isSsReactivated: Boolean,
curVisibleMediaKey: MediaPlayerData.MediaSortKey?,
mediaViewHolder: MediaViewHolder,
) {
@@ -862,17 +826,9 @@ constructor(
newPlayer.mediaViewHolder?.player?.setLayoutParams(lp)
newPlayer.bindPlayer(data, key)
newPlayer.setListening(mediaCarouselScrollHandler.visibleToUser && currentlyExpanded)
- MediaPlayerData.addMediaPlayer(
- key,
- data,
- newPlayer,
- systemClock,
- isSsReactivated,
- debugLogger,
- )
+ MediaPlayerData.addMediaPlayer(key, data, newPlayer, systemClock, debugLogger)
updateViewControllerToState(newPlayer.mediaViewController, noAnimation = true)
- // Media data added from a recommendation card should starts playing.
- if ((shouldScrollToKey && data.isPlaying == true) || (!shouldScrollToKey && data.active)) {
+ if (data.active) {
reorderAllPlayers(curVisibleMediaKey, key)
} else {
needsReordering = true
@@ -887,16 +843,9 @@ constructor(
fun removePlayer(
key: String,
dismissMediaData: Boolean = true,
- dismissRecommendation: Boolean = true,
userInitiated: Boolean = false,
): MediaControlPanel? {
- if (key == MediaPlayerData.smartspaceMediaKey()) {
- MediaPlayerData.smartspaceMediaData?.let {
- logger.logRecommendationRemoved(it.packageName, it.instanceId)
- }
- }
- val removed =
- MediaPlayerData.removeMediaPlayer(key, dismissMediaData || dismissRecommendation)
+ val removed = MediaPlayerData.removeMediaPlayer(key, dismissMediaData)
return removed?.apply {
mediaCarouselScrollHandler.onPrePlayerRemoved(removed.mediaViewHolder?.player)
mediaContent.removeView(removed.mediaViewHolder?.player)
@@ -911,10 +860,6 @@ constructor(
// Inform the media manager of a potentially late dismissal
mediaManager.dismissMediaData(key, delay = 0L, userInitiated = userInitiated)
}
- if (dismissRecommendation) {
- // Inform the media manager of a potentially late dismissal
- mediaManager.dismissSmartspaceRecommendation(key, delay = 0L)
- }
}
}
@@ -937,16 +882,14 @@ constructor(
val mediaDataList = MediaPlayerData.mediaData()
// Do not loop through the original list of media data because the re-addition of media data
// is being executed in background thread.
- mediaDataList.forEach { (key, data, _) ->
- val isSsReactivated = MediaPlayerData.isSsReactivated(key)
+ mediaDataList.forEach { (key, data) ->
if (recreateMedia) {
- removePlayer(key, dismissMediaData = false, dismissRecommendation = false)
+ removePlayer(key, dismissMediaData = false)
}
addOrUpdatePlayer(
key = key,
oldKey = null,
data = data,
- isSsReactivated = isSsReactivated,
onUiExecutionEnd = onUiExecutionEnd,
)
}
@@ -1290,8 +1233,6 @@ constructor(
println("orderedPlayerSortKeys: ${MediaPlayerData.playerKeys()}")
println("visiblePlayerSortKeys: ${MediaPlayerData.visiblePlayerKeys()}")
println("controlViewModels: $controlViewModels")
- println("smartspaceMediaData: ${MediaPlayerData.smartspaceMediaData}")
- println("shouldPrioritizeSs: ${MediaPlayerData.shouldPrioritizeSs}")
println("current size: $currentCarouselWidth x $currentCarouselHeight")
println("location: $desiredLocation")
println(
@@ -1328,20 +1269,7 @@ internal object MediaPlayerData {
appUid = -1,
)
- // Whether should prioritize Smartspace card.
- internal var shouldPrioritizeSs: Boolean = false
- private set
-
- internal var smartspaceMediaData: SmartspaceMediaData? = null
- private set
-
- data class MediaSortKey(
- val isSsMediaRec: Boolean, // Whether the item represents a Smartspace media recommendation.
- val data: MediaData,
- val key: String,
- val updateTime: Long = 0,
- val isSsReactivated: Boolean = false,
- )
+ data class MediaSortKey(val data: MediaData, val key: String, val updateTime: Long = 0)
private val comparator =
compareByDescending<MediaSortKey> {
@@ -1352,7 +1280,6 @@ internal object MediaPlayerData {
it.data.playbackLocation == MediaData.PLAYBACK_CAST_LOCAL
}
.thenByDescending { it.data.active }
- .thenByDescending { shouldPrioritizeSs == it.isSsMediaRec }
.thenByDescending { !it.data.resumption }
.thenByDescending { it.data.playbackLocation != MediaData.PLAYBACK_CAST_REMOTE }
.thenByDescending { it.data.lastActive }
@@ -1373,7 +1300,6 @@ internal object MediaPlayerData {
data: MediaData,
player: MediaControlPanel,
clock: SystemClock,
- isSsReactivated: Boolean,
debugLogger: MediaCarouselControllerLogger? = null,
) {
val removedPlayer = removeMediaPlayer(key)
@@ -1381,48 +1307,12 @@ internal object MediaPlayerData {
debugLogger?.logPotentialMemoryLeak(key)
removedPlayer.onDestroy()
}
- val sortKey =
- MediaSortKey(
- isSsMediaRec = false,
- data,
- key,
- clock.currentTimeMillis(),
- isSsReactivated = isSsReactivated,
- )
+ val sortKey = MediaSortKey(data, key, clock.currentTimeMillis())
mediaData.put(key, sortKey)
mediaPlayers.put(sortKey, player)
visibleMediaPlayers.put(key, sortKey)
}
- fun addMediaRecommendation(
- key: String,
- data: SmartspaceMediaData,
- player: MediaControlPanel,
- shouldPrioritize: Boolean,
- clock: SystemClock,
- debugLogger: MediaCarouselControllerLogger? = null,
- update: Boolean = false,
- ) {
- shouldPrioritizeSs = shouldPrioritize
- val removedPlayer = removeMediaPlayer(key)
- if (!update && removedPlayer != null && removedPlayer != player) {
- debugLogger?.logPotentialMemoryLeak(key)
- removedPlayer.onDestroy()
- }
- val sortKey =
- MediaSortKey(
- isSsMediaRec = true,
- EMPTY.copy(active = data.isActive, isPlaying = false),
- key,
- clock.currentTimeMillis(),
- isSsReactivated = true,
- )
- mediaData.put(key, sortKey)
- mediaPlayers.put(sortKey, player)
- visibleMediaPlayers.put(key, sortKey)
- smartspaceMediaData = data
- }
-
fun moveIfExists(
oldKey: String?,
newKey: String,
@@ -1469,17 +1359,13 @@ internal object MediaPlayerData {
*/
fun removeMediaPlayer(key: String, isDismissed: Boolean = false) =
mediaData.remove(key)?.let {
- if (it.isSsMediaRec) {
- smartspaceMediaData = null
- }
if (isDismissed) {
visibleMediaPlayers.remove(key)
}
mediaPlayers.remove(it)
}
- fun mediaData() =
- mediaData.entries.map { e -> Triple(e.key, e.value.data, e.value.isSsMediaRec) }
+ fun mediaData() = mediaData.entries.map { e -> Pair(e.key, e.value.data) }
fun dataKeys() = mediaData.keys
@@ -1491,8 +1377,9 @@ internal object MediaPlayerData {
/** Returns the [MediaData] associated with the first mediaPlayer in the mediaCarousel. */
fun getFirstActiveMediaData(): MediaData? {
+ // TODO simplify ..??
mediaPlayers.entries.forEach { entry ->
- if (!entry.key.isSsMediaRec && entry.key.data.active) {
+ if (entry.key.data.active) {
return entry.key.data
}
}
@@ -1501,24 +1388,15 @@ internal object MediaPlayerData {
/** Returns the index of the first non-timeout media. */
fun firstActiveMediaIndex(): Int {
+ // TODO simplify?
mediaPlayers.entries.forEachIndexed { index, e ->
- if (!e.key.isSsMediaRec && e.key.data.active) {
+ if (e.key.data.active) {
return index
}
}
return -1
}
- /** Returns the existing Smartspace target id. */
- fun smartspaceMediaKey(): String? {
- mediaData.entries.forEach { e ->
- if (e.value.isSsMediaRec) {
- return e.key
- }
- }
- return null
- }
-
@VisibleForTesting
fun clear() {
mediaData.clear()
@@ -1526,19 +1404,6 @@ internal object MediaPlayerData {
visibleMediaPlayers.clear()
}
- /* Returns true if there is active media player card or recommendation card */
- fun hasActiveMediaOrRecommendationCard(): Boolean {
- if (smartspaceMediaData != null && smartspaceMediaData?.isActive!!) {
- return true
- }
- if (firstActiveMediaIndex() != -1) {
- return true
- }
- return false
- }
-
- fun isSsReactivated(key: String): Boolean = mediaData.get(key)?.isSsReactivated ?: false
-
/**
* This method is called when media players are reordered. To make sure we have the new version
* of the order of media players visible to user.
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaControlPanel.java b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaControlPanel.java
index 9cf7356a0ab2..740af02a654c 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaControlPanel.java
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaControlPanel.java
@@ -752,7 +752,7 @@ public class MediaControlPanel {
/* userInitiated */ true)) {
Log.w(TAG, "Manager failed to dismiss media " + mKey);
// Remove directly from carousel so user isn't stuck with defunct controls
- mMediaCarouselController.removePlayer(mKey, false, false, true);
+ mMediaCarouselController.removePlayer(mKey, false, true);
}
} else {
Log.w(TAG, "Dismiss media with null notification. Token uid="
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaCarouselViewModel.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaCarouselViewModel.kt
index 54d3151694bd..572a167838fd 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaCarouselViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaCarouselViewModel.kt
@@ -62,14 +62,9 @@ constructor(
val mediaList = buildList {
sortedItems.forEach { commonModel ->
// When view is started we should make sure to clean models that are pending
- // removal.
- // This action should only be triggered once.
+ // removal. This action should only be triggered once.
if (!allowReorder || !modelsPendingRemoval.contains(commonModel)) {
- when (commonModel) {
- is MediaCommonModel.MediaControl -> add(toViewModel(commonModel))
- is MediaCommonModel.MediaRecommendations ->
- return@forEach // TODO(b/382680767): remove
- }
+ add(toViewModel(commonModel))
}
}
}
@@ -107,7 +102,7 @@ constructor(
interactor.reorderMedia()
}
- private fun toViewModel(commonModel: MediaCommonModel.MediaControl): MediaControlViewModel {
+ private fun toViewModel(commonModel: MediaCommonModel): MediaControlViewModel {
val instanceId = commonModel.mediaLoadedModel.instanceId
return mediaControlByInstanceId[instanceId]?.copy(updateTime = commonModel.updateTime)
?: MediaControlViewModel(
@@ -134,7 +129,7 @@ constructor(
private fun onMediaControlAddedOrUpdated(
controlViewModel: MediaControlViewModel,
- commonModel: MediaCommonModel.MediaControl,
+ commonModel: MediaCommonModel,
) {
if (commonModel.canBeRemoved && !Utils.useMediaResumption(applicationContext)) {
// This media control is due for removal as it is now paused + timed out, and resumption
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/SeekBarViewModel.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/SeekBarViewModel.kt
index 78a8cf8e9432..6175b6e47b52 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/SeekBarViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/SeekBarViewModel.kt
@@ -36,7 +36,6 @@ import androidx.annotation.WorkerThread
import androidx.core.view.GestureDetectorCompat
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
-import com.android.systemui.Flags
import com.android.systemui.classifier.Classifier.MEDIA_SEEKBAR
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.media.NotificationMediaManager
@@ -151,7 +150,6 @@ constructor(
}
override fun onMetadataChanged(metadata: MediaMetadata?) {
- if (!Flags.mediaControlsPostsOptimization()) return
val (enabled, duration) = getEnabledStateAndDuration(metadata)
if (_data.duration != duration) {
_data = _data.copy(enabled = enabled, duration = duration)
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFlags.kt b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFlags.kt
index 172998e09266..8ad10ba2a240 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFlags.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFlags.kt
@@ -46,9 +46,6 @@ class MediaFlags @Inject constructor(private val featureFlags: FeatureFlagsClass
*/
fun isRetainingPlayersEnabled() = featureFlags.isEnabled(FlagsClassic.MEDIA_RETAIN_SESSIONS)
- /** Check whether to get progress information for resume players */
- fun isResumeProgressEnabled() = featureFlags.isEnabled(FlagsClassic.MEDIA_RESUME_PROGRESS)
-
/** Check whether we allow remote media to generate resume controls */
fun isRemoteResumeAllowed() = featureFlags.isEnabled(FlagsClassic.MEDIA_REMOTE_RESUME)
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaItem.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaItem.java
index 7b1c62e2a0e5..78e66235112a 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaItem.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaItem.java
@@ -37,16 +37,21 @@ public class MediaItem {
@MediaItemType
private final int mMediaItemType;
private final boolean mIsFirstDeviceInGroup;
+ private final boolean mIsExpandableDivider;
+ private final boolean mHasTopSeparator;
@Retention(RetentionPolicy.SOURCE)
@IntDef({
MediaItemType.TYPE_DEVICE,
MediaItemType.TYPE_GROUP_DIVIDER,
- MediaItemType.TYPE_PAIR_NEW_DEVICE})
+ MediaItemType.TYPE_PAIR_NEW_DEVICE,
+ MediaItemType.TYPE_DEVICE_GROUP
+ })
public @interface MediaItemType {
int TYPE_DEVICE = 0;
int TYPE_GROUP_DIVIDER = 1;
int TYPE_PAIR_NEW_DEVICE = 2;
+ int TYPE_DEVICE_GROUP = 3;
}
/**
@@ -70,6 +75,18 @@ public class MediaItem {
}
/**
+ * Returns a new {@link MediaItemType#TYPE_DEVICE_GROUP} {@link MediaItem}. This items controls
+ * the volume of the group session.
+ */
+ public static MediaItem createDeviceGroupMediaItem() {
+ return new MediaItem(
+ /* device */ null,
+ /* title */ null,
+ /* type */ MediaItemType.TYPE_DEVICE_GROUP,
+ /* misFirstDeviceInGroup */ false);
+ }
+
+ /**
* Returns a new {@link MediaItemType#TYPE_PAIR_NEW_DEVICE} {@link MediaItem} with both {@link
* #getMediaDevice() media device} and title set to {@code null}.
*/
@@ -93,15 +110,58 @@ public class MediaItem {
/* misFirstDeviceInGroup */ false);
}
+ /**
+ * Returns a new {@link MediaItemType#TYPE_GROUP_DIVIDER} {@link MediaItem} with the specified
+ * title and a {@code null} {@link #getMediaDevice() media device}. This item needs to be
+ * rendered with a separator above it.
+ */
+ public static MediaItem createGroupDividerWithSeparatorMediaItem(@Nullable String title) {
+ return new MediaItem(
+ /* device */ null,
+ title,
+ MediaItemType.TYPE_GROUP_DIVIDER,
+ /* isFirstDeviceInGroup */ false,
+ /* isExpandableDivider */ false,
+ /* hasTopSeparator */ true);
+ }
+
+ /**
+ * Returns a new {@link MediaItemType#TYPE_GROUP_DIVIDER} {@link MediaItem} with the specified
+ * title and a {@code null} {@link #getMediaDevice() media device}. The item serves as a toggle
+ * for expanding/collapsing the group of devices.
+ */
+ public static MediaItem createExpandableGroupDividerMediaItem(@Nullable String title) {
+ return new MediaItem(
+ /* device */ null,
+ title,
+ MediaItemType.TYPE_GROUP_DIVIDER,
+ /* isFirstDeviceInGroup */ false,
+ /* isExpandableDivider */ true,
+ /* hasTopSeparator */ false);
+ }
+
private MediaItem(
@Nullable MediaDevice device,
@Nullable String title,
@MediaItemType int type,
boolean isFirstDeviceInGroup) {
+ this(device, title, type, isFirstDeviceInGroup, /* isExpandableDivider */
+ false, /* hasTopSeparator */ false);
+ }
+
+ private MediaItem(
+ @Nullable MediaDevice device,
+ @Nullable String title,
+ @MediaItemType int type,
+ boolean isFirstDeviceInGroup,
+ boolean isExpandableDivider,
+ boolean hasTopSeparator) {
this.mMediaDeviceOptional = Optional.ofNullable(device);
this.mTitle = title;
this.mMediaItemType = type;
this.mIsFirstDeviceInGroup = isFirstDeviceInGroup;
+ this.mIsExpandableDivider = isExpandableDivider;
+ this.mHasTopSeparator = hasTopSeparator;
}
public Optional<MediaDevice> getMediaDevice() {
@@ -133,4 +193,14 @@ public class MediaItem {
public boolean isFirstDeviceInGroup() {
return mIsFirstDeviceInGroup;
}
+
+ /** Returns whether a group divider has a button that expands group device list */
+ public boolean isExpandableDivider() {
+ return mIsExpandableDivider;
+ }
+
+ /** Returns whether a group divider has a border at the top */
+ public boolean hasTopSeparator() {
+ return mHasTopSeparator;
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.kt b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.kt
new file mode 100644
index 000000000000..4c34250c9653
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.kt
@@ -0,0 +1,688 @@
+/*
+ * 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.dialog
+
+import android.content.Context
+import android.content.res.ColorStateList
+import android.graphics.Color
+import android.graphics.Typeface
+import android.graphics.drawable.AnimatedVectorDrawable
+import android.graphics.drawable.Drawable
+import android.util.Log
+import android.view.LayoutInflater
+import android.view.View
+import android.view.View.GONE
+import android.view.View.VISIBLE
+import android.view.ViewGroup
+import android.widget.ImageButton
+import android.widget.ImageView
+import android.widget.LinearLayout
+import android.widget.ProgressBar
+import android.widget.TextView
+import androidx.annotation.VisibleForTesting
+import androidx.appcompat.content.res.AppCompatResources
+import androidx.recyclerview.widget.RecyclerView
+import com.android.settingslib.media.InputMediaDevice
+import com.android.settingslib.media.MediaDevice
+import com.android.systemui.FontStyles.GSF_TITLE_MEDIUM_EMPHASIZED
+import com.android.systemui.FontStyles.GSF_TITLE_SMALL
+import com.android.systemui.media.dialog.MediaItem.MediaItemType.TYPE_DEVICE
+import com.android.systemui.media.dialog.MediaItem.MediaItemType.TYPE_DEVICE_GROUP
+import com.android.systemui.media.dialog.MediaItem.MediaItemType.TYPE_GROUP_DIVIDER
+import com.android.systemui.media.dialog.MediaOutputAdapterBase.ConnectionState.CONNECTED
+import com.android.systemui.media.dialog.MediaOutputAdapterBase.ConnectionState.CONNECTING
+import com.android.systemui.media.dialog.MediaOutputAdapterBase.ConnectionState.DISCONNECTED
+import com.android.systemui.res.R
+import com.android.systemui.util.kotlin.getOrNull
+import com.google.android.material.slider.Slider
+
+/** A RecyclerView adapter for the legacy UI media output dialog device list. */
+class MediaOutputAdapter(controller: MediaSwitchingController) :
+ MediaOutputAdapterBase(controller) {
+ private val mGroupSelectedItems = mController.selectedMediaDevice.size > 1
+
+ /** Refreshes the RecyclerView dataset and forces re-render. */
+ override fun updateItems() {
+ val newList =
+ mController.getMediaItemList(false /* addConnectNewDeviceButton */).toMutableList()
+
+ addSeparatorForTheFirstGroupDivider(newList)
+ coalesceSelectedDevices(newList)
+
+ mMediaItemList.clear()
+ mMediaItemList.addAll(newList)
+
+ notifyDataSetChanged()
+ }
+
+ private fun addSeparatorForTheFirstGroupDivider(newList: MutableList<MediaItem>) {
+ for ((i, item) in newList.withIndex()) {
+ if (item.mediaItemType == TYPE_GROUP_DIVIDER) {
+ newList[i] = MediaItem.createGroupDividerWithSeparatorMediaItem(item.title)
+ break
+ }
+ }
+ }
+
+ /**
+ * If there are 2+ selected devices, adds an "Connected speakers" expandable group divider and
+ * displays a single session control instead of individual device controls.
+ */
+ private fun coalesceSelectedDevices(newList: MutableList<MediaItem>) {
+ val selectedDevices = newList.filter { this.isSelectedDevice(it) }
+
+ if (mGroupSelectedItems && selectedDevices.size > 1) {
+ newList.removeAll(selectedDevices.toSet())
+ if (mController.isGroupListCollapsed) {
+ newList.add(0, MediaItem.createDeviceGroupMediaItem())
+ } else {
+ newList.addAll(0, selectedDevices)
+ }
+ newList.add(0, mController.connectedSpeakersExpandableGroupDivider)
+ }
+ }
+
+ private fun isSelectedDevice(mediaItem: MediaItem): Boolean {
+ return mediaItem.mediaDevice.getOrNull()?.let { device ->
+ isDeviceIncluded(mController.selectedMediaDevice, device)
+ } ?: false
+ }
+
+ override fun getItemId(position: Int): Long {
+ if (position >= mMediaItemList.size) {
+ Log.e(TAG, "Item position exceeds list size: $position")
+ return RecyclerView.NO_ID
+ }
+ val currentMediaItem = mMediaItemList[position]
+ return when (currentMediaItem.mediaItemType) {
+ TYPE_DEVICE ->
+ currentMediaItem.mediaDevice.getOrNull()?.id?.hashCode()?.toLong()
+ ?: RecyclerView.NO_ID
+ TYPE_GROUP_DIVIDER -> currentMediaItem.title.hashCode().toLong()
+ TYPE_DEVICE_GROUP -> currentMediaItem.hashCode().toLong()
+ else -> RecyclerView.NO_ID
+ }
+ }
+
+ override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
+ val context = viewGroup.context
+ return when (viewType) {
+ TYPE_GROUP_DIVIDER -> {
+ val holderView =
+ LayoutInflater.from(context)
+ .inflate(R.layout.media_output_list_item_group_divider, viewGroup, false)
+ MediaGroupDividerViewHolder(holderView, context)
+ }
+
+ TYPE_DEVICE,
+ TYPE_DEVICE_GROUP -> {
+ val holderView =
+ LayoutInflater.from(context)
+ .inflate(R.layout.media_output_list_item_device, viewGroup, false)
+ MediaDeviceViewHolder(holderView, context)
+ }
+
+ else -> throw IllegalArgumentException("Invalid view type: $viewType")
+ }
+ }
+
+ override fun onBindViewHolder(viewHolder: RecyclerView.ViewHolder, position: Int) {
+ require(position < itemCount) { "Invalid position: $position, list size: $itemCount" }
+ val currentMediaItem = mMediaItemList[position]
+ when (currentMediaItem.mediaItemType) {
+ TYPE_GROUP_DIVIDER ->
+ (viewHolder as MediaGroupDividerViewHolder).onBind(
+ groupDividerTitle = currentMediaItem.title,
+ isExpandableDivider = currentMediaItem.isExpandableDivider,
+ hasTopSeparator = currentMediaItem.hasTopSeparator(),
+ )
+
+ TYPE_DEVICE ->
+ (viewHolder as MediaDeviceViewHolder).onBindDevice(
+ mediaItem = currentMediaItem,
+ position = position,
+ )
+
+ TYPE_DEVICE_GROUP -> (viewHolder as MediaDeviceViewHolder).onBindDeviceGroup()
+ else ->
+ throw IllegalArgumentException(
+ "Invalid item type ${currentMediaItem.mediaItemType} for position: $position"
+ )
+ }
+ }
+
+ val controller: MediaSwitchingController
+ get() = mController
+
+ /** ViewHolder for binding device view. */
+ inner class MediaDeviceViewHolder(view: View, context: Context?) :
+ MediaDeviceViewHolderBase(view, context) {
+ @VisibleForTesting val mMainContent: LinearLayout = view.requireViewById(R.id.main_content)
+
+ @VisibleForTesting val mItemLayout: LinearLayout = view.requireViewById(R.id.item_layout)
+
+ @VisibleForTesting val mTitleText: TextView = view.requireViewById(R.id.title)
+
+ @VisibleForTesting val mSubTitleText: TextView = view.requireViewById(R.id.subtitle)
+
+ @VisibleForTesting val mTitleIcon: ImageView = view.requireViewById(R.id.title_icon)
+
+ @VisibleForTesting
+ val mLoadingIndicator: ProgressBar = view.requireViewById(R.id.loading_indicator)
+
+ @VisibleForTesting val mStatusIcon: ImageView = view.requireViewById(R.id.status_icon)
+
+ @VisibleForTesting val mGroupButton: ImageButton = view.requireViewById(R.id.group_button)
+
+ @VisibleForTesting val mDivider: View = view.requireViewById(R.id.divider)
+
+ @VisibleForTesting
+ val mOngoingSessionButton: ImageButton = view.requireViewById(R.id.ongoing_session_button)
+
+ @VisibleForTesting var mSlider: Slider = view.requireViewById(R.id.volume_seekbar)
+ private var mLatestUpdateVolume = NO_VOLUME_SET
+
+ private val mInactivePadding =
+ mContext.resources.getDimension(R.dimen.media_output_item_content_vertical_margin)
+ private val mActivePadding =
+ mContext.resources.getDimension(
+ R.dimen.media_output_item_content_vertical_margin_active
+ )
+ private val mSubtitleAlpha =
+ mContext.resources.getFloat(R.dimen.media_output_item_subtitle_alpha)
+
+ fun onBindDevice(mediaItem: MediaItem, position: Int) {
+ resetViewState()
+ renderItem(mediaItem, position)
+ }
+
+ fun onBindDeviceGroup() {
+ resetViewState()
+ renderDeviceGroupItem()
+ }
+
+ private fun resetViewState() {
+ mItemLayout.visibility = VISIBLE
+ mGroupButton.visibility = GONE
+ mOngoingSessionButton.visibility = GONE
+ mStatusIcon.visibility = GONE
+ mLoadingIndicator.visibility = GONE
+ mDivider.visibility = GONE
+ mSubTitleText.visibility = GONE
+ mMainContent.setOnClickListener(null)
+ }
+
+ override fun renderDeviceItem(
+ hideGroupItem: Boolean,
+ device: MediaDevice,
+ connectionState: ConnectionState,
+ restrictVolumeAdjustment: Boolean,
+ groupStatus: GroupStatus?,
+ ongoingSessionStatus: OngoingSessionStatus?,
+ clickListener: View.OnClickListener?,
+ deviceDisabled: Boolean,
+ subtitle: String?,
+ deviceStatusIcon: Drawable?,
+ ) {
+ val fixedVolumeConnected = connectionState == CONNECTED && restrictVolumeAdjustment
+ val colorTheme = ColorTheme(fixedVolumeConnected, deviceDisabled)
+
+ updateTitle(device.name, connectionState, colorTheme)
+ updateTitleIcon(device, connectionState, restrictVolumeAdjustment, colorTheme)
+ updateSubtitle(subtitle, colorTheme)
+ updateSeekBar(device, connectionState, restrictVolumeAdjustment, colorTheme)
+ updateEndArea(device, connectionState, groupStatus, ongoingSessionStatus, colorTheme)
+ updateLoadingIndicator(connectionState, colorTheme)
+ updateDeviceStatusIcon(deviceStatusIcon, colorTheme)
+ updateContentBackground(fixedVolumeConnected, colorTheme)
+ updateContentClickListener(clickListener)
+ }
+
+ override fun renderDeviceGroupItem() {
+ mTitleIcon.visibility = GONE
+ val colorTheme = ColorTheme()
+ updateTitle(
+ title = mController.sessionName ?: "",
+ connectionState = CONNECTED,
+ colorTheme = colorTheme,
+ )
+ updateGroupSeekBar(colorTheme)
+ }
+
+ private fun updateTitle(
+ title: CharSequence,
+ connectionState: ConnectionState,
+ colorTheme: ColorTheme,
+ ) {
+ mTitleText.text = title
+ val fontFamilyName: String =
+ if (connectionState == CONNECTED) GSF_TITLE_MEDIUM_EMPHASIZED else GSF_TITLE_SMALL
+ mTitleText.typeface = Typeface.create(fontFamilyName, Typeface.NORMAL)
+ mTitleText.setTextColor(colorTheme.titleColor)
+ mTitleText.alpha = colorTheme.contentAlpha
+ }
+
+ private fun updateContentBackground(fixedVolumeConnected: Boolean, colorTheme: ColorTheme) {
+ if (fixedVolumeConnected) {
+ mMainContent.backgroundTintList =
+ ColorStateList.valueOf(colorTheme.containerRestrictedVolumeBackground)
+ mMainContent.background =
+ AppCompatResources.getDrawable(
+ mContext,
+ R.drawable.media_output_dialog_item_fixed_volume_background,
+ )
+ } else {
+ mMainContent.background = null
+ mMainContent.setBackgroundColor(Color.TRANSPARENT)
+ }
+ }
+
+ private fun updateContentPadding(verticalPadding: Float) {
+ mMainContent.setPadding(0, verticalPadding.toInt(), 0, verticalPadding.toInt())
+ }
+
+ private fun updateLayoutForSlider(showSlider: Boolean) {
+ updateContentPadding(if (showSlider) mActivePadding else mInactivePadding)
+ mSlider.visibility = if (showSlider) VISIBLE else GONE
+ mSlider.alpha = if (showSlider) 1f else 0f
+ }
+
+ private fun updateSeekBar(
+ device: MediaDevice,
+ connectionState: ConnectionState,
+ restrictVolumeAdjustment: Boolean,
+ colorTheme: ColorTheme,
+ ) {
+ val showSlider = connectionState == CONNECTED && !restrictVolumeAdjustment
+ if (showSlider) {
+ updateLayoutForSlider(showSlider = true)
+ initSeekbar(
+ volumeChangeCallback = { volume: Int ->
+ mController.adjustVolume(device, volume)
+ },
+ settleCallback = { mController.logInteractionAdjustVolume(device) },
+ deviceDrawable = mController.getDeviceIconDrawable(device),
+ isInputDevice = device is InputMediaDevice,
+ isVolumeControlAllowed = mController.isVolumeControlEnabled(device),
+ currentVolume = device.currentVolume,
+ maxVolume = device.maxVolume,
+ colorTheme = colorTheme,
+ )
+ } else {
+ updateLayoutForSlider(showSlider = false)
+ }
+ }
+
+ private fun updateGroupSeekBar(colorTheme: ColorTheme) {
+ mSlider.visibility = VISIBLE
+ updateContentPadding(mActivePadding)
+ val groupDrawable =
+ AppCompatResources.getDrawable(
+ mContext,
+ com.android.settingslib.R.drawable.ic_media_group_device,
+ )
+ initSeekbar(
+ volumeChangeCallback = { volume: Int -> mController.adjustSessionVolume(volume) },
+ deviceDrawable = groupDrawable,
+ isVolumeControlAllowed = mController.isVolumeControlEnabledForSession,
+ currentVolume = mController.sessionVolume,
+ maxVolume = mController.sessionVolumeMax,
+ colorTheme = colorTheme,
+ )
+ }
+
+ private fun updateSubtitle(subtitle: String?, colorTheme: ColorTheme) {
+ if (subtitle.isNullOrEmpty()) {
+ mSubTitleText.visibility = GONE
+ } else {
+ mSubTitleText.text = subtitle
+ mSubTitleText.setTextColor(colorTheme.subtitleColor)
+ mSubTitleText.alpha = mSubtitleAlpha * colorTheme.contentAlpha
+ mSubTitleText.visibility = VISIBLE
+ }
+ }
+
+ private fun updateLoadingIndicator(
+ connectionState: ConnectionState,
+ colorTheme: ColorTheme,
+ ) {
+ if (connectionState == CONNECTING) {
+ mLoadingIndicator.visibility = VISIBLE
+ mLoadingIndicator.indeterminateDrawable.setTintList(
+ ColorStateList.valueOf(colorTheme.statusIconColor)
+ )
+ } else {
+ mLoadingIndicator.visibility = GONE
+ }
+ }
+
+ private fun initializeSeekbarVolume(currentVolume: Int) {
+ tryResolveVolumeUserRequest(currentVolume)
+ if (!isDragging && hasNoPendingVolumeRequests()) {
+ mSlider.value = currentVolume.toFloat()
+ }
+ }
+
+ private fun tryResolveVolumeUserRequest(currentVolume: Int) {
+ if (currentVolume == mLatestUpdateVolume) {
+ mLatestUpdateVolume = NO_VOLUME_SET
+ }
+ }
+
+ private fun hasNoPendingVolumeRequests(): Boolean {
+ return mLatestUpdateVolume == NO_VOLUME_SET
+ }
+
+ private fun setLatestVolumeRequest(volume: Int) {
+ mLatestUpdateVolume = volume
+ }
+
+ private fun initSeekbar(
+ volumeChangeCallback: (Int) -> Unit,
+ settleCallback: () -> Unit = {},
+ deviceDrawable: Drawable?,
+ isInputDevice: Boolean = false,
+ isVolumeControlAllowed: Boolean,
+ currentVolume: Int,
+ maxVolume: Int,
+ colorTheme: ColorTheme,
+ ) {
+ if (maxVolume == 0) {
+ Log.e(TAG, "Invalid maxVolume value")
+ // Slider doesn't allow valueFrom == valueTo, return to prevent crash.
+ return
+ }
+
+ mSlider.isEnabled = isVolumeControlAllowed
+ mSlider.valueFrom = 0f
+ mSlider.valueTo = maxVolume.toFloat()
+ mSlider.stepSize = 1f
+ mSlider.thumbTintList = ColorStateList.valueOf(colorTheme.sliderActiveColor)
+ mSlider.trackActiveTintList = ColorStateList.valueOf(colorTheme.sliderActiveColor)
+ mSlider.trackInactiveTintList = ColorStateList.valueOf(colorTheme.sliderInactiveColor)
+ mSlider.trackIconActiveColor = ColorStateList.valueOf(colorTheme.sliderActiveIconColor)
+ mSlider.trackIconInactiveColor =
+ ColorStateList.valueOf(colorTheme.sliderInactiveIconColor)
+ val muteDrawable = getMuteDrawable(isInputDevice)
+ updateSliderIconsVisibility(
+ deviceDrawable = deviceDrawable,
+ muteDrawable = muteDrawable,
+ isMuted = currentVolume == 0,
+ )
+ initializeSeekbarVolume(currentVolume)
+
+ mSlider.clearOnChangeListeners() // Prevent adding multiple listeners
+ mSlider.addOnChangeListener { _: Slider, value: Float, fromUser: Boolean ->
+ if (fromUser) {
+ val seekBarVolume = value.toInt()
+ updateSliderIconsVisibility(
+ deviceDrawable = deviceDrawable,
+ muteDrawable = muteDrawable,
+ isMuted = seekBarVolume == 0,
+ )
+ if (seekBarVolume != currentVolume) {
+ setLatestVolumeRequest(seekBarVolume)
+ volumeChangeCallback(seekBarVolume)
+ }
+ }
+ }
+
+ mSlider.clearOnSliderTouchListeners() // Prevent adding multiple listeners
+ mSlider.addOnSliderTouchListener(
+ object : Slider.OnSliderTouchListener {
+ override fun onStartTrackingTouch(slider: Slider) {
+ setIsDragging(true)
+ }
+
+ override fun onStopTrackingTouch(slider: Slider) {
+ setIsDragging(false)
+ settleCallback()
+ }
+ }
+ )
+ }
+
+ private fun getMuteDrawable(isInputDevice: Boolean): Drawable? {
+ return AppCompatResources.getDrawable(
+ mContext,
+ if (isInputDevice) R.drawable.ic_mic_off
+ else R.drawable.media_output_icon_volume_off,
+ )
+ }
+
+ private fun updateSliderIconsVisibility(
+ deviceDrawable: Drawable?,
+ muteDrawable: Drawable?,
+ isMuted: Boolean,
+ ) {
+ mSlider.trackIconInactiveStart = if (isMuted) muteDrawable else null
+ // A workaround for the slider glitch that sometimes shows the active icon in inactive
+ // state.
+ mSlider.trackIconActiveStart = if (isMuted) null else deviceDrawable
+ }
+
+ private fun updateTitleIcon(
+ device: MediaDevice,
+ connectionState: ConnectionState,
+ restrictVolumeAdjustment: Boolean,
+ colorTheme: ColorTheme,
+ ) {
+ if (connectionState == CONNECTED && !restrictVolumeAdjustment) {
+ mTitleIcon.visibility = GONE
+ } else {
+ mTitleIcon.imageTintList = ColorStateList.valueOf(colorTheme.iconColor)
+ val drawable = mController.getDeviceIconDrawable(device)
+ mTitleIcon.setImageDrawable(drawable)
+ mTitleIcon.visibility = VISIBLE
+ mTitleIcon.alpha = colorTheme.contentAlpha
+ }
+ }
+
+ private fun updateDeviceStatusIcon(deviceStatusIcon: Drawable?, colorTheme: ColorTheme) {
+ if (deviceStatusIcon == null) {
+ mStatusIcon.visibility = GONE
+ } else {
+ mStatusIcon.setImageDrawable(deviceStatusIcon)
+ mStatusIcon.alpha = colorTheme.contentAlpha
+ mStatusIcon.imageTintList = ColorStateList.valueOf(colorTheme.statusIconColor)
+ mStatusIcon.visibility = VISIBLE
+ }
+ }
+
+ private fun updateEndArea(
+ device: MediaDevice,
+ connectionState: ConnectionState,
+ groupStatus: GroupStatus?,
+ ongoingSessionStatus: OngoingSessionStatus?,
+ colorTheme: ColorTheme,
+ ) {
+ var showDivider = false
+
+ if (ongoingSessionStatus != null) {
+ showDivider = true
+ mOngoingSessionButton.visibility = VISIBLE
+ updateOngoingSessionButton(device, ongoingSessionStatus.host, colorTheme)
+ }
+
+ if (groupStatus != null && shouldShowGroupCheckbox(groupStatus)) {
+ showDivider = true
+ mGroupButton.visibility = VISIBLE
+ updateGroupButton(device, groupStatus, colorTheme)
+ }
+
+ mDivider.visibility =
+ if (showDivider && connectionState == DISCONNECTED) VISIBLE else GONE
+ mDivider.setBackgroundColor(mController.colorScheme.getOutline())
+ }
+
+ private fun shouldShowGroupCheckbox(groupStatus: GroupStatus): Boolean {
+ val disabled = groupStatus.selected && !groupStatus.deselectable
+ return !disabled
+ }
+
+ private fun updateOngoingSessionButton(
+ device: MediaDevice,
+ isHost: Boolean,
+ colorTheme: ColorTheme,
+ ) {
+ val iconDrawableId =
+ if (isHost) R.drawable.media_output_status_edit_session
+ else R.drawable.ic_sound_bars_anim
+ mOngoingSessionButton.setOnClickListener { v: View? ->
+ mController.tryToLaunchInAppRoutingIntent(device.id, v)
+ }
+ val drawable = AppCompatResources.getDrawable(mContext, iconDrawableId)
+ mOngoingSessionButton.setImageDrawable(drawable)
+ mOngoingSessionButton.imageTintList = ColorStateList.valueOf(colorTheme.iconColor)
+ if (drawable is AnimatedVectorDrawable) {
+ drawable.start()
+ }
+ }
+
+ private fun updateGroupButton(
+ device: MediaDevice,
+ groupStatus: GroupStatus,
+ colorTheme: ColorTheme,
+ ) {
+ mGroupButton.contentDescription =
+ mContext.getString(
+ if (groupStatus.selected) R.string.accessibility_remove_device_from_group
+ else R.string.accessibility_add_device_to_group
+ )
+ mGroupButton.setImageResource(
+ if (groupStatus.selected) R.drawable.ic_check_circle_filled
+ else R.drawable.ic_add_circle_rounded
+ )
+ mGroupButton.setOnClickListener {
+ onGroupActionTriggered(!groupStatus.selected, device)
+ }
+ mGroupButton.imageTintList = ColorStateList.valueOf(colorTheme.iconColor)
+ }
+
+ private fun updateContentClickListener(listener: View.OnClickListener?) {
+ mMainContent.setOnClickListener(listener)
+ if (listener == null) {
+ mMainContent.isClickable = false // clickable is not removed automatically.
+ }
+ }
+
+ override fun disableSeekBar() {
+ mSlider.isEnabled = false
+ }
+ }
+
+ inner class MediaGroupDividerViewHolder(itemView: View, val mContext: Context) :
+ RecyclerView.ViewHolder(itemView) {
+ private val mTopSeparator: View = itemView.requireViewById(R.id.top_separator)
+ private val mTitleText: TextView = itemView.requireViewById(R.id.title)
+ @VisibleForTesting
+ val mExpandButton: ViewGroup = itemView.requireViewById(R.id.expand_button)
+ private val mExpandButtonIcon: ImageView = itemView.requireViewById(R.id.expand_button_icon)
+
+ fun onBind(
+ groupDividerTitle: String?,
+ isExpandableDivider: Boolean,
+ hasTopSeparator: Boolean,
+ ) {
+ mTitleText.text = groupDividerTitle
+ mTitleText.setTextColor(mController.colorScheme.getPrimary())
+ if (hasTopSeparator) {
+ mTopSeparator.visibility = VISIBLE
+ mTopSeparator.setBackgroundColor(mController.colorScheme.getOutlineVariant())
+ } else {
+ mTopSeparator.visibility = GONE
+ }
+ updateExpandButton(isExpandableDivider)
+ }
+
+ private fun updateExpandButton(isExpandableDivider: Boolean) {
+ if (!isExpandableDivider) {
+ mExpandButton.visibility = GONE
+ return
+ }
+ val isCollapsed = mController.isGroupListCollapsed
+ mExpandButtonIcon.setImageDrawable(
+ AppCompatResources.getDrawable(
+ mContext,
+ if (isCollapsed) R.drawable.ic_expand_more_rounded
+ else R.drawable.ic_expand_less_rounded,
+ )
+ )
+ mExpandButtonIcon.contentDescription =
+ mContext.getString(
+ if (isCollapsed) R.string.accessibility_expand_group
+ else R.string.accessibility_collapse_group
+ )
+ mExpandButton.visibility = VISIBLE
+ mExpandButton.setOnClickListener { toggleGroupList() }
+ mExpandButtonIcon.backgroundTintList =
+ ColorStateList.valueOf(mController.colorScheme.getOnSurface())
+ .withAlpha((255 * 0.1).toInt())
+ mExpandButtonIcon.imageTintList =
+ ColorStateList.valueOf(mController.colorScheme.getOnSurface())
+ }
+
+ private fun toggleGroupList() {
+ mController.isGroupListCollapsed = !mController.isGroupListCollapsed
+ updateItems()
+ }
+ }
+
+ private inner class ColorTheme(
+ isConnectedWithFixedVolume: Boolean = false,
+ deviceDisabled: Boolean = false,
+ ) {
+ private val colorScheme: MediaOutputColorScheme = mController.colorScheme
+
+ val titleColor =
+ if (isConnectedWithFixedVolume) {
+ colorScheme.getOnPrimary()
+ } else {
+ colorScheme.getOnSurface()
+ }
+ val subtitleColor =
+ if (isConnectedWithFixedVolume) {
+ colorScheme.getOnPrimary()
+ } else {
+ colorScheme.getOnSurfaceVariant()
+ }
+ val iconColor =
+ if (isConnectedWithFixedVolume) {
+ colorScheme.getOnPrimary()
+ } else {
+ colorScheme.getOnSurface()
+ }
+ val statusIconColor =
+ if (isConnectedWithFixedVolume) {
+ colorScheme.getOnPrimary()
+ } else {
+ colorScheme.getOnSurfaceVariant()
+ }
+ val sliderActiveColor = colorScheme.getPrimary()
+ val sliderActiveIconColor = colorScheme.getOnPrimary()
+ val sliderInactiveColor = colorScheme.getSecondaryContainer()
+ val sliderInactiveIconColor = colorScheme.getOnSurface()
+ val containerRestrictedVolumeBackground = colorScheme.getPrimary()
+ val contentAlpha = if (deviceDisabled) DEVICE_DISABLED_ALPHA else DEVICE_ACTIVE_ALPHA
+ }
+
+ companion object {
+ private const val TAG = "MediaOutputAdapter"
+ private const val DEVICE_DISABLED_ALPHA = 0.5f
+ private const val DEVICE_ACTIVE_ALPHA = 1f
+ private const val NO_VOLUME_SET = -1
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapterBase.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapterBase.java
index e3990d25f94e..d46cca2736da 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapterBase.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapterBase.java
@@ -19,6 +19,7 @@ package com.android.systemui.media.dialog;
import static com.android.settingslib.media.MediaDevice.SelectionBehavior.SELECTION_BEHAVIOR_GO_TO_APP;
import static com.android.settingslib.media.MediaDevice.SelectionBehavior.SELECTION_BEHAVIOR_NONE;
import static com.android.settingslib.media.MediaDevice.SelectionBehavior.SELECTION_BEHAVIOR_TRANSFER;
+import static com.android.media.flags.Flags.enableOutputSwitcherRedesign;
import android.content.Context;
import android.graphics.drawable.Drawable;
@@ -46,11 +47,11 @@ import java.util.concurrent.CopyOnWriteArrayList;
* manipulate the layout directly.
*/
public abstract class MediaOutputAdapterBase extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
- record OngoingSessionStatus(boolean host) {}
+ public record OngoingSessionStatus(boolean host) {}
- record GroupStatus(Boolean selected, Boolean deselectable) {}
+ public record GroupStatus(Boolean selected, Boolean deselectable) {}
- enum ConnectionState {
+ public enum ConnectionState {
CONNECTED,
CONNECTING,
DISCONNECTED,
@@ -138,7 +139,7 @@ public abstract class MediaOutputAdapterBase extends RecyclerView.Adapter<Recycl
return mMediaItemList.size();
}
- abstract class MediaDeviceViewHolderBase extends RecyclerView.ViewHolder {
+ public abstract class MediaDeviceViewHolderBase extends RecyclerView.ViewHolder {
Context mContext;
@@ -211,7 +212,8 @@ public abstract class MediaOutputAdapterBase extends RecyclerView.Adapter<Recycl
clickListener = v -> cancelMuteAwaitConnection();
} else if (device.getState() == MediaDeviceState.STATE_GROUPING) {
connectionState = ConnectionState.CONNECTING;
- } else if (mShouldGroupSelectedMediaItems && hasMultipleSelectedDevices()
+ } else if (!enableOutputSwitcherRedesign() && mShouldGroupSelectedMediaItems
+ && hasMultipleSelectedDevices()
&& isSelected) {
if (mediaItem.isFirstDeviceInGroup()) {
isDeviceGroup = true;
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java
index 49d09cf64c8e..7f9370ca671d 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java
@@ -19,16 +19,23 @@ package com.android.systemui.media.dialog;
import static android.view.WindowInsets.Type.navigationBars;
import static android.view.WindowInsets.Type.statusBars;
+import static com.android.media.flags.Flags.enableOutputSwitcherRedesign;
+import static com.android.systemui.FontStyles.GSF_LABEL_LARGE;
+import static com.android.systemui.FontStyles.GSF_TITLE_MEDIUM_EMPHASIZED;
+import static com.android.systemui.FontStyles.GSF_TITLE_SMALL;
+
import android.annotation.NonNull;
import android.app.WallpaperColors;
import android.bluetooth.BluetoothLeBroadcast;
import android.bluetooth.BluetoothLeBroadcastMetadata;
import android.content.Context;
import android.content.SharedPreferences;
+import android.content.res.ColorStateList;
import android.content.res.Configuration;
import android.graphics.ColorFilter;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffColorFilter;
+import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.Icon;
import android.os.Bundle;
@@ -49,6 +56,7 @@ import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.VisibleForTesting;
+import androidx.appcompat.content.res.AppCompatResources;
import androidx.core.graphics.drawable.IconCompat;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
@@ -57,6 +65,8 @@ import com.android.systemui.broadcast.BroadcastSender;
import com.android.systemui.res.R;
import com.android.systemui.statusbar.phone.SystemUIDialog;
+import com.google.android.material.button.MaterialButton;
+
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
@@ -71,7 +81,7 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog
private static final int HANDLE_BROADCAST_FAILED_DELAY = 3000;
protected final Handler mMainThreadHandler = new Handler(Looper.getMainLooper());
- private final RecyclerView.LayoutManager mLayoutManager;
+ private final LinearLayoutManager mLayoutManager;
final Context mContext;
final MediaSwitchingController mMediaSwitchingController;
@@ -93,8 +103,12 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog
private ImageView mBroadcastIcon;
private RecyclerView mDevicesRecyclerView;
private ViewGroup mDeviceListLayout;
+ private ViewGroup mQuickAccessShelf;
+ private MaterialButton mConnectDeviceButton;
private LinearLayout mMediaMetadataSectionLayout;
private Button mDoneButton;
+ private ViewGroup mDialogFooter;
+ private View mFooterSpacer;
private Button mStopButton;
private WallpaperColors mWallpaperColors;
private boolean mShouldLaunchLeBroadcastDialog;
@@ -229,7 +243,11 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog
mHeaderTitle = mDialogView.requireViewById(R.id.header_title);
mHeaderSubtitle = mDialogView.requireViewById(R.id.header_subtitle);
mHeaderIcon = mDialogView.requireViewById(R.id.header_icon);
+ mQuickAccessShelf = mDialogView.requireViewById(R.id.quick_access_shelf);
+ mConnectDeviceButton = mDialogView.requireViewById(R.id.connect_device);
mDevicesRecyclerView = mDialogView.requireViewById(R.id.list_result);
+ mDialogFooter = mDialogView.requireViewById(R.id.dialog_footer);
+ mFooterSpacer = mDialogView.requireViewById(R.id.footer_spacer);
mMediaMetadataSectionLayout = mDialogView.requireViewById(R.id.media_metadata_section);
mDeviceListLayout = mDialogView.requireViewById(R.id.device_list);
mDoneButton = mDialogView.requireViewById(R.id.done);
@@ -252,6 +270,49 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog
}
mDismissing = false;
+
+ if (enableOutputSwitcherRedesign()) {
+ // Reduce radius of dialog background.
+ mDialogView.setBackground(AppCompatResources.getDrawable(mContext,
+ R.drawable.media_output_dialog_background_reduced_radius));
+ // Set non-transparent footer background to change it color on scroll.
+ mDialogFooter.setBackground(AppCompatResources.getDrawable(mContext,
+ R.drawable.media_output_dialog_footer_background));
+ // Right-align the footer buttons.
+ LinearLayout.LayoutParams layoutParams =
+ (LinearLayout.LayoutParams) mFooterSpacer.getLayoutParams();
+ layoutParams.width = (int) mContext.getResources().getDimension(
+ R.dimen.media_output_dialog_button_gap);
+ mFooterSpacer.setLayoutParams(layoutParams);
+ layoutParams.weight = 0;
+ // Update font family to Google Sans Flex.
+ Typeface buttonTypeface = Typeface.create(GSF_LABEL_LARGE, Typeface.NORMAL);
+ mDoneButton.setTypeface(buttonTypeface);
+ mStopButton.setTypeface(buttonTypeface);
+ mHeaderTitle
+ .setTypeface(Typeface.create(GSF_TITLE_MEDIUM_EMPHASIZED, Typeface.NORMAL));
+ mHeaderSubtitle
+ .setTypeface(Typeface.create(GSF_TITLE_SMALL, Typeface.NORMAL));
+ // Reduce the size of the app icon.
+ float appIconSize = mContext.getResources().getDimension(
+ R.dimen.media_output_dialog_app_icon_size);
+ float appIconBottomMargin = mContext.getResources().getDimension(
+ R.dimen.media_output_dialog_app_icon_bottom_margin);
+ ViewGroup.MarginLayoutParams params =
+ (ViewGroup.MarginLayoutParams) mAppResourceIcon.getLayoutParams();
+ params.bottomMargin = (int) appIconBottomMargin;
+ params.width = (int) appIconSize;
+ params.height = (int) appIconSize;
+ mAppResourceIcon.setLayoutParams(params);
+ // Change footer background color on scroll.
+ mDevicesRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
+ @Override
+ public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
+ super.onScrolled(recyclerView, dx, dy);
+ changeFooterColorForScroll();
+ }
+ });
+ }
}
@Override
@@ -366,6 +427,18 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog
}
}
+ if (enableOutputSwitcherRedesign()) {
+ if (mMediaSwitchingController.getConnectNewDeviceItem() != null) {
+ mQuickAccessShelf.setVisibility(View.VISIBLE);
+ mConnectDeviceButton.setVisibility(View.VISIBLE);
+ mConnectDeviceButton.setOnClickListener(
+ mMediaSwitchingController::launchBluetoothPairing);
+ } else {
+ mQuickAccessShelf.setVisibility(View.GONE);
+ mConnectDeviceButton.setVisibility(View.GONE);
+ }
+ }
+
// Show when remote media session is available or
// when the device supports BT LE audio + media is playing
mStopButton.setVisibility(getStopButtonVisibility());
@@ -390,21 +463,48 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog
}
private void updateButtonBackgroundColorFilter() {
- ColorFilter buttonColorFilter =
- new PorterDuffColorFilter(
- mMediaSwitchingController.getColorSchemeLegacy().getColorButtonBackground(),
- PorterDuff.Mode.SRC_IN);
- mDoneButton.getBackground().setColorFilter(buttonColorFilter);
- mStopButton.getBackground().setColorFilter(buttonColorFilter);
- mDoneButton.setTextColor(
- mMediaSwitchingController.getColorSchemeLegacy().getColorPositiveButtonText());
+ if (enableOutputSwitcherRedesign()) {
+ mDoneButton.getBackground().setTint(
+ mMediaSwitchingController.getColorScheme().getPrimary());
+ mDoneButton.setTextColor(mMediaSwitchingController.getColorScheme().getOnPrimary());
+ mStopButton.getBackground().setTint(
+ mMediaSwitchingController.getColorScheme().getOutlineVariant());
+ mStopButton.setTextColor(mMediaSwitchingController.getColorScheme().getPrimary());
+ mConnectDeviceButton.setTextColor(
+ mMediaSwitchingController.getColorScheme().getOnSurfaceVariant());
+ mConnectDeviceButton.setStrokeColor(ColorStateList.valueOf(
+ mMediaSwitchingController.getColorScheme().getOutlineVariant()));
+ mConnectDeviceButton.setIconTint(ColorStateList.valueOf(
+ mMediaSwitchingController.getColorScheme().getPrimary()));
+ } else {
+ ColorFilter buttonColorFilter = new PorterDuffColorFilter(
+ mMediaSwitchingController.getColorSchemeLegacy().getColorButtonBackground(),
+ PorterDuff.Mode.SRC_IN);
+ mDoneButton.getBackground().setColorFilter(buttonColorFilter);
+ mStopButton.getBackground().setColorFilter(buttonColorFilter);
+ mDoneButton.setTextColor(
+ mMediaSwitchingController.getColorSchemeLegacy().getColorPositiveButtonText());
+ }
}
private void updateDialogBackgroundColor() {
- getDialogView().getBackground().setTint(
- mMediaSwitchingController.getColorSchemeLegacy().getColorDialogBackground());
- mDeviceListLayout.setBackgroundColor(
- mMediaSwitchingController.getColorSchemeLegacy().getColorDialogBackground());
+ int backgroundColor = enableOutputSwitcherRedesign()
+ ? mMediaSwitchingController.getColorScheme().getSurfaceContainer()
+ : mMediaSwitchingController.getColorSchemeLegacy().getColorDialogBackground();
+ getDialogView().getBackground().setTint(backgroundColor);
+ mDeviceListLayout.setBackgroundColor(backgroundColor);
+ }
+
+ private void changeFooterColorForScroll() {
+ int totalItemCount = mLayoutManager.getItemCount();
+ int lastVisibleItemPosition =
+ mLayoutManager.findLastCompletelyVisibleItemPosition();
+ boolean hasBottomScroll =
+ totalItemCount > 0 && lastVisibleItemPosition != totalItemCount - 1;
+ mDialogFooter.getBackground().setTint(
+ hasBottomScroll
+ ? mMediaSwitchingController.getColorScheme().getSurfaceContainerHigh()
+ : mMediaSwitchingController.getColorScheme().getSurfaceContainer());
}
public void handleLeBroadcastStarted() {
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputColorScheme.kt b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputColorScheme.kt
new file mode 100644
index 000000000000..21b92cc11406
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputColorScheme.kt
@@ -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.systemui.media.dialog
+
+import android.content.Context
+import com.android.systemui.monet.ColorScheme
+import com.android.systemui.res.R
+
+abstract class MediaOutputColorScheme {
+ companion object Factory {
+ @JvmStatic
+ fun fromDynamicColors(dynamicScheme: ColorScheme): MediaOutputColorScheme {
+ return MediaOutputColorSchemeDynamic(dynamicScheme)
+ }
+
+ @JvmStatic
+ fun fromSystemColors(context: Context): MediaOutputColorScheme {
+ return MediaOutputColorSchemeSystem(context)
+ }
+ }
+
+ abstract fun getPrimary(): Int
+
+ abstract fun getOnPrimary(): Int
+
+ abstract fun getSecondary(): Int
+
+ abstract fun getSecondaryContainer(): Int
+
+ abstract fun getSurfaceContainer(): Int
+
+ abstract fun getSurfaceContainerHigh(): Int
+
+ abstract fun getOnSurface(): Int
+
+ abstract fun getOnSurfaceVariant(): Int
+
+ abstract fun getOutline(): Int
+
+ abstract fun getOutlineVariant(): Int
+}
+
+class MediaOutputColorSchemeDynamic(dynamicScheme: ColorScheme) : MediaOutputColorScheme() {
+ private val mMaterialScheme = dynamicScheme.materialScheme
+
+ override fun getPrimary() = mMaterialScheme.primary
+
+ override fun getOnPrimary() = mMaterialScheme.onPrimary
+
+ override fun getSecondary() = mMaterialScheme.secondary
+
+ override fun getSecondaryContainer() = mMaterialScheme.secondaryContainer
+
+ override fun getSurfaceContainer() = mMaterialScheme.surfaceContainer
+
+ override fun getSurfaceContainerHigh() = mMaterialScheme.surfaceContainerHigh
+
+ override fun getOnSurface() = mMaterialScheme.onSurface
+
+ override fun getOnSurfaceVariant() = mMaterialScheme.onSurfaceVariant
+
+ override fun getOutline() = mMaterialScheme.outline
+
+ override fun getOutlineVariant() = mMaterialScheme.outlineVariant
+}
+
+class MediaOutputColorSchemeSystem(private val mContext: Context) : MediaOutputColorScheme() {
+ override fun getPrimary() = mContext.getColor(R.color.media_dialog_primary)
+
+ override fun getOnPrimary() = mContext.getColor(R.color.media_dialog_on_primary)
+
+ override fun getSecondary() = mContext.getColor(R.color.media_dialog_secondary)
+
+ override fun getSecondaryContainer() =
+ mContext.getColor(R.color.media_dialog_secondary_container)
+
+ override fun getSurfaceContainer() = mContext.getColor(R.color.media_dialog_surface_container)
+
+ override fun getSurfaceContainerHigh() =
+ mContext.getColor(R.color.media_dialog_surface_container_high)
+
+ override fun getOnSurface() = mContext.getColor(R.color.media_dialog_on_surface)
+
+ override fun getOnSurfaceVariant() = mContext.getColor(R.color.media_dialog_on_surface_variant)
+
+ override fun getOutline() = mContext.getColor(R.color.media_dialog_outline)
+
+ override fun getOutlineVariant() = mContext.getColor(R.color.media_dialog_outline_variant)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java
index 163ff248b9df..225ad724ce71 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java
@@ -17,6 +17,7 @@
package com.android.systemui.media.dialog;
import static com.android.settingslib.flags.Flags.legacyLeAudioSharing;
+import static com.android.media.flags.Flags.enableOutputSwitcherRedesign;
import android.content.Context;
import android.os.Bundle;
@@ -57,8 +58,10 @@ public class MediaOutputDialog extends MediaOutputBaseDialog {
super(context, broadcastSender, mediaSwitchingController, includePlaybackAndAppMetadata);
mDialogTransitionAnimator = dialogTransitionAnimator;
mUiEventLogger = uiEventLogger;
- mAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController, mainExecutor,
- backgroundExecutor);
+ mAdapter = enableOutputSwitcherRedesign()
+ ? new MediaOutputAdapter(mMediaSwitchingController)
+ : new MediaOutputAdapterLegacy(mMediaSwitchingController, mainExecutor,
+ backgroundExecutor);
if (!aboveStatusbar) {
getWindow().setType(WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY);
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java
index bf1f971c0f8c..0b4a9321618d 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java
@@ -171,7 +171,9 @@ public class MediaSwitchingController
private FeatureFlags mFeatureFlags;
private UserTracker mUserTracker;
private VolumePanelGlobalStateInteractor mVolumePanelGlobalStateInteractor;
+ @NonNull private MediaOutputColorScheme mMediaOutputColorScheme;
@NonNull private MediaOutputColorSchemeLegacy mMediaOutputColorSchemeLegacy;
+ private boolean mIsGroupListCollapsed = true;
public enum BroadcastNotifyDialog {
ACTION_FIRST_LAUNCH,
@@ -229,6 +231,7 @@ public class MediaSwitchingController
mOutputMediaItemListProxy = new OutputMediaItemListProxy(context);
mDialogTransitionAnimator = dialogTransitionAnimator;
mNearbyMediaDevicesManager = nearbyMediaDevicesManager;
+ mMediaOutputColorScheme = MediaOutputColorScheme.fromSystemColors(mContext);
mMediaOutputColorSchemeLegacy = MediaOutputColorSchemeLegacy.fromSystemColors(mContext);
if (enableInputRouting()) {
@@ -499,7 +502,7 @@ public class MediaSwitchingController
return getNotificationIcon();
}
- IconCompat getDeviceIconCompat(MediaDevice device) {
+ Drawable getDeviceIconDrawable(MediaDevice device) {
Drawable drawable = device.getIcon();
if (drawable == null) {
if (DEBUG) {
@@ -509,7 +512,19 @@ public class MediaSwitchingController
// Use default Bluetooth device icon to handle getIcon() is null case.
drawable = mContext.getDrawable(com.android.internal.R.drawable.ic_bt_headphones_a2dp);
}
- return BluetoothUtils.createIconWithDrawable(drawable);
+ return drawable;
+ }
+
+ IconCompat getDeviceIconCompat(MediaDevice device) {
+ return BluetoothUtils.createIconWithDrawable(getDeviceIconDrawable(device));
+ }
+
+ public void setGroupListCollapsed(boolean isCollapsed) {
+ mIsGroupListCollapsed = isCollapsed;
+ }
+
+ public boolean isGroupListCollapsed() {
+ return mIsGroupListCollapsed;
}
boolean isActiveItem(MediaDevice device) {
@@ -560,10 +575,16 @@ public class MediaSwitchingController
void updateCurrentColorScheme(WallpaperColors wallpaperColors, boolean isDarkTheme) {
ColorScheme currentColorScheme = new ColorScheme(wallpaperColors,
isDarkTheme);
+ mMediaOutputColorScheme = MediaOutputColorScheme.fromDynamicColors(
+ currentColorScheme);
mMediaOutputColorSchemeLegacy = MediaOutputColorSchemeLegacy.fromDynamicColors(
currentColorScheme, isDarkTheme);
}
+ MediaOutputColorScheme getColorScheme() {
+ return mMediaOutputColorScheme;
+ }
+
MediaOutputColorSchemeLegacy getColorSchemeLegacy() {
return mMediaOutputColorSchemeLegacy;
}
@@ -609,8 +630,7 @@ public class MediaSwitchingController
devices,
getSelectedMediaDevice(),
connectedMediaDevice,
- needToHandleMutingExpectedDevice,
- getConnectNewDeviceItem());
+ needToHandleMutingExpectedDevice);
} else {
List<MediaItem> updatedMediaItems =
buildMediaItems(
@@ -701,7 +721,6 @@ public class MediaSwitchingController
}
}
dividerItems.forEach(finalMediaItems::add);
- attachConnectNewDeviceItemIfNeeded(finalMediaItems);
return finalMediaItems;
}
}
@@ -765,7 +784,6 @@ public class MediaSwitchingController
finalMediaItems.add(MediaItem.createDeviceMediaItem(device));
}
}
- attachConnectNewDeviceItemIfNeeded(finalMediaItems);
return finalMediaItems;
}
@@ -789,8 +807,14 @@ public class MediaSwitchingController
}
}
+ @NonNull
+ MediaItem getConnectedSpeakersExpandableGroupDivider() {
+ return MediaItem.createExpandableGroupDividerMediaItem(
+ mContext.getString(R.string.media_output_group_title_connected_speakers));
+ }
+
@Nullable
- private MediaItem getConnectNewDeviceItem() {
+ MediaItem getConnectNewDeviceItem() {
boolean isSelectedDeviceNotAGroup = getSelectedMediaDevice().size() == 1;
if (enableInputRouting()) {
// When input routing is enabled, there are expected to be at least 2 total selected
@@ -879,6 +903,15 @@ public class MediaSwitchingController
});
}
+ private List<MediaItem> getOutputDeviceList(boolean addConnectDeviceButton) {
+ List<MediaItem> mediaItems = new ArrayList<>(
+ mOutputMediaItemListProxy.getOutputMediaItemList());
+ if (addConnectDeviceButton) {
+ attachConnectNewDeviceItemIfNeeded(mediaItems);
+ }
+ return mediaItems;
+ }
+
private void addInputDevices(List<MediaItem> mediaItems) {
mediaItems.add(
MediaItem.createGroupDividerMediaItem(
@@ -886,22 +919,34 @@ public class MediaSwitchingController
mediaItems.addAll(mInputMediaItemList);
}
- private void addOutputDevices(List<MediaItem> mediaItems) {
+ private void addOutputDevices(List<MediaItem> mediaItems, boolean addConnectDeviceButton) {
mediaItems.add(
MediaItem.createGroupDividerMediaItem(
mContext.getString(R.string.media_output_group_title)));
- mediaItems.addAll(mOutputMediaItemListProxy.getOutputMediaItemList());
+ mediaItems.addAll(getOutputDeviceList(addConnectDeviceButton));
}
+ /**
+ * Returns a list of media items to be rendered in the device list. For backward compatibility
+ * reasons, adds a "Connect a device" button by default.
+ */
public List<MediaItem> getMediaItemList() {
+ return getMediaItemList(true /* addConnectDeviceButton */);
+ }
+
+ /**
+ * Returns a list of media items to be rendered in the device list.
+ * @param addConnectDeviceButton Whether to add a "Connect a device" button to the list.
+ */
+ public List<MediaItem> getMediaItemList(boolean addConnectDeviceButton) {
// If input routing is not enabled, only return output media items.
if (!enableInputRouting()) {
- return mOutputMediaItemListProxy.getOutputMediaItemList();
+ return getOutputDeviceList(addConnectDeviceButton);
}
// If input routing is enabled, return both output and input media items.
List<MediaItem> mediaItems = new ArrayList<>();
- addOutputDevices(mediaItems);
+ addOutputDevices(mediaItems, addConnectDeviceButton);
addInputDevices(mediaItems);
return mediaItems;
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/OutputMediaItemListProxy.java b/packages/SystemUI/src/com/android/systemui/media/dialog/OutputMediaItemListProxy.java
index 45ca2c6ee8e5..c15ef82f0378 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/OutputMediaItemListProxy.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/OutputMediaItemListProxy.java
@@ -44,7 +44,6 @@ public class OutputMediaItemListProxy {
private final List<MediaItem> mSelectedMediaItems;
private final List<MediaItem> mSuggestedMediaItems;
private final List<MediaItem> mSpeakersAndDisplaysMediaItems;
- @Nullable private MediaItem mConnectNewDeviceMediaItem;
public OutputMediaItemListProxy(Context context) {
mContext = context;
@@ -88,9 +87,6 @@ public class OutputMediaItemListProxy {
R.string.media_output_group_title_speakers_and_displays)));
finalMediaItems.addAll(mSpeakersAndDisplaysMediaItems);
}
- if (mConnectNewDeviceMediaItem != null) {
- finalMediaItems.add(mConnectNewDeviceMediaItem);
- }
return finalMediaItems;
}
@@ -99,8 +95,7 @@ public class OutputMediaItemListProxy {
List<MediaDevice> devices,
List<MediaDevice> selectedDevices,
@Nullable MediaDevice connectedMediaDevice,
- boolean needToHandleMutingExpectedDevice,
- @Nullable MediaItem connectNewDeviceMediaItem) {
+ boolean needToHandleMutingExpectedDevice) {
Set<String> selectedOrConnectedMediaDeviceIds =
selectedDevices.stream().map(MediaDevice::getId).collect(Collectors.toSet());
if (connectedMediaDevice != null) {
@@ -177,7 +172,6 @@ public class OutputMediaItemListProxy {
mSuggestedMediaItems.addAll(updatedSuggestedMediaItems);
mSpeakersAndDisplaysMediaItems.clear();
mSpeakersAndDisplaysMediaItems.addAll(updatedSpeakersAndDisplaysMediaItems);
- mConnectNewDeviceMediaItem = connectNewDeviceMediaItem;
// The cached mOutputMediaItemList is cleared upon any update to individual media item
// lists. This ensures getOutputMediaItemList() computes and caches a fresh list on the next
@@ -197,10 +191,6 @@ public class OutputMediaItemListProxy {
mSelectedMediaItems.removeIf((MediaItem::isMutingExpectedDevice));
mSuggestedMediaItems.removeIf((MediaItem::isMutingExpectedDevice));
mSpeakersAndDisplaysMediaItems.removeIf((MediaItem::isMutingExpectedDevice));
- if (mConnectNewDeviceMediaItem != null
- && mConnectNewDeviceMediaItem.isMutingExpectedDevice()) {
- mConnectNewDeviceMediaItem = null;
- }
}
mOutputMediaItemList.removeIf((MediaItem::isMutingExpectedDevice));
}
@@ -211,7 +201,6 @@ public class OutputMediaItemListProxy {
mSelectedMediaItems.clear();
mSuggestedMediaItems.clear();
mSpeakersAndDisplaysMediaItems.clear();
- mConnectNewDeviceMediaItem = null;
}
mOutputMediaItemList.clear();
}
@@ -221,8 +210,7 @@ public class OutputMediaItemListProxy {
if (Flags.fixOutputMediaItemListIndexOutOfBoundsException()) {
return mSelectedMediaItems.isEmpty()
&& mSuggestedMediaItems.isEmpty()
- && mSpeakersAndDisplaysMediaItems.isEmpty()
- && (mConnectNewDeviceMediaItem == null);
+ && mSpeakersAndDisplaysMediaItems.isEmpty();
} else {
return mOutputMediaItemList.isEmpty();
}
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionUtils.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionUtils.kt
index 88cbc3867744..a8d0e0573d89 100644
--- a/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionUtils.kt
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionUtils.kt
@@ -18,6 +18,7 @@ package com.android.systemui.mediaprojection.permission
import android.content.Context
import android.media.projection.MediaProjectionConfig
+import com.android.media.projection.flags.Flags
import com.android.systemui.res.R
/** Various utility methods related to media projection permissions. */
@@ -28,13 +29,27 @@ object MediaProjectionPermissionUtils {
mediaProjectionConfig: MediaProjectionConfig?,
overrideDisableSingleAppOption: Boolean,
): String? {
- // The single app option should only be disabled if the client has setup a
- // MediaProjection with MediaProjectionConfig#createConfigForDefaultDisplay AND
- // it hasn't been overridden by the OVERRIDE_DISABLE_SINGLE_APP_OPTION per-app override.
+
val singleAppOptionDisabled =
!overrideDisableSingleAppOption &&
- mediaProjectionConfig?.regionToCapture ==
- MediaProjectionConfig.CAPTURE_REGION_FIXED_DISPLAY
+ if (Flags.appContentSharing()) {
+ // The single app option should only be disabled if the client has setup a
+ // MediaProjection with MediaProjection.isChoiceAppEnabled == false (e.g by
+ // creating it
+ // with MediaProjectionConfig#createConfigForDefaultDisplay AND
+ // it hasn't been overridden by the OVERRIDE_DISABLE_SINGLE_APP_OPTION per-app
+ // override.
+ mediaProjectionConfig?.isSourceEnabled(
+ MediaProjectionConfig.PROJECTION_SOURCE_APP
+ ) == false
+ } else {
+ // The single app option should only be disabled if the client has setup a
+ // MediaProjection with MediaProjectionConfig#createConfigForDefaultDisplay AND
+ // it hasn't been overridden by the OVERRIDE_DISABLE_SINGLE_APP_OPTION per-app
+ // override.
+ mediaProjectionConfig?.regionToCapture ==
+ MediaProjectionConfig.CAPTURE_REGION_FIXED_DISPLAY
+ }
return if (singleAppOptionDisabled) {
context.getString(
R.string.media_projection_entry_app_permission_dialog_single_app_disabled,
diff --git a/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModel.kt b/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModel.kt
index 465c78e91e53..2a7fb5467173 100644
--- a/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModel.kt
@@ -23,17 +23,14 @@ import com.android.systemui.lifecycle.Hydrator
import com.android.systemui.media.controls.domain.pipeline.interactor.MediaCarouselInteractor
import com.android.systemui.scene.domain.interactor.SceneInteractor
import com.android.systemui.shade.domain.interactor.ShadeInteractor
-import com.android.systemui.shade.domain.interactor.ShadeModeInteractor
import com.android.systemui.shade.ui.viewmodel.ShadeHeaderViewModel
import com.android.systemui.statusbar.disableflags.domain.interactor.DisableFlagsInteractor
-import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor
import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel
import com.android.systemui.utils.coroutines.flow.flatMapLatestConflated
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.coroutineScope
-import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.flowOf
@@ -51,31 +48,12 @@ constructor(
val notificationsPlaceholderViewModelFactory: NotificationsPlaceholderViewModel.Factory,
val sceneInteractor: SceneInteractor,
private val shadeInteractor: ShadeInteractor,
- shadeModeInteractor: ShadeModeInteractor,
disableFlagsInteractor: DisableFlagsInteractor,
mediaCarouselInteractor: MediaCarouselInteractor,
- activeNotificationsInteractor: ActiveNotificationsInteractor,
) : ExclusiveActivatable() {
private val hydrator = Hydrator("NotificationsShadeOverlayContentViewModel.hydrator")
- val showClock: Boolean by
- hydrator.hydratedStateOf(
- traceName = "showClock",
- initialValue =
- shouldShowClock(
- isShadeLayoutWide = shadeModeInteractor.isShadeLayoutWide.value,
- areAnyNotificationsPresent =
- activeNotificationsInteractor.areAnyNotificationsPresentValue,
- ),
- source =
- combine(
- shadeModeInteractor.isShadeLayoutWide,
- activeNotificationsInteractor.areAnyNotificationsPresent,
- this::shouldShowClock,
- ),
- )
-
val showMedia: Boolean by
hydrator.hydratedStateOf(
traceName = "showMedia",
@@ -114,13 +92,6 @@ constructor(
shadeInteractor.collapseNotificationsShade(loggingReason = "shade scrim clicked")
}
- private fun shouldShowClock(
- isShadeLayoutWide: Boolean,
- areAnyNotificationsPresent: Boolean,
- ): Boolean {
- return !isShadeLayoutWide && areAnyNotificationsPresent
- }
-
@AssistedFactory
interface Factory {
fun create(): NotificationsShadeOverlayContentViewModel
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CommonTile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CommonTile.kt
index 699778f3b6f9..bd7e7832751a 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CommonTile.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CommonTile.kt
@@ -162,6 +162,7 @@ fun LargeTileContent(
colors = colors,
accessibilityUiState = accessibilityUiState,
isVisible = isVisible,
+ modifier = Modifier.weight(1f),
)
if (sideDrawable != null) {
@@ -289,6 +290,8 @@ private fun TileLabel(
) {
var textSize by remember { mutableIntStateOf(0) }
+ val iterations = if (isVisible()) TILE_MARQUEE_ITERATIONS else 0
+
BasicText(
text = text,
color = color,
@@ -321,14 +324,10 @@ private fun TileLabel(
)
}
}
- .thenIf(isVisible()) {
- // Only apply the marquee when the label is visible, which is needed for the
- // always composed QS
- Modifier.basicMarquee(
- iterations = TILE_MARQUEE_ITERATIONS,
- initialDelayMillis = TILE_INITIAL_DELAY_MILLIS,
- )
- },
+ .basicMarquee(
+ iterations = iterations,
+ initialDelayMillis = TILE_INITIAL_DELAY_MILLIS,
+ ),
)
}
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 f8eaa6c3bcfb..b8cb2c4844e4 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
@@ -22,8 +22,11 @@ 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.Spring
+import androidx.compose.animation.core.VisibilityThreshold
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.animateFloatAsState
+import androidx.compose.animation.core.spring
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
@@ -108,6 +111,7 @@ import androidx.compose.ui.layout.positionInRoot
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.dimensionResource
+import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.CustomAccessibilityAction
import androidx.compose.ui.semantics.contentDescription
@@ -118,6 +122,7 @@ import androidx.compose.ui.text.style.Hyphens
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastMap
import com.android.compose.gesture.effect.rememberOffsetOverscrollEffectFactory
@@ -157,9 +162,9 @@ 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.BounceableTileViewModel
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
import com.android.systemui.qs.shared.model.groupAndSort
import com.android.systemui.res.R
import kotlin.math.abs
@@ -220,7 +225,6 @@ private fun EditModeTopBar(onStopEditing: () -> Unit, onReset: (() -> Unit)?) {
)
}
-@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun DefaultEditTileGrid(
listState: EditTileListState,
@@ -526,11 +530,7 @@ private fun CurrentTilesGrid(
var gridContentOffset by remember { mutableStateOf(Offset(0f, 0f)) }
val coroutineScope = rememberCoroutineScope()
- val cells =
- remember(listState.tiles) {
- listState.tiles.fastMap { Pair(it, BounceableTileViewModel()) }
- }
-
+ val cells = listState.tiles
val primaryColor = MaterialTheme.colorScheme.primary
TileLazyGrid(
state = gridState,
@@ -561,11 +561,11 @@ private fun CurrentTilesGrid(
.testTag(CURRENT_TILES_GRID_TEST_TAG),
) {
EditTiles(
- cells,
- listState,
- selectionState,
- coroutineScope,
- largeTilesSpan,
+ cells = cells,
+ dragAndDropState = listState,
+ selectionState = selectionState,
+ coroutineScope = coroutineScope,
+ largeTilesSpan = largeTilesSpan,
onRemoveTile = onRemoveTile,
) { resizingOperation ->
when (resizingOperation) {
@@ -618,11 +618,9 @@ private fun AvailableTileGrid(
}
.padding(16.dp),
) {
- Text(
- text = category.label.load() ?: "",
- style = MaterialTheme.typography.titleMediumEmphasized,
- color = MaterialTheme.colorScheme.onSurface,
- modifier = Modifier.fillMaxWidth().padding(start = 8.dp, bottom = 16.dp),
+ CategoryHeader(
+ category,
+ modifier = Modifier.fillMaxWidth().padding(bottom = 16.dp),
)
tiles.chunked(columns).forEach { row ->
Row(
@@ -662,7 +660,7 @@ private fun GridCell.key(index: Int): Any {
/**
* Adds a list of [GridCell] to the lazy grid
*
- * @param cells the pairs of [GridCell] to [BounceableTileViewModel]
+ * @param cells the list of [GridCell]
* @param dragAndDropState the [DragAndDropState] for this grid
* @param selectionState the [MutableSelectionState] for this grid
* @param coroutineScope the [CoroutineScope] to be used for the tiles
@@ -671,7 +669,7 @@ private fun GridCell.key(index: Int): Any {
* @param onResize the callback when a tile has a new [ResizeOperation]
*/
fun LazyGridScope.EditTiles(
- cells: List<Pair<GridCell, BounceableTileViewModel>>,
+ cells: List<GridCell>,
dragAndDropState: DragAndDropState,
selectionState: MutableSelectionState,
coroutineScope: CoroutineScope,
@@ -681,11 +679,11 @@ fun LazyGridScope.EditTiles(
) {
items(
count = cells.size,
- key = { cells[it].first.key(it) },
- span = { cells[it].first.span },
+ key = { cells[it].key(it) },
+ span = { cells[it].span },
contentType = { TileType },
) { index ->
- when (val cell = cells[index].first) {
+ when (val cell = cells[index]) {
is TileGridCell ->
if (dragAndDropState.isMoving(cell.tile.tileSpec)) {
// If the tile is being moved, replace it with a visible spacer
@@ -708,7 +706,15 @@ fun LazyGridScope.EditTiles(
onRemoveTile = onRemoveTile,
coroutineScope = coroutineScope,
largeTilesSpan = largeTilesSpan,
- modifier = Modifier.animateItem(),
+ modifier =
+ Modifier.animateItem(
+ placementSpec =
+ spring(
+ stiffness = Spring.StiffnessMediumLow,
+ dampingRatio = Spring.DampingRatioLowBouncy,
+ visibilityThreshold = IntOffset.VisibilityThreshold,
+ )
+ ),
)
}
is SpacerGridCell ->
@@ -853,6 +859,26 @@ private fun TileGridCell(
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
+private fun CategoryHeader(category: TileCategory, modifier: Modifier = Modifier) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = spacedBy(8.dp),
+ modifier = modifier,
+ ) {
+ Icon(
+ painter = painterResource(category.iconId),
+ contentDescription = null,
+ tint = MaterialTheme.colorScheme.onSurface,
+ )
+ Text(
+ text = category.label.load() ?: "",
+ style = MaterialTheme.typography.titleMediumEmphasized,
+ color = MaterialTheme.colorScheme.onSurface,
+ )
+ }
+}
+
+@Composable
private fun AvailableTileGridCell(
cell: AvailableTileGridCell,
dragAndDropState: DragAndDropState,
diff --git a/packages/SystemUI/src/com/android/systemui/qs/shared/model/TileCategory.kt b/packages/SystemUI/src/com/android/systemui/qs/shared/model/TileCategory.kt
index 59cb7d3d5345..c8225e7a3509 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/shared/model/TileCategory.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/shared/model/TileCategory.kt
@@ -20,14 +20,35 @@ import com.android.systemui.common.shared.model.Text
import com.android.systemui.res.R
/** Categories for tiles. This can be used to sort tiles in edit mode. */
-enum class TileCategory(val label: Text) {
- CONNECTIVITY(Text.Resource(R.string.qs_edit_mode_category_connectivity)),
- UTILITIES(Text.Resource(R.string.qs_edit_mode_category_utilities)),
- DISPLAY(Text.Resource(R.string.qs_edit_mode_category_display)),
- PRIVACY(Text.Resource(R.string.qs_edit_mode_category_privacy)),
- ACCESSIBILITY(Text.Resource(R.string.qs_edit_mode_category_accessibility)),
- PROVIDED_BY_APP(Text.Resource(R.string.qs_edit_mode_category_providedByApps)),
- UNKNOWN(Text.Resource(R.string.qs_edit_mode_category_unknown)),
+enum class TileCategory(val label: Text, val iconId: Int) {
+ CONNECTIVITY(
+ Text.Resource(R.string.qs_edit_mode_category_connectivity),
+ R.drawable.ic_qs_category_connectivty,
+ ),
+ UTILITIES(
+ Text.Resource(R.string.qs_edit_mode_category_utilities),
+ R.drawable.ic_qs_category_utilities,
+ ),
+ DISPLAY(
+ Text.Resource(R.string.qs_edit_mode_category_display),
+ R.drawable.ic_qs_category_display,
+ ),
+ PRIVACY(
+ Text.Resource(R.string.qs_edit_mode_category_privacy),
+ R.drawable.ic_qs_category_privacy,
+ ),
+ ACCESSIBILITY(
+ Text.Resource(R.string.qs_edit_mode_category_accessibility),
+ R.drawable.ic_qs_category_accessibility,
+ ),
+ PROVIDED_BY_APP(
+ Text.Resource(R.string.qs_edit_mode_category_providedByApps),
+ R.drawable.ic_qs_category_provided_by_apps,
+ ),
+ UNKNOWN(
+ Text.Resource(R.string.qs_edit_mode_category_unknown),
+ R.drawable.ic_qs_category_unknown,
+ ),
}
interface CategoryAndName {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetAdapter.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetAdapter.java
index b21c3e4e44e1..6236fff87f63 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetAdapter.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetAdapter.java
@@ -196,11 +196,16 @@ public class InternetAdapter extends RecyclerView.Adapter<InternetAdapter.Intern
if (mJob == null) {
mJob = WifiUtils.checkWepAllowed(mContext, mCoroutineScope, wifiEntry.getSsid(),
WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG, intent -> {
- mInternetDetailsContentController.startActivityForDialog(intent);
+ mInternetDetailsContentController
+ .startActivityForDialog(intent);
return null;
}, () -> {
wifiConnect(wifiEntry, view);
return null;
+ }, intent -> {
+ mInternetDetailsContentController
+ .startActivityForDialogDismissDialogFirst(intent, view);
+ return null;
});
}
return;
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContentController.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContentController.java
index 945e051606b9..2497daebdd6d 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContentController.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContentController.java
@@ -784,6 +784,17 @@ public class InternetDetailsContentController implements AccessPointController.A
mActivityStarter.startActivity(intent, false /* dismissShade */);
}
+ // Closes the dialog first, as the WEP dialog is in a different process and can have weird
+ // interactions otherwise.
+ void startActivityForDialogDismissDialogFirst(Intent intent, View view) {
+ ActivityTransitionAnimator.Controller controller =
+ mDialogTransitionAnimator.createActivityTransitionController(view);
+ if (mCallback != null) {
+ mCallback.dismissDialog();
+ }
+ mActivityStarter.startActivity(intent, false /* dismissShade */, controller);
+ }
+
void launchNetworkSetting(View view) {
startActivity(getSettingsIntent(), view);
}
diff --git a/packages/SystemUI/src/com/android/systemui/scene/shared/logger/SceneLogger.kt b/packages/SystemUI/src/com/android/systemui/scene/shared/logger/SceneLogger.kt
index 73c71f6088e1..452ea3f719fa 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/shared/logger/SceneLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/shared/logger/SceneLogger.kt
@@ -90,7 +90,7 @@ class SceneLogger @Inject constructor(@SceneFrameworkLog private val logBuffer:
fun logSceneChangeRejection(
from: ContentKey?,
to: ContentKey?,
- originalChangeReason: String,
+ originalChangeReason: String?,
rejectionReason: String,
) {
logBuffer.log(
@@ -112,8 +112,10 @@ class SceneLogger @Inject constructor(@SceneFrameworkLog private val logBuffer:
"scene "
}
)
- append("change $str1 because \"$str2\" ")
- append("(original change reason: \"$str3\")")
+ append("change $str1 because \"$str2\"")
+ if (str3 != null) {
+ append(" (original change reason: \"$str3\")")
+ }
}
},
)
@@ -136,8 +138,11 @@ class SceneLogger @Inject constructor(@SceneFrameworkLog private val logBuffer:
logBuffer.log(
tag = TAG,
level = LogLevel.INFO,
- messageInitializer = { str1 = transitionState.currentScene.toString() },
- messagePrinter = { "Scene transition idle on: $str1" },
+ messageInitializer = {
+ str1 = transitionState.currentScene.toString()
+ str2 = transitionState.currentOverlays.joinToString()
+ },
+ messagePrinter = { "Scene transition idle on: $str1, overlays: $str2" },
)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt
index a81fcec94989..d8bb84af6023 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt
@@ -219,15 +219,24 @@ constructor(
* it being a false touch.
*/
fun canChangeScene(toScene: SceneKey): Boolean {
- return isInteractionAllowedByFalsing(toScene).also {
- // A scene change is guaranteed; log it.
- logger.logSceneChanged(
- from = currentScene.value,
- to = toScene,
- sceneState = null,
- reason = "user interaction",
- isInstant = false,
- )
+ return isInteractionAllowedByFalsing(toScene).also { sceneChangeAllowed ->
+ if (sceneChangeAllowed) {
+ // A scene change is guaranteed; log it.
+ logger.logSceneChanged(
+ from = currentScene.value,
+ to = toScene,
+ sceneState = null,
+ reason = "user interaction",
+ isInstant = false,
+ )
+ } else {
+ logger.logSceneChangeRejection(
+ from = currentScene.value,
+ to = toScene,
+ originalChangeReason = null,
+ rejectionReason = "Falsing: false touch detected",
+ )
+ }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/scroll/ScrollCaptureController.java b/packages/SystemUI/src/com/android/systemui/screenshot/scroll/ScrollCaptureController.java
index f4c77da674b0..742067a98057 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/scroll/ScrollCaptureController.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/scroll/ScrollCaptureController.java
@@ -24,6 +24,8 @@ import android.provider.Settings;
import android.util.Log;
import android.view.ScrollCaptureResponse;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import androidx.concurrent.futures.CallbackToFutureAdapter;
import androidx.concurrent.futures.CallbackToFutureAdapter.Completer;
@@ -68,11 +70,15 @@ public class ScrollCaptureController {
private final UiEventLogger mEventLogger;
private final ScrollCaptureClient mClient;
+ @Nullable
private Completer<LongScreenshot> mCaptureCompleter;
+ @Nullable
private ListenableFuture<Session> mSessionFuture;
private Session mSession;
+ @Nullable
private ListenableFuture<CaptureResult> mTileFuture;
+ @Nullable
private ListenableFuture<Void> mEndFuture;
private String mWindowOwner;
private volatile boolean mCancelled;
@@ -148,8 +154,9 @@ public class ScrollCaptureController {
}
@Inject
- ScrollCaptureController(Context context, @Background Executor bgExecutor,
- ScrollCaptureClient client, ImageTileSet imageTileSet, UiEventLogger logger) {
+ ScrollCaptureController(@NonNull Context context, @Background Executor bgExecutor,
+ @NonNull ScrollCaptureClient client, @NonNull ImageTileSet imageTileSet,
+ @NonNull UiEventLogger logger) {
mContext = context;
mBgExecutor = bgExecutor;
mClient = client;
@@ -214,7 +221,9 @@ public class ScrollCaptureController {
} catch (InterruptedException | ExecutionException e) {
// Failure to start, propagate to caller
Log.e(TAG, "session start failed!");
- mCaptureCompleter.setException(e);
+ if (mCaptureCompleter != null) {
+ mCaptureCompleter.setException(e);
+ }
mEventLogger.log(ScreenshotEvent.SCREENSHOT_LONG_SCREENSHOT_FAILURE, 0, mWindowOwner);
}
}
@@ -235,7 +244,9 @@ public class ScrollCaptureController {
Log.e(TAG, "requestTile cancelled");
} catch (InterruptedException | ExecutionException e) {
Log.e(TAG, "requestTile failed!", e);
- mCaptureCompleter.setException(e);
+ if (mCaptureCompleter != null) {
+ mCaptureCompleter.setException(e);
+ }
}
}, mBgExecutor);
}
@@ -350,7 +361,9 @@ public class ScrollCaptureController {
}
// Provide result to caller and complete the top-level future
// Caller is responsible for releasing this resource (ImageReader/HardwareBuffers)
- mCaptureCompleter.set(new LongScreenshot(mSession, mImageTileSet));
+ if (mCaptureCompleter != null) {
+ mCaptureCompleter.set(new LongScreenshot(mSession, mImageTileSet));
+ }
}, mContext.getMainExecutor());
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt b/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt
index c800ab3d0bf2..913aacb53e12 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt
@@ -20,7 +20,6 @@ import android.content.Context
import android.content.res.Configuration
import android.graphics.Rect
import android.os.PowerManager
-import android.os.SystemClock
import android.util.ArraySet
import android.view.GestureDetector
import android.view.MotionEvent
@@ -54,6 +53,7 @@ import com.android.systemui.communal.ui.compose.CommunalContainer
import com.android.systemui.communal.ui.compose.CommunalContent
import com.android.systemui.communal.ui.viewmodel.CommunalViewModel
import com.android.systemui.communal.util.CommunalColors
+import com.android.systemui.communal.util.UserTouchActivityNotifier
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
@@ -101,6 +101,7 @@ constructor(
private val notificationStackScrollLayoutController: NotificationStackScrollLayoutController,
private val keyguardMediaController: KeyguardMediaController,
private val lockscreenSmartspaceController: LockscreenSmartspaceController,
+ private val userTouchActivityNotifier: UserTouchActivityNotifier,
@CommunalTouchLog logBuffer: LogBuffer,
private val userActivityNotifier: UserActivityNotifier,
) : LifecycleOwner {
@@ -646,8 +647,8 @@ constructor(
// result in broken states.
return true
}
+ var handled = hubShowing
try {
- var handled = false
if (!touchTakenByKeyguardGesture) {
communalContainerWrapper?.dispatchTouchEvent(ev) {
if (it) {
@@ -655,18 +656,10 @@ constructor(
}
}
}
- return handled || hubShowing
+ return handled
} finally {
- if (Flags.bouncerUiRevamp()) {
- userActivityNotifier.notifyUserActivity(
- event = PowerManager.USER_ACTIVITY_EVENT_TOUCH
- )
- } else {
- powerManager.userActivity(
- SystemClock.uptimeMillis(),
- PowerManager.USER_ACTIVITY_EVENT_TOUCH,
- 0,
- )
+ if (handled) {
+ userTouchActivityNotifier.notifyActivity(ev)
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/OWNERS b/packages/SystemUI/src/com/android/systemui/shade/OWNERS
index 89454b84a528..47ca531b8502 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/OWNERS
+++ b/packages/SystemUI/src/com/android/systemui/shade/OWNERS
@@ -1,5 +1,4 @@
justinweir@google.com
-syeonlee@google.com
nicomazz@google.com
burakov@google.com
@@ -8,16 +7,16 @@ per-file *Notification* = file:../statusbar/notification/OWNERS
per-file NotificationsQuickSettingsContainer.java = kozynski@google.com, asc@google.com
per-file NotificationsQSContainerController.kt = kozynski@google.com, asc@google.com
-per-file *ShadeHeader* = syeonlee@google.com, kozynski@google.com, asc@google.com
+per-file *ShadeHeader* = kozynski@google.com, asc@google.com
per-file *Interactor* = set noparent
-per-file *Interactor* = justinweir@google.com, syeonlee@google.com, nijamkin@google.com, nicomazz@google.com, burakov@google.com
+per-file *Interactor* = justinweir@google.com, nijamkin@google.com, nicomazz@google.com, burakov@google.com
per-file *Repository* = set noparent
-per-file *Repository* = justinweir@google.com, syeonlee@google.com, nijamkin@google.com, nicomazz@google.com, burakov@google.com
+per-file *Repository* = justinweir@google.com, nijamkin@google.com, nicomazz@google.com, burakov@google.com
-per-file NotificationShadeWindow* = pixel@google.com, cinek@google.com, juliacr@google.com, justinweir@google.com, syeonlee@google.com, nicomazz@google.com, burakov@google.com
+per-file NotificationShadeWindow* = pixel@google.com, cinek@google.com, juliacr@google.com, justinweir@google.com, nicomazz@google.com, burakov@google.com
per-file NotificationPanelUnfoldAnimationController.kt = alexflo@google.com, jeffdq@google.com, juliacr@google.com
-per-file NotificationPanelView.java = pixel@google.com, cinek@google.com, juliacr@google.com, justinweir@google.com, syeonlee@google.com, nicomazz@google.com, burakov@google.com
-per-file NotificationPanelViewController.java = pixel@google.com, cinek@google.com, juliacr@google.com, justinweir@google.com, syeonlee@google.com, nicomazz@google.com, burakov@google.com
+per-file NotificationPanelView.java = pixel@google.com, cinek@google.com, juliacr@google.com, justinweir@google.com, nicomazz@google.com, burakov@google.com
+per-file NotificationPanelViewController.java = pixel@google.com, cinek@google.com, juliacr@google.com, justinweir@google.com, nicomazz@google.com, burakov@google.com
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt
index b211f0729318..82d361797f96 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt
@@ -101,6 +101,7 @@ constructor(
shadeInteractor.collapseQuickSettingsShade(
loggingReason = "ShadeControllerSceneImpl.instantCollapseShade",
transitionKey = Instant,
+ bypassNotificationsShade = true,
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayAwareModule.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayAwareModule.kt
index 446d4b450edc..0132390f9ce8 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayAwareModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayAwareModule.kt
@@ -23,6 +23,7 @@ import android.view.WindowManager
import android.view.WindowManager.LayoutParams
import android.view.WindowManager.LayoutParams.TYPE_NOTIFICATION_SHADE
import android.window.WindowContext
+import com.android.app.tracing.TrackGroupUtils.trackGroup
import com.android.systemui.CoreStartable
import com.android.systemui.common.ui.ConfigurationState
import com.android.systemui.common.ui.ConfigurationStateImpl
@@ -34,6 +35,8 @@ import com.android.systemui.common.ui.view.ChoreographerUtils
import com.android.systemui.common.ui.view.ChoreographerUtilsImpl
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.LogBufferFactory
import com.android.systemui.res.R
import com.android.systemui.scene.ui.view.WindowRootView
import com.android.systemui.shade.data.repository.MutableShadeDisplaysRepository
@@ -266,6 +269,20 @@ object ShadeDisplayAwareModule {
@Provides
@ShadeOnDefaultDisplayWhenLocked
fun provideShadeOnDefaultDisplayWhenLocked(): Boolean = true
+
+ /** Provides a [LogBuffer] for use by classes related to shade movement */
+ @Provides
+ @SysUISingleton
+ @ShadeDisplayLog
+ fun provideShadeDisplayLogLogBuffer(factory: LogBufferFactory): LogBuffer {
+ val logBufferName = "ShadeDisplayLog"
+ return factory.create(
+ logBufferName,
+ maxSize = 400,
+ alwaysLogToLogcat = true,
+ systraceTrackName = trackGroup("shade", logBufferName),
+ )
+ }
}
/** Module that should be included only if the shade window [WindowRootView] is available. */
@@ -298,3 +315,6 @@ object ShadeDisplayAwareWithShadeWindowModule {
* how well this solution behaves from the performance point of view.
*/
@Qualifier @Retention(AnnotationRetention.RUNTIME) annotation class ShadeOnDefaultDisplayWhenLocked
+
+/** A [com.android.systemui.log.LogBuffer] for changes to the shade display. */
+@Qualifier @Retention(AnnotationRetention.RUNTIME) annotation class ShadeDisplayLog
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeStateTraceLogger.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeStateTraceLogger.kt
index de1b180f5a7a..1ec83835ab43 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeStateTraceLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeStateTraceLogger.kt
@@ -24,6 +24,8 @@ import com.android.systemui.CoreStartable
import com.android.systemui.common.ui.data.repository.ConfigurationRepository
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.core.LogLevel
import com.android.systemui.shade.data.repository.ShadeDisplaysRepository
import com.android.systemui.shade.domain.interactor.ShadeInteractor
import com.android.systemui.shade.domain.interactor.ShadeModeInteractor
@@ -42,6 +44,7 @@ constructor(
private val shadeDisplaysRepository: Lazy<ShadeDisplaysRepository>,
@ShadeDisplayAware private val configurationRepository: ConfigurationRepository,
@Application private val scope: CoroutineScope,
+ @ShadeDisplayLog private val logBuffer: LogBuffer,
) : CoreStartable {
override fun start() {
scope.launchTraced("ShadeStateTraceLogger") {
@@ -72,6 +75,18 @@ constructor(
"configurationChange#smallestScreenWidthDp",
it.smallestScreenWidthDp,
)
+ logBuffer.log(
+ "ShadeStateTraceLogger",
+ LogLevel.DEBUG,
+ {
+ int1 = it.smallestScreenWidthDp
+ int2 = it.densityDpi
+ },
+ {
+ "New configuration change from Shade window. " +
+ "smallestScreenWidthDp: $int1, densityDpi: $int2"
+ },
+ )
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractor.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractor.kt
index 0e0f58dc8d0e..d48d56c2403b 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractor.kt
@@ -27,8 +27,11 @@ import com.android.systemui.common.ui.data.repository.ConfigurationRepository
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.core.LogLevel
import com.android.systemui.shade.ShadeDisplayAware
import com.android.systemui.shade.ShadeDisplayChangeLatencyTracker
+import com.android.systemui.shade.ShadeDisplayLog
import com.android.systemui.shade.ShadeTraceLogger.logMoveShadeWindowTo
import com.android.systemui.shade.ShadeTraceLogger.t
import com.android.systemui.shade.ShadeTraceLogger.traceReparenting
@@ -69,6 +72,7 @@ constructor(
private val notificationRebindingTracker: NotificationRebindingTracker,
private val notificationStackRebindingHider: NotificationStackRebindingHider,
@ShadeDisplayAware private val configForwarder: ConfigurationForwarder,
+ @ShadeDisplayLog private val logBuffer: LogBuffer,
) : CoreStartable {
private val hasActiveNotifications: Boolean
@@ -101,7 +105,12 @@ constructor(
/** Tries to move the shade. If anything wrong happens, fails gracefully without crashing. */
private suspend fun moveShadeWindowTo(destinationId: Int) {
- Log.d(TAG, "Trying to move shade window to display with id $destinationId")
+ logBuffer.log(
+ TAG,
+ LogLevel.DEBUG,
+ { int1 = destinationId },
+ { "Trying to move shade window to display with id $int1" },
+ )
logMoveShadeWindowTo(destinationId)
var currentId = -1
try {
@@ -113,7 +122,12 @@ constructor(
val currentDisplay = shadeContext.display ?: error("Current shade display is null")
currentId = currentDisplay.displayId
if (currentId == destinationId) {
- Log.w(TAG, "Trying to move the shade to a display ($currentId) it was already in ")
+ logBuffer.log(
+ TAG,
+ LogLevel.WARNING,
+ { int1 = currentId },
+ { "Trying to move the shade to a display ($int1) it was already in." },
+ )
return
}
@@ -128,9 +142,14 @@ constructor(
}
}
} catch (e: IllegalStateException) {
- Log.e(
+ logBuffer.log(
TAG,
- "Unable to move the shade window from display $currentId to $destinationId",
+ LogLevel.ERROR,
+ {
+ int1 = currentId
+ int2 = destinationId
+ },
+ { "Unable to move the shade window from display $int1 to $int2" },
e,
)
}
@@ -200,7 +219,7 @@ constructor(
}
private fun errorLog(s: String) {
- Log.e(TAG, s)
+ logBuffer.log(TAG, LogLevel.ERROR, s)
}
private fun checkContextDisplayMatchesExpected(destinationId: Int) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/BlurUtils.kt b/packages/SystemUI/src/com/android/systemui/statusbar/BlurUtils.kt
index 9940ae523b60..6d04c27b9e5e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/BlurUtils.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/BlurUtils.kt
@@ -28,7 +28,7 @@ import android.util.Log
import android.util.MathUtils
import android.view.CrossWindowBlurListeners
import android.view.CrossWindowBlurListeners.CROSS_WINDOW_BLUR_SUPPORTED
-import android.view.SurfaceControl
+import android.view.SyncRtSurfaceTransactionApplier
import android.view.ViewRootImpl
import androidx.annotation.VisibleForTesting
import com.android.systemui.Dumpable
@@ -36,26 +36,35 @@ import com.android.systemui.Flags
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.dump.DumpManager
-import java.io.PrintWriter
-import javax.inject.Inject
import com.android.systemui.keyguard.ui.transitions.BlurConfig
import com.android.systemui.res.R
+import java.io.PrintWriter
+import javax.inject.Inject
@SysUISingleton
-open class BlurUtils @Inject constructor(
+open class BlurUtils
+@Inject
+constructor(
@Main resources: Resources,
blurConfig: BlurConfig,
private val crossWindowBlurListeners: CrossWindowBlurListeners,
- dumpManager: DumpManager
+ dumpManager: DumpManager,
) : Dumpable {
val minBlurRadius = resources.getDimensionPixelSize(R.dimen.min_window_blur_radius).toFloat()
- val maxBlurRadius = if (Flags.notificationShadeBlur()) {
- blurConfig.maxBlurRadiusPx
- } else {
- resources.getDimensionPixelSize(R.dimen.max_window_blur_radius).toFloat()
- }
+ val maxBlurRadius =
+ if (Flags.notificationShadeBlur()) {
+ blurConfig.maxBlurRadiusPx
+ } else {
+ resources.getDimensionPixelSize(R.dimen.max_window_blur_radius).toFloat()
+ }
private var lastAppliedBlur = 0
+ private var lastTargetViewRootImpl: ViewRootImpl? = null
+ private var _transactionApplier = SyncRtSurfaceTransactionApplier(null)
+ @VisibleForTesting
+ open val transactionApplier: SyncRtSurfaceTransactionApplier
+ get() = _transactionApplier
+
private var earlyWakeupEnabled = false
/** When this is true, early wakeup flag is not reset on surface flinger when blur drops to 0 */
@@ -65,9 +74,7 @@ open class BlurUtils @Inject constructor(
dumpManager.registerDumpable(this)
}
- /**
- * Translates a ratio from 0 to 1 to a blur radius in pixels.
- */
+ /** Translates a ratio from 0 to 1 to a blur radius in pixels. */
fun blurRadiusOfRatio(ratio: Float): Float {
if (ratio == 0f) {
return 0f
@@ -75,15 +82,18 @@ open class BlurUtils @Inject constructor(
return MathUtils.lerp(minBlurRadius, maxBlurRadius, ratio)
}
- /**
- * Translates a blur radius in pixels to a ratio between 0 to 1.
- */
+ /** Translates a blur radius in pixels to a ratio between 0 to 1. */
fun ratioOfBlurRadius(blur: Float): Float {
if (blur == 0f) {
return 0f
}
- return MathUtils.map(minBlurRadius, maxBlurRadius,
- 0f /* maxStart */, 1f /* maxStop */, blur)
+ return MathUtils.map(
+ minBlurRadius,
+ maxBlurRadius,
+ 0f /* maxStart */,
+ 1f /* maxStop */,
+ blur,
+ )
}
/**
@@ -91,16 +101,20 @@ open class BlurUtils @Inject constructor(
* early-wakeup flag in SurfaceFlinger.
*/
fun prepareBlur(viewRootImpl: ViewRootImpl?, radius: Int) {
- if (viewRootImpl == null || !viewRootImpl.surfaceControl.isValid ||
- !shouldBlur(radius) || earlyWakeupEnabled
+ if (
+ viewRootImpl == null ||
+ !viewRootImpl.surfaceControl.isValid ||
+ !shouldBlur(radius) ||
+ earlyWakeupEnabled
) {
return
}
+ updateTransactionApplier(viewRootImpl)
+ val builder =
+ SyncRtSurfaceTransactionApplier.SurfaceParams.Builder(viewRootImpl.surfaceControl)
if (lastAppliedBlur == 0 && radius != 0) {
- createTransaction().use {
- earlyWakeupStart(it, "eEarlyWakeup (prepareBlur)")
- it.apply()
- }
+ earlyWakeupStart(builder, "eEarlyWakeup (prepareBlur)")
+ transactionApplier.scheduleApply(builder.build())
}
}
@@ -115,25 +129,32 @@ open class BlurUtils @Inject constructor(
if (viewRootImpl == null || !viewRootImpl.surfaceControl.isValid) {
return
}
- createTransaction().use {
- if (shouldBlur(radius)) {
- it.setBackgroundBlurRadius(viewRootImpl.surfaceControl, radius)
- if (!earlyWakeupEnabled && lastAppliedBlur == 0 && radius != 0) {
- earlyWakeupStart(it, "eEarlyWakeup (applyBlur)")
- }
- if (
- earlyWakeupEnabled &&
- lastAppliedBlur != 0 &&
- radius == 0 &&
- !persistentEarlyWakeupRequired
- ) {
- earlyWakeupEnd(it, "applyBlur")
- }
- lastAppliedBlur = radius
+ updateTransactionApplier(viewRootImpl)
+ val builder =
+ SyncRtSurfaceTransactionApplier.SurfaceParams.Builder(viewRootImpl.surfaceControl)
+ if (shouldBlur(radius)) {
+ builder.withBackgroundBlur(radius)
+ if (!earlyWakeupEnabled && lastAppliedBlur == 0 && radius != 0) {
+ earlyWakeupStart(builder, "eEarlyWakeup (applyBlur)")
+ }
+ if (
+ earlyWakeupEnabled &&
+ lastAppliedBlur != 0 &&
+ radius == 0 &&
+ !persistentEarlyWakeupRequired
+ ) {
+ earlyWakeupEnd(builder, "applyBlur")
}
- it.setOpaque(viewRootImpl.surfaceControl, opaque)
- it.apply()
+ lastAppliedBlur = radius
}
+ builder.withOpaque(opaque)
+ transactionApplier.scheduleApply(builder.build())
+ }
+
+ private fun updateTransactionApplier(viewRootImpl: ViewRootImpl) {
+ if (lastTargetViewRootImpl == viewRootImpl) return
+ _transactionApplier = SyncRtSurfaceTransactionApplier(viewRootImpl.view)
+ lastTargetViewRootImpl = viewRootImpl
}
private fun v(verboseLog: String) {
@@ -141,47 +162,49 @@ open class BlurUtils @Inject constructor(
}
@SuppressLint("MissingPermission")
- private fun earlyWakeupStart(transaction: SurfaceControl.Transaction, traceMethodName: String) {
+ private fun earlyWakeupStart(
+ builder: SyncRtSurfaceTransactionApplier.SurfaceParams.Builder,
+ traceMethodName: String,
+ ) {
v("earlyWakeupStart from $traceMethodName")
Trace.asyncTraceForTrackBegin(TRACE_TAG_APP, TRACK_NAME, traceMethodName, 0)
- transaction.setEarlyWakeupStart()
+ builder.withEarlyWakeupStart()
earlyWakeupEnabled = true
}
@SuppressLint("MissingPermission")
- private fun earlyWakeupEnd(transaction: SurfaceControl.Transaction, loggingContext: String) {
+ private fun earlyWakeupEnd(
+ builder: SyncRtSurfaceTransactionApplier.SurfaceParams.Builder,
+ loggingContext: String,
+ ) {
v("earlyWakeupEnd from $loggingContext")
- transaction.setEarlyWakeupEnd()
+ builder.withEarlyWakeupEnd()
Trace.asyncTraceForTrackEnd(TRACE_TAG_APP, TRACK_NAME, 0)
earlyWakeupEnabled = false
}
- @VisibleForTesting
- open fun createTransaction(): SurfaceControl.Transaction {
- return SurfaceControl.Transaction()
- }
-
private fun shouldBlur(radius: Int): Boolean {
return supportsBlursOnWindows() ||
- ((Flags.notificationShadeBlur() || Flags.bouncerUiRevamp()) &&
- supportsBlursOnWindowsBase() &&
- lastAppliedBlur > 0 &&
- radius == 0)
+ ((Flags.notificationShadeBlur() || Flags.bouncerUiRevamp()) &&
+ supportsBlursOnWindowsBase() &&
+ lastAppliedBlur > 0 &&
+ radius == 0)
}
/**
* If this device can render blurs.
*
- * @see android.view.SurfaceControl.Transaction#setBackgroundBlurRadius(SurfaceControl, int)
* @return {@code true} when supported.
+ * @see android.view.SurfaceControl.Transaction#setBackgroundBlurRadius(SurfaceControl, int)
*/
open fun supportsBlursOnWindows(): Boolean {
return supportsBlursOnWindowsBase() && crossWindowBlurListeners.isCrossWindowBlurEnabled
}
private fun supportsBlursOnWindowsBase(): Boolean {
- return CROSS_WINDOW_BLUR_SUPPORTED && ActivityManager.isHighEndGfx() &&
- !SystemProperties.getBoolean("persist.sysui.disableBlur", false)
+ return CROSS_WINDOW_BLUR_SUPPORTED &&
+ ActivityManager.isHighEndGfx() &&
+ !SystemProperties.getBoolean("persist.sysui.disableBlur", false)
}
override fun dump(pw: PrintWriter, args: Array<out String>) {
@@ -203,12 +226,14 @@ open class BlurUtils @Inject constructor(
fun setPersistentEarlyWakeup(persistentWakeup: Boolean, viewRootImpl: ViewRootImpl?) {
persistentEarlyWakeupRequired = persistentWakeup
if (viewRootImpl == null || !supportsBlursOnWindows()) return
+
+ updateTransactionApplier(viewRootImpl)
+ val builder =
+ SyncRtSurfaceTransactionApplier.SurfaceParams.Builder(viewRootImpl.surfaceControl)
if (persistentEarlyWakeupRequired) {
if (earlyWakeupEnabled) return
- createTransaction().use {
- earlyWakeupStart(it, "setEarlyWakeup")
- it.apply()
- }
+ earlyWakeupStart(builder, "setEarlyWakeup")
+ transactionApplier.scheduleApply(builder.build())
} else {
if (!earlyWakeupEnabled) return
if (lastAppliedBlur > 0) {
@@ -219,10 +244,8 @@ open class BlurUtils @Inject constructor(
" was still active",
)
}
- createTransaction().use {
- earlyWakeupEnd(it, "resetEarlyWakeup")
- it.apply()
- }
+ earlyWakeupEnd(builder, "resetEarlyWakeup")
+ transactionApplier.scheduleApply(builder.build())
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ImmersiveModeConfirmation.java b/packages/SystemUI/src/com/android/systemui/statusbar/ImmersiveModeConfirmation.java
index c12742eed169..2a9a47d83dd4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/ImmersiveModeConfirmation.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/ImmersiveModeConfirmation.java
@@ -79,6 +79,7 @@ import com.android.systemui.res.R;
import com.android.systemui.shared.system.TaskStackChangeListener;
import com.android.systemui.shared.system.TaskStackChangeListeners;
import com.android.systemui.util.settings.SecureSettings;
+import com.android.systemui.utils.windowmanager.WindowManagerProvider;
import javax.inject.Inject;
@@ -105,6 +106,7 @@ public class ImmersiveModeConfirmation implements CoreStartable, CommandQueue.Ca
private long mShowDelayMs = 0L;
private final IBinder mWindowToken = new Binder();
private final CommandQueue mCommandQueue;
+ private final WindowManagerProvider mWindowManagerProvider;
private ClingWindowView mClingWindow;
/** The wrapper on the last {@link WindowManager} used to add the confirmation window. */
@@ -131,7 +133,8 @@ public class ImmersiveModeConfirmation implements CoreStartable, CommandQueue.Ca
@Inject
public ImmersiveModeConfirmation(Context context, CommandQueue commandQueue,
- SecureSettings secureSettings, @Background Handler backgroundHandler) {
+ SecureSettings secureSettings, @Background Handler backgroundHandler,
+ WindowManagerProvider windowManagerProvider) {
mSysUiContext = context;
final Display display = mSysUiContext.getDisplay();
mDisplayContext = display.getDisplayId() == DEFAULT_DISPLAY
@@ -139,6 +142,7 @@ public class ImmersiveModeConfirmation implements CoreStartable, CommandQueue.Ca
mCommandQueue = commandQueue;
mSecureSettings = secureSettings;
mBackgroundHandler = backgroundHandler;
+ mWindowManagerProvider = windowManagerProvider;
}
boolean loadSetting(int currentUserId) {
@@ -523,7 +527,7 @@ public class ImmersiveModeConfirmation implements CoreStartable, CommandQueue.Ca
mWindowContextRootDisplayAreaId = rootDisplayAreaId;
mWindowContext = mDisplayContext.createWindowContext(
IMMERSIVE_MODE_CONFIRMATION_WINDOW_TYPE, options);
- mWindowManager = mWindowContext.getSystemService(WindowManager.class);
+ mWindowManager = mWindowManagerProvider.getWindowManager(mWindowContext);
return mWindowManager;
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt
index f844d1da1a8d..50d634f6ac54 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt
@@ -334,6 +334,14 @@ constructor(
private fun onBlurApplied(appliedBlurRadius: Int, zoomOutFromShadeRadius: Float) {
lastAppliedBlur = appliedBlurRadius
+ onZoomOutChanged(zoomOutFromShadeRadius)
+ listeners.forEach { it.onBlurRadiusChanged(appliedBlurRadius) }
+ notificationShadeWindowController.setBackgroundBlurRadius(appliedBlurRadius)
+ }
+
+ private fun onZoomOutChanged(zoomOutFromShadeRadius: Float) {
+ TrackTracer.instantForGroup("shade", "zoom_out", zoomOutFromShadeRadius)
+ Log.v(TAG, "onZoomOutChanged $zoomOutFromShadeRadius")
wallpaperController.setNotificationShadeZoom(zoomOutFromShadeRadius)
if (spatialModelAppPushback()) {
appZoomOutOptional.ifPresent { appZoomOut ->
@@ -341,12 +349,15 @@ constructor(
}
keyguardInteractor.setZoomOut(zoomOutFromShadeRadius)
}
- listeners.forEach {
- it.onBlurRadiusChanged(appliedBlurRadius)
- }
- notificationShadeWindowController.setBackgroundBlurRadius(appliedBlurRadius)
}
+ private val applyZoomOutForFrame =
+ Choreographer.FrameCallback {
+ updateScheduled = false
+ val (_, zoomOutFromShadeRadius) = computeBlurAndZoomOut()
+ onZoomOutChanged(zoomOutFromShadeRadius)
+ }
+
/** Animate blurs when unlocking. */
private val keyguardStateCallback =
object : KeyguardStateController.Callback {
@@ -627,8 +638,17 @@ constructor(
val (blur, zoomOutFromShadeRadius) = computeBlurAndZoomOut()
zoomOutCalculatedFromShadeRadius = zoomOutFromShadeRadius
if (Flags.bouncerUiRevamp() || Flags.glanceableHubBlurredBackground()) {
- updateScheduled =
- windowRootViewBlurInteractor.requestBlurForShade(blur, shouldBlurBeOpaque)
+ if (windowRootViewBlurInteractor.isBlurCurrentlySupported.value) {
+ updateScheduled =
+ windowRootViewBlurInteractor.requestBlurForShade(blur, shouldBlurBeOpaque)
+ return
+ }
+ // When blur is not supported, zoom out still needs to happen when scheduleUpdate
+ // is invoked and a separate frame callback has to be wired-up to support that.
+ if (!updateScheduled) {
+ updateScheduled = true
+ choreographer.postFrameCallback(applyZoomOutForFrame)
+ }
return
}
if (updateScheduled) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/OperatorNameViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/OperatorNameViewController.java
index f5d443443838..60a62d480633 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/OperatorNameViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/OperatorNameViewController.java
@@ -97,7 +97,13 @@ public class OperatorNameViewController extends ViewController<OperatorNameView>
boolean showOperatorName =
mCarrierConfigTracker
.getShowOperatorNameInStatusBarConfig(defaultSubInfo.getSubId())
- && (mTunerService.getValue(KEY_SHOW_OPERATOR_NAME, 1) != 0);
+ && (mTunerService.getValue(
+ KEY_SHOW_OPERATOR_NAME,
+ mView.getResources()
+ .getInteger(
+ com.android.internal.R.integer
+ .config_showOperatorNameDefault))
+ != 0);
mView.update(
showOperatorName,
mTelephonyManager.isDataCapable(),
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/shared/StatusBarNotifChips.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/shared/StatusBarNotifChips.kt
index 6431f303089f..5b989d8e1e7c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/shared/StatusBarNotifChips.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/shared/StatusBarNotifChips.kt
@@ -16,15 +16,18 @@
package com.android.systemui.statusbar.chips.notification.shared
-import com.android.systemui.Flags
+import android.app.Flags
import com.android.systemui.flags.FlagToken
import com.android.systemui.flags.RefactorFlagUtils
+// NOTE: We're merging this flag with the `ui_rich_ongoing` flag.
+// We'll replace all usages of this class with PromotedNotificationUi as a follow-up.
+
/** Helper for reading or using the status bar promoted notification chips flag state. */
@Suppress("NOTHING_TO_INLINE")
object StatusBarNotifChips {
/** The aconfig flag name */
- const val FLAG_NAME = Flags.FLAG_STATUS_BAR_NOTIFICATION_CHIPS
+ const val FLAG_NAME = Flags.FLAG_UI_RICH_ONGOING
/** A token used for dependency declaration */
val token: FlagToken
@@ -33,7 +36,7 @@ object StatusBarNotifChips {
/** Is the refactor enabled */
@JvmStatic
inline val isEnabled
- get() = Flags.statusBarNotificationChips()
+ get() = Flags.uiRichOngoing()
/**
* Called to ensure code is only run when the flag is enabled. This protects users from the
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/ChipContent.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/ChipContent.kt
index fa8d25623d67..18cecb4abc31 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/ChipContent.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/ChipContent.kt
@@ -132,7 +132,11 @@ fun ChipContent(viewModel: OngoingActivityChipModel.Active, modifier: Modifier =
}
is OngoingActivityChipModel.Active.ShortTimeDelta -> {
- val timeRemainingState = rememberTimeRemainingState(futureTimeMillis = viewModel.time)
+ val timeRemainingState =
+ rememberTimeRemainingState(
+ futureTimeMillis = viewModel.time,
+ timeSource = viewModel.timeSource,
+ )
timeRemainingState.timeRemainingData?.let {
val text = formatTimeRemainingData(it)
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 d7b67b1f7bfb..2c4746f5fafb 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
@@ -136,7 +136,8 @@ sealed class OngoingActivityChipModel {
/**
* The [TimeSource] that should be used to track the current time for this timer. Should
- * be compatible with [startTimeMs].
+ * be compatible units with [startTimeMs]. Only used in the Compose version of the
+ * chips.
*/
val timeSource: TimeSource = TimeSource { SystemClock.elapsedRealtime() },
@@ -187,6 +188,12 @@ sealed class OngoingActivityChipModel {
* this model and the [Timer] model use the same units.
*/
@CurrentTimeMillisLong val time: Long,
+
+ /**
+ * The [TimeSource] that should be used to track the current time for this timer. Should
+ * be compatible units with [time]. Only used in the Compose version of the chips.
+ */
+ val timeSource: TimeSource = TimeSource { System.currentTimeMillis() },
override val onClickListenerLegacy: View.OnClickListener?,
override val clickBehavior: ClickBehavior,
override val transitionManager: TransitionManager? = null,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/TimeRemainingState.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/TimeRemainingState.kt
index 803d422c0f0f..2d2d13ce6c27 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/TimeRemainingState.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/TimeRemainingState.kt
@@ -100,10 +100,7 @@ class TimeRemainingState(private val timeSource: TimeSource, private val futureT
/** Remember and manage the TimeRemainingState */
@Composable
-fun rememberTimeRemainingState(
- futureTimeMillis: Long,
- timeSource: TimeSource = remember { TimeSource { System.currentTimeMillis() } },
-): TimeRemainingState {
+fun rememberTimeRemainingState(futureTimeMillis: Long, timeSource: TimeSource): TimeRemainingState {
val state =
remember(timeSource, futureTimeMillis) { TimeRemainingState(timeSource, futureTimeMillis) }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/media/domain/interactor/MediaControlChipInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/media/domain/interactor/MediaControlChipInteractor.kt
index f439bb297de0..1bee1ea1b6c3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/media/domain/interactor/MediaControlChipInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/media/domain/interactor/MediaControlChipInteractor.kt
@@ -19,7 +19,6 @@ package com.android.systemui.statusbar.featurepods.media.domain.interactor
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.media.controls.data.repository.MediaFilterRepository
-import com.android.systemui.media.controls.shared.model.MediaCommonModel
import com.android.systemui.media.controls.shared.model.MediaData
import com.android.systemui.res.R
import com.android.systemui.scene.shared.flag.SceneContainerFlag
@@ -56,7 +55,6 @@ constructor(
mediaList,
userEntries ->
mediaList
- .filterIsInstance<MediaCommonModel.MediaControl>()
.mapNotNull { userEntries[it.mediaLoadedModel.instanceId] }
.firstOrNull { it.active }
?.toMediaControlChipModel()
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java
index 6b32c6a18ec0..a0b3c1729154 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java
@@ -298,6 +298,7 @@ public class NotificationRowBinderImpl implements NotificationRowBinder {
mRowContentBindStage.requestRebind(entry, en -> {
mLogger.logRebindComplete(entry);
row.setIsMinimized(isMinimized);
+ row.setRedactionType(redactionType);
if (inflationCallback != null) {
inflationCallback.onAsyncInflationFinished(en);
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/AODPromotedNotification.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/AODPromotedNotification.kt
index 9282e166f605..2238db505948 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/AODPromotedNotification.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/AODPromotedNotification.kt
@@ -81,7 +81,7 @@ fun AODPromotedNotification(
viewModelFactory: AODPromotedNotificationViewModel.Factory,
modifier: Modifier = Modifier,
) {
- if (!PromotedNotificationUiAod.isEnabled) {
+ if (!PromotedNotificationUi.isEnabled) {
return
}
@@ -170,24 +170,35 @@ private class FrameLayoutWithMaxHeight(maxHeight: Int, context: Context) : Frame
// This mirrors the logic in NotificationContentView.onMeasure.
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
- if (childCount < 1) {
- return
+ if (childCount != 1) {
+ Log.wtf(TAG, "Should contain exactly one child.")
+ return super.onMeasure(widthMeasureSpec, heightMeasureSpec)
}
- val child = getChildAt(0)
- val childLayoutHeight = child.layoutParams.height
- val childHeightSpec =
- if (childLayoutHeight >= 0) {
- makeMeasureSpec(maxHeight.coerceAtMost(childLayoutHeight), EXACTLY)
- } else {
- makeMeasureSpec(maxHeight, AT_MOST)
- }
- measureChildWithMargins(child, widthMeasureSpec, 0, childHeightSpec, 0)
- val childMeasuredHeight = child.measuredHeight
+ val horizPadding = paddingStart + paddingEnd
+ val vertPadding = paddingTop + paddingBottom
+ val ownWidthSize = MeasureSpec.getSize(widthMeasureSpec)
val ownHeightMode = MeasureSpec.getMode(heightMeasureSpec)
val ownHeightSize = MeasureSpec.getSize(heightMeasureSpec)
+ val availableHeight =
+ if (ownHeightMode != UNSPECIFIED) {
+ maxHeight.coerceAtMost(ownHeightSize)
+ } else {
+ maxHeight
+ }
+
+ val child = getChildAt(0)
+ val childWidthSpec = makeMeasureSpec(ownWidthSize, EXACTLY)
+ val childHeightSpec =
+ child.layoutParams.height
+ .takeIf { it >= 0 }
+ ?.let { makeMeasureSpec(availableHeight.coerceAtMost(it), EXACTLY) }
+ ?: run { makeMeasureSpec(availableHeight, AT_MOST) }
+ measureChildWithMargins(child, childWidthSpec, horizPadding, childHeightSpec, vertPadding)
+ val childMeasuredHeight = child.measuredHeight
+
val ownMeasuredWidth = MeasureSpec.getSize(widthMeasureSpec)
val ownMeasuredHeight =
if (ownHeightMode != UNSPECIFIED) {
@@ -195,7 +206,6 @@ private class FrameLayoutWithMaxHeight(maxHeight: Int, context: Context) : Frame
} else {
childMeasuredHeight
}
-
setMeasuredDimension(ownMeasuredWidth, ownMeasuredHeight)
}
}
@@ -205,18 +215,22 @@ private val PromotedNotificationContentModel.layoutResource: Int?
return if (notificationsRedesignTemplates()) {
when (style) {
Style.Base -> R.layout.notification_2025_template_expanded_base
+ Style.CollapsedBase -> R.layout.notification_2025_template_collapsed_base
Style.BigPicture -> R.layout.notification_2025_template_expanded_big_picture
Style.BigText -> R.layout.notification_2025_template_expanded_big_text
Style.Call -> R.layout.notification_2025_template_expanded_call
+ Style.CollapsedCall -> R.layout.notification_2025_template_collapsed_call
Style.Progress -> R.layout.notification_2025_template_expanded_progress
Style.Ineligible -> null
}
} else {
when (style) {
Style.Base -> R.layout.notification_template_material_big_base
+ Style.CollapsedBase -> R.layout.notification_template_material_base
Style.BigPicture -> R.layout.notification_template_material_big_picture
Style.BigText -> R.layout.notification_template_material_big_text
Style.Call -> R.layout.notification_template_material_big_call
+ Style.CollapsedCall -> R.layout.notification_template_material_call
Style.Progress -> R.layout.notification_template_material_progress
Style.Ineligible -> null
}
@@ -333,10 +347,12 @@ private class AODPromotedNotificationViewUpdater(root: View) {
fun update(content: PromotedNotificationContentModel, audiblyAlertedIconVisible: Boolean) {
when (content.style) {
- Style.Base -> updateBase(content)
+ Style.Base -> updateBase(content, collapsed = false)
+ Style.CollapsedBase -> updateBase(content, collapsed = true)
Style.BigPicture -> updateBigPictureStyle(content)
Style.BigText -> updateBigTextStyle(content)
- Style.Call -> updateCallStyle(content)
+ Style.Call -> updateCallStyle(content, collapsed = false)
+ Style.CollapsedCall -> updateCallStyle(content, collapsed = true)
Style.Progress -> updateProgressStyle(content)
Style.Ineligible -> {}
}
@@ -346,11 +362,15 @@ private class AODPromotedNotificationViewUpdater(root: View) {
private fun updateBase(
content: PromotedNotificationContentModel,
+ collapsed: Boolean,
textView: ImageFloatingTextView? = text,
) {
- updateHeader(content)
+ val headerTitleView = if (collapsed) title else null
+ updateHeader(content, titleView = headerTitleView, collapsed = collapsed)
- updateTitle(title, content)
+ if (headerTitleView == null) {
+ updateTitle(title, content)
+ }
updateText(textView, content)
updateSmallIcon(icon, content)
updateImageView(rightIcon, content.skeletonLargeIcon)
@@ -358,21 +378,21 @@ private class AODPromotedNotificationViewUpdater(root: View) {
}
private fun updateBigPictureStyle(content: PromotedNotificationContentModel) {
- updateBase(content)
+ updateBase(content, collapsed = false)
}
private fun updateBigTextStyle(content: PromotedNotificationContentModel) {
- updateBase(content, textView = bigText)
+ updateBase(content, collapsed = false, textView = bigText)
}
- private fun updateCallStyle(content: PromotedNotificationContentModel) {
- updateConversationHeader(content)
+ private fun updateCallStyle(content: PromotedNotificationContentModel, collapsed: Boolean) {
+ updateConversationHeader(content, collapsed = collapsed)
updateText(text, content)
}
private fun updateProgressStyle(content: PromotedNotificationContentModel) {
- updateBase(content)
+ updateBase(content, collapsed = false)
updateNewProgressBar(content)
}
@@ -409,24 +429,35 @@ private class AODPromotedNotificationViewUpdater(root: View) {
}
}
- private fun updateHeader(content: PromotedNotificationContentModel) {
- updateAppName(content)
+ private fun updateHeader(
+ content: PromotedNotificationContentModel,
+ collapsed: Boolean,
+ titleView: TextView?,
+ ) {
+ val hasTitle = titleView != null && content.title != null
+ val hasSubText = content.subText != null
+ // the collapsed form doesn't show the app name unless there is no other text in the header
+ val appNameRequired = !hasTitle && !hasSubText
+ val hideAppName = (!appNameRequired && collapsed)
+
+ updateAppName(content, forceHide = hideAppName)
updateTextView(headerTextSecondary, content.subText)
- // Not calling updateTitle(headerText, content) because the title is always a separate
- // element in the expanded layout used for AOD RONs.
+ updateTitle(titleView, content)
updateTimeAndChronometer(content)
- updateHeaderDividers(content)
+ updateHeaderDividers(content, hideTitle = !hasTitle, hideAppName = hideAppName)
updateTopLine(content)
}
- private fun updateHeaderDividers(content: PromotedNotificationContentModel) {
- val hasAppName = content.appName != null
+ private fun updateHeaderDividers(
+ content: PromotedNotificationContentModel,
+ hideAppName: Boolean,
+ hideTitle: Boolean,
+ ) {
+ val hasAppName = content.appName != null && !hideAppName
val hasSubText = content.subText != null
- // Not setting hasHeader = content.title because the title is always a separate element in
- // the expanded layout used for AOD RONs.
- val hasHeader = false
+ val hasHeader = content.title != null && !hideTitle
val hasTimeOrChronometer = content.time != null
val hasTextBeforeSubText = hasAppName
@@ -442,13 +473,17 @@ private class AODPromotedNotificationViewUpdater(root: View) {
timeDivider?.isVisible = showDividerBeforeTime
}
- private fun updateConversationHeader(content: PromotedNotificationContentModel) {
- updateAppName(content)
+ private fun updateConversationHeader(
+ content: PromotedNotificationContentModel,
+ collapsed: Boolean,
+ ) {
+ updateAppName(content, forceHide = collapsed)
updateTimeAndChronometer(content)
+
updateImageView(verificationIcon, content.verificationIcon)
updateTextView(verificationText, content.verificationText)
- updateConversationHeaderDividers(content)
+ updateConversationHeaderDividers(content, hideTitle = true, hideAppName = collapsed)
updateTopLine(content)
@@ -456,11 +491,13 @@ private class AODPromotedNotificationViewUpdater(root: View) {
updateTitle(conversationText, content)
}
- private fun updateConversationHeaderDividers(content: PromotedNotificationContentModel) {
- // Not setting hasTitle = content.title because the title is always a separate element in
- // the expanded layout used for AOD RONs.
- val hasTitle = false
- val hasAppName = content.appName != null
+ private fun updateConversationHeaderDividers(
+ content: PromotedNotificationContentModel,
+ hideTitle: Boolean,
+ hideAppName: Boolean,
+ ) {
+ val hasTitle = content.title != null && !hideTitle
+ val hasAppName = content.appName != null && !hideAppName
val hasTimeOrChronometer = content.time != null
val hasVerification =
!content.verificationIcon.isNullOrEmpty() || content.verificationText != null
@@ -478,8 +515,8 @@ private class AODPromotedNotificationViewUpdater(root: View) {
verificationDivider?.isVisible = showDividerBeforeVerification
}
- private fun updateAppName(content: PromotedNotificationContentModel) {
- updateTextView(appNameText, content.appName)
+ private fun updateAppName(content: PromotedNotificationContentModel, forceHide: Boolean) {
+ updateTextView(appNameText, content.appName?.takeUnless { forceHide })
}
private fun updateTitle(titleView: TextView?, content: PromotedNotificationContentModel) {
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 d9bdfbc81145..ff97bff53a65 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
@@ -112,12 +112,13 @@ constructor(
if (redactionType == REDACTION_TYPE_NONE) {
privateVersion
} else {
- if (notification.publicVersion == null) {
- privateVersion.toDefaultPublicVersion()
- } else {
- // TODO(b/400991304): implement extraction for [Notification.publicVersion]
- privateVersion.toDefaultPublicVersion()
- }
+ notification.publicVersion?.let { publicNotification ->
+ createAppDefinedPublicVersion(
+ privateModel = privateVersion,
+ publicNotification = publicNotification,
+ imageModelProvider = imageModelProvider,
+ )
+ } ?: createDefaultPublicVersion(privateModel = privateVersion)
}
return PromotedNotificationContentModels(
privateVersion = privateVersion,
@@ -126,19 +127,59 @@ constructor(
.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 copyNonSensitiveFields(
+ privateModel: PromotedNotificationContentModel,
+ publicBuilder: PromotedNotificationContentModel.Builder,
+ ) {
+ publicBuilder.smallIcon = privateModel.smallIcon
+ publicBuilder.iconLevel = privateModel.iconLevel
+ publicBuilder.appName = privateModel.appName
+ publicBuilder.time = privateModel.time
+ publicBuilder.lastAudiblyAlertedMs = privateModel.lastAudiblyAlertedMs
+ publicBuilder.profileBadgeResId = privateModel.profileBadgeResId
+ publicBuilder.colors = privateModel.colors
+ }
+
+ private fun createDefaultPublicVersion(
+ privateModel: PromotedNotificationContentModel
+ ): PromotedNotificationContentModel =
+ PromotedNotificationContentModel.Builder(key = privateModel.identity.key)
+ .also {
+ it.style =
+ if (privateModel.style == Style.Ineligible) Style.Ineligible else Style.Base
+ copyNonSensitiveFields(privateModel, it)
+ }
+ .build()
+
+ private fun createAppDefinedPublicVersion(
+ privateModel: PromotedNotificationContentModel,
+ publicNotification: Notification,
+ imageModelProvider: ImageModelProvider,
+ ): PromotedNotificationContentModel =
+ PromotedNotificationContentModel.Builder(key = privateModel.identity.key)
+ .also { publicBuilder ->
+ val notificationStyle = publicNotification.notificationStyle
+ publicBuilder.style =
+ when {
+ privateModel.style == Style.Ineligible -> Style.Ineligible
+ notificationStyle == CallStyle::class.java -> Style.CollapsedCall
+ else -> Style.CollapsedBase
+ }
+ copyNonSensitiveFields(privateModel = privateModel, publicBuilder = publicBuilder)
+ publicBuilder.shortCriticalText = publicNotification.shortCriticalText()
+ publicBuilder.subText = publicNotification.subText()
+ // The standard public version is extracted as a collapsed notification,
+ // so avoid using bigTitle or bigText, and instead get the collapsed versions.
+ publicBuilder.title = publicNotification.title(notificationStyle, expanded = false)
+ publicBuilder.text = publicNotification.text()
+ publicBuilder.skeletonLargeIcon =
+ publicNotification.skeletonLargeIcon(imageModelProvider)
+ // Only CallStyle has styled content that shows in the collapsed version.
+ if (publicBuilder.style == Style.Call) {
+ extractCallStyleContent(publicNotification, publicBuilder, imageModelProvider)
+ }
+ }
+ .build()
private fun extractPrivateContent(
key: String,
@@ -163,8 +204,8 @@ constructor(
contentBuilder.shortCriticalText = notification.shortCriticalText()
contentBuilder.lastAudiblyAlertedMs = lastAudiblyAlertedMs
contentBuilder.profileBadgeResId = null // TODO
- contentBuilder.title = notification.title(recoveredBuilder.style)
- contentBuilder.text = notification.text(recoveredBuilder.style)
+ contentBuilder.title = notification.title(recoveredBuilder.style?.javaClass)
+ contentBuilder.text = notification.text(recoveredBuilder.style?.javaClass)
contentBuilder.skeletonLargeIcon = notification.skeletonLargeIcon(imageModelProvider)
contentBuilder.oldProgress = notification.oldProgress()
@@ -191,12 +232,16 @@ constructor(
private fun Notification.callPerson(): Person? =
extras?.getParcelable(EXTRA_CALL_PERSON, Person::class.java)
- private fun Notification.title(style: Notification.Style?): CharSequence? {
- return when (style) {
- is BigTextStyle,
- is BigPictureStyle,
- is InboxStyle -> bigTitle()
- is CallStyle -> callPerson()?.name
+ private fun Notification.title(
+ styleClass: Class<out Notification.Style>?,
+ expanded: Boolean = true,
+ ): CharSequence? {
+ // bigTitle is only used in the expanded form of 3 styles.
+ return when (styleClass) {
+ BigTextStyle::class.java,
+ BigPictureStyle::class.java,
+ InboxStyle::class.java -> if (expanded) bigTitle() else null
+ CallStyle::class.java -> callPerson()?.name?.takeUnlessEmpty()
else -> null
} ?: title()
}
@@ -206,9 +251,9 @@ constructor(
private fun Notification.bigText(): CharSequence? =
getCharSequenceExtraUnlessEmpty(EXTRA_BIG_TEXT)
- private fun Notification.text(style: Notification.Style?): CharSequence? {
- return when (style) {
- is BigTextStyle -> bigText()
+ private fun Notification.text(styleClass: Class<out Notification.Style>?): CharSequence? {
+ return when (styleClass) {
+ BigTextStyle::class.java -> bigText()
else -> null
} ?: text()
}
@@ -293,17 +338,15 @@ constructor(
null -> Style.Base
is BigPictureStyle -> {
- style.extractContent(contentBuilder)
Style.BigPicture
}
is BigTextStyle -> {
- style.extractContent(contentBuilder)
Style.BigText
}
is CallStyle -> {
- style.extractContent(notification, contentBuilder, imageModelProvider)
+ extractCallStyleContent(notification, contentBuilder, imageModelProvider)
Style.Call
}
@@ -316,25 +359,11 @@ constructor(
}
}
- private fun BigPictureStyle.extractContent(
- contentBuilder: PromotedNotificationContentModel.Builder
- ) {
- // Big title is handled in resolveTitle, and big picture is unsupported.
- }
-
- private fun BigTextStyle.extractContent(
- contentBuilder: PromotedNotificationContentModel.Builder
- ) {
- // Big title and big text are handled in resolveTitle and resolveText.
- }
-
- private fun CallStyle.extractContent(
+ private fun extractCallStyleContent(
notification: Notification,
contentBuilder: PromotedNotificationContentModel.Builder,
imageModelProvider: ImageModelProvider,
) {
- contentBuilder.personIcon = null // TODO
- contentBuilder.personName = null // TODO
contentBuilder.verificationIcon = notification.skeletonVerificationIcon(imageModelProvider)
contentBuilder.verificationText = notification.verificationText()
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationUiForceExpanded.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationUiForceExpanded.kt
deleted file mode 100644
index adeddde8ccc3..000000000000
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationUiForceExpanded.kt
+++ /dev/null
@@ -1,63 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.statusbar.notification.promoted
-
-import com.android.systemui.Flags
-import com.android.systemui.flags.FlagToken
-import com.android.systemui.flags.RefactorFlagUtils
-
-/** Helper for reading or using the expanded ui rich ongoing flag state. */
-@Suppress("NOTHING_TO_INLINE")
-object PromotedNotificationUiForceExpanded {
- /** The aconfig flag name */
- const val FLAG_NAME = Flags.FLAG_UI_RICH_ONGOING_FORCE_EXPANDED
-
- /** A token used for dependency declaration */
- val token: FlagToken
- get() = FlagToken(FLAG_NAME, isEnabled)
-
- /** Is the refactor enabled */
- @JvmStatic
- inline val isEnabled
- get() = Flags.uiRichOngoingForceExpanded()
-
- /**
- * 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 enabled. This will throw an exception if
- * the flag is not enabled to ensure that the refactor author catches issues in testing.
- * Caution!! Using this check incorrectly will cause crashes in nextfood builds!
- */
- @JvmStatic
- @Deprecated("Avoid crashing.", ReplaceWith("if (this.isUnexpectedlyInLegacyMode()) return"))
- inline fun unsafeAssertInNewMode() =
- RefactorFlagUtils.unsafeAssertInNewMode(isEnabled, FLAG_NAME)
-
- /**
- * Called to ensure code is only run when the flag is disabled. This will throw an exception if
- * the flag is enabled to ensure that the refactor author catches issues in testing.
- */
- @JvmStatic
- inline fun assertInLegacyMode() = RefactorFlagUtils.assertInLegacyMode(isEnabled, FLAG_NAME)
-}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/AODPromotedNotificationInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/AODPromotedNotificationInteractor.kt
index d9778bdde0a5..fa9a7b9b524e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/AODPromotedNotificationInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/AODPromotedNotificationInteractor.kt
@@ -18,10 +18,13 @@ package com.android.systemui.statusbar.notification.promoted.domain.interactor
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dump.DumpManager
+import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel
+import com.android.systemui.statusbar.policy.domain.interactor.SensitiveNotificationProtectionInteractor
import com.android.systemui.util.kotlin.FlowDumperImpl
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
@@ -30,14 +33,31 @@ class AODPromotedNotificationInteractor
@Inject
constructor(
promotedNotificationsInteractor: PromotedNotificationsInteractor,
+ keyguardInteractor: KeyguardInteractor,
+ sensitiveNotificationProtectionInteractor: SensitiveNotificationProtectionInteractor,
dumpManager: DumpManager,
) : FlowDumperImpl(dumpManager) {
+
+ /**
+ * Whether the system is unlocked and not screensharing such that private notification content
+ * is allowed to show on the aod
+ */
+ private val canShowPrivateNotificationContent: Flow<Boolean> =
+ combine(
+ keyguardInteractor.isKeyguardDismissible,
+ sensitiveNotificationProtectionInteractor.isSensitiveStateActive,
+ ) { isKeyguardDismissible, isSensitive ->
+ isKeyguardDismissible && !isSensitive
+ }
+
/** 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
+ combine(
+ promotedNotificationsInteractor.aodPromotedNotification,
+ canShowPrivateNotificationContent,
+ ) { promotedContent, showPrivateContent ->
+ if (showPrivateContent) promotedContent?.privateVersion
+ else promotedContent?.publicVersion
}
.distinctUntilNewInstance()
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 339a5bb29a34..79081f42c686 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
@@ -88,8 +88,6 @@ data class PromotedNotificationContentModel(
val style: Style,
// for CallStyle:
- val personIcon: ImageModel?,
- val personName: CharSequence?,
val verificationIcon: ImageModel?,
val verificationText: CharSequence?,
@@ -114,8 +112,6 @@ data class PromotedNotificationContentModel(
var colors: Colors = Colors(backgroundColor = 0, primaryTextColor = 0)
// for CallStyle:
- var personIcon: ImageModel? = null
- var personName: CharSequence? = null
var verificationIcon: ImageModel? = null
var verificationText: CharSequence? = null
@@ -140,8 +136,6 @@ data class PromotedNotificationContentModel(
oldProgress = oldProgress,
colors = colors,
style = style,
- personIcon = personIcon,
- personName = personName,
verificationIcon = verificationIcon,
verificationText = verificationText,
newProgress = newProgress,
@@ -174,9 +168,11 @@ data class PromotedNotificationContentModel(
/** The promotion-eligible style of a notification, or [Style.Ineligible] if not. */
enum class Style {
Base, // style == null
+ CollapsedBase, // style == null
BigPicture,
BigText,
Call,
+ CollapsedCall,
Progress,
Ineligible,
}
@@ -198,8 +194,6 @@ data class PromotedNotificationContentModel(
"oldProgress=$oldProgress, " +
"colors=$colors, " +
"style=$style, " +
- "personIcon=${personIcon?.toRedactedString()}, " +
- "personName=${personName?.toRedactedString()}, " +
"verificationIcon=$verificationIcon, " +
"verificationText=$verificationText, " +
"newProgress=$newProgress)")
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 bef3c691cb4d..3fed78674cf9 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
@@ -26,6 +26,7 @@ import static com.android.systemui.Flags.notificationRowAccessibilityExpanded;
import static com.android.systemui.Flags.notificationRowTransparency;
import static com.android.systemui.Flags.notificationsPinnedHunInShade;
import static com.android.systemui.flags.Flags.ENABLE_NOTIFICATIONS_SIMULATE_SLOW_MEASURE;
+import static com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_NONE;
import static com.android.systemui.statusbar.notification.NotificationUtils.logKey;
import static com.android.systemui.statusbar.notification.collection.NotificationEntry.DismissState.PARENT_DISMISSED;
import static com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_HEADSUP;
@@ -102,6 +103,7 @@ import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin.MenuItem
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.res.R;
import com.android.systemui.scene.shared.flag.SceneContainerFlag;
+import com.android.systemui.statusbar.NotificationLockscreenUserManager.RedactionType;
import com.android.systemui.statusbar.RemoteInputController;
import com.android.systemui.statusbar.SmartReplyController;
import com.android.systemui.statusbar.StatusBarIconView;
@@ -126,7 +128,6 @@ import com.android.systemui.statusbar.notification.headsup.PinnedStatus;
import com.android.systemui.statusbar.notification.logging.NotificationCounters;
import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier;
import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUi;
-import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUiForceExpanded;
import com.android.systemui.statusbar.notification.row.shared.AsyncGroupHeaderViewInflation;
import com.android.systemui.statusbar.notification.row.shared.LockscreenOtpRedaction;
import com.android.systemui.statusbar.notification.row.ui.viewmodel.BundleHeaderViewModelImpl;
@@ -503,7 +504,8 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
private final ListenerSet<DismissButtonTargetVisibilityListener>
mDismissButtonTargetVisibilityListeners = new ListenerSet<>();
-
+ @RedactionType
+ private int mRedactionType = REDACTION_TYPE_NONE;
public NotificationContentView[] getLayouts() {
return Arrays.copyOf(mLayouts, mLayouts.length);
}
@@ -879,7 +881,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
private void updateLimitsForView(NotificationContentView layout) {
final int maxExpandedHeight;
- if (PromotedNotificationUiForceExpanded.isEnabled() && isPromotedOngoing()) {
+ if (isPromotedOngoing()) {
maxExpandedHeight = mMaxExpandedHeightForPromotedOngoing;
} else {
maxExpandedHeight = mMaxExpandedHeight;
@@ -1378,7 +1380,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
if (mIsSummaryWithChildren) {
return mChildrenContainer.getIntrinsicHeight();
}
- if (PromotedNotificationUiForceExpanded.isEnabled() && isPromotedOngoing()) {
+ if (isPromotedOngoing()) {
return getMaxExpandHeight();
}
if (mExpandedWhenPinned) {
@@ -1867,6 +1869,13 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
}
/**
+ * Set the redaction type of the row.
+ */
+ public void setRedactionType(@RedactionType int redactionType) {
+ mRedactionType = redactionType;
+ }
+
+ /**
* Init the bundle header view. The ComposeView is initialized within with the passed viewModel.
* This can only be init once and not in conjunction with any other header view.
*/
@@ -3020,7 +3029,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
if (mIsSummaryWithChildren && !shouldShowPublic()) {
return !mChildrenExpanded;
}
- if (PromotedNotificationUiForceExpanded.isEnabled() && isPromotedOngoing()) {
+ if (isPromotedOngoing()) {
return false;
}
return mEnableNonGroupedNotificationExpand && mExpandable;
@@ -3131,7 +3140,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
}
public void setUserLocked(boolean userLocked) {
- if (PromotedNotificationUiForceExpanded.isEnabled() && isPromotedOngoing()) return;
+ if (isPromotedOngoing()) return;
mUserLocked = userLocked;
mPrivateLayout.setUserExpanding(userLocked);
@@ -3401,7 +3410,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
}
public boolean isExpanded(boolean allowOnKeyguard) {
- if (PromotedNotificationUiForceExpanded.isEnabled() && isPromotedOngoing()) {
+ if (isPromotedOngoing()) {
return isPromotedNotificationExpanded(allowOnKeyguard);
}
@@ -4516,6 +4525,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
pw.print(", expandedWhenPinned: " + mExpandedWhenPinned);
pw.print(", isMinimized: " + mIsMinimized);
pw.print(", isAboveShelf: " + isAboveShelf());
+ pw.print(", redactionType: " + mRedactionType);
pw.println();
if (NotificationContentView.INCLUDE_HEIGHTS_TO_DUMP) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java
index 488aa44ddd3b..756a2c19c10e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java
@@ -1248,7 +1248,7 @@ public class NotificationContentView extends FrameLayout implements Notification
final boolean isSingleLineViewPresent = mSingleLineView != null;
if (shouldShowSingleLineView && !isSingleLineViewPresent) {
- Log.e(TAG, "calculateVisibleType: SingleLineView is not available!");
+ Log.wtf(TAG, "calculateVisibleType: SingleLineView is not available!");
}
final int collapsedVisualType = shouldShowSingleLineView && isSingleLineViewPresent
@@ -1274,9 +1274,6 @@ public class NotificationContentView extends FrameLayout implements Notification
}
final boolean shouldShowSingleLineView = mIsChildInGroup && !isGroupExpanded();
final boolean isSingleLinePresent = mSingleLineView != null;
- if (shouldShowSingleLineView && !isSingleLinePresent) {
- Log.e(TAG, "getVisualTypeForHeight: singleLineView is not available.");
- }
if (!mUserExpanding && shouldShowSingleLineView && isSingleLinePresent) {
return VISIBLE_TYPE_SINGLELINE;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfo.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfo.java
index 2cf3b14bb8c5..0257b4c2397e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfo.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfo.java
@@ -218,7 +218,7 @@ public class NotificationConversationInfo extends LinearLayout implements
@Background Handler bgHandler,
OnConversationSettingsClickListener onConversationSettingsClickListener,
Optional<BubblesManager> bubblesManagerOptional,
- ShadeController shadeController) {
+ ShadeController shadeController, boolean isDismissable, OnClickListener onCloseClick) {
mINotificationManager = iNotificationManager;
mPeopleSpaceWidgetManager = peopleSpaceWidgetManager;
mOnUserInteractionCallback = onUserInteractionCallback;
@@ -263,6 +263,11 @@ public class NotificationConversationInfo extends LinearLayout implements
bindHeader();
bindActions();
+ View dismissButton = findViewById(R.id.inline_dismiss);
+ dismissButton.setOnClickListener(onCloseClick);
+ dismissButton.setVisibility(dismissButton.hasOnClickListeners() && isDismissable
+ ? VISIBLE : GONE);
+
View done = findViewById(R.id.done);
done.setOnClickListener(mOnDone);
done.setAccessibilityDelegate(mGutsContainer.getAccessibilityDelegate());
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 6c7c7a79348f..d0567f08c2f1 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
@@ -608,7 +608,9 @@ public class NotificationGutsManager implements NotifGutsViewManager, CoreStarta
mBgHandler,
onConversationSettingsListener,
mBubblesManagerOptional,
- mShadeController);
+ mShadeController,
+ row.canViewBeDismissed(),
+ row.getCloseButtonOnClickListener(row));
}
/**
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowImageInflater.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowImageInflater.kt
index 7bac17f4c227..215988471f00 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowImageInflater.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowImageInflater.kt
@@ -21,7 +21,7 @@ import android.graphics.drawable.Drawable
import android.graphics.drawable.Icon
import androidx.annotation.VisibleForTesting
import com.android.app.tracing.traceSection
-import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUiAod
+import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUi
import com.android.systemui.statusbar.notification.row.shared.IconData
import com.android.systemui.statusbar.notification.row.shared.ImageModel
import com.android.systemui.statusbar.notification.row.shared.ImageModelProvider
@@ -80,7 +80,7 @@ interface RowImageInflater {
companion object {
@Suppress("NOTHING_TO_INLINE")
@JvmStatic
- inline fun featureFlagEnabled() = PromotedNotificationUiAod.isEnabled
+ inline fun featureFlagEnabled() = PromotedNotificationUi.isEnabled
@JvmStatic
fun newInstance(previousIndex: ImageModelIndex?, reinflating: Boolean): RowImageInflater =
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationTemplateViewWrapper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationTemplateViewWrapper.java
index 19321dcef5c7..c4fe25031de3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationTemplateViewWrapper.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationTemplateViewWrapper.java
@@ -43,7 +43,6 @@ import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.ContrastColorUtil;
import com.android.internal.widget.NotificationActionListLayout;
import com.android.systemui.Dependency;
-import com.android.systemui.Flags;
import com.android.systemui.UiOffloadThread;
import com.android.systemui.res.R;
import com.android.systemui.statusbar.CrossFadeHelper;
@@ -196,7 +195,7 @@ public class NotificationTemplateViewWrapper extends NotificationHeaderViewWrapp
}
private void adjustTitleAndRightIconForPromotedOngoing() {
- if (Flags.uiRichOngoingForceExpanded() && mRow.isPromotedOngoing() && mRightIcon != null) {
+ if (mRow.isPromotedOngoing() && mRightIcon != null) {
final int horizontalMargin;
if (notificationsRedesignTemplates()) {
horizontalMargin = mView.getResources().getDimensionPixelSize(
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationUiAod.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/AvalancheReplaceHunWhenCritical.kt
index c6e3da1c5750..0ccd6064d9a3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationUiAod.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/AvalancheReplaceHunWhenCritical.kt
@@ -14,16 +14,17 @@
* limitations under the License.
*/
-package com.android.systemui.statusbar.notification.promoted
+package com.android.systemui.statusbar.notification.shared
import com.android.systemui.Flags
import com.android.systemui.flags.FlagToken
import com.android.systemui.flags.RefactorFlagUtils
-/** Helper for reading or using the promoted ongoing notifications AOD flag state. */
-object PromotedNotificationUiAod {
+/** Helper for reading or using the avalanche replace Hun when critical flag state. */
+@Suppress("NOTHING_TO_INLINE")
+object AvalancheReplaceHunWhenCritical {
/** The aconfig flag name */
- const val FLAG_NAME = Flags.FLAG_AOD_UI_RICH_ONGOING
+ const val FLAG_NAME = Flags.FLAG_AVALANCHE_REPLACE_HUN_WHEN_CRITICAL
/** A token used for dependency declaration */
val token: FlagToken
@@ -32,7 +33,7 @@ object PromotedNotificationUiAod {
/** Is the refactor enabled */
@JvmStatic
inline val isEnabled
- get() = Flags.aodUiRichOngoing()
+ get() = Flags.avalancheReplaceHunWhenCritical()
/**
* Called to ensure code is only run when the flag is enabled. This protects users from the
@@ -45,16 +46,6 @@ object PromotedNotificationUiAod {
/**
* Called to ensure code is only run when the flag is disabled. This will throw an exception if
- * the flag is not enabled to ensure that the refactor author catches issues in testing.
- * Caution!! Using this check incorrectly will cause crashes in nextfood builds!
- */
- @JvmStatic
- @Deprecated("Avoid crashing.", ReplaceWith("if (this.isUnexpectedlyInLegacyMode()) return"))
- inline fun unsafeAssertInNewMode() =
- RefactorFlagUtils.unsafeAssertInNewMode(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
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 afa988dd8e89..a01e504d47c6 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
@@ -64,7 +64,6 @@ import android.util.AttributeSet;
import android.util.IndentingPrintWriter;
import android.util.Log;
import android.util.MathUtils;
-import android.util.Pair;
import android.view.DisplayCutout;
import android.view.InputDevice;
import android.view.LayoutInflater;
@@ -141,7 +140,6 @@ import com.android.systemui.statusbar.notification.stack.ui.view.NotificationScr
import com.android.systemui.statusbar.phone.HeadsUpAppearanceController;
import com.android.systemui.statusbar.policy.ScrollAdapter;
import com.android.systemui.statusbar.policy.SplitShadeStateController;
-import com.android.systemui.statusbar.ui.SystemBarUtilsProxy;
import com.android.systemui.util.Assert;
import com.android.systemui.util.ColorUtilKt;
import com.android.systemui.util.DumpUtilsKt;
@@ -2253,6 +2251,7 @@ public class NotificationStackScrollLayout
}
public void setFinishScrollingCallback(Runnable runnable) {
+ SceneContainerFlag.assertInLegacyMode();
mFinishScrollingCallback = runnable;
}
@@ -2763,6 +2762,8 @@ public class NotificationStackScrollLayout
* which means we want to scroll towards the top.
*/
protected void fling(int velocityY) {
+ // Scrolls and flings are handled by the Composables with SceneContainer enabled
+ SceneContainerFlag.assertInLegacyMode();
if (getChildCount() > 0) {
float topAmount = getCurrentOverScrollAmount(true);
float bottomAmount = getCurrentOverScrollAmount(false);
@@ -3857,7 +3858,10 @@ public class NotificationStackScrollLayout
}
break;
case ACTION_UP:
- if (mIsBeingDragged) {
+ if (SceneContainerFlag.isEnabled() && mIsBeingDragged) {
+ mActivePointerId = INVALID_POINTER;
+ endDrag();
+ } else if (mIsBeingDragged) {
final VelocityTracker velocityTracker = mVelocityTracker;
velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId);
@@ -3920,6 +3924,7 @@ public class NotificationStackScrollLayout
}
boolean isFlingAfterUpEvent() {
+ SceneContainerFlag.assertInLegacyMode();
return mFlingAfterUpEvent;
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
index 7ac7905c8a48..db56718e9f22 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
@@ -2091,7 +2091,7 @@ public class NotificationStackScrollLayoutController implements Dumpable {
// We log any touches other than down, which will be captured by onTouchEvent.
// In the intercept we only start tracing when it's not a down (otherwise that down
// would be duplicated when intercepted).
- if (mJankMonitor != null && scrollWantsIt
+ if (!SceneContainerFlag.isEnabled() && mJankMonitor != null && scrollWantsIt
&& ev.getActionMasked() != MotionEvent.ACTION_DOWN) {
mJankMonitor.begin(mView, CUJ_NOTIFICATION_SHADE_SCROLL_FLING);
}
@@ -2172,7 +2172,9 @@ public class NotificationStackScrollLayoutController implements Dumpable {
}
mView.setCheckForLeaveBehind(true);
}
- traceJankOnTouchEvent(ev.getActionMasked(), scrollerWantsIt);
+ if (!SceneContainerFlag.isEnabled()) {
+ traceJankOnTouchEvent(ev.getActionMasked(), scrollerWantsIt);
+ }
return horizontalSwipeWantsIt || scrollerWantsIt || expandWantsIt || longPressWantsIt
|| hunWantsIt;
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculator.kt
index e5071d9c1e53..58df1703a925 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculator.kt
@@ -29,7 +29,6 @@ import com.android.systemui.statusbar.LockscreenShadeTransitionController
import com.android.systemui.statusbar.StatusBarState.KEYGUARD
import com.android.systemui.statusbar.SysuiStatusBarStateController
import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor
-import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUiForceExpanded
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
import com.android.systemui.statusbar.notification.row.ExpandableView
import com.android.systemui.statusbar.notification.shared.NotificationBundleUi
@@ -476,9 +475,7 @@ constructor(
if (onLockscreen) {
if (
view is ExpandableNotificationRow &&
- (canPeek ||
- (PromotedNotificationUiForceExpanded.isEnabled &&
- view.isPromotedOngoing))
+ (canPeek || view.isPromotedOngoing)
) {
height
} else {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt
index 0ea9509f0c13..e3c2fb5b6e59 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt
@@ -25,9 +25,11 @@ import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
import com.android.systemui.common.shared.model.NotificationContainerBounds
import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor
import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor
+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.dump.DumpManager
+import com.android.systemui.kairos.awaitClose
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
import com.android.systemui.keyguard.shared.model.Edge
@@ -95,6 +97,7 @@ import com.android.systemui.util.kotlin.BooleanFlowOperators.not
import com.android.systemui.util.kotlin.FlowDumperImpl
import com.android.systemui.util.kotlin.Utils.Companion.sample as sampleCombine
import com.android.systemui.util.kotlin.sample
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
import dagger.Lazy
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
@@ -357,14 +360,31 @@ constructor(
)
.dumpValue("isOnLockscreenWithoutShade")
+ private val aboutToTransitionToHub: Flow<Unit> =
+ if (SceneContainerFlag.isEnabled) {
+ emptyFlow()
+ } else {
+ conflatedCallbackFlow {
+ val callback =
+ CommunalSceneInteractor.OnSceneAboutToChangeListener { toScene, _ ->
+ if (toScene == CommunalScenes.Communal) {
+ trySend(Unit)
+ }
+ }
+ communalSceneInteractor.registerSceneStateProcessor(callback)
+ awaitClose { communalSceneInteractor.unregisterSceneStateProcessor(callback) }
+ }
+ }
+
/** If the user is visually on the glanceable hub or transitioning to/from it */
private val isOnGlanceableHub: Flow<Boolean> =
- combine(
- keyguardTransitionInteractor.isFinishedIn(
- content = Scenes.Communal,
- stateWithoutSceneContainer = GLANCEABLE_HUB,
- ),
+ merge(
+ aboutToTransitionToHub.map { true },
anyOf(
+ keyguardTransitionInteractor.isFinishedIn(
+ content = Scenes.Communal,
+ stateWithoutSceneContainer = GLANCEABLE_HUB,
+ ),
keyguardTransitionInteractor.isInTransition(
edge = Edge.create(to = Scenes.Communal),
edgeWithoutSceneContainer = Edge.create(to = GLANCEABLE_HUB),
@@ -374,9 +394,7 @@ constructor(
edgeWithoutSceneContainer = Edge.create(from = GLANCEABLE_HUB),
),
),
- ) { isOnGlanceableHub, transitioningToOrFromHub ->
- isOnGlanceableHub || transitioningToOrFromHub
- }
+ )
.distinctUntilChanged()
.dumpWhileCollecting("isOnGlanceableHub")
@@ -532,6 +550,7 @@ constructor(
emit(1f - qsExpansion)
}
}
+
Split ->
combineTransform(isAnyExpanded, bouncerInteractor.bouncerExpansion) {
isAnyExpanded,
@@ -544,6 +563,7 @@ constructor(
emit(1f)
}
}
+
Dual ->
combineTransform(
shadeModeInteractor.isShadeLayoutWide,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt
index 10821dffd394..1f4ccd59b063 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt
@@ -84,7 +84,7 @@ interface MobileIconsInteractor {
val icons: StateFlow<List<MobileIconInteractor>>
/** Whether the mobile icons can be stacked vertically. */
- val isStackable: StateFlow<Boolean>
+ val isStackable: Flow<Boolean>
/**
* Observable for the subscriptionId of the current mobile data connection. Null if we don't
@@ -309,21 +309,20 @@ constructor(
override val isStackable =
if (NewStatusBarIcons.isEnabled && StatusBarRootModernization.isEnabled) {
- icons.flatMapLatest { icons ->
- combine(icons.map { it.signalLevelIcon }) { signalLevelIcons ->
- // These are only stackable if:
- // - They are cellular
- // - There's exactly two
- // - They have the same number of levels
- signalLevelIcons.filterIsInstance<SignalIconModel.Cellular>().let {
- it.size == 2 && it[0].numberOfLevels == it[1].numberOfLevels
- }
+ icons.flatMapLatest { icons ->
+ combine(icons.map { it.signalLevelIcon }) { signalLevelIcons ->
+ // These are only stackable if:
+ // - They are cellular
+ // - There's exactly two
+ // - They have the same number of levels
+ signalLevelIcons.filterIsInstance<SignalIconModel.Cellular>().let {
+ it.size == 2 && it[0].numberOfLevels == it[1].numberOfLevels
}
}
- } else {
- flowOf(false)
}
- .stateIn(scope, SharingStarted.WhileSubscribed(), false)
+ } else {
+ flowOf(false)
+ }
/**
* Copied from the old pipeline. We maintain a 2s period of time where we will keep the
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 54cd8e3c46e4..72ff3b67c317 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
@@ -18,9 +18,11 @@ package com.android.systemui.statusbar.pipeline.mobile.ui.binder
import androidx.compose.material3.LocalContentColor
import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.getValue
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.repeatOnLifecycle
import com.android.systemui.Flags
import com.android.systemui.kairos.ExperimentalKairosApi
@@ -48,7 +50,7 @@ object StackedMobileIconBinder {
return SingleBindableStatusBarComposeIconView.withDefaultBinding(
view = view,
shouldBeVisible = { mobileIconsViewModel.isStackable.value },
- ) { _, tint ->
+ ) { _, tintFlow ->
view.repeatWhenAttached {
repeatOnLifecycle(Lifecycle.State.STARTED) {
view.composeView.apply {
@@ -66,8 +68,9 @@ object StackedMobileIconBinder {
viewModelFactory.create()
}
}
+ val tint by tintFlow.collectAsStateWithLifecycle()
if (viewModel.isIconVisible) {
- CompositionLocalProvider(LocalContentColor provides Color(tint())) {
+ CompositionLocalProvider(LocalContentColor provides Color(tint)) {
StackedMobileIcon(viewModel)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModel.kt
index 494d95e7f177..997b185fdee5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModel.kt
@@ -36,6 +36,8 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
@@ -99,7 +101,17 @@ constructor(
}
.stateIn(scope, SharingStarted.WhileSubscribed(), false)
- val isStackable: StateFlow<Boolean> = interactor.isStackable
+ /** Whether all of [mobileSubViewModels] are visible or not. */
+ private val iconsAreAllVisible =
+ mobileSubViewModels.flatMapLatest { viewModels ->
+ combine(viewModels.map { it.isVisible }) { isVisibleArray -> isVisibleArray.all { it } }
+ }
+
+ val isStackable: StateFlow<Boolean> =
+ combine(iconsAreAllVisible, interactor.isStackable) { isVisible, isStackable ->
+ isVisible && isStackable
+ }
+ .stateIn(scope, SharingStarted.WhileSubscribed(), false)
init {
scope.launch { subscriptionIdsFlow.collect { invalidateCaches(it) } }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/view/SingleBindableStatusBarComposeIconView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/view/SingleBindableStatusBarComposeIconView.kt
index 8076040564fb..9d1df8967fc0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/view/SingleBindableStatusBarComposeIconView.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/view/SingleBindableStatusBarComposeIconView.kt
@@ -35,6 +35,7 @@ import com.android.systemui.statusbar.pipeline.shared.ui.binder.ModernStatusBarV
import com.android.systemui.statusbar.pipeline.shared.ui.binder.ModernStatusBarViewVisibilityHelper
import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
/** Compose view that is bound to bindable_status_bar_compose_icon.xml */
class SingleBindableStatusBarComposeIconView(context: Context, attrs: AttributeSet?) :
@@ -78,7 +79,7 @@ class SingleBindableStatusBarComposeIconView(context: Context, attrs: AttributeS
fun withDefaultBinding(
view: SingleBindableStatusBarComposeIconView,
shouldBeVisible: () -> Boolean,
- block: suspend LifecycleOwner.(View, () -> Int) -> Unit,
+ block: suspend LifecycleOwner.(View, StateFlow<Int>) -> Unit,
): ModernStatusBarViewBinding {
@StatusBarIconView.VisibleState
val visibilityState: MutableStateFlow<Int> = MutableStateFlow(STATE_HIDDEN)
@@ -90,7 +91,7 @@ class SingleBindableStatusBarComposeIconView(context: Context, attrs: AttributeS
view.repeatWhenAttached {
// Child binding
- block(view) { iconTint.value }
+ block(view, iconTint)
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyStateInflater.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyStateInflater.kt
index b33c2005479e..4e18935834cf 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyStateInflater.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyStateInflater.kt
@@ -34,6 +34,10 @@ import android.graphics.drawable.Icon
import android.os.Build
import android.os.Bundle
import android.os.SystemClock
+import android.text.Annotation
+import android.text.SpannableStringBuilder
+import android.text.Spanned
+import android.text.style.ForegroundColorSpan
import android.util.Log
import android.view.ContextThemeWrapper
import android.view.LayoutInflater
@@ -504,15 +508,40 @@ constructor(
choice: CharSequence,
delayOnClickListener: Boolean,
): Button {
- val layoutRes =
+ val enableAnimatedReply = Flags.notificationAnimatedActionsTreatment() &&
+ smartReplies.fromAssistant && isAnimatedReply(choice)
+ val layoutRes = if (enableAnimatedReply) {
+ R.layout.animated_action_button
+ } else {
if (notificationsRedesignTemplates()) R.layout.notification_2025_smart_reply_button
else R.layout.smart_reply_button
+ }
+
return (LayoutInflater.from(parent.context).inflate(layoutRes, parent, false) as Button)
.apply {
- text = choice
+ // choiceToDeliver does not contain Annotation with extra data
+ val choiceToDeliver: CharSequence
+ if (enableAnimatedReply) {
+ choiceToDeliver = choice.toString()
+ // If the choice is animated reply, format the text by concatenating
+ // attributionText with different color to choice text
+ val fullTextWithAttribution = formatChoiceWithAttribution(choice)
+ text = fullTextWithAttribution
+ } else {
+ choiceToDeliver = choice
+ text = choice
+ }
+
val onClickListener =
View.OnClickListener {
- onSmartReplyClick(entry, smartReplies, replyIndex, parent, this, choice)
+ onSmartReplyClick(
+ entry,
+ smartReplies,
+ replyIndex,
+ parent,
+ this,
+ choiceToDeliver
+ )
}
setOnClickListener(
if (delayOnClickListener)
@@ -600,6 +629,47 @@ constructor(
RemoteInput.setResultsSource(intent, RemoteInput.SOURCE_CHOICE)
return intent
}
+
+ // Check if the choice is animated reply
+ private fun isAnimatedReply(choice: CharSequence): Boolean {
+ if (choice is Spanned) {
+ val annotations = choice.getSpans(0, choice.length, Annotation::class.java)
+ for (annotation in annotations) {
+ if (annotation.key == "isAnimatedReply" && annotation.value == "1") {
+ return true
+ }
+ }
+ }
+ return false
+ }
+
+ // Format the text by concatenating attributionText with attribution text color to choice text
+ private fun formatChoiceWithAttribution(choice: CharSequence): CharSequence {
+ val colorInt = context.getColor(R.color.animated_action_button_attribution_color)
+ if (choice is Spanned) {
+ val annotations = choice.getSpans(0, choice.length, Annotation::class.java)
+ for (annotation in annotations) {
+ if (annotation.key == "attributionText") {
+ // Extract the attribution text
+ val extraText = annotation.value
+ // Concatenate choice text and attribution text
+ val spannableWithColor = SpannableStringBuilder(choice)
+ spannableWithColor.append(" $extraText")
+ // Apply color to attribution text
+ spannableWithColor.setSpan(
+ ForegroundColorSpan(colorInt),
+ choice.length,
+ spannableWithColor.length,
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
+ )
+ return spannableWithColor
+ }
+ }
+ }
+
+ // Return the original if no attributionText found
+ return choice.toString()
+ }
}
/**
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/SensitiveNotificationProtectionInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/SensitiveNotificationProtectionInteractor.kt
new file mode 100644
index 000000000000..0a6a4c2e44e7
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/SensitiveNotificationProtectionInteractor.kt
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.policy.domain.interactor
+
+import com.android.server.notification.Flags
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.statusbar.policy.SensitiveNotificationProtectionController
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
+import javax.inject.Inject
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flowOf
+
+/** A interactor which provides the current sensitive notification protections status */
+@SysUISingleton
+class SensitiveNotificationProtectionInteractor
+@Inject
+constructor(private val controller: SensitiveNotificationProtectionController) {
+
+ /** sensitive notification protections status */
+ val isSensitiveStateActive: Flow<Boolean> =
+ if (Flags.screenshareNotificationHiding()) {
+ conflatedCallbackFlow {
+ val listener = Runnable { trySend(controller.isSensitiveStateActive) }
+ controller.registerSensitiveStateListener(listener)
+ trySend(controller.isSensitiveStateActive)
+ awaitClose { controller.unregisterSensitiveStateListener(listener) }
+ }
+ .distinctUntilChanged()
+ } else {
+ flowOf(false)
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/util/display/DisplayHelper.java b/packages/SystemUI/src/com/android/systemui/util/display/DisplayHelper.java
index 7d1c631e606a..757b2d973312 100644
--- a/packages/SystemUI/src/com/android/systemui/util/display/DisplayHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/util/display/DisplayHelper.java
@@ -21,6 +21,8 @@ import android.hardware.display.DisplayManager;
import android.view.Display;
import android.view.WindowManager;
+import com.android.systemui.utils.windowmanager.WindowManagerProvider;
+
import javax.inject.Inject;
/**
@@ -29,14 +31,17 @@ import javax.inject.Inject;
public class DisplayHelper {
private final Context mContext;
private final DisplayManager mDisplayManager;
+ private final WindowManagerProvider mWindowManagerProvider;
/**
* Default constructor.
*/
@Inject
- public DisplayHelper(Context context, DisplayManager displayManager) {
+ public DisplayHelper(Context context, DisplayManager displayManager,
+ WindowManagerProvider windowManagerProvider) {
mContext = context;
mDisplayManager = displayManager;
+ mWindowManagerProvider = windowManagerProvider;
}
@@ -45,9 +50,8 @@ public class DisplayHelper {
*/
public Rect getMaxBounds(int displayId, int windowContextType) {
final Display display = mDisplayManager.getDisplay(displayId);
- WindowManager windowManager = mContext
- .createDisplayContext(display).createWindowContext(windowContextType, null)
- .getSystemService(WindowManager.class);
+ WindowManager windowManager = mWindowManagerProvider.getWindowManager(mContext
+ .createDisplayContext(display).createWindowContext(windowContextType, null));
return windowManager.getMaximumWindowMetrics().getBounds();
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/AsyncSensorManagerExt.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/AsyncSensorManagerExt.kt
new file mode 100644
index 000000000000..e66ad2c209ae
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/AsyncSensorManagerExt.kt
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.util.kotlin
+
+import android.hardware.Sensor
+import android.hardware.TriggerEvent
+import android.hardware.TriggerEventListener
+import com.android.systemui.util.sensors.AsyncSensorManager
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
+import java.util.concurrent.atomic.AtomicBoolean
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+
+/**
+ * Helper for continuously observing a trigger sensor, which automatically unregisters itself after
+ * it executes once. We therefore re-register ourselves after each emission.
+ */
+fun AsyncSensorManager.observeTriggerSensor(sensor: Sensor): Flow<Unit> = conflatedCallbackFlow {
+ val isRegistered = AtomicBoolean(false)
+ fun registerCallbackInternal(callback: TriggerEventListener) {
+ if (isRegistered.compareAndSet(false, true)) {
+ requestTriggerSensor(callback, sensor)
+ }
+ }
+
+ val callback =
+ object : TriggerEventListener() {
+ override fun onTrigger(event: TriggerEvent) {
+ trySend(Unit)
+ if (isRegistered.getAndSet(false)) {
+ registerCallbackInternal(this)
+ }
+ }
+ }
+
+ registerCallbackInternal(callback)
+
+ awaitClose {
+ if (isRegistered.getAndSet(false)) {
+ cancelTriggerSensor(callback, sensor)
+ }
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/IMagnificationConnectionTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/IMagnificationConnectionTest.java
index 8d9d06c8766e..d3d4e24001cb 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/IMagnificationConnectionTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/IMagnificationConnectionTest.java
@@ -49,6 +49,7 @@ import com.android.systemui.recents.LauncherProxyService;
import com.android.systemui.settings.FakeDisplayTracker;
import com.android.systemui.statusbar.CommandQueue;
import com.android.systemui.util.settings.SecureSettings;
+import com.android.systemui.utils.windowmanager.WindowManagerProvider;
import org.junit.Before;
import org.junit.Test;
@@ -92,6 +93,8 @@ public class IMagnificationConnectionTest extends SysuiTestCase {
private AccessibilityLogger mA11yLogger;
@Mock
private IWindowManager mIWindowManager;
+ @Mock
+ private WindowManagerProvider mWindowManagerProvider;
private IMagnificationConnection mIMagnificationConnection;
private MagnificationImpl mMagnification;
@@ -113,7 +116,7 @@ public class IMagnificationConnectionTest extends SysuiTestCase {
mTestableLooper.getLooper(), mContext.getMainExecutor(), mCommandQueue,
mModeSwitchesController, mSysUiState, mLauncherProxyService, mSecureSettings,
mDisplayTracker, getContext().getSystemService(DisplayManager.class),
- mA11yLogger, mIWindowManager, mAccessibilityManager);
+ mA11yLogger, mIWindowManager, mAccessibilityManager, mWindowManagerProvider);
mMagnification.mWindowMagnificationControllerSupplier =
new FakeWindowMagnificationControllerSupplier(
mContext.getSystemService(DisplayManager.class));
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/MagnificationTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/MagnificationTest.java
index 505432e81b98..ae96e8fe7b8b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/MagnificationTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/MagnificationTest.java
@@ -56,6 +56,7 @@ import com.android.systemui.recents.LauncherProxyService;
import com.android.systemui.settings.FakeDisplayTracker;
import com.android.systemui.statusbar.CommandQueue;
import com.android.systemui.util.settings.SecureSettings;
+import com.android.systemui.utils.windowmanager.WindowManagerProvider;
import org.junit.Before;
import org.junit.Test;
@@ -96,6 +97,8 @@ public class MagnificationTest extends SysuiTestCase {
private AccessibilityLogger mA11yLogger;
@Mock
private IWindowManager mIWindowManager;
+ @Mock
+ private WindowManagerProvider mWindowManagerProvider;
@Before
public void setUp() throws Exception {
@@ -129,7 +132,7 @@ public class MagnificationTest extends SysuiTestCase {
mCommandQueue, mModeSwitchesController,
mSysUiState, mLauncherProxyService, mSecureSettings, mDisplayTracker,
getContext().getSystemService(DisplayManager.class), mA11yLogger, mIWindowManager,
- getContext().getSystemService(AccessibilityManager.class));
+ getContext().getSystemService(AccessibilityManager.class), mWindowManagerProvider);
mMagnification.mWindowMagnificationControllerSupplier = new FakeControllerSupplier(
mContext.getSystemService(DisplayManager.class), mWindowMagnificationController);
mMagnification.mMagnificationSettingsSupplier = new FakeSettingsSupplier(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt
index bf79d11b2fb8..515a10792c02 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt
@@ -1482,6 +1482,14 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa
} else {
assertThat(hint.isNullOrBlank()).isTrue()
}
+
+ kosmos.promptViewModel.onClearUdfpsGuidanceHint(true)
+
+ if (testCase.modalities.hasUdfps) {
+ assertThat(hint).isNull()
+ } else {
+ assertThat(hint.isNullOrBlank()).isTrue()
+ }
}
@Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsDialogLiteTest.java b/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsDialogLiteTest.java
index 2898a02a1da8..fdf420b7013f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsDialogLiteTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsDialogLiteTest.java
@@ -38,7 +38,9 @@ import android.content.res.Resources;
import android.graphics.Color;
import android.media.AudioManager;
import android.os.Handler;
+import android.os.PowerManager;
import android.os.UserManager;
+import android.platform.test.annotations.EnableFlags;
import android.provider.Settings;
import android.testing.TestableLooper;
import android.view.Display;
@@ -60,6 +62,7 @@ import com.android.internal.logging.UiEventLogger;
import com.android.internal.statusbar.IStatusBarService;
import com.android.internal.widget.LockPatternUtils;
import com.android.keyguard.KeyguardUpdateMonitor;
+import com.android.systemui.Flags;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.animation.DialogTransitionAnimator;
import com.android.systemui.broadcast.BroadcastDispatcher;
@@ -142,6 +145,7 @@ public class GlobalActionsDialogLiteTest extends SysuiTestCase {
@Mock private SelectedUserInteractor mSelectedUserInteractor;
@Mock private UserLogoutInteractor mLogoutInteractor;
@Mock private OnBackInvokedDispatcher mOnBackInvokedDispatcher;
+ @Mock private PowerManager mPowerManager;
@Captor private ArgumentCaptor<OnBackInvokedCallback> mOnBackInvokedCallback;
private TestableLooper mTestableLooper;
@@ -204,7 +208,8 @@ public class GlobalActionsDialogLiteTest extends SysuiTestCase {
mSelectedUserInteractor,
mLogoutInteractor,
mInteractor,
- () -> new FakeDisplayWindowPropertiesRepository(mContext)
+ () -> new FakeDisplayWindowPropertiesRepository(mContext),
+ mPowerManager
);
mGlobalActionsDialogLite.setZeroDialogPressDelayForTesting();
@@ -806,6 +811,170 @@ public class GlobalActionsDialogLiteTest extends SysuiTestCase {
assertThat(mGlobalActionsDialogLite.mDialog).isNull();
}
+ @Test
+ public void testShouldLogStandbyPress() {
+ GlobalActionsDialogLite.StandbyAction standbyAction =
+ mGlobalActionsDialogLite.new StandbyAction();
+ standbyAction.onPress();
+ verifyLogPosted(GlobalActionsDialogLite.GlobalActionsEvent.GA_STANDBY_PRESS);
+ }
+
+ @Test
+ public void testCreateActionItems_standbyEnabled_doesShowStandby() {
+ // Test like a TV, which only has standby and shut down
+ mGlobalActionsDialogLite = spy(mGlobalActionsDialogLite);
+ doReturn(2).when(mGlobalActionsDialogLite).getMaxShownPowerItems();
+ String[] actions = {
+ GlobalActionsDialogLite.GLOBAL_ACTION_KEY_STANDBY,
+ GlobalActionsDialogLite.GLOBAL_ACTION_KEY_POWER
+ };
+ doReturn(actions).when(mGlobalActionsDialogLite).getDefaultActions();
+ mGlobalActionsDialogLite.createActionItems();
+
+ assertItemsOfType(mGlobalActionsDialogLite.mItems,
+ GlobalActionsDialogLite.StandbyAction.class,
+ GlobalActionsDialogLite.ShutDownAction.class);
+ assertThat(mGlobalActionsDialogLite.mOverflowItems).isEmpty();
+ assertThat(mGlobalActionsDialogLite.mPowerItems).isEmpty();
+ }
+
+ @Test
+ public void testCreateActionItems_standbyDisabled_doesntStandbyAction() {
+ mGlobalActionsDialogLite = spy(mGlobalActionsDialogLite);
+ doReturn(5).when(mGlobalActionsDialogLite).getMaxShownPowerItems();
+ doReturn(true).when(mGlobalActionsDialogLite).shouldDisplayEmergency();
+ doReturn(true).when(mGlobalActionsDialogLite).shouldDisplayLockdown(any());
+ doReturn(true).when(mGlobalActionsDialogLite).shouldShowAction(any());
+ String[] actions = {
+ GlobalActionsDialogLite.GLOBAL_ACTION_KEY_EMERGENCY,
+ GlobalActionsDialogLite.GLOBAL_ACTION_KEY_LOCKDOWN,
+ GlobalActionsDialogLite.GLOBAL_ACTION_KEY_POWER,
+ GlobalActionsDialogLite.GLOBAL_ACTION_KEY_RESTART
+ };
+ doReturn(actions).when(mGlobalActionsDialogLite).getDefaultActions();
+ mGlobalActionsDialogLite.createActionItems();
+
+ assertNoItemsOfType(mGlobalActionsDialogLite.mItems,
+ GlobalActionsDialogLite.StandbyAction.class);
+ assertThat(mGlobalActionsDialogLite.mOverflowItems).isEmpty();
+ assertThat(mGlobalActionsDialogLite.mPowerItems).isEmpty();
+ }
+
+ @Test
+ public void testCreateActionItems_standbyEnabled_locked_showsStandby() {
+ // Test like a TV, which only has standby and shut down
+ mGlobalActionsDialogLite = spy(mGlobalActionsDialogLite);
+ doReturn(2).when(mGlobalActionsDialogLite).getMaxShownPowerItems();
+ String[] actions = {
+ GlobalActionsDialogLite.GLOBAL_ACTION_KEY_STANDBY,
+ GlobalActionsDialogLite.GLOBAL_ACTION_KEY_POWER
+ };
+ doReturn(actions).when(mGlobalActionsDialogLite).getDefaultActions();
+
+ // Show dialog with keyguard showing and provisioned
+ mGlobalActionsDialogLite.showOrHideDialog(true, true, null, Display.DEFAULT_DISPLAY);
+ // Clear the dismiss override so we don't have behavior after dismissing the dialog
+ mGlobalActionsDialogLite.mDialog.setDismissOverride(null);
+
+ assertOneItemOfType(mGlobalActionsDialogLite.mItems,
+ GlobalActionsDialogLite.StandbyAction.class);
+
+ // Hide dialog
+ mGlobalActionsDialogLite.showOrHideDialog(true, true, null, Display.DEFAULT_DISPLAY);
+ }
+
+ @Test
+ public void testCreateActionItems_standbyEnabled_notProvisioned_showsStandby() {
+ // Test like a TV, which only has standby and shut down.
+ mGlobalActionsDialogLite = spy(mGlobalActionsDialogLite);
+ doReturn(2).when(mGlobalActionsDialogLite).getMaxShownPowerItems();
+ String[] actions = {
+ GlobalActionsDialogLite.GLOBAL_ACTION_KEY_STANDBY,
+ GlobalActionsDialogLite.GLOBAL_ACTION_KEY_POWER
+ };
+ doReturn(actions).when(mGlobalActionsDialogLite).getDefaultActions();
+
+ // Show dialog without keyguard showing and not provisioned
+ mGlobalActionsDialogLite.showOrHideDialog(false, false, null, Display.DEFAULT_DISPLAY);
+ // Clear the dismiss override so we don't have behavior after dismissing the dialog
+ mGlobalActionsDialogLite.mDialog.setDismissOverride(null);
+
+ assertOneItemOfType(mGlobalActionsDialogLite.mItems,
+ GlobalActionsDialogLite.StandbyAction.class);
+
+ // Hide dialog
+ mGlobalActionsDialogLite.showOrHideDialog(false, false, null, Display.DEFAULT_DISPLAY);
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_TV_GLOBAL_ACTIONS_FOCUS)
+ public void testCreateActionItems_noneTv_actionsNotFocuseableAndClickable() {
+ // Test like a TV, which only has standby and shut down.
+ mGlobalActionsDialogLite = spy(mGlobalActionsDialogLite);
+ doReturn(2).when(mGlobalActionsDialogLite).getMaxShownPowerItems();
+ doReturn(false).when(mGlobalActionsDialogLite).isTv();
+ String[] actions = {
+ GlobalActionsDialogLite.GLOBAL_ACTION_KEY_STANDBY,
+ GlobalActionsDialogLite.GLOBAL_ACTION_KEY_POWER};
+ doReturn(actions).when(mGlobalActionsDialogLite).getDefaultActions();
+
+ GlobalActionsDialogLite.ActionsDialogLite dialog = mGlobalActionsDialogLite.createDialog();
+ dialog.create();
+ dialog.show();
+ mTestableLooper.processAllMessages();
+ assertThat(dialog.isShowing()).isTrue();
+
+ final GlobalActionsDialogLite.SinglePressAction action =
+ (GlobalActionsDialogLite.SinglePressAction) mGlobalActionsDialogLite.mItems.get(0);
+ assertThat(action.mIconView.isClickable()).isFalse();
+ assertThat(action.mIconView.isFocusable()).isFalse();
+ assertThat(action.mIconView.performClick()).isFalse();
+ assertThat(dialog.isShowing()).isTrue();
+
+ final GlobalActionsDialogLite.SinglePressAction action1 =
+ (GlobalActionsDialogLite.SinglePressAction) mGlobalActionsDialogLite.mItems.get(1);
+ assertThat(action1.mIconView.isClickable()).isFalse();
+ assertThat(action1.mIconView.isFocusable()).isFalse();
+ assertThat(action1.mIconView.performClick()).isFalse();
+ assertThat(dialog.isShowing()).isTrue();
+
+ dialog.dismiss();
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_TV_GLOBAL_ACTIONS_FOCUS)
+ public void testCreateActionItems_tv_actionsFocusableAndClickable() {
+ // Test like a TV, which only has standby and shut down.
+ mGlobalActionsDialogLite = spy(mGlobalActionsDialogLite);
+ doReturn(2).when(mGlobalActionsDialogLite).getMaxShownPowerItems();
+ doReturn(true).when(mGlobalActionsDialogLite).isTv();
+ String[] actions = {
+ GlobalActionsDialogLite.GLOBAL_ACTION_KEY_STANDBY,
+ GlobalActionsDialogLite.GLOBAL_ACTION_KEY_POWER};
+ doReturn(actions).when(mGlobalActionsDialogLite).getDefaultActions();
+
+ GlobalActionsDialogLite.ActionsDialogLite dialog = mGlobalActionsDialogLite.createDialog();
+ dialog.create();
+ dialog.show();
+ mTestableLooper.processAllMessages();
+ assertThat(dialog.isShowing()).isTrue();
+
+ final GlobalActionsDialogLite.SinglePressAction action =
+ (GlobalActionsDialogLite.SinglePressAction) mGlobalActionsDialogLite.mItems.get(0);
+ assertThat(action.mIconView.isClickable()).isTrue();
+ assertThat(action.mIconView.isFocusable()).isTrue();
+
+ final GlobalActionsDialogLite.SinglePressAction action1 =
+ (GlobalActionsDialogLite.SinglePressAction) mGlobalActionsDialogLite.mItems.get(1);
+ assertThat(action1.mIconView.isClickable()).isTrue();
+ assertThat(action1.mIconView.isFocusable()).isTrue();
+
+ assertThat(action.mIconView.performClick()).isTrue();
+ verifyLogPosted(GlobalActionsDialogLite.GlobalActionsEvent.GA_STANDBY_PRESS);
+
+ dialog.dismiss();
+ }
+
private UserInfo mockCurrentUser(int flags) {
return new UserInfo(10, "A User", flags);
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 d6b778fe2bc2..0a8572a8a270 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTestKt.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTestKt.kt
@@ -52,8 +52,10 @@ import com.android.systemui.dump.dumpManager
import com.android.systemui.flags.featureFlagsClassic
import com.android.systemui.flags.systemPropertiesHelper
import com.android.systemui.jank.interactionJankMonitor
+import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
import com.android.systemui.keyguard.domain.interactor.keyguardTransitionBootInteractor
+import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.backgroundScope
import com.android.systemui.kosmos.runTest
@@ -174,6 +176,12 @@ class KeyguardViewMediatorTestKt : SysuiTestCase() {
@Test
fun doKeyguardTimeout_changesCommunalScene() =
kosmos.runTest {
+ // Transition fully to gone
+ fakeKeyguardTransitionRepository.transitionTo(
+ KeyguardState.LOCKSCREEN,
+ KeyguardState.GONE,
+ )
+
// Hub is enabled and hub condition is active.
setCommunalV2Enabled(true)
enableHubOnCharging()
@@ -192,6 +200,11 @@ class KeyguardViewMediatorTestKt : SysuiTestCase() {
@Test
fun doKeyguardTimeout_communalNotAvailable_sleeps() =
kosmos.runTest {
+ fakeKeyguardTransitionRepository.transitionTo(
+ KeyguardState.LOCKSCREEN,
+ KeyguardState.GONE,
+ )
+
// Hub disabled.
setCommunalV2Enabled(false)
@@ -212,6 +225,11 @@ class KeyguardViewMediatorTestKt : SysuiTestCase() {
@Test
fun doKeyguardTimeout_hubConditionNotActive_sleeps() =
kosmos.runTest {
+ fakeKeyguardTransitionRepository.transitionTo(
+ KeyguardState.LOCKSCREEN,
+ KeyguardState.GONE,
+ )
+
// Communal enabled, but hub condition set to never.
setCommunalV2Enabled(true)
disableHubShowingAutomatically()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataFilterImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataFilterImplTest.kt
index 38dc03e9b5ea..07e47afe1822 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataFilterImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataFilterImplTest.kt
@@ -16,21 +16,14 @@
package com.android.systemui.media.controls.domain.pipeline
-import android.app.smartspace.SmartspaceAction
-import android.os.Bundle
import android.testing.TestableLooper
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.internal.logging.InstanceId
import com.android.systemui.SysuiTestCase
-import com.android.systemui.broadcast.BroadcastSender
import com.android.systemui.media.controls.MediaTestUtils
-import com.android.systemui.media.controls.shared.model.EXTRA_KEY_TRIGGER_RESUME
import com.android.systemui.media.controls.shared.model.MediaData
-import com.android.systemui.media.controls.shared.model.SmartspaceMediaData
import com.android.systemui.media.controls.ui.controller.MediaPlayerData
-import com.android.systemui.media.controls.util.MediaFlags
-import com.android.systemui.media.controls.util.MediaUiEventLogger
import com.android.systemui.settings.UserTracker
import com.android.systemui.statusbar.NotificationLockscreenUserManager
import com.android.systemui.util.time.FakeSystemClock
@@ -58,9 +51,6 @@ private const val PRIVATE_PROFILE = 12
private const val PACKAGE = "PKG"
private val INSTANCE_ID = InstanceId.fakeInstanceId(123)!!
private const val APP_UID = 99
-private const val SMARTSPACE_KEY = "SMARTSPACE_KEY"
-private const val SMARTSPACE_PACKAGE = "SMARTSPACE_PKG"
-private val SMARTSPACE_INSTANCE_ID = InstanceId.fakeInstanceId(456)!!
@SmallTest
@RunWith(AndroidJUnit4::class)
@@ -69,15 +59,9 @@ class LegacyMediaDataFilterImplTest : SysuiTestCase() {
@Mock private lateinit var listener: MediaDataManager.Listener
@Mock private lateinit var userTracker: UserTracker
- @Mock private lateinit var broadcastSender: BroadcastSender
@Mock private lateinit var mediaDataManager: MediaDataManager
@Mock private lateinit var lockscreenUserManager: NotificationLockscreenUserManager
@Mock private lateinit var executor: Executor
- @Mock private lateinit var smartspaceData: SmartspaceMediaData
- @Mock private lateinit var smartspaceMediaRecommendationItem: SmartspaceAction
- @Mock private lateinit var logger: MediaUiEventLogger
- @Mock private lateinit var mediaFlags: MediaFlags
- @Mock private lateinit var cardAction: SmartspaceAction
private lateinit var mediaDataFilter: LegacyMediaDataFilterImpl
private lateinit var dataMain: MediaData
@@ -90,16 +74,7 @@ class LegacyMediaDataFilterImplTest : SysuiTestCase() {
MockitoAnnotations.initMocks(this)
MediaPlayerData.clear()
mediaDataFilter =
- LegacyMediaDataFilterImpl(
- context,
- userTracker,
- broadcastSender,
- lockscreenUserManager,
- executor,
- clock,
- logger,
- mediaFlags,
- )
+ LegacyMediaDataFilterImpl(userTracker, lockscreenUserManager, executor, clock)
mediaDataFilter.mediaDataManager = mediaDataManager
mediaDataFilter.addListener(listener)
@@ -116,17 +91,6 @@ class LegacyMediaDataFilterImplTest : SysuiTestCase() {
)
dataGuest = dataMain.copy(userId = USER_GUEST)
dataPrivateProfile = dataMain.copy(userId = PRIVATE_PROFILE)
-
- whenever(smartspaceData.targetId).thenReturn(SMARTSPACE_KEY)
- whenever(smartspaceData.isActive).thenReturn(true)
- whenever(smartspaceData.isValid()).thenReturn(true)
- whenever(smartspaceData.packageName).thenReturn(SMARTSPACE_PACKAGE)
- whenever(smartspaceData.recommendations)
- .thenReturn(listOf(smartspaceMediaRecommendationItem))
- whenever(smartspaceData.headphoneConnectionTimeMillis)
- .thenReturn(clock.currentTimeMillis() - 100)
- whenever(smartspaceData.instanceId).thenReturn(SMARTSPACE_INSTANCE_ID)
- whenever(smartspaceData.cardAction).thenReturn(cardAction)
}
private fun setUser(id: Int) {
@@ -244,32 +208,6 @@ class LegacyMediaDataFilterImplTest : SysuiTestCase() {
}
@Test
- fun hasAnyMedia_recommendationSet_returnsFalse() {
- mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
-
- assertThat(mediaDataFilter.hasAnyMedia()).isFalse()
- }
-
- @Test
- fun hasAnyMediaOrRecommendation_noMediaSet_returnsFalse() {
- assertThat(mediaDataFilter.hasAnyMediaOrRecommendation()).isFalse()
- }
-
- @Test
- fun hasAnyMediaOrRecommendation_mediaSet_returnsTrue() {
- mediaDataFilter.onMediaDataLoaded(KEY, oldKey = null, data = dataMain)
-
- assertThat(mediaDataFilter.hasAnyMediaOrRecommendation()).isTrue()
- }
-
- @Test
- fun hasAnyMediaOrRecommendation_recommendationSet_returnsTrue() {
- mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
-
- assertThat(mediaDataFilter.hasAnyMediaOrRecommendation()).isTrue()
- }
-
- @Test
fun hasActiveMedia_noMediaSet_returnsFalse() {
assertThat(mediaDataFilter.hasActiveMedia()).isFalse()
}
@@ -291,67 +229,20 @@ class LegacyMediaDataFilterImplTest : SysuiTestCase() {
}
@Test
- fun hasActiveMediaOrRecommendation_nothingSet_returnsFalse() {
- assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isFalse()
- }
-
- @Test
- fun hasActiveMediaOrRecommendation_inactiveMediaSet_returnsFalse() {
- val data = dataMain.copy(active = false)
- mediaDataFilter.onMediaDataLoaded(KEY, oldKey = null, data = data)
-
- assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isFalse()
- }
-
- @Test
- fun hasActiveMediaOrRecommendation_activeMediaSet_returnsTrue() {
- val data = dataMain.copy(active = true)
- mediaDataFilter.onMediaDataLoaded(KEY, oldKey = null, data = data)
-
- assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isTrue()
- }
-
- @Test
- fun hasActiveMediaOrRecommendation_inactiveRecommendationSet_returnsFalse() {
- whenever(smartspaceData.isActive).thenReturn(false)
- mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
-
- assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isFalse()
- }
-
- @Test
- fun hasActiveMediaOrRecommendation_invalidRecommendationSet_returnsFalse() {
- whenever(smartspaceData.isValid()).thenReturn(false)
- mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
-
- assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isFalse()
- }
-
- @Test
- fun hasActiveMediaOrRecommendation_activeAndValidRecommendationSet_returnsTrue() {
- whenever(smartspaceData.isActive).thenReturn(true)
- whenever(smartspaceData.isValid()).thenReturn(true)
- mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
-
- assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isTrue()
- }
-
- @Test
- fun testHasAnyMediaOrRecommendation_onlyCurrentUser() {
- assertThat(mediaDataFilter.hasAnyMediaOrRecommendation()).isFalse()
+ fun testHasAnyMedia_onlyCurrentUser() {
+ assertThat(mediaDataFilter.hasAnyMedia()).isFalse()
mediaDataFilter.onMediaDataLoaded(KEY, oldKey = null, data = dataGuest)
- assertThat(mediaDataFilter.hasAnyMediaOrRecommendation()).isFalse()
assertThat(mediaDataFilter.hasAnyMedia()).isFalse()
}
@Test
- fun testHasActiveMediaOrRecommendation_onlyCurrentUser() {
- assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isFalse()
+ fun testHasActiveMedia_onlyCurrentUser() {
+ assertThat(mediaDataFilter.hasActiveMedia()).isFalse()
val data = dataGuest.copy(active = true)
mediaDataFilter.onMediaDataLoaded(KEY, oldKey = null, data = data)
- assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isFalse()
+ assertThat(mediaDataFilter.hasActiveMedia()).isFalse()
assertThat(mediaDataFilter.hasAnyMedia()).isFalse()
}
@@ -359,7 +250,6 @@ class LegacyMediaDataFilterImplTest : SysuiTestCase() {
fun testOnNotificationRemoved_doesntHaveMedia() {
mediaDataFilter.onMediaDataLoaded(KEY, oldKey = null, data = dataMain)
mediaDataFilter.onMediaDataRemoved(KEY, false)
- assertThat(mediaDataFilter.hasAnyMediaOrRecommendation()).isFalse()
assertThat(mediaDataFilter.hasAnyMedia()).isFalse()
}
@@ -370,234 +260,4 @@ class LegacyMediaDataFilterImplTest : SysuiTestCase() {
verify(mediaDataManager).setInactive(eq(KEY), eq(true), eq(true))
}
-
- @Test
- fun testOnSmartspaceMediaDataLoaded_noMedia_activeValidRec_prioritizesSmartspace() {
- mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
-
- verify(listener)
- .onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(true))
- assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isTrue()
- assertThat(mediaDataFilter.hasActiveMedia()).isFalse()
- verify(logger).logRecommendationAdded(SMARTSPACE_PACKAGE, SMARTSPACE_INSTANCE_ID)
- verify(logger, never()).logRecommendationActivated(any(), any(), any())
- }
-
- @Test
- fun testOnSmartspaceMediaDataLoaded_noMedia_inactiveRec_showsNothing() {
- whenever(smartspaceData.isActive).thenReturn(false)
-
- mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
-
- verify(listener, never())
- .onMediaDataLoaded(any(), any(), any(), anyBoolean(), anyInt(), anyBoolean())
- verify(listener, never()).onSmartspaceMediaDataLoaded(any(), any(), anyBoolean())
- assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isFalse()
- assertThat(mediaDataFilter.hasActiveMedia()).isFalse()
- verify(logger, never()).logRecommendationAdded(any(), any())
- verify(logger, never()).logRecommendationActivated(any(), any(), any())
- }
-
- @Test
- fun testOnSmartspaceMediaDataLoaded_noRecentMedia_activeValidRec_prioritizesSmartspace() {
- val dataOld = dataMain.copy(active = false, lastActive = clock.elapsedRealtime())
- mediaDataFilter.onMediaDataLoaded(KEY, null, dataOld)
- clock.advanceTime(SMARTSPACE_MAX_AGE + 100)
- mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
-
- verify(listener)
- .onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(true))
- assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isTrue()
- assertThat(mediaDataFilter.hasActiveMedia()).isFalse()
- verify(logger).logRecommendationAdded(SMARTSPACE_PACKAGE, SMARTSPACE_INSTANCE_ID)
- verify(logger, never()).logRecommendationActivated(any(), any(), any())
- }
-
- @Test
- fun testOnSmartspaceMediaDataLoaded_noRecentMedia_inactiveRec_showsNothing() {
- whenever(smartspaceData.isActive).thenReturn(false)
-
- val dataOld = dataMain.copy(active = false, lastActive = clock.elapsedRealtime())
- mediaDataFilter.onMediaDataLoaded(KEY, null, dataOld)
- clock.advanceTime(SMARTSPACE_MAX_AGE + 100)
- mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
-
- verify(listener, never()).onSmartspaceMediaDataLoaded(any(), any(), anyBoolean())
- assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isFalse()
- assertThat(mediaDataFilter.hasActiveMedia()).isFalse()
- verify(logger, never()).logRecommendationAdded(any(), any())
- verify(logger, never()).logRecommendationActivated(any(), any(), any())
- }
-
- @Test
- fun testOnSmartspaceMediaDataLoaded_hasRecentMedia_inactiveRec_showsNothing() {
- whenever(smartspaceData.isActive).thenReturn(false)
-
- // WHEN we have media that was recently played, but not currently active
- val dataCurrent = dataMain.copy(active = false, lastActive = clock.elapsedRealtime())
- mediaDataFilter.onMediaDataLoaded(KEY, null, dataCurrent)
- verify(listener)
- .onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true), eq(0), eq(false))
-
- // AND we get a smartspace signal
- mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
-
- // THEN we should tell listeners to treat the media as not active instead
- verify(listener, never())
- .onMediaDataLoaded(eq(KEY), eq(KEY), any(), anyBoolean(), anyInt(), anyBoolean())
- verify(listener, never()).onSmartspaceMediaDataLoaded(any(), any(), anyBoolean())
- assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isFalse()
- assertThat(mediaDataFilter.hasActiveMedia()).isFalse()
- verify(logger, never()).logRecommendationAdded(any(), any())
- verify(logger, never()).logRecommendationActivated(any(), any(), any())
- }
-
- @Test
- fun testOnSmartspaceMediaDataLoaded_hasRecentMedia_activeInvalidRec_usesMedia() {
- whenever(smartspaceData.isValid()).thenReturn(false)
-
- // WHEN we have media that was recently played, but not currently active
- val dataCurrent = dataMain.copy(active = false, lastActive = clock.elapsedRealtime())
- mediaDataFilter.onMediaDataLoaded(KEY, null, dataCurrent)
- verify(listener)
- .onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true), eq(0), eq(false))
-
- // AND we get a smartspace signal
- mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
-
- // THEN we should tell listeners to treat the media as active instead
- val dataCurrentAndActive = dataCurrent.copy(active = true)
- verify(listener)
- .onMediaDataLoaded(
- eq(KEY),
- eq(KEY),
- eq(dataCurrentAndActive),
- eq(true),
- eq(100),
- eq(true),
- )
- assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isTrue()
- // Smartspace update shouldn't be propagated for the empty rec list.
- verify(listener, never()).onSmartspaceMediaDataLoaded(any(), any(), anyBoolean())
- verify(logger, never()).logRecommendationAdded(any(), any())
- verify(logger).logRecommendationActivated(eq(APP_UID), eq(PACKAGE), eq(INSTANCE_ID))
- }
-
- @Test
- fun testOnSmartspaceMediaDataLoaded_hasRecentMedia_activeValidRec_usesBoth() {
- // WHEN we have media that was recently played, but not currently active
- val dataCurrent = dataMain.copy(active = false, lastActive = clock.elapsedRealtime())
- mediaDataFilter.onMediaDataLoaded(KEY, null, dataCurrent)
- verify(listener)
- .onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true), eq(0), eq(false))
-
- // AND we get a smartspace signal
- mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
-
- // THEN we should tell listeners to treat the media as active instead
- val dataCurrentAndActive = dataCurrent.copy(active = true)
- verify(listener)
- .onMediaDataLoaded(
- eq(KEY),
- eq(KEY),
- eq(dataCurrentAndActive),
- eq(true),
- eq(100),
- eq(true),
- )
- assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isTrue()
- // Smartspace update should also be propagated but not prioritized.
- verify(listener)
- .onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(false))
- verify(logger).logRecommendationAdded(SMARTSPACE_PACKAGE, SMARTSPACE_INSTANCE_ID)
- verify(logger).logRecommendationActivated(eq(APP_UID), eq(PACKAGE), eq(INSTANCE_ID))
- }
-
- @Test
- fun testOnSmartspaceMediaDataRemoved_usedSmartspace_clearsSmartspace() {
- mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
- mediaDataFilter.onSmartspaceMediaDataRemoved(SMARTSPACE_KEY)
-
- verify(listener).onSmartspaceMediaDataRemoved(SMARTSPACE_KEY)
- assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isFalse()
- assertThat(mediaDataFilter.hasActiveMedia()).isFalse()
- }
-
- @Test
- fun testOnSmartspaceMediaDataRemoved_usedMediaAndSmartspace_clearsBoth() {
- val dataCurrent = dataMain.copy(active = false, lastActive = clock.elapsedRealtime())
- mediaDataFilter.onMediaDataLoaded(KEY, null, dataCurrent)
- verify(listener)
- .onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true), eq(0), eq(false))
-
- mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
-
- val dataCurrentAndActive = dataCurrent.copy(active = true)
- verify(listener)
- .onMediaDataLoaded(
- eq(KEY),
- eq(KEY),
- eq(dataCurrentAndActive),
- eq(true),
- eq(100),
- eq(true),
- )
-
- mediaDataFilter.onSmartspaceMediaDataRemoved(SMARTSPACE_KEY)
-
- verify(listener).onSmartspaceMediaDataRemoved(SMARTSPACE_KEY)
- assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isFalse()
- assertThat(mediaDataFilter.hasActiveMedia()).isFalse()
- }
-
- @Test
- fun testSmartspaceLoaded_shouldTriggerResume_doesTrigger() {
- // WHEN we have media that was recently played, but not currently active
- val dataCurrent = dataMain.copy(active = false, lastActive = clock.elapsedRealtime())
- mediaDataFilter.onMediaDataLoaded(KEY, null, dataCurrent)
- verify(listener)
- .onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true), eq(0), eq(false))
-
- // AND we get a smartspace signal with extra to trigger resume
- val extras = Bundle().apply { putBoolean(EXTRA_KEY_TRIGGER_RESUME, true) }
- whenever(cardAction.extras).thenReturn(extras)
- mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
-
- // THEN we should tell listeners to treat the media as active instead
- val dataCurrentAndActive = dataCurrent.copy(active = true)
- verify(listener)
- .onMediaDataLoaded(
- eq(KEY),
- eq(KEY),
- eq(dataCurrentAndActive),
- eq(true),
- eq(100),
- eq(true),
- )
- assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isTrue()
- // And send the smartspace data, but not prioritized
- verify(listener)
- .onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(false))
- }
-
- @Test
- fun testSmartspaceLoaded_notShouldTriggerResume_doesNotTrigger() {
- // WHEN we have media that was recently played, but not currently active
- val dataCurrent = dataMain.copy(active = false, lastActive = clock.elapsedRealtime())
- mediaDataFilter.onMediaDataLoaded(KEY, null, dataCurrent)
- verify(listener)
- .onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true), eq(0), eq(false))
-
- // AND we get a smartspace signal with extra to not trigger resume
- val extras = Bundle().apply { putBoolean(EXTRA_KEY_TRIGGER_RESUME, false) }
- whenever(cardAction.extras).thenReturn(extras)
- mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
-
- // THEN listeners are not updated to show media
- verify(listener, never())
- .onMediaDataLoaded(eq(KEY), eq(KEY), any(), eq(true), eq(100), eq(true))
- // But the smartspace update is still propagated
- verify(listener)
- .onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(false))
- }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImplTest.kt
index 122af0639030..ecc4361abc4f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImplTest.kt
@@ -22,15 +22,10 @@ import android.app.Notification.FLAG_NO_CLEAR
import android.app.Notification.MediaStyle
import android.app.PendingIntent
import android.app.UriGrantsManager
-import android.app.smartspace.SmartspaceAction
-import android.app.smartspace.SmartspaceConfig
-import android.app.smartspace.SmartspaceManager
-import android.app.smartspace.SmartspaceTarget
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.Bitmap
import android.graphics.ImageDecoder
-import android.graphics.drawable.Icon
import android.media.MediaDescription
import android.media.MediaMetadata
import android.media.session.MediaController
@@ -39,7 +34,6 @@ import android.media.session.PlaybackState
import android.net.Uri
import android.os.Bundle
import android.platform.test.annotations.DisableFlags
-import android.platform.test.annotations.EnableFlags
import android.platform.test.flag.junit.FlagsParameterization
import android.service.notification.StatusBarNotification
import android.testing.TestableLooper.RunWithLooper
@@ -53,7 +47,6 @@ import com.android.systemui.SysuiTestCase
import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.dump.DumpManager
import com.android.systemui.flags.Flags.MEDIA_REMOTE_RESUME
-import com.android.systemui.flags.Flags.MEDIA_RESUME_PROGRESS
import com.android.systemui.flags.Flags.MEDIA_RETAIN_SESSIONS
import com.android.systemui.flags.fakeFeatureFlagsClassic
import com.android.systemui.kosmos.testDispatcher
@@ -63,7 +56,6 @@ import com.android.systemui.media.controls.domain.resume.ResumeMediaBrowser
import com.android.systemui.media.controls.shared.mediaLogger
import com.android.systemui.media.controls.shared.mockMediaLogger
import com.android.systemui.media.controls.shared.model.MediaData
-import com.android.systemui.media.controls.shared.model.SmartspaceMediaDataProvider
import com.android.systemui.media.controls.util.MediaUiEventLogger
import com.android.systemui.media.controls.util.fakeMediaControllerFactory
import com.android.systemui.media.controls.util.mediaFlags
@@ -103,9 +95,6 @@ import platform.test.runner.parameterized.Parameters
private const val KEY = "KEY"
private const val KEY_2 = "KEY_2"
-private const val KEY_MEDIA_SMARTSPACE = "MEDIA_SMARTSPACE_ID"
-private const val SMARTSPACE_CREATION_TIME = 1234L
-private const val SMARTSPACE_EXPIRY_TIME = 5678L
private const val PACKAGE_NAME = "com.example.app"
private const val SYSTEM_PACKAGE_NAME = "com.android.systemui"
private const val APP_NAME = "SystemUI"
@@ -114,7 +103,6 @@ private const val SESSION_TITLE = "title"
private const val SESSION_BLANK_TITLE = " "
private const val SESSION_EMPTY_TITLE = ""
private const val USER_ID = 0
-private val DISMISS_INTENT = Intent().apply { action = "dismiss" }
@SmallTest
@RunWithLooper(setAsMainLooper = true)
@@ -140,13 +128,7 @@ class LegacyMediaDataManagerImplTest(flags: FlagsParameterization) : SysuiTestCa
@Mock lateinit var mediaDataFilter: LegacyMediaDataFilterImpl
@Mock lateinit var listener: MediaDataManager.Listener
@Mock lateinit var pendingIntent: PendingIntent
- @Mock lateinit var smartspaceManager: SmartspaceManager
@Mock lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
- lateinit var smartspaceMediaDataProvider: SmartspaceMediaDataProvider
- @Mock lateinit var mediaSmartspaceTarget: SmartspaceTarget
- @Mock private lateinit var mediaRecommendationItem: SmartspaceAction
- lateinit var validRecommendationList: List<SmartspaceAction>
- @Mock private lateinit var mediaSmartspaceBaseAction: SmartspaceAction
@Mock private lateinit var logger: MediaUiEventLogger
lateinit var mediaDataManager: LegacyMediaDataManagerImpl
lateinit var mediaNotification: StatusBarNotification
@@ -155,7 +137,6 @@ class LegacyMediaDataManagerImplTest(flags: FlagsParameterization) : SysuiTestCa
private val clock = FakeSystemClock()
@Captor lateinit var stateCallbackCaptor: ArgumentCaptor<(String, PlaybackState) -> Unit>
@Captor lateinit var sessionCallbackCaptor: ArgumentCaptor<(String) -> Unit>
- @Captor lateinit var smartSpaceConfigBuilderCaptor: ArgumentCaptor<SmartspaceConfig>
@Mock private lateinit var ugm: IUriGrantsManager
@Mock private lateinit var imageSource: ImageDecoder.Source
@@ -186,21 +167,19 @@ class LegacyMediaDataManagerImplTest(flags: FlagsParameterization) : SysuiTestCa
fun setup() {
staticMockSession =
ExtendedMockito.mockitoSession()
- .mockStatic<UriGrantsManager>(UriGrantsManager::class.java)
- .mockStatic<ImageDecoder>(ImageDecoder::class.java)
+ .mockStatic(UriGrantsManager::class.java)
+ .mockStatic(ImageDecoder::class.java)
.strictness(Strictness.LENIENT)
.startMocking()
whenever(UriGrantsManager.getService()).thenReturn(ugm)
foregroundExecutor = FakeExecutor(clock)
backgroundExecutor = FakeExecutor(clock)
uiExecutor = FakeExecutor(clock)
- smartspaceMediaDataProvider = SmartspaceMediaDataProvider()
mediaDataManager =
LegacyMediaDataManagerImpl(
context = context,
backgroundExecutor = backgroundExecutor,
backgroundDispatcher = testDispatcher,
- uiExecutor = uiExecutor,
foregroundExecutor = foregroundExecutor,
mainDispatcher = testDispatcher,
applicationScope = testScope,
@@ -213,13 +192,11 @@ class LegacyMediaDataManagerImplTest(flags: FlagsParameterization) : SysuiTestCa
mediaDeviceManager = mediaDeviceManager,
mediaDataCombineLatest = mediaDataCombineLatest,
mediaDataFilter = mediaDataFilter,
- smartspaceMediaDataProvider = smartspaceMediaDataProvider,
useMediaResumption = true,
useQsMediaPlayer = true,
systemClock = clock,
mediaFlags = kosmos.mediaFlags,
logger = logger,
- smartspaceManager = smartspaceManager,
keyguardUpdateMonitor = keyguardUpdateMonitor,
mediaDataLoader = { kosmos.mediaDataLoader },
mediaLogger = kosmos.mediaLogger,
@@ -256,7 +233,6 @@ class LegacyMediaDataManagerImplTest(flags: FlagsParameterization) : SysuiTestCa
putString(MediaMetadata.METADATA_KEY_ARTIST, SESSION_ARTIST)
putString(MediaMetadata.METADATA_KEY_TITLE, SESSION_TITLE)
}
- verify(smartspaceManager).createSmartspaceSession(capture(smartSpaceConfigBuilderCaptor))
mediaControllerFactory.setControllerForToken(session.sessionToken, controller)
whenever(controller.sessionToken).thenReturn(session.sessionToken)
whenever(controller.transportControls).thenReturn(transportControls)
@@ -266,30 +242,12 @@ class LegacyMediaDataManagerImplTest(flags: FlagsParameterization) : SysuiTestCa
.thenReturn(MediaController.PlaybackInfo.PLAYBACK_TYPE_LOCAL)
// This is an ugly hack for now. The mediaSessionBasedFilter is one of the internal
- // listeners in the internal processing pipeline. It receives events, but ince it is a
+ // listeners in the internal processing pipeline. It receives events, but since it is a
// mock, it doesn't pass those events along the chain to the external listeners. So, just
// treat mediaSessionBasedFilter as a listener for testing.
listener = mediaSessionBasedFilter
- val recommendationExtras =
- Bundle().apply {
- putString("package_name", PACKAGE_NAME)
- putParcelable("dismiss_intent", DISMISS_INTENT)
- }
- val icon = Icon.createWithResource(context, android.R.drawable.ic_media_play)
- whenever(mediaSmartspaceBaseAction.extras).thenReturn(recommendationExtras)
- whenever(mediaSmartspaceTarget.baseAction).thenReturn(mediaSmartspaceBaseAction)
- whenever(mediaRecommendationItem.extras).thenReturn(recommendationExtras)
- whenever(mediaRecommendationItem.icon).thenReturn(icon)
- validRecommendationList =
- listOf(mediaRecommendationItem, mediaRecommendationItem, mediaRecommendationItem)
- whenever(mediaSmartspaceTarget.smartspaceTargetId).thenReturn(KEY_MEDIA_SMARTSPACE)
- whenever(mediaSmartspaceTarget.featureType).thenReturn(SmartspaceTarget.FEATURE_MEDIA)
- whenever(mediaSmartspaceTarget.iconGrid).thenReturn(validRecommendationList)
- whenever(mediaSmartspaceTarget.creationTimeMillis).thenReturn(SMARTSPACE_CREATION_TIME)
- whenever(mediaSmartspaceTarget.expiryTimeMillis).thenReturn(SMARTSPACE_EXPIRY_TIME)
fakeFeatureFlags.set(MEDIA_RETAIN_SESSIONS, false)
- fakeFeatureFlags.set(MEDIA_RESUME_PROGRESS, false)
fakeFeatureFlags.set(MEDIA_REMOTE_RESUME, false)
whenever(logger.getNewInstanceId()).thenReturn(instanceIdSequence.newInstanceId())
whenever(keyguardUpdateMonitor.isUserInLockdown(any())).thenReturn(false)
@@ -971,8 +929,6 @@ class LegacyMediaDataManagerImplTest(flags: FlagsParameterization) : SysuiTestCa
@Test
fun testAddResumptionControls_hasPartialProgress() {
- fakeFeatureFlags.set(MEDIA_RESUME_PROGRESS, true)
-
// WHEN resumption controls are added with partial progress
val progress = 0.5
val extras =
@@ -998,8 +954,6 @@ class LegacyMediaDataManagerImplTest(flags: FlagsParameterization) : SysuiTestCa
@Test
fun testAddResumptionControls_hasNotPlayedProgress() {
- fakeFeatureFlags.set(MEDIA_RESUME_PROGRESS, true)
-
// WHEN resumption controls are added that have not been played
val extras =
Bundle().apply {
@@ -1023,8 +977,6 @@ class LegacyMediaDataManagerImplTest(flags: FlagsParameterization) : SysuiTestCa
@Test
fun testAddResumptionControls_hasFullProgress() {
- fakeFeatureFlags.set(MEDIA_RESUME_PROGRESS, true)
-
// WHEN resumption controls are added with progress info
val extras =
Bundle().apply {
@@ -1049,8 +1001,6 @@ class LegacyMediaDataManagerImplTest(flags: FlagsParameterization) : SysuiTestCa
@Test
fun testAddResumptionControls_hasNoExtras() {
- fakeFeatureFlags.set(MEDIA_RESUME_PROGRESS, true)
-
// WHEN resumption controls are added that do not have any extras
val desc =
MediaDescription.Builder().run {
@@ -1067,8 +1017,6 @@ class LegacyMediaDataManagerImplTest(flags: FlagsParameterization) : SysuiTestCa
@Test
fun testAddResumptionControls_hasEmptyTitle() {
- fakeFeatureFlags.set(MEDIA_RESUME_PROGRESS, true)
-
// WHEN resumption controls are added that have empty title
val desc =
MediaDescription.Builder().run {
@@ -1100,8 +1048,6 @@ class LegacyMediaDataManagerImplTest(flags: FlagsParameterization) : SysuiTestCa
@Test
fun testAddResumptionControls_hasBlankTitle() {
- fakeFeatureFlags.set(MEDIA_RESUME_PROGRESS, true)
-
// WHEN resumption controls are added that have a blank title
val desc =
MediaDescription.Builder().run {
@@ -2117,7 +2063,6 @@ class LegacyMediaDataManagerImplTest(flags: FlagsParameterization) : SysuiTestCa
}
@Test
- @EnableFlags(Flags.FLAG_MEDIA_CONTROLS_POSTS_OPTIMIZATION)
fun postDuplicateNotification_doesNotCallListeners() {
addNotificationAndLoad()
reset(listener)
@@ -2137,26 +2082,6 @@ class LegacyMediaDataManagerImplTest(flags: FlagsParameterization) : SysuiTestCa
}
@Test
- @DisableFlags(Flags.FLAG_MEDIA_CONTROLS_POSTS_OPTIMIZATION)
- fun postDuplicateNotification_callsListeners() {
- addNotificationAndLoad()
- reset(listener)
- mediaDataManager.onNotificationAdded(KEY, mediaNotification)
- testScope.assertRunAllReady(foreground = 1, background = 1)
- verify(listener)
- .onMediaDataLoaded(
- eq(KEY),
- eq(KEY),
- capture(mediaDataCaptor),
- eq(true),
- eq(0),
- eq(false),
- )
- verify(kosmos.mediaLogger, never()).logDuplicateMediaNotification(eq(KEY))
- }
-
- @Test
- @EnableFlags(Flags.FLAG_MEDIA_CONTROLS_POSTS_OPTIMIZATION)
fun postDifferentIntentNotifications_CallsListeners() {
addNotificationAndLoad()
reset(listener)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterImplTest.kt
index 0a8325de343d..956462e4120f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterImplTest.kt
@@ -16,29 +16,20 @@
package com.android.systemui.media.controls.domain.pipeline
-import android.R
-import android.app.smartspace.SmartspaceAction
-import android.graphics.drawable.Icon
-import android.os.Bundle
import android.testing.TestableLooper
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.internal.logging.InstanceId
import com.android.systemui.SysuiTestCase
-import com.android.systemui.broadcast.BroadcastSender
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.media.controls.MediaTestUtils
import com.android.systemui.media.controls.data.repository.MediaFilterRepository
import com.android.systemui.media.controls.data.repository.mediaFilterRepository
import com.android.systemui.media.controls.shared.mockMediaLogger
-import com.android.systemui.media.controls.shared.model.EXTRA_KEY_TRIGGER_RESUME
import com.android.systemui.media.controls.shared.model.MediaCommonModel
import com.android.systemui.media.controls.shared.model.MediaData
import com.android.systemui.media.controls.shared.model.MediaDataLoadingModel
-import com.android.systemui.media.controls.shared.model.SmartspaceMediaData
-import com.android.systemui.media.controls.shared.model.SmartspaceMediaLoadingModel
import com.android.systemui.media.controls.ui.controller.MediaPlayerData
-import com.android.systemui.media.controls.util.MediaUiEventLogger
import com.android.systemui.settings.UserTracker
import com.android.systemui.statusbar.NotificationLockscreenUserManager
import com.android.systemui.testKosmos
@@ -46,7 +37,6 @@ import com.android.systemui.util.time.FakeSystemClock
import com.google.common.truth.Truth.assertThat
import java.util.concurrent.Executor
import kotlinx.coroutines.test.TestScope
-import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
@@ -72,9 +62,6 @@ private const val PACKAGE = "PKG"
private val INSTANCE_ID = InstanceId.fakeInstanceId(123)!!
private val INSTANCE_ID_GUEST = InstanceId.fakeInstanceId(321)!!
private const val APP_UID = 99
-private const val SMARTSPACE_KEY = "SMARTSPACE_KEY"
-private const val SMARTSPACE_PACKAGE = "SMARTSPACE_PKG"
-private val SMARTSPACE_INSTANCE_ID = InstanceId.fakeInstanceId(456)!!
@SmallTest
@RunWith(AndroidJUnit4::class)
@@ -84,14 +71,9 @@ class MediaDataFilterImplTest : SysuiTestCase() {
@Mock private lateinit var listener: MediaDataProcessor.Listener
@Mock private lateinit var userTracker: UserTracker
- @Mock private lateinit var broadcastSender: BroadcastSender
@Mock private lateinit var mediaDataProcessor: MediaDataProcessor
@Mock private lateinit var lockscreenUserManager: NotificationLockscreenUserManager
@Mock private lateinit var executor: Executor
- @Mock private lateinit var smartspaceData: SmartspaceMediaData
- @Mock private lateinit var smartspaceMediaRecommendationItem: SmartspaceAction
- @Mock private lateinit var logger: MediaUiEventLogger
- @Mock private lateinit var cardAction: SmartspaceAction
private lateinit var mediaDataFilter: MediaDataFilterImpl
private lateinit var testScope: TestScope
@@ -109,13 +91,10 @@ class MediaDataFilterImplTest : SysuiTestCase() {
testScope = TestScope()
mediaDataFilter =
MediaDataFilterImpl(
- context,
userTracker,
- broadcastSender,
lockscreenUserManager,
executor,
clock,
- logger,
repository,
mediaLogger,
)
@@ -135,19 +114,6 @@ class MediaDataFilterImplTest : SysuiTestCase() {
)
dataGuest = dataMain.copy(userId = USER_GUEST, instanceId = INSTANCE_ID_GUEST)
dataPrivateProfile = dataMain.copy(userId = PRIVATE_PROFILE, instanceId = INSTANCE_ID_GUEST)
-
- whenever(smartspaceData.targetId).thenReturn(SMARTSPACE_KEY)
- whenever(smartspaceData.isActive).thenReturn(true)
- whenever(smartspaceData.isValid()).thenReturn(true)
- whenever(smartspaceData.packageName).thenReturn(SMARTSPACE_PACKAGE)
- whenever(smartspaceData.recommendations)
- .thenReturn(listOf(smartspaceMediaRecommendationItem))
- whenever(smartspaceMediaRecommendationItem.icon)
- .thenReturn(Icon.createWithResource(context, R.drawable.ic_media_play))
- whenever(smartspaceData.headphoneConnectionTimeMillis)
- .thenReturn(clock.currentTimeMillis() - 100)
- whenever(smartspaceData.instanceId).thenReturn(SMARTSPACE_INSTANCE_ID)
- whenever(smartspaceData.cardAction).thenReturn(cardAction)
}
private fun setUser(id: Int) {
@@ -172,7 +138,7 @@ class MediaDataFilterImplTest : SysuiTestCase() {
testScope.runTest {
val currentMedia by collectLastValue(repository.currentMedia)
val mediaCommonModel =
- MediaCommonModel.MediaControl(MediaDataLoadingModel.Loaded(dataMain.instanceId))
+ MediaCommonModel(MediaDataLoadingModel.Loaded(dataMain.instanceId))
mediaDataFilter.onMediaDataLoaded(KEY, null, dataMain)
@@ -188,7 +154,7 @@ class MediaDataFilterImplTest : SysuiTestCase() {
testScope.runTest {
val currentMedia by collectLastValue(repository.currentMedia)
val mediaCommonModel =
- MediaCommonModel.MediaControl(MediaDataLoadingModel.Loaded(dataMain.instanceId))
+ MediaCommonModel(MediaDataLoadingModel.Loaded(dataMain.instanceId))
mediaDataFilter.onMediaDataLoaded(KEY, null, dataGuest)
@@ -203,7 +169,7 @@ class MediaDataFilterImplTest : SysuiTestCase() {
testScope.runTest {
val currentMedia by collectLastValue(repository.currentMedia)
val mediaCommonModel =
- MediaCommonModel.MediaControl(MediaDataLoadingModel.Loaded(dataMain.instanceId))
+ MediaCommonModel(MediaDataLoadingModel.Loaded(dataMain.instanceId))
// GIVEN a media was removed for main user
mediaDataFilter.onMediaDataLoaded(KEY, null, dataMain)
@@ -244,7 +210,7 @@ class MediaDataFilterImplTest : SysuiTestCase() {
verify(mediaLogger)
.logMediaLoaded(eq(dataMain.instanceId), eq(dataMain.active), anyString())
- assertThat(currentMedia).containsExactly(MediaCommonModel.MediaControl(mediaLoaded))
+ assertThat(currentMedia).containsExactly(MediaCommonModel(mediaLoaded))
// and we switch to guest user
setUser(USER_GUEST)
@@ -289,10 +255,8 @@ class MediaDataFilterImplTest : SysuiTestCase() {
)
verify(mediaLogger, never())
.logMediaLoaded(eq(dataMain.instanceId), anyBoolean(), anyString())
- assertThat(currentMedia)
- .containsExactly(MediaCommonModel.MediaControl(guestLoadedStatesModel))
- assertThat(currentMedia)
- .doesNotContain(MediaCommonModel.MediaControl(mainLoadedStatesModel))
+ assertThat(currentMedia).containsExactly(MediaCommonModel(guestLoadedStatesModel))
+ assertThat(currentMedia).doesNotContain(MediaCommonModel(mainLoadedStatesModel))
}
@Test
@@ -311,8 +275,7 @@ class MediaDataFilterImplTest : SysuiTestCase() {
// THEN we should remove the private profile media
verify(listener).onMediaDataRemoved(eq(KEY_ALT), eq(false))
verify(mediaLogger).logMediaRemoved(eq(dataGuest.instanceId), anyString())
- assertThat(currentMedia)
- .containsExactly(MediaCommonModel.MediaControl(mediaLoadedStatesModel))
+ assertThat(currentMedia).containsExactly(MediaCommonModel(mediaLoadedStatesModel))
}
@Test
@@ -325,37 +288,6 @@ class MediaDataFilterImplTest : SysuiTestCase() {
}
@Test
- fun hasAnyMedia_recommendationSet_returnsFalse() =
- testScope.runTest {
- val selectedUserEntries by collectLastValue(repository.selectedUserEntries)
- mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
-
- assertThat(hasAnyMedia(selectedUserEntries)).isFalse()
- }
-
- @Test
- fun hasAnyMediaOrRecommendation_mediaSet_returnsTrue() =
- testScope.runTest {
- val selectedUserEntries by collectLastValue(repository.selectedUserEntries)
- val smartspaceMediaData by collectLastValue(repository.smartspaceMediaData)
- mediaDataFilter.onMediaDataLoaded(KEY, oldKey = null, data = dataMain)
-
- assertThat(hasAnyMediaOrRecommendation(selectedUserEntries, smartspaceMediaData))
- .isTrue()
- }
-
- @Test
- fun hasAnyMediaOrRecommendation_recommendationSet_returnsTrue() =
- testScope.runTest {
- val selectedUserEntries by collectLastValue(repository.selectedUserEntries)
- val smartspaceMediaData by collectLastValue(repository.smartspaceMediaData)
- mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
-
- assertThat(hasAnyMediaOrRecommendation(selectedUserEntries, smartspaceMediaData))
- .isTrue()
- }
-
- @Test
fun hasActiveMedia_inactiveMediaSet_returnsFalse() =
testScope.runTest {
val selectedUserEntries by collectLastValue(repository.selectedUserEntries)
@@ -377,140 +309,24 @@ class MediaDataFilterImplTest : SysuiTestCase() {
}
@Test
- fun hasActiveMediaOrRecommendation_inactiveMediaSet_returnsFalse() =
- testScope.runTest {
- val selectedUserEntries by collectLastValue(repository.selectedUserEntries)
- val smartspaceMediaData by collectLastValue(repository.smartspaceMediaData)
- val reactivatedKey by collectLastValue(repository.reactivatedId)
- val data = dataMain.copy(active = false)
- mediaDataFilter.onMediaDataLoaded(KEY, oldKey = null, data = data)
-
- assertThat(
- hasActiveMediaOrRecommendation(
- selectedUserEntries,
- smartspaceMediaData,
- reactivatedKey,
- )
- )
- .isFalse()
- }
-
- @Test
- fun hasActiveMediaOrRecommendation_activeMediaSet_returnsTrue() =
+ fun hasAnyMedia_onlyCurrentUser() =
testScope.runTest {
val selectedUserEntries by collectLastValue(repository.selectedUserEntries)
- val smartspaceMediaData by collectLastValue(repository.smartspaceMediaData)
- val reactivatedKey by collectLastValue(repository.reactivatedId)
- val data = dataMain.copy(active = true)
- mediaDataFilter.onMediaDataLoaded(KEY, oldKey = null, data = data)
-
- assertThat(
- hasActiveMediaOrRecommendation(
- selectedUserEntries,
- smartspaceMediaData,
- reactivatedKey,
- )
- )
- .isTrue()
- }
-
- @Test
- fun hasActiveMediaOrRecommendation_inactiveRecommendationSet_returnsFalse() =
- testScope.runTest {
- val selectedUserEntries by collectLastValue(repository.selectedUserEntries)
- val smartspaceMediaData by collectLastValue(repository.smartspaceMediaData)
- val reactivatedKey by collectLastValue(repository.reactivatedId)
- whenever(smartspaceData.isActive).thenReturn(false)
- mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
-
- assertThat(
- hasActiveMediaOrRecommendation(
- selectedUserEntries,
- smartspaceMediaData,
- reactivatedKey,
- )
- )
- .isFalse()
- }
-
- @Test
- fun hasActiveMediaOrRecommendation_invalidRecommendationSet_returnsFalse() =
- testScope.runTest {
- val selectedUserEntries by collectLastValue(repository.selectedUserEntries)
- val smartspaceMediaData by collectLastValue(repository.smartspaceMediaData)
- val reactivatedKey by collectLastValue(repository.reactivatedId)
- whenever(smartspaceData.isValid()).thenReturn(false)
- mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
-
- assertThat(
- hasActiveMediaOrRecommendation(
- selectedUserEntries,
- smartspaceMediaData,
- reactivatedKey,
- )
- )
- .isFalse()
- }
-
- @Test
- fun hasActiveMediaOrRecommendation_activeAndValidRecommendationSet_returnsTrue() =
- testScope.runTest {
- val selectedUserEntries by collectLastValue(repository.selectedUserEntries)
- val smartspaceMediaData by collectLastValue(repository.smartspaceMediaData)
- val reactivatedKey by collectLastValue(repository.reactivatedId)
- whenever(smartspaceData.isActive).thenReturn(true)
- whenever(smartspaceData.isValid()).thenReturn(true)
- mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
-
- assertThat(
- hasActiveMediaOrRecommendation(
- selectedUserEntries,
- smartspaceMediaData,
- reactivatedKey,
- )
- )
- .isTrue()
- }
-
- @Test
- fun hasAnyMediaOrRecommendation_onlyCurrentUser() =
- testScope.runTest {
- val selectedUserEntries by collectLastValue(repository.selectedUserEntries)
- val smartspaceMediaData by collectLastValue(repository.smartspaceMediaData)
- assertThat(hasAnyMediaOrRecommendation(selectedUserEntries, smartspaceMediaData))
- .isFalse()
+ assertThat(hasAnyMedia(selectedUserEntries)).isFalse()
mediaDataFilter.onMediaDataLoaded(KEY, oldKey = null, data = dataGuest)
- assertThat(hasAnyMediaOrRecommendation(selectedUserEntries, smartspaceMediaData))
- .isFalse()
assertThat(hasAnyMedia(selectedUserEntries)).isFalse()
}
@Test
- fun hasActiveMediaOrRecommendation_onlyCurrentUser() =
+ fun hasActiveMedia_onlyCurrentUser() =
testScope.runTest {
val selectedUserEntries by collectLastValue(repository.selectedUserEntries)
- val smartspaceMediaData by collectLastValue(repository.smartspaceMediaData)
- val reactivatedKey by collectLastValue(repository.reactivatedId)
- assertThat(
- hasActiveMediaOrRecommendation(
- selectedUserEntries,
- smartspaceMediaData,
- reactivatedKey,
- )
- )
- .isFalse()
+ assertThat(hasActiveMedia(selectedUserEntries)).isFalse()
val data = dataGuest.copy(active = true)
mediaDataFilter.onMediaDataLoaded(KEY, oldKey = null, data = data)
- assertThat(
- hasActiveMediaOrRecommendation(
- selectedUserEntries,
- smartspaceMediaData,
- reactivatedKey,
- )
- )
- .isFalse()
+ assertThat(hasActiveMedia(selectedUserEntries)).isFalse()
assertThat(hasAnyMedia(selectedUserEntries)).isFalse()
}
@@ -518,12 +334,9 @@ class MediaDataFilterImplTest : SysuiTestCase() {
fun onNotificationRemoved_doesNotHaveMedia() =
testScope.runTest {
val selectedUserEntries by collectLastValue(repository.selectedUserEntries)
- val smartspaceMediaData by collectLastValue(repository.smartspaceMediaData)
mediaDataFilter.onMediaDataLoaded(KEY, oldKey = null, data = dataMain)
mediaDataFilter.onMediaDataRemoved(KEY, false)
- assertThat(hasAnyMediaOrRecommendation(selectedUserEntries, smartspaceMediaData))
- .isFalse()
assertThat(hasAnyMedia(selectedUserEntries)).isFalse()
}
@@ -535,555 +348,10 @@ class MediaDataFilterImplTest : SysuiTestCase() {
verify(mediaDataProcessor).setInactive(eq(KEY), eq(true), eq(true))
}
- @Test
- fun onSmartspaceMediaDataLoaded_noMedia_activeValidRec_prioritizesSmartspace() =
- testScope.runTest {
- val selectedUserEntries by collectLastValue(repository.selectedUserEntries)
- val smartspaceMediaData by collectLastValue(repository.smartspaceMediaData)
- val reactivatedKey by collectLastValue(repository.reactivatedId)
- val currentMedia by collectLastValue(repository.currentMedia)
- val recommendationsLoadingModel =
- SmartspaceMediaLoadingModel.Loaded(SMARTSPACE_KEY, isPrioritized = true)
-
- mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
-
- assertThat(currentMedia)
- .containsExactly(MediaCommonModel.MediaRecommendations(recommendationsLoadingModel))
- assertThat(
- hasActiveMediaOrRecommendation(
- selectedUserEntries,
- smartspaceMediaData,
- reactivatedKey,
- )
- )
- .isTrue()
- assertThat(hasActiveMedia(selectedUserEntries)).isFalse()
- verify(listener)
- .onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(true))
- verify(mediaLogger).logRecommendationLoaded(eq(SMARTSPACE_KEY), eq(true), anyString())
- verify(logger).logRecommendationAdded(SMARTSPACE_PACKAGE, SMARTSPACE_INSTANCE_ID)
- verify(logger, never()).logRecommendationActivated(any(), any(), any())
- }
-
- @Test
- fun onSmartspaceMediaDataLoaded_noMedia_inactiveRec_showsNothing() =
- testScope.runTest {
- val selectedUserEntries by collectLastValue(repository.selectedUserEntries)
- val smartspaceMediaData by collectLastValue(repository.smartspaceMediaData)
- val reactivatedKey by collectLastValue(repository.reactivatedId)
- val currentMedia by collectLastValue(repository.currentMedia)
-
- whenever(smartspaceData.isActive).thenReturn(false)
-
- mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
-
- assertThat(currentMedia).isEmpty()
- assertThat(
- hasActiveMediaOrRecommendation(
- selectedUserEntries,
- smartspaceMediaData,
- reactivatedKey,
- )
- )
- .isFalse()
- assertThat(hasActiveMedia(selectedUserEntries)).isFalse()
- verify(listener, never())
- .onMediaDataLoaded(any(), any(), any(), anyBoolean(), anyInt(), anyBoolean())
- verify(listener, never()).onSmartspaceMediaDataLoaded(any(), any(), anyBoolean())
- verify(mediaLogger, never()).logMediaLoaded(any(), anyBoolean(), anyString())
- verify(mediaLogger, never()).logRecommendationLoaded(any(), anyBoolean(), anyString())
- verify(logger, never()).logRecommendationAdded(any(), any())
- verify(logger, never()).logRecommendationActivated(any(), any(), any())
- }
-
- @Test
- fun onSmartspaceMediaDataLoaded_noRecentMedia_activeValidRec_prioritizesSmartspace() =
- testScope.runTest {
- val selectedUserEntries by collectLastValue(repository.selectedUserEntries)
- val smartspaceMediaData by collectLastValue(repository.smartspaceMediaData)
- val reactivatedKey by collectLastValue(repository.reactivatedId)
- val currentMedia by collectLastValue(repository.currentMedia)
- val recsCommonModel =
- MediaCommonModel.MediaRecommendations(
- SmartspaceMediaLoadingModel.Loaded(SMARTSPACE_KEY, isPrioritized = true)
- )
- val controlCommonModel =
- MediaCommonModel.MediaControl(
- MediaDataLoadingModel.Loaded(dataMain.instanceId),
- true,
- )
- val dataOld = dataMain.copy(active = false, lastActive = clock.elapsedRealtime())
- mediaDataFilter.onMediaDataLoaded(KEY, null, dataOld)
- clock.advanceTime(MediaDataFilterImpl.SMARTSPACE_MAX_AGE + 100)
- mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
-
- assertThat(currentMedia).containsExactly(recsCommonModel, controlCommonModel)
- assertThat(
- hasActiveMediaOrRecommendation(
- selectedUserEntries,
- smartspaceMediaData,
- reactivatedKey,
- )
- )
- .isTrue()
- assertThat(hasActiveMedia(selectedUserEntries)).isFalse()
- verify(listener)
- .onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(true))
- verify(mediaLogger).logRecommendationLoaded(eq(SMARTSPACE_KEY), eq(true), anyString())
- verify(logger).logRecommendationAdded(SMARTSPACE_PACKAGE, SMARTSPACE_INSTANCE_ID)
- verify(logger, never()).logRecommendationActivated(any(), any(), any())
- }
-
- @Test
- fun onSmartspaceMediaDataLoaded_noRecentMedia_inactiveRec_showsNothing() =
- testScope.runTest {
- val selectedUserEntries by collectLastValue(repository.selectedUserEntries)
- val smartspaceMediaData by collectLastValue(repository.smartspaceMediaData)
- val reactivatedKey by collectLastValue(repository.reactivatedId)
- val currentMedia by collectLastValue(repository.currentMedia)
- whenever(smartspaceData.isActive).thenReturn(false)
-
- val dataOld = dataMain.copy(active = false, lastActive = clock.elapsedRealtime())
- mediaDataFilter.onMediaDataLoaded(KEY, null, dataOld)
- clock.advanceTime(MediaDataFilterImpl.SMARTSPACE_MAX_AGE + 100)
- mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
-
- assertThat(currentMedia)
- .doesNotContain(
- MediaCommonModel.MediaRecommendations(
- SmartspaceMediaLoadingModel.Loaded(SMARTSPACE_KEY)
- )
- )
- assertThat(
- hasActiveMediaOrRecommendation(
- selectedUserEntries,
- smartspaceMediaData,
- reactivatedKey,
- )
- )
- .isFalse()
- assertThat(hasActiveMedia(selectedUserEntries)).isFalse()
- verify(listener, never()).onSmartspaceMediaDataLoaded(any(), any(), anyBoolean())
- verify(mediaLogger, never()).logRecommendationLoaded(any(), anyBoolean(), anyString())
- verify(logger, never()).logRecommendationAdded(any(), any())
- verify(logger, never()).logRecommendationActivated(any(), any(), any())
- }
-
- @Test
- fun onSmartspaceMediaDataLoaded_hasRecentMedia_inactiveRec_showsNothing() =
- testScope.runTest {
- val selectedUserEntries by collectLastValue(repository.selectedUserEntries)
- val smartspaceMediaData by collectLastValue(repository.smartspaceMediaData)
- val reactivatedKey by collectLastValue(repository.reactivatedId)
- val currentMedia by collectLastValue(repository.currentMedia)
-
- whenever(smartspaceData.isActive).thenReturn(false)
-
- // WHEN we have media that was recently played, but not currently active
- val dataCurrent = dataMain.copy(active = false, lastActive = clock.elapsedRealtime())
- val controlCommonModel =
- MediaCommonModel.MediaControl(
- MediaDataLoadingModel.Loaded(dataMain.instanceId),
- true,
- )
- mediaDataFilter.onMediaDataLoaded(KEY, null, dataCurrent)
- repository.setOrderedMedia()
-
- assertThat(currentMedia).containsExactly(controlCommonModel)
- verify(listener)
- .onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true), eq(0), eq(false))
- verify(mediaLogger)
- .logMediaLoaded(eq(dataCurrent.instanceId), eq(dataCurrent.active), anyString())
-
- reset(mediaLogger)
-
- // AND we get a smartspace signal
- mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
-
- // THEN we should treat the media as not active instead
- assertThat(currentMedia).containsExactly(controlCommonModel)
- assertThat(
- hasActiveMediaOrRecommendation(
- selectedUserEntries,
- smartspaceMediaData,
- reactivatedKey,
- )
- )
- .isFalse()
- assertThat(hasActiveMedia(selectedUserEntries)).isFalse()
- verify(listener, never())
- .onMediaDataLoaded(eq(KEY), eq(KEY), any(), anyBoolean(), anyInt(), anyBoolean())
- verify(listener, never()).onSmartspaceMediaDataLoaded(any(), any(), anyBoolean())
- verify(mediaLogger, never())
- .logMediaLoaded(eq(dataCurrent.instanceId), anyBoolean(), anyString())
- verify(mediaLogger, never()).logRecommendationLoaded(any(), anyBoolean(), anyString())
- verify(logger, never()).logRecommendationAdded(any(), any())
- verify(logger, never()).logRecommendationActivated(any(), any(), any())
- }
-
- @Test
- fun onSmartspaceMediaDataLoaded_hasRecentMedia_activeInvalidRec_usesMedia() =
- testScope.runTest {
- val selectedUserEntries by collectLastValue(repository.selectedUserEntries)
- val smartspaceMediaData by collectLastValue(repository.smartspaceMediaData)
- val reactivatedKey by collectLastValue(repository.reactivatedId)
- val currentMedia by collectLastValue(repository.currentMedia)
- whenever(smartspaceData.isValid()).thenReturn(false)
-
- // WHEN we have media that was recently played, but not currently active
- val dataCurrent = dataMain.copy(active = false, lastActive = clock.elapsedRealtime())
- val mediaLoadingModel = MediaDataLoadingModel.Loaded(dataMain.instanceId)
- var controlCommonModel = MediaCommonModel.MediaControl(mediaLoadingModel, true)
- mediaDataFilter.onMediaDataLoaded(KEY, null, dataCurrent)
- repository.setOrderedMedia()
- assertThat(currentMedia).containsExactly(controlCommonModel)
- verify(listener)
- .onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true), eq(0), eq(false))
- verify(mediaLogger)
- .logMediaLoaded(eq(dataCurrent.instanceId), eq(dataCurrent.active), anyString())
-
- // AND we get a smartspace signal
- runCurrent()
- mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
-
- // THEN we should treat the media as active instead
- val dataCurrentAndActive =
- dataMain.copy(active = true, lastActive = clock.elapsedRealtime())
- controlCommonModel =
- controlCommonModel.copy(
- mediaLoadingModel.copy(
- receivedSmartspaceCardLatency = 100,
- isSsReactivated = true,
- )
- )
- assertThat(currentMedia).containsExactly(controlCommonModel)
- assertThat(
- hasActiveMediaOrRecommendation(
- selectedUserEntries,
- smartspaceMediaData,
- reactivatedKey,
- )
- )
- .isTrue()
- verify(listener)
- .onMediaDataLoaded(
- eq(KEY),
- eq(KEY),
- eq(dataCurrentAndActive),
- eq(true),
- eq(100),
- eq(true),
- )
- verify(mediaLogger)
- .logMediaLoaded(
- eq(dataCurrentAndActive.instanceId),
- eq(dataCurrentAndActive.active),
- anyString(),
- )
- // Smartspace update shouldn't be propagated for the empty rec list.
- verify(listener, never()).onSmartspaceMediaDataLoaded(any(), any(), anyBoolean())
- verify(mediaLogger, never()).logRecommendationLoaded(any(), anyBoolean(), anyString())
- verify(logger, never()).logRecommendationAdded(any(), any())
- verify(logger).logRecommendationActivated(eq(APP_UID), eq(PACKAGE), eq(INSTANCE_ID))
- }
-
- @Test
- fun onSmartspaceMediaDataLoaded_hasRecentMedia_activeValidRec_usesBoth() =
- testScope.runTest {
- val selectedUserEntries by collectLastValue(repository.selectedUserEntries)
- val smartspaceMediaData by collectLastValue(repository.smartspaceMediaData)
- val reactivatedKey by collectLastValue(repository.reactivatedId)
- val currentMedia by collectLastValue(repository.currentMedia)
- // WHEN we have media that was recently played, but not currently active
- val dataCurrent = dataMain.copy(active = false, lastActive = clock.elapsedRealtime())
- val mediaLoadingModel = MediaDataLoadingModel.Loaded(dataMain.instanceId)
- var controlCommonModel = MediaCommonModel.MediaControl(mediaLoadingModel, true)
- val recsCommonModel =
- MediaCommonModel.MediaRecommendations(
- SmartspaceMediaLoadingModel.Loaded(SMARTSPACE_KEY)
- )
-
- mediaDataFilter.onMediaDataLoaded(KEY, null, dataCurrent)
- repository.setOrderedMedia()
-
- assertThat(currentMedia).containsExactly(controlCommonModel)
- verify(listener)
- .onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true), eq(0), eq(false))
- verify(mediaLogger)
- .logMediaLoaded(eq(dataCurrent.instanceId), eq(dataCurrent.active), anyString())
-
- // AND we get a smartspace signal
- runCurrent()
- mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
-
- // THEN we should treat the media as active instead
- val dataCurrentAndActive =
- dataMain.copy(active = true, lastActive = clock.elapsedRealtime())
- verify(listener)
- .onMediaDataLoaded(
- eq(KEY),
- eq(KEY),
- eq(dataCurrentAndActive),
- eq(true),
- eq(100),
- eq(true),
- )
- verify(mediaLogger)
- .logMediaLoaded(
- eq(dataCurrentAndActive.instanceId),
- eq(dataCurrentAndActive.active),
- anyString(),
- )
- assertThat(
- hasActiveMediaOrRecommendation(
- selectedUserEntries,
- smartspaceMediaData,
- reactivatedKey,
- )
- )
- .isTrue()
- // Smartspace update should also be propagated but not prioritized.
- controlCommonModel =
- controlCommonModel.copy(
- mediaLoadingModel.copy(
- receivedSmartspaceCardLatency = 100,
- isSsReactivated = true,
- )
- )
- assertThat(currentMedia).containsExactly(controlCommonModel, recsCommonModel)
- verify(listener)
- .onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(false))
- verify(mediaLogger).logRecommendationLoaded(eq(SMARTSPACE_KEY), eq(true), anyString())
- verify(logger).logRecommendationAdded(SMARTSPACE_PACKAGE, SMARTSPACE_INSTANCE_ID)
- verify(logger).logRecommendationActivated(eq(APP_UID), eq(PACKAGE), eq(INSTANCE_ID))
- }
-
- @Test
- fun onSmartspaceMediaDataRemoved_usedSmartspace_clearsSmartspace() =
- testScope.runTest {
- val selectedUserEntries by collectLastValue(repository.selectedUserEntries)
- val smartspaceMediaData by collectLastValue(repository.smartspaceMediaData)
- val reactivatedKey by collectLastValue(repository.reactivatedId)
- val currentMedia by collectLastValue(repository.currentMedia)
-
- mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
- mediaDataFilter.onSmartspaceMediaDataRemoved(SMARTSPACE_KEY)
-
- verify(listener).onSmartspaceMediaDataRemoved(SMARTSPACE_KEY)
- verify(mediaLogger).logRecommendationRemoved(eq(SMARTSPACE_KEY), eq(true), anyString())
- assertThat(currentMedia).isEmpty()
- assertThat(
- hasActiveMediaOrRecommendation(
- selectedUserEntries,
- smartspaceMediaData,
- reactivatedKey,
- )
- )
- .isFalse()
- assertThat(hasActiveMedia(selectedUserEntries)).isFalse()
- }
-
- @Test
- fun onSmartspaceMediaDataRemoved_usedMediaAndSmartspace_clearsBoth() =
- testScope.runTest {
- val selectedUserEntries by collectLastValue(repository.selectedUserEntries)
- val smartspaceMediaData by collectLastValue(repository.smartspaceMediaData)
- val reactivatedKey by collectLastValue(repository.reactivatedId)
- val currentMedia by collectLastValue(repository.currentMedia)
- val controlCommonModel =
- MediaCommonModel.MediaControl(
- MediaDataLoadingModel.Loaded(dataMain.instanceId),
- true,
- )
- val dataCurrent = dataMain.copy(active = false, lastActive = clock.elapsedRealtime())
- mediaDataFilter.onMediaDataLoaded(KEY, null, dataCurrent)
- repository.setOrderedMedia()
-
- assertThat(currentMedia).containsExactly(controlCommonModel)
- verify(listener)
- .onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true), eq(0), eq(false))
- verify(mediaLogger)
- .logMediaLoaded(eq(dataCurrent.instanceId), eq(dataCurrent.active), anyString())
-
- runCurrent()
- mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
-
- val dataCurrentAndActive =
- dataMain.copy(active = true, lastActive = clock.elapsedRealtime())
- verify(listener)
- .onMediaDataLoaded(
- eq(KEY),
- eq(KEY),
- eq(dataCurrentAndActive),
- eq(true),
- eq(100),
- eq(true),
- )
- verify(mediaLogger)
- .logMediaLoaded(
- eq(dataCurrentAndActive.instanceId),
- eq(dataCurrentAndActive.active),
- anyString(),
- )
-
- mediaDataFilter.onSmartspaceMediaDataRemoved(SMARTSPACE_KEY)
-
- verify(listener).onSmartspaceMediaDataRemoved(SMARTSPACE_KEY)
- verify(mediaLogger).logRecommendationRemoved(eq(SMARTSPACE_KEY), eq(true), anyString())
- assertThat(currentMedia).containsExactly(controlCommonModel)
- assertThat(
- hasActiveMediaOrRecommendation(
- selectedUserEntries,
- smartspaceMediaData,
- reactivatedKey,
- )
- )
- .isFalse()
- assertThat(hasActiveMedia(selectedUserEntries)).isFalse()
- }
-
- @Test
- fun smartspaceLoaded_shouldTriggerResume_doesTrigger() =
- testScope.runTest {
- val selectedUserEntries by collectLastValue(repository.selectedUserEntries)
- val smartspaceMediaData by collectLastValue(repository.smartspaceMediaData)
- val reactivatedKey by collectLastValue(repository.reactivatedId)
- val currentMedia by collectLastValue(repository.currentMedia)
- val recsCommonModel =
- MediaCommonModel.MediaRecommendations(
- SmartspaceMediaLoadingModel.Loaded(SMARTSPACE_KEY)
- )
- val mediaLoadingModel = MediaDataLoadingModel.Loaded(dataMain.instanceId)
- var controlCommonModel = MediaCommonModel.MediaControl(mediaLoadingModel, true)
- // WHEN we have media that was recently played, but not currently active
- val dataCurrent = dataMain.copy(active = false, lastActive = clock.elapsedRealtime())
- mediaDataFilter.onMediaDataLoaded(KEY, null, dataCurrent)
- repository.setOrderedMedia()
-
- verify(listener)
- .onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true), eq(0), eq(false))
- verify(mediaLogger)
- .logMediaLoaded(eq(dataCurrent.instanceId), eq(dataCurrent.active), anyString())
- assertThat(currentMedia).containsExactly(controlCommonModel)
-
- // AND we get a smartspace signal with extra to trigger resume
- runCurrent()
- val extras = Bundle().apply { putBoolean(EXTRA_KEY_TRIGGER_RESUME, true) }
- whenever(cardAction.extras).thenReturn(extras)
- mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
-
- // THEN we should treat the media as active instead
- val dataCurrentAndActive =
- dataMain.copy(active = true, lastActive = clock.elapsedRealtime())
- controlCommonModel =
- controlCommonModel.copy(
- mediaLoadingModel.copy(
- receivedSmartspaceCardLatency = 100,
- isSsReactivated = true,
- )
- )
- verify(listener)
- .onMediaDataLoaded(
- eq(KEY),
- eq(KEY),
- eq(dataCurrentAndActive),
- eq(true),
- eq(100),
- eq(true),
- )
- verify(mediaLogger)
- .logMediaLoaded(
- eq(dataCurrentAndActive.instanceId),
- eq(dataCurrentAndActive.active),
- anyString(),
- )
- assertThat(currentMedia).containsExactly(controlCommonModel, recsCommonModel)
- assertThat(
- hasActiveMediaOrRecommendation(
- selectedUserEntries,
- smartspaceMediaData,
- reactivatedKey,
- )
- )
- .isTrue()
- // And update the smartspace data state, but not prioritized
- verify(listener)
- .onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(false))
- verify(mediaLogger).logRecommendationLoaded(eq(SMARTSPACE_KEY), eq(true), anyString())
- }
-
- @Test
- fun smartspaceLoaded_notShouldTriggerResume_doesNotTrigger() =
- testScope.runTest {
- val currentMedia by collectLastValue(repository.currentMedia)
- val recsCommonModel =
- MediaCommonModel.MediaRecommendations(
- SmartspaceMediaLoadingModel.Loaded(SMARTSPACE_KEY)
- )
- val controlCommonModel =
- MediaCommonModel.MediaControl(
- MediaDataLoadingModel.Loaded(dataMain.instanceId),
- true,
- )
-
- // WHEN we have media that was recently played, but not currently active
- val dataCurrent = dataMain.copy(active = false, lastActive = clock.elapsedRealtime())
- mediaDataFilter.onMediaDataLoaded(KEY, null, dataCurrent)
- repository.setOrderedMedia()
-
- verify(listener)
- .onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true), eq(0), eq(false))
- verify(mediaLogger)
- .logMediaLoaded(eq(dataCurrent.instanceId), eq(dataCurrent.active), anyString())
- assertThat(currentMedia).containsExactly(controlCommonModel)
-
- reset(mediaLogger)
-
- // AND we get a smartspace signal with extra to not trigger resume
- val extras = Bundle().apply { putBoolean(EXTRA_KEY_TRIGGER_RESUME, false) }
- whenever(cardAction.extras).thenReturn(extras)
- mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
-
- // THEN listeners are not updated to show media
- verify(listener, never())
- .onMediaDataLoaded(eq(KEY), eq(KEY), any(), eq(true), eq(100), eq(true))
- verify(mediaLogger, never())
- .logMediaLoaded(eq(dataCurrent.instanceId), anyBoolean(), anyString())
- // But the smartspace update is still propagated
- verify(listener)
- .onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(false))
- verify(mediaLogger).logRecommendationLoaded(eq(SMARTSPACE_KEY), eq(true), anyString())
- assertThat(currentMedia).containsExactly(controlCommonModel, recsCommonModel)
- }
-
- private fun hasActiveMediaOrRecommendation(
- entries: Map<InstanceId, MediaData>?,
- smartspaceMediaData: SmartspaceMediaData?,
- reactivatedId: InstanceId?,
- ): Boolean {
- if (entries == null || smartspaceMediaData == null) {
- return false
- }
- return entries.any { it.value.active } ||
- (smartspaceMediaData.isActive &&
- (smartspaceMediaData.isValid() || reactivatedId != null))
- }
-
private fun hasActiveMedia(entries: Map<InstanceId, MediaData>?): Boolean {
return entries?.any { it.value.active } ?: false
}
- private fun hasAnyMediaOrRecommendation(
- entries: Map<InstanceId, MediaData>?,
- smartspaceMediaData: SmartspaceMediaData?,
- ): Boolean {
- if (entries == null || smartspaceMediaData == null) {
- return false
- }
- return entries.isNotEmpty() ||
- (smartspaceMediaData.isActive && smartspaceMediaData.isValid())
- }
-
private fun hasAnyMedia(entries: Map<InstanceId, MediaData>?): Boolean {
return entries?.isNotEmpty() ?: false
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt
index 93c27f01b5ff..988f5545a62a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt
@@ -22,15 +22,10 @@ import android.app.Notification.FLAG_NO_CLEAR
import android.app.Notification.MediaStyle
import android.app.PendingIntent
import android.app.UriGrantsManager
-import android.app.smartspace.SmartspaceAction
-import android.app.smartspace.SmartspaceConfig
-import android.app.smartspace.SmartspaceManager
-import android.app.smartspace.SmartspaceTarget
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.Bitmap
import android.graphics.ImageDecoder
-import android.graphics.drawable.Icon
import android.media.MediaDescription
import android.media.MediaMetadata
import android.media.session.MediaController
@@ -56,7 +51,6 @@ import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.dump.DumpManager
import com.android.systemui.flags.EnableSceneContainer
import com.android.systemui.flags.Flags.MEDIA_REMOTE_RESUME
-import com.android.systemui.flags.Flags.MEDIA_RESUME_PROGRESS
import com.android.systemui.flags.Flags.MEDIA_RETAIN_SESSIONS
import com.android.systemui.flags.fakeFeatureFlagsClassic
import com.android.systemui.kosmos.testDispatcher
@@ -69,7 +63,6 @@ import com.android.systemui.media.controls.domain.resume.ResumeMediaBrowser
import com.android.systemui.media.controls.shared.mediaLogger
import com.android.systemui.media.controls.shared.mockMediaLogger
import com.android.systemui.media.controls.shared.model.MediaData
-import com.android.systemui.media.controls.shared.model.SmartspaceMediaDataProvider
import com.android.systemui.media.controls.util.MediaUiEventLogger
import com.android.systemui.media.controls.util.fakeMediaControllerFactory
import com.android.systemui.media.controls.util.mediaFlags
@@ -109,9 +102,6 @@ import platform.test.runner.parameterized.Parameters
private const val KEY = "KEY"
private const val KEY_2 = "KEY_2"
-private const val KEY_MEDIA_SMARTSPACE = "MEDIA_SMARTSPACE_ID"
-private const val SMARTSPACE_CREATION_TIME = 1234L
-private const val SMARTSPACE_EXPIRY_TIME = 5678L
private const val PACKAGE_NAME = "com.example.app"
private const val SYSTEM_PACKAGE_NAME = "com.android.systemui"
private const val APP_NAME = "SystemUI"
@@ -149,13 +139,7 @@ class MediaDataProcessorTest(flags: FlagsParameterization) : SysuiTestCase() {
@Mock lateinit var mediaDataCombineLatest: MediaDataCombineLatest
@Mock lateinit var listener: MediaDataManager.Listener
@Mock lateinit var pendingIntent: PendingIntent
- @Mock lateinit var smartspaceManager: SmartspaceManager
@Mock lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
- private lateinit var smartspaceMediaDataProvider: SmartspaceMediaDataProvider
- @Mock lateinit var mediaSmartspaceTarget: SmartspaceTarget
- @Mock private lateinit var mediaRecommendationItem: SmartspaceAction
- private lateinit var validRecommendationList: List<SmartspaceAction>
- @Mock private lateinit var mediaSmartspaceBaseAction: SmartspaceAction
@Mock private lateinit var logger: MediaUiEventLogger
private lateinit var mediaCarouselInteractor: MediaCarouselInteractor
private lateinit var mediaDataProcessor: MediaDataProcessor
@@ -165,7 +149,6 @@ class MediaDataProcessorTest(flags: FlagsParameterization) : SysuiTestCase() {
private val clock = FakeSystemClock()
@Captor lateinit var stateCallbackCaptor: ArgumentCaptor<(String, PlaybackState) -> Unit>
@Captor lateinit var sessionCallbackCaptor: ArgumentCaptor<(String) -> Unit>
- @Captor lateinit var smartSpaceConfigBuilderCaptor: ArgumentCaptor<SmartspaceConfig>
@Mock private lateinit var ugm: IUriGrantsManager
@Mock private lateinit var imageSource: ImageDecoder.Source
@@ -197,15 +180,14 @@ class MediaDataProcessorTest(flags: FlagsParameterization) : SysuiTestCase() {
fun setup() {
staticMockSession =
ExtendedMockito.mockitoSession()
- .mockStatic<UriGrantsManager>(UriGrantsManager::class.java)
- .mockStatic<ImageDecoder>(ImageDecoder::class.java)
+ .mockStatic(UriGrantsManager::class.java)
+ .mockStatic(ImageDecoder::class.java)
.strictness(Strictness.LENIENT)
.startMocking()
whenever(UriGrantsManager.getService()).thenReturn(ugm)
foregroundExecutor = FakeExecutor(clock)
backgroundExecutor = FakeExecutor(clock)
uiExecutor = FakeExecutor(clock)
- smartspaceMediaDataProvider = SmartspaceMediaDataProvider()
mediaDataProcessor =
MediaDataProcessor(
context = context,
@@ -218,13 +200,11 @@ class MediaDataProcessorTest(flags: FlagsParameterization) : SysuiTestCase() {
mediaControllerFactory = mediaControllerFactory,
broadcastDispatcher = broadcastDispatcher,
dumpManager = dumpManager,
- smartspaceMediaDataProvider = smartspaceMediaDataProvider,
useMediaResumption = true,
useQsMediaPlayer = true,
systemClock = clock,
mediaFlags = kosmos.mediaFlags,
logger = logger,
- smartspaceManager = smartspaceManager,
keyguardUpdateMonitor = keyguardUpdateMonitor,
mediaDataRepository = kosmos.mediaDataRepository,
mediaDataLoader = { kosmos.mediaDataLoader },
@@ -279,7 +259,6 @@ class MediaDataProcessorTest(flags: FlagsParameterization) : SysuiTestCase() {
putString(MediaMetadata.METADATA_KEY_ARTIST, SESSION_ARTIST)
putString(MediaMetadata.METADATA_KEY_TITLE, SESSION_TITLE)
}
- verify(smartspaceManager).createSmartspaceSession(capture(smartSpaceConfigBuilderCaptor))
mediaControllerFactory.setControllerForToken(session.sessionToken, controller)
whenever(controller.sessionToken).thenReturn(session.sessionToken)
whenever(controller.transportControls).thenReturn(transportControls)
@@ -289,30 +268,12 @@ class MediaDataProcessorTest(flags: FlagsParameterization) : SysuiTestCase() {
.thenReturn(MediaController.PlaybackInfo.PLAYBACK_TYPE_LOCAL)
// This is an ugly hack for now. The mediaSessionBasedFilter is one of the internal
- // listeners in the internal processing pipeline. It receives events, but ince it is a
+ // listeners in the internal processing pipeline. It receives events, but since it is a
// mock, it doesn't pass those events along the chain to the external listeners. So, just
// treat mediaSessionBasedFilter as a listener for testing.
listener = mediaSessionBasedFilter
- val recommendationExtras =
- Bundle().apply {
- putString("package_name", PACKAGE_NAME)
- putParcelable("dismiss_intent", DISMISS_INTENT)
- }
- val icon = Icon.createWithResource(context, android.R.drawable.ic_media_play)
- whenever(mediaSmartspaceBaseAction.extras).thenReturn(recommendationExtras)
- whenever(mediaSmartspaceTarget.baseAction).thenReturn(mediaSmartspaceBaseAction)
- whenever(mediaRecommendationItem.extras).thenReturn(recommendationExtras)
- whenever(mediaRecommendationItem.icon).thenReturn(icon)
- validRecommendationList =
- listOf(mediaRecommendationItem, mediaRecommendationItem, mediaRecommendationItem)
- whenever(mediaSmartspaceTarget.smartspaceTargetId).thenReturn(KEY_MEDIA_SMARTSPACE)
- whenever(mediaSmartspaceTarget.featureType).thenReturn(SmartspaceTarget.FEATURE_MEDIA)
- whenever(mediaSmartspaceTarget.iconGrid).thenReturn(validRecommendationList)
- whenever(mediaSmartspaceTarget.creationTimeMillis).thenReturn(SMARTSPACE_CREATION_TIME)
- whenever(mediaSmartspaceTarget.expiryTimeMillis).thenReturn(SMARTSPACE_EXPIRY_TIME)
fakeFeatureFlags.set(MEDIA_RETAIN_SESSIONS, false)
- fakeFeatureFlags.set(MEDIA_RESUME_PROGRESS, false)
fakeFeatureFlags.set(MEDIA_REMOTE_RESUME, false)
whenever(logger.getNewInstanceId()).thenReturn(instanceIdSequence.newInstanceId())
whenever(keyguardUpdateMonitor.isUserInLockdown(any())).thenReturn(false)
@@ -990,8 +951,6 @@ class MediaDataProcessorTest(flags: FlagsParameterization) : SysuiTestCase() {
@Test
fun testAddResumptionControls_hasPartialProgress() {
- fakeFeatureFlags.set(MEDIA_RESUME_PROGRESS, true)
-
// WHEN resumption controls are added with partial progress
val progress = 0.5
val extras =
@@ -1017,8 +976,6 @@ class MediaDataProcessorTest(flags: FlagsParameterization) : SysuiTestCase() {
@Test
fun testAddResumptionControls_hasNotPlayedProgress() {
- fakeFeatureFlags.set(MEDIA_RESUME_PROGRESS, true)
-
// WHEN resumption controls are added that have not been played
val extras =
Bundle().apply {
@@ -1042,8 +999,6 @@ class MediaDataProcessorTest(flags: FlagsParameterization) : SysuiTestCase() {
@Test
fun testAddResumptionControls_hasFullProgress() {
- fakeFeatureFlags.set(MEDIA_RESUME_PROGRESS, true)
-
// WHEN resumption controls are added with progress info
val extras =
Bundle().apply {
@@ -1068,8 +1023,6 @@ class MediaDataProcessorTest(flags: FlagsParameterization) : SysuiTestCase() {
@Test
fun testAddResumptionControls_hasNoExtras() {
- fakeFeatureFlags.set(MEDIA_RESUME_PROGRESS, true)
-
// WHEN resumption controls are added that do not have any extras
val desc =
MediaDescription.Builder().run {
@@ -1086,8 +1039,6 @@ class MediaDataProcessorTest(flags: FlagsParameterization) : SysuiTestCase() {
@Test
fun testAddResumptionControls_hasEmptyTitle() {
- fakeFeatureFlags.set(MEDIA_RESUME_PROGRESS, true)
-
// WHEN resumption controls are added that have empty title
val desc =
MediaDescription.Builder().run {
@@ -1119,8 +1070,6 @@ class MediaDataProcessorTest(flags: FlagsParameterization) : SysuiTestCase() {
@Test
fun testAddResumptionControls_hasBlankTitle() {
- fakeFeatureFlags.set(MEDIA_RESUME_PROGRESS, true)
-
// WHEN resumption controls are added that have a blank title
val desc =
MediaDescription.Builder().run {
@@ -2201,7 +2150,6 @@ class MediaDataProcessorTest(flags: FlagsParameterization) : SysuiTestCase() {
}
@Test
- @EnableFlags(Flags.FLAG_MEDIA_CONTROLS_POSTS_OPTIMIZATION)
fun postDuplicateNotification_doesNotCallListeners() {
whenever(notificationLockscreenUserManager.isCurrentProfile(USER_ID)).thenReturn(true)
whenever(notificationLockscreenUserManager.isProfileAvailable(USER_ID)).thenReturn(true)
@@ -2226,31 +2174,6 @@ class MediaDataProcessorTest(flags: FlagsParameterization) : SysuiTestCase() {
}
@Test
- @DisableFlags(Flags.FLAG_MEDIA_CONTROLS_POSTS_OPTIMIZATION)
- fun postDuplicateNotification_callsListeners() {
- whenever(notificationLockscreenUserManager.isCurrentProfile(USER_ID)).thenReturn(true)
- whenever(notificationLockscreenUserManager.isProfileAvailable(USER_ID)).thenReturn(true)
-
- mediaDataProcessor.addInternalListener(mediaDataFilter)
- mediaDataFilter.mediaDataProcessor = mediaDataProcessor
- addNotificationAndLoad()
- reset(listener)
- mediaDataProcessor.onNotificationAdded(KEY, mediaNotification)
- testScope.assertRunAllReady(foreground = 1, background = 1)
- verify(listener)
- .onMediaDataLoaded(
- eq(KEY),
- eq(KEY),
- capture(mediaDataCaptor),
- eq(true),
- eq(0),
- eq(false),
- )
- verify(kosmos.mediaLogger, never()).logDuplicateMediaNotification(eq(KEY))
- }
-
- @Test
- @EnableFlags(Flags.FLAG_MEDIA_CONTROLS_POSTS_OPTIMIZATION)
fun postDifferentIntentNotifications_CallsListeners() {
whenever(notificationLockscreenUserManager.isCurrentProfile(USER_ID)).thenReturn(true)
whenever(notificationLockscreenUserManager.isProfileAvailable(USER_ID)).thenReturn(true)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaPlayerDataTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaPlayerDataTest.kt
index addf008619c9..f10a476f1572 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaPlayerDataTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaPlayerDataTest.kt
@@ -35,7 +35,7 @@ import org.mockito.junit.MockitoJUnit
@SmallTest
@RunWith(AndroidJUnit4::class)
-public class MediaPlayerDataTest : SysuiTestCase() {
+class MediaPlayerDataTest : SysuiTestCase() {
@Mock private lateinit var playerIsPlaying: MediaControlPanel
private var systemClock: FakeSystemClock = FakeSystemClock()
@@ -62,20 +62,8 @@ public class MediaPlayerDataTest : SysuiTestCase() {
val playerIsRemote = mock(MediaControlPanel::class.java)
val dataIsRemote = createMediaData("app2", PLAYING, REMOTE, !RESUMPTION)
- MediaPlayerData.addMediaPlayer(
- "2",
- dataIsRemote,
- playerIsRemote,
- systemClock,
- isSsReactivated = false
- )
- MediaPlayerData.addMediaPlayer(
- "1",
- dataIsPlaying,
- playerIsPlaying,
- systemClock,
- isSsReactivated = false
- )
+ MediaPlayerData.addMediaPlayer("2", dataIsRemote, playerIsRemote, systemClock)
+ MediaPlayerData.addMediaPlayer("1", dataIsPlaying, playerIsPlaying, systemClock)
val players = MediaPlayerData.players()
assertThat(players).hasSize(2)
@@ -90,42 +78,18 @@ public class MediaPlayerDataTest : SysuiTestCase() {
val playerIsPlaying2 = mock(MediaControlPanel::class.java)
var dataIsPlaying2 = createMediaData("app2", !PLAYING, LOCAL, !RESUMPTION)
- MediaPlayerData.addMediaPlayer(
- "1",
- dataIsPlaying1,
- playerIsPlaying1,
- systemClock,
- isSsReactivated = false
- )
+ MediaPlayerData.addMediaPlayer("1", dataIsPlaying1, playerIsPlaying1, systemClock)
systemClock.advanceTime(1)
- MediaPlayerData.addMediaPlayer(
- "2",
- dataIsPlaying2,
- playerIsPlaying2,
- systemClock,
- isSsReactivated = false
- )
+ MediaPlayerData.addMediaPlayer("2", dataIsPlaying2, playerIsPlaying2, systemClock)
systemClock.advanceTime(1)
dataIsPlaying1 = createMediaData("app1", !PLAYING, LOCAL, !RESUMPTION)
dataIsPlaying2 = createMediaData("app2", PLAYING, LOCAL, !RESUMPTION)
- MediaPlayerData.addMediaPlayer(
- "1",
- dataIsPlaying1,
- playerIsPlaying1,
- systemClock,
- isSsReactivated = false
- )
+ MediaPlayerData.addMediaPlayer("1", dataIsPlaying1, playerIsPlaying1, systemClock)
systemClock.advanceTime(1)
- MediaPlayerData.addMediaPlayer(
- "2",
- dataIsPlaying2,
- playerIsPlaying2,
- systemClock,
- isSsReactivated = false
- )
+ MediaPlayerData.addMediaPlayer("2", dataIsPlaying2, playerIsPlaying2, systemClock)
systemClock.advanceTime(1)
val players = MediaPlayerData.players()
@@ -157,43 +121,22 @@ public class MediaPlayerDataTest : SysuiTestCase() {
dataIsStoppedAndLocal,
playerIsStoppedAndLocal,
systemClock,
- isSsReactivated = false
)
MediaPlayerData.addMediaPlayer(
"5",
dataIsStoppedAndRemote,
playerIsStoppedAndRemote,
systemClock,
- isSsReactivated = false
- )
- MediaPlayerData.addMediaPlayer(
- "4",
- dataCanResume,
- playerCanResume,
- systemClock,
- isSsReactivated = false
- )
- MediaPlayerData.addMediaPlayer(
- "1",
- dataIsPlaying,
- playerIsPlaying,
- systemClock,
- isSsReactivated = false
)
+ MediaPlayerData.addMediaPlayer("4", dataCanResume, playerCanResume, systemClock)
+ MediaPlayerData.addMediaPlayer("1", dataIsPlaying, playerIsPlaying, systemClock)
MediaPlayerData.addMediaPlayer(
"2",
dataIsPlayingAndRemote,
playerIsPlayingAndRemote,
systemClock,
- isSsReactivated = false
- )
- MediaPlayerData.addMediaPlayer(
- "6",
- dataUndetermined,
- playerUndetermined,
- systemClock,
- isSsReactivated = false
)
+ MediaPlayerData.addMediaPlayer("6", dataUndetermined, playerUndetermined, systemClock)
val players = MediaPlayerData.players()
assertThat(players).hasSize(6)
@@ -204,7 +147,7 @@ public class MediaPlayerDataTest : SysuiTestCase() {
playerIsStoppedAndRemote,
playerIsStoppedAndLocal,
playerUndetermined,
- playerCanResume
+ playerCanResume,
)
.inOrder()
}
@@ -218,23 +161,11 @@ public class MediaPlayerDataTest : SysuiTestCase() {
assertThat(MediaPlayerData.players()).hasSize(0)
- MediaPlayerData.addMediaPlayer(
- keyA,
- data,
- playerIsPlaying,
- systemClock,
- isSsReactivated = false
- )
+ MediaPlayerData.addMediaPlayer(keyA, data, playerIsPlaying, systemClock)
systemClock.advanceTime(1)
assertThat(MediaPlayerData.players()).hasSize(1)
- MediaPlayerData.addMediaPlayer(
- keyB,
- data,
- playerIsPlaying,
- systemClock,
- isSsReactivated = false
- )
+ MediaPlayerData.addMediaPlayer(keyB, data, playerIsPlaying, systemClock)
systemClock.advanceTime(1)
assertThat(MediaPlayerData.players()).hasSize(2)
@@ -251,7 +182,7 @@ public class MediaPlayerDataTest : SysuiTestCase() {
app: String,
isPlaying: Boolean?,
location: Int,
- resumption: Boolean
+ resumption: Boolean,
) =
MediaTestUtils.emptyMediaData.copy(
app = app,
@@ -259,6 +190,6 @@ public class MediaPlayerDataTest : SysuiTestCase() {
playbackLocation = location,
resumption = resumption,
notificationKey = "key: $app",
- isPlaying = isPlaying
+ isPlaying = isPlaying,
)
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerTest.kt
index dad08e014c0f..c897e7face16 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerTest.kt
@@ -29,7 +29,6 @@ import android.view.View
import androidx.test.filters.SmallTest
import com.android.compose.animation.scene.ObservableTransitionState
import com.android.compose.animation.scene.SceneKey
-import com.android.internal.logging.InstanceId
import com.android.keyguard.KeyguardUpdateMonitor
import com.android.keyguard.KeyguardUpdateMonitorCallback
import com.android.systemui.Flags.mediaControlsUmoInflationInBackground
@@ -38,7 +37,6 @@ import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor
import com.android.systemui.dump.DumpManager
import com.android.systemui.flags.DisableSceneContainer
import com.android.systemui.flags.EnableSceneContainer
-import com.android.systemui.flags.Flags
import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintAuthRepository
import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
@@ -49,7 +47,6 @@ import com.android.systemui.kosmos.testDispatcher
import com.android.systemui.kosmos.testScope
import com.android.systemui.kosmos.useUnconfinedTestDispatcher
import com.android.systemui.media.controls.MediaTestUtils
-import com.android.systemui.media.controls.domain.pipeline.EMPTY_SMARTSPACE_MEDIA_DATA
import com.android.systemui.media.controls.domain.pipeline.MediaDataManager
import com.android.systemui.media.controls.shared.model.MediaData
import com.android.systemui.media.controls.ui.controller.MediaHierarchyManager.Companion.LOCATION_QS
@@ -107,7 +104,6 @@ import platform.test.runner.parameterized.Parameters
private val DATA = MediaTestUtils.emptyMediaData
-private val SMARTSPACE_KEY = "smartspace"
private const val PAUSED_LOCAL = "paused local"
private const val PLAYING_LOCAL = "playing local"
@@ -340,31 +336,6 @@ class MediaCarouselControllerTest(flags: FlagsParameterization) : SysuiTestCase(
1000L,
)
- val activeMoreRecent =
- Triple(
- "active more recent",
- DATA.copy(
- active = false,
- isPlaying = false,
- playbackLocation = MediaData.PLAYBACK_LOCAL,
- resumption = true,
- lastActive = 2L,
- ),
- 1000L,
- )
-
- val activeLessRecent =
- Triple(
- "active less recent",
- DATA.copy(
- active = false,
- isPlaying = false,
- playbackLocation = MediaData.PLAYBACK_LOCAL,
- resumption = true,
- lastActive = 1L,
- ),
- 1000L,
- )
// Expected ordering for media players:
// Actively playing local sessions
// Actively playing cast sessions
@@ -392,7 +363,6 @@ class MediaCarouselControllerTest(flags: FlagsParameterization) : SysuiTestCase(
it.second.copy(notificationKey = it.first),
panel,
clock,
- isSsReactivated = false,
)
}
@@ -405,41 +375,6 @@ class MediaCarouselControllerTest(flags: FlagsParameterization) : SysuiTestCase(
}
}
- @Test
- fun testOrderWithSmartspace_prioritized() {
- testPlayerOrdering()
-
- // If smartspace is prioritized
- MediaPlayerData.addMediaRecommendation(
- SMARTSPACE_KEY,
- EMPTY_SMARTSPACE_MEDIA_DATA.copy(isActive = true),
- panel,
- true,
- clock,
- )
-
- // Then it should be shown immediately after any actively playing controls
- assertTrue(MediaPlayerData.playerKeys().elementAt(2).isSsMediaRec)
- }
-
- @Test
- fun testOrderWithSmartspace_notPrioritized() {
- testPlayerOrdering()
-
- // If smartspace is not prioritized
- MediaPlayerData.addMediaRecommendation(
- SMARTSPACE_KEY,
- EMPTY_SMARTSPACE_MEDIA_DATA.copy(isActive = true),
- panel,
- false,
- clock,
- )
-
- // Then it should be shown at the end of the carousel's active entries
- val idx = MediaPlayerData.playerKeys().count { it.data.active } - 1
- assertTrue(MediaPlayerData.playerKeys().elementAt(idx).isSsMediaRec)
- }
-
@DisableSceneContainer
@Test
fun testPlayingExistingMediaPlayerFromCarousel_visibleMediaPlayersNotUpdated() {
@@ -540,19 +475,6 @@ class MediaCarouselControllerTest(flags: FlagsParameterization) : SysuiTestCase(
}
@Test
- fun testRecommendationRemoved_logged() {
- val packageName = "smartspace package"
- val instanceId = InstanceId.fakeInstanceId(123)
-
- val smartspaceData =
- EMPTY_SMARTSPACE_MEDIA_DATA.copy(packageName = packageName, instanceId = instanceId)
- MediaPlayerData.addMediaRecommendation(SMARTSPACE_KEY, smartspaceData, panel, true, clock)
- mediaCarouselController.removePlayer(SMARTSPACE_KEY)
-
- verify(logger).logRecommendationRemoved(eq(packageName), eq(instanceId!!))
- }
-
- @Test
fun testGetCurrentVisibleMediaContentIntent() {
val clickIntent1 = mock(PendingIntent::class.java)
val player1 = Triple("player1", DATA.copy(clickIntent = clickIntent1), 1000L)
@@ -562,7 +484,6 @@ class MediaCarouselControllerTest(flags: FlagsParameterization) : SysuiTestCase(
player1.second.copy(notificationKey = player1.first),
panel,
clock,
- isSsReactivated = false,
)
assertEquals(mediaCarouselController.getCurrentVisibleMediaContentIntent(), clickIntent1)
@@ -575,7 +496,6 @@ class MediaCarouselControllerTest(flags: FlagsParameterization) : SysuiTestCase(
player2.second.copy(notificationKey = player2.first),
panel,
clock,
- isSsReactivated = false,
)
// mediaCarouselScrollHandler.visibleMediaIndex is unchanged (= 0), and the new player is
@@ -590,7 +510,6 @@ class MediaCarouselControllerTest(flags: FlagsParameterization) : SysuiTestCase(
player3.second.copy(notificationKey = player3.first),
panel,
clock,
- isSsReactivated = false,
)
// mediaCarouselScrollHandler.visibleMediaIndex is unchanged (= 0), and the new player is
@@ -929,7 +848,7 @@ class MediaCarouselControllerTest(flags: FlagsParameterization) : SysuiTestCase(
@Test
fun testAnimationScaleChanged_mediaControlPanelsNotified() {
- MediaPlayerData.addMediaPlayer("key", DATA, panel, clock, isSsReactivated = false)
+ MediaPlayerData.addMediaPlayer("key", DATA, panel, clock)
globalSettings.putFloat(Settings.Global.ANIMATOR_DURATION_SCALE, 0f)
settingsObserverCaptor.value!!.onChange(false)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaControlPanelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaControlPanelTest.kt
index 88fcc706f072..90fe2a6a97f5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaControlPanelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaControlPanelTest.kt
@@ -1300,7 +1300,7 @@ public class MediaControlPanelTest : SysuiTestCase() {
dismiss.callOnClick()
verify(mediaDataManager).dismissMediaData(eq(mediaKey), anyLong(), eq(true))
- verify(mediaCarouselController).removePlayer(eq(mediaKey), eq(false), eq(false), eq(true))
+ verify(mediaCarouselController).removePlayer(eq(mediaKey), eq(false), eq(true))
}
@Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaSwitchingControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaSwitchingControllerTest.java
index eb72acc0dade..ca8fae875244 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaSwitchingControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaSwitchingControllerTest.java
@@ -1483,6 +1483,44 @@ public class MediaSwitchingControllerTest extends SysuiTestCase {
verify(mLocalMediaManager, atLeastOnce()).connectDevice(outputMediaDevice);
}
+ @Test
+ public void connectDeviceButton_remoteDevice_noButton() {
+ when(mMediaDevice1.getFeatures()).thenReturn(
+ ImmutableList.of(MediaRoute2Info.FEATURE_REMOTE_PLAYBACK));
+ when(mLocalMediaManager.getCurrentConnectedDevice()).thenReturn(mMediaDevice1);
+ mMediaSwitchingController.start(mCb);
+ mMediaSwitchingController.onDeviceListUpdate(mMediaDevices);
+
+ List<MediaItem> resultList = mMediaSwitchingController.getMediaItemList();
+
+ assertThat(getNumberOfConnectDeviceButtons(resultList)).isEqualTo(0);
+ }
+
+ @Test
+ public void connectDeviceButton_localDevice_hasButton() {
+ when(mLocalMediaManager.getCurrentConnectedDevice()).thenReturn(mMediaDevice1);
+ mMediaSwitchingController.start(mCb);
+ mMediaSwitchingController.onDeviceListUpdate(mMediaDevices);
+
+ List<MediaItem> resultList = mMediaSwitchingController.getMediaItemList();
+
+ assertThat(getNumberOfConnectDeviceButtons(resultList)).isEqualTo(1);
+ assertThat(resultList.get(resultList.size() - 1).getMediaItemType()).isEqualTo(
+ MediaItem.MediaItemType.TYPE_PAIR_NEW_DEVICE);
+ }
+
+ @Test
+ public void connectDeviceButton_localDeviceButtonDisabledByParam_noButton() {
+ when(mLocalMediaManager.getCurrentConnectedDevice()).thenReturn(mMediaDevice1);
+ mMediaSwitchingController.start(mCb);
+ mMediaSwitchingController.onDeviceListUpdate(mMediaDevices);
+
+ List<MediaItem> resultList = mMediaSwitchingController.getMediaItemList(
+ false /* addConnectDeviceButton */);
+
+ assertThat(getNumberOfConnectDeviceButtons(resultList)).isEqualTo(0);
+ }
+
@DisableFlags(Flags.FLAG_ENABLE_AUDIO_INPUT_DEVICE_ROUTING_AND_VOLUME_CONTROL)
@Test
public void connectDeviceButton_presentAtAllTimesForNonGroupOutputs() {
@@ -1495,7 +1533,8 @@ public class MediaSwitchingControllerTest extends SysuiTestCase {
.getSelectedMediaDevice();
// Verify that there is initially one "Connect a device" button present.
- assertThat(getNumberOfConnectDeviceButtons()).isEqualTo(1);
+ assertThat(getNumberOfConnectDeviceButtons(
+ mMediaSwitchingController.getMediaItemList())).isEqualTo(1);
// Change the selected device, and verify that there is still one "Connect a device" button
// present.
@@ -1504,7 +1543,8 @@ public class MediaSwitchingControllerTest extends SysuiTestCase {
.getSelectedMediaDevice();
mMediaSwitchingController.onDeviceListUpdate(mMediaDevices);
- assertThat(getNumberOfConnectDeviceButtons()).isEqualTo(1);
+ assertThat(getNumberOfConnectDeviceButtons(
+ mMediaSwitchingController.getMediaItemList())).isEqualTo(1);
}
@EnableFlags(Flags.FLAG_ENABLE_AUDIO_INPUT_DEVICE_ROUTING_AND_VOLUME_CONTROL)
@@ -1523,7 +1563,8 @@ public class MediaSwitchingControllerTest extends SysuiTestCase {
doReturn(selectedInputMediaDevice).when(mInputRouteManager).getSelectedInputDevice();
// Verify that there is initially one "Connect a device" button present.
- assertThat(getNumberOfConnectDeviceButtons()).isEqualTo(1);
+ assertThat(getNumberOfConnectDeviceButtons(
+ mMediaSwitchingController.getMediaItemList())).isEqualTo(1);
// Change the selected device, and verify that there is still one "Connect a device" button
// present.
@@ -1532,7 +1573,8 @@ public class MediaSwitchingControllerTest extends SysuiTestCase {
.getSelectedMediaDevice();
mMediaSwitchingController.onDeviceListUpdate(mMediaDevices);
- assertThat(getNumberOfConnectDeviceButtons()).isEqualTo(1);
+ assertThat(getNumberOfConnectDeviceButtons(
+ mMediaSwitchingController.getMediaItemList())).isEqualTo(1);
}
@EnableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_DEVICE_GROUPING)
@@ -1633,7 +1675,7 @@ public class MediaSwitchingControllerTest extends SysuiTestCase {
when(mLocalMediaManager.isPreferenceRouteListingExist()).thenReturn(false);
mMediaSwitchingController.start(mCb);
reset(mCb);
- mMediaSwitchingController.getMediaItemList().clear();
+ mMediaSwitchingController.clearMediaItemList();
}
@DisableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_DEVICE_GROUPING)
@@ -1691,9 +1733,9 @@ public class MediaSwitchingControllerTest extends SysuiTestCase {
assertThat(items.get(0).isFirstDeviceInGroup()).isTrue();
}
- private int getNumberOfConnectDeviceButtons() {
+ private int getNumberOfConnectDeviceButtons(List<MediaItem> itemList) {
int numberOfConnectDeviceButtons = 0;
- for (MediaItem item : mMediaSwitchingController.getMediaItemList()) {
+ for (MediaItem item : itemList) {
if (item.getMediaItemType() == MediaItem.MediaItemType.TYPE_PAIR_NEW_DEVICE) {
numberOfConnectDeviceButtons++;
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/OutputMediaItemListProxyTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/OutputMediaItemListProxyTest.java
index f6edd49f142f..11a3670c20f6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/OutputMediaItemListProxyTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/OutputMediaItemListProxyTest.java
@@ -58,7 +58,6 @@ public class OutputMediaItemListProxyTest extends SysuiTestCase {
private MediaItem mMediaItem1;
private MediaItem mMediaItem2;
- private MediaItem mConnectNewDeviceMediaItem;
private OutputMediaItemListProxy mOutputMediaItemListProxy;
@Parameters(name = "{0}")
@@ -83,7 +82,6 @@ public class OutputMediaItemListProxyTest extends SysuiTestCase {
when(mMediaDevice4.getId()).thenReturn(DEVICE_ID_4);
mMediaItem1 = MediaItem.createDeviceMediaItem(mMediaDevice1);
mMediaItem2 = MediaItem.createDeviceMediaItem(mMediaDevice2);
- mConnectNewDeviceMediaItem = MediaItem.createPairNewDeviceMediaItem();
mOutputMediaItemListProxy = new OutputMediaItemListProxy(mContext);
}
@@ -98,8 +96,7 @@ public class OutputMediaItemListProxyTest extends SysuiTestCase {
/* devices= */ List.of(mMediaDevice2, mMediaDevice3),
/* selectedDevices */ List.of(mMediaDevice3),
/* connectedMediaDevice= */ null,
- /* needToHandleMutingExpectedDevice= */ false,
- /* connectNewDeviceMediaItem= */ null);
+ /* needToHandleMutingExpectedDevice= */ false);
// Check the output media items to be
// * a media item with the selected mMediaDevice3
@@ -115,8 +112,7 @@ public class OutputMediaItemListProxyTest extends SysuiTestCase {
/* devices= */ List.of(mMediaDevice4, mMediaDevice1, mMediaDevice3, mMediaDevice2),
/* selectedDevices */ List.of(mMediaDevice3),
/* connectedMediaDevice= */ null,
- /* needToHandleMutingExpectedDevice= */ false,
- /* connectNewDeviceMediaItem= */ null);
+ /* needToHandleMutingExpectedDevice= */ false);
// Check the output media items to be
// * a media item with the selected route mMediaDevice3
@@ -136,8 +132,7 @@ public class OutputMediaItemListProxyTest extends SysuiTestCase {
/* devices= */ List.of(mMediaDevice1, mMediaDevice3, mMediaDevice2),
/* selectedDevices */ List.of(mMediaDevice1, mMediaDevice3),
/* connectedMediaDevice= */ null,
- /* needToHandleMutingExpectedDevice= */ false,
- /* connectNewDeviceMediaItem= */ null);
+ /* needToHandleMutingExpectedDevice= */ false);
// Check the output media items to be
// * a media item with the selected route mMediaDevice3
@@ -161,8 +156,7 @@ public class OutputMediaItemListProxyTest extends SysuiTestCase {
/* devices= */ List.of(mMediaDevice2, mMediaDevice4, mMediaDevice3, mMediaDevice1),
/* selectedDevices */ List.of(mMediaDevice1, mMediaDevice2, mMediaDevice3),
/* connectedMediaDevice= */ null,
- /* needToHandleMutingExpectedDevice= */ false,
- /* connectNewDeviceMediaItem= */ null);
+ /* needToHandleMutingExpectedDevice= */ false);
if (Flags.enableOutputSwitcherDeviceGrouping()) {
// When the device grouping is enabled, the order of selected devices are preserved:
@@ -197,8 +191,7 @@ public class OutputMediaItemListProxyTest extends SysuiTestCase {
/* devices= */ List.of(mMediaDevice4, mMediaDevice1, mMediaDevice3, mMediaDevice2),
/* selectedDevices */ List.of(mMediaDevice2, mMediaDevice3),
/* connectedMediaDevice= */ null,
- /* needToHandleMutingExpectedDevice= */ false,
- /* connectNewDeviceMediaItem= */ null);
+ /* needToHandleMutingExpectedDevice= */ false);
if (Flags.enableOutputSwitcherDeviceGrouping()) {
// When the device grouping is enabled, the order of selected devices are preserved:
@@ -233,8 +226,7 @@ public class OutputMediaItemListProxyTest extends SysuiTestCase {
/* devices= */ List.of(mMediaDevice1, mMediaDevice3, mMediaDevice4),
/* selectedDevices */ List.of(mMediaDevice3),
/* connectedMediaDevice= */ null,
- /* needToHandleMutingExpectedDevice= */ false,
- /* connectNewDeviceMediaItem= */ null);
+ /* needToHandleMutingExpectedDevice= */ false);
if (Flags.enableOutputSwitcherDeviceGrouping()) {
// When the device grouping is enabled, the order of selected devices are preserved:
@@ -261,47 +253,6 @@ public class OutputMediaItemListProxyTest extends SysuiTestCase {
}
}
- @EnableFlags(Flags.FLAG_FIX_OUTPUT_MEDIA_ITEM_LIST_INDEX_OUT_OF_BOUNDS_EXCEPTION)
- @Test
- public void updateMediaDevices_withConnectNewDeviceMediaItem_shouldUpdateMediaItemList() {
- assertThat(mOutputMediaItemListProxy.isEmpty()).isTrue();
-
- // Create the initial output media item list with a connect new device media item.
- mOutputMediaItemListProxy.updateMediaDevices(
- /* devices= */ List.of(mMediaDevice2, mMediaDevice3),
- /* selectedDevices */ List.of(mMediaDevice3),
- /* connectedMediaDevice= */ null,
- /* needToHandleMutingExpectedDevice= */ false,
- mConnectNewDeviceMediaItem);
-
- // Check the output media items to be
- // * a media item with the selected mMediaDevice3
- // * a group divider for suggested devices
- // * a media item with the mMediaDevice2
- // * a connect new device media item
- assertThat(mOutputMediaItemListProxy.getOutputMediaItemList())
- .contains(mConnectNewDeviceMediaItem);
- assertThat(getMediaDevices(mOutputMediaItemListProxy.getOutputMediaItemList()))
- .containsExactly(mMediaDevice3, null, mMediaDevice2, null);
-
- // Update the output media item list without a connect new device media item.
- mOutputMediaItemListProxy.updateMediaDevices(
- /* devices= */ List.of(mMediaDevice2, mMediaDevice3),
- /* selectedDevices */ List.of(mMediaDevice3),
- /* connectedMediaDevice= */ null,
- /* needToHandleMutingExpectedDevice= */ false,
- /* connectNewDeviceMediaItem= */ null);
-
- // Check the output media items to be
- // * a media item with the selected mMediaDevice3
- // * a group divider for suggested devices
- // * a media item with the mMediaDevice2
- assertThat(mOutputMediaItemListProxy.getOutputMediaItemList())
- .doesNotContain(mConnectNewDeviceMediaItem);
- assertThat(getMediaDevices(mOutputMediaItemListProxy.getOutputMediaItemList()))
- .containsExactly(mMediaDevice3, null, mMediaDevice2);
- }
-
@DisableFlags(Flags.FLAG_FIX_OUTPUT_MEDIA_ITEM_LIST_INDEX_OUT_OF_BOUNDS_EXCEPTION)
@Test
public void clearAndAddAll_shouldUpdateMediaItemList() {
@@ -325,8 +276,7 @@ public class OutputMediaItemListProxyTest extends SysuiTestCase {
/* devices= */ List.of(mMediaDevice1),
/* selectedDevices */ List.of(),
/* connectedMediaDevice= */ null,
- /* needToHandleMutingExpectedDevice= */ false,
- /* connectNewDeviceMediaItem= */ null);
+ /* needToHandleMutingExpectedDevice= */ false);
assertThat(mOutputMediaItemListProxy.isEmpty()).isFalse();
mOutputMediaItemListProxy.clear();
@@ -354,8 +304,7 @@ public class OutputMediaItemListProxyTest extends SysuiTestCase {
/* devices= */ List.of(mMediaDevice1),
/* selectedDevices */ List.of(),
/* connectedMediaDevice= */ null,
- /* needToHandleMutingExpectedDevice= */ false,
- /* connectNewDeviceMediaItem= */ null);
+ /* needToHandleMutingExpectedDevice= */ false);
assertThat(mOutputMediaItemListProxy.isEmpty()).isFalse();
mOutputMediaItemListProxy.removeMutingExpectedDevices();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt
index 7728f684f0f2..c21570928bde 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt
@@ -49,6 +49,7 @@ import com.android.systemui.communal.shared.model.CommunalScenes
import com.android.systemui.communal.ui.compose.CommunalContent
import com.android.systemui.communal.ui.viewmodel.CommunalViewModel
import com.android.systemui.communal.util.CommunalColors
+import com.android.systemui.communal.util.userTouchActivityNotifier
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
@@ -64,6 +65,7 @@ import com.android.systemui.kosmos.testScope
import com.android.systemui.kosmos.useUnconfinedTestDispatcher
import com.android.systemui.log.logcatLogBuffer
import com.android.systemui.media.controls.controller.keyguardMediaController
+import com.android.systemui.power.data.repository.fakePowerRepository
import com.android.systemui.res.R
import com.android.systemui.scene.shared.model.sceneDataSourceDelegator
import com.android.systemui.shade.domain.interactor.shadeInteractor
@@ -137,6 +139,7 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() {
notificationStackScrollLayoutController,
keyguardMediaController,
lockscreenSmartspaceController,
+ userTouchActivityNotifier,
logcatLogBuffer("GlanceableHubContainerControllerTest"),
kosmos.userActivityNotifier,
)
@@ -178,6 +181,7 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() {
notificationStackScrollLayoutController,
keyguardMediaController,
lockscreenSmartspaceController,
+ userTouchActivityNotifier,
logcatLogBuffer("GlanceableHubContainerControllerTest"),
kosmos.userActivityNotifier,
)
@@ -208,6 +212,7 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() {
notificationStackScrollLayoutController,
keyguardMediaController,
lockscreenSmartspaceController,
+ userTouchActivityNotifier,
logcatLogBuffer("GlanceableHubContainerControllerTest"),
kosmos.userActivityNotifier,
)
@@ -234,6 +239,7 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() {
notificationStackScrollLayoutController,
keyguardMediaController,
lockscreenSmartspaceController,
+ userTouchActivityNotifier,
logcatLogBuffer("GlanceableHubContainerControllerTest"),
kosmos.userActivityNotifier,
)
@@ -539,6 +545,18 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() {
}
@Test
+ fun onTouchEvent_touchHandled_notifyUserActivity() =
+ kosmos.runTest {
+ // Communal is open.
+ goToScene(CommunalScenes.Communal)
+
+ // Touch event is sent to the container view.
+ assertThat(underTest.onTouchEvent(DOWN_EVENT)).isTrue()
+ verify(containerView).onTouchEvent(DOWN_EVENT)
+ assertThat(fakePowerRepository.userTouchRegistered).isTrue()
+ }
+
+ @Test
fun onTouchEvent_editActivityShowing_touchesConsumedButNotDispatched() =
kosmos.runTest {
// Communal is open.
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 eae23e70027b..e70ce53e74cb 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
@@ -83,7 +83,6 @@ import com.android.systemui.statusbar.notification.headsup.HeadsUpManager;
import com.android.systemui.statusbar.notification.headsup.PinnedStatus;
import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier;
import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUi;
-import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUiForceExpanded;
import com.android.systemui.statusbar.notification.row.ExpandableView.OnHeightChangedListener;
import com.android.systemui.statusbar.notification.row.icon.NotificationIconStyleProvider;
import com.android.systemui.statusbar.notification.row.wrapper.NotificationViewWrapper;
@@ -948,7 +947,7 @@ public class ExpandableNotificationRowTest extends SysuiTestCase {
}
@Test
- @EnableFlags({PromotedNotificationUi.FLAG_NAME, PromotedNotificationUiForceExpanded.FLAG_NAME})
+ @EnableFlags(PromotedNotificationUi.FLAG_NAME)
@DisableFlags(NotificationBundleUi.FLAG_NAME)
public void isExpanded_sensitivePromotedNotification_notExpanded() throws Exception {
// GIVEN
@@ -965,7 +964,7 @@ public class ExpandableNotificationRowTest extends SysuiTestCase {
}
@Test
- @EnableFlags({PromotedNotificationUi.FLAG_NAME, PromotedNotificationUiForceExpanded.FLAG_NAME})
+ @EnableFlags(PromotedNotificationUi.FLAG_NAME)
@DisableFlags(NotificationBundleUi.FLAG_NAME)
public void isExpanded_promotedNotificationNotOnKeyguard_expanded() throws Exception {
// GIVEN
@@ -981,7 +980,7 @@ public class ExpandableNotificationRowTest extends SysuiTestCase {
}
@Test
- @EnableFlags({PromotedNotificationUi.FLAG_NAME, PromotedNotificationUiForceExpanded.FLAG_NAME})
+ @EnableFlags(PromotedNotificationUi.FLAG_NAME)
@DisableFlags(NotificationBundleUi.FLAG_NAME)
public void isExpanded_promotedNotificationAllowOnKeyguard_expanded() throws Exception {
// GIVEN
@@ -997,7 +996,7 @@ public class ExpandableNotificationRowTest extends SysuiTestCase {
}
@Test
- @EnableFlags({PromotedNotificationUi.FLAG_NAME, PromotedNotificationUiForceExpanded.FLAG_NAME})
+ @EnableFlags(PromotedNotificationUi.FLAG_NAME)
@DisableFlags(NotificationBundleUi.FLAG_NAME)
public void isExpanded_promotedNotificationIgnoreLockscreenConstraints_expanded()
throws Exception {
@@ -1035,7 +1034,7 @@ public class ExpandableNotificationRowTest extends SysuiTestCase {
}
@Test
- @EnableFlags({PromotedNotificationUi.FLAG_NAME, PromotedNotificationUiForceExpanded.FLAG_NAME})
+ @EnableFlags(PromotedNotificationUi.FLAG_NAME)
@DisableFlags(NotificationBundleUi.FLAG_NAME)
public void isExpanded_promotedNotificationSaveSpaceOnLockScreen_notExpanded()
throws Exception {
@@ -1053,7 +1052,7 @@ public class ExpandableNotificationRowTest extends SysuiTestCase {
}
@Test
- @EnableFlags({PromotedNotificationUi.FLAG_NAME, PromotedNotificationUiForceExpanded.FLAG_NAME})
+ @EnableFlags(PromotedNotificationUi.FLAG_NAME)
@DisableFlags(NotificationBundleUi.FLAG_NAME)
public void isExpanded_promotedNotificationNotSaveSpaceOnLockScreen_expanded()
throws Exception {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfoTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfoTest.java
index 2c800bd87ef5..a515c3f6ed6e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfoTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfoTest.java
@@ -171,6 +171,8 @@ public class NotificationConversationInfoTest extends SysuiTestCase {
private ConversationIconFactory mIconFactory;
@Mock
private Notification.BubbleMetadata mBubbleMetadata;
+ @Mock
+ private View.OnClickListener mCloseListener;
private Handler mTestHandler;
@Rule
public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
@@ -298,7 +300,7 @@ public class NotificationConversationInfoTest extends SysuiTestCase {
true,
mTestHandler,
mTestHandler, null, Optional.of(mBubblesManager),
- mShadeController);
+ mShadeController, true, mCloseListener);
}
@Test
@@ -402,7 +404,7 @@ public class NotificationConversationInfoTest extends SysuiTestCase {
true,
mTestHandler,
mTestHandler, null, Optional.of(mBubblesManager),
- mShadeController);
+ mShadeController, true, null);
final TextView nameView = mNotificationInfo.findViewById(R.id.delegate_name);
assertEquals(VISIBLE, nameView.getVisibility());
assertTrue(nameView.getText().toString().contains("Proxied"));
@@ -442,7 +444,7 @@ public class NotificationConversationInfoTest extends SysuiTestCase {
true,
mTestHandler,
mTestHandler, null, Optional.of(mBubblesManager),
- mShadeController);
+ mShadeController, true, null);
final View feedback = mNotificationInfo.findViewById(R.id.feedback);
assertEquals(VISIBLE, feedback.getVisibility());
@@ -484,7 +486,7 @@ public class NotificationConversationInfoTest extends SysuiTestCase {
true,
mTestHandler,
mTestHandler, null, Optional.of(mBubblesManager),
- mShadeController);
+ mShadeController, true, null);
final View settingsButton = mNotificationInfo.findViewById(R.id.info);
settingsButton.performClick();
@@ -524,7 +526,7 @@ public class NotificationConversationInfoTest extends SysuiTestCase {
false,
mTestHandler,
mTestHandler, null, Optional.of(mBubblesManager),
- mShadeController);
+ mShadeController, true, null);
final View settingsButton = mNotificationInfo.findViewById(R.id.info);
assertTrue(settingsButton.getVisibility() != View.VISIBLE);
}
@@ -601,7 +603,7 @@ public class NotificationConversationInfoTest extends SysuiTestCase {
true,
mTestHandler,
mTestHandler, null, Optional.of(mBubblesManager),
- mShadeController);
+ mShadeController, true, null);
assertThat(((TextView) mNotificationInfo.findViewById(R.id.priority_summary)).getText())
.isEqualTo(mContext.getString(
R.string.notification_channel_summary_priority_dnd));
@@ -633,7 +635,7 @@ public class NotificationConversationInfoTest extends SysuiTestCase {
true,
mTestHandler,
mTestHandler, null, Optional.of(mBubblesManager),
- mShadeController);
+ mShadeController, true, null);
assertThat(((TextView) mNotificationInfo.findViewById(R.id.priority_summary)).getText())
.isEqualTo(mContext.getString(
R.string.notification_channel_summary_priority_baseline));
@@ -1018,4 +1020,19 @@ public class NotificationConversationInfoTest extends SysuiTestCase {
// THEN the user is not presented with the People Tile pinning request
verify(mPeopleSpaceWidgetManager, never()).requestPinAppWidget(eq(mShortcutInfo), any());
}
+
+
+ @Test
+ public void testDismiss() throws Exception {
+ doStandardBind();
+
+ View dismiss = mNotificationInfo.findViewById(R.id.inline_dismiss);
+ dismiss.performClick();
+ mTestableLooper.processAllMessages();
+
+ // Verify action performed on button click
+ verify(mCloseListener).onClick(any());
+
+ }
+
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/RowImageInflaterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/RowImageInflaterTest.kt
index d0357603665d..4d9f20c0038f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/RowImageInflaterTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/RowImageInflaterTest.kt
@@ -20,7 +20,7 @@ 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.notification.promoted.PromotedNotificationUiAod
+import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUi
import com.android.systemui.statusbar.notification.row.shared.IconData
import com.android.systemui.statusbar.notification.row.shared.ImageModel
import com.android.systemui.statusbar.notification.row.shared.ImageModelProvider.ImageSizeClass.SmallSquare
@@ -31,7 +31,7 @@ import org.junit.runner.RunWith
@SmallTest
@RunWith(AndroidJUnit4::class)
-@EnableFlags(PromotedNotificationUiAod.FLAG_NAME)
+@EnableFlags(PromotedNotificationUi.FLAG_NAME)
class RowImageInflaterTest : SysuiTestCase() {
private lateinit var rowImageInflater: RowImageInflater
diff --git a/packages/SystemUI/tests/utils/src/android/hardware/display/FakeAmbientDisplayConfiguration.kt b/packages/SystemUI/tests/utils/src/android/hardware/display/FakeAmbientDisplayConfiguration.kt
index cdd0ff7c38f7..9d3d7e6ee292 100644
--- a/packages/SystemUI/tests/utils/src/android/hardware/display/FakeAmbientDisplayConfiguration.kt
+++ b/packages/SystemUI/tests/utils/src/android/hardware/display/FakeAmbientDisplayConfiguration.kt
@@ -4,12 +4,13 @@ import android.content.Context
class FakeAmbientDisplayConfiguration(context: Context) : AmbientDisplayConfiguration(context) {
var fakePulseOnNotificationEnabled = true
+ var fakePickupGestureEnabled = true
override fun pulseOnNotificationEnabled(user: Int) = fakePulseOnNotificationEnabled
override fun pulseOnNotificationAvailable() = TODO("Not yet implemented")
- override fun pickupGestureEnabled(user: Int) = TODO("Not yet implemented")
+ override fun pickupGestureEnabled(user: Int) = fakePickupGestureEnabled
override fun dozePickupSensorAvailable() = TODO("Not yet implemented")
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalSceneRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalSceneRepository.kt
index 3f35bb9f3520..9d3b983b3c86 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalSceneRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalSceneRepository.kt
@@ -1,5 +1,6 @@
package com.android.systemui.communal.data.repository
+import android.content.res.Configuration
import com.android.compose.animation.scene.ObservableTransitionState
import com.android.compose.animation.scene.SceneKey
import com.android.compose.animation.scene.TransitionKey
@@ -9,6 +10,7 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.stateIn
@@ -30,10 +32,6 @@ class FakeCommunalSceneRepository(
}
}
- override suspend fun showHubFromPowerButton() {
- snapToScene(CommunalScenes.Communal)
- }
-
private val defaultTransitionState = ObservableTransitionState.Idle(CommunalScenes.Default)
private val _transitionState = MutableStateFlow<Flow<ObservableTransitionState>?>(null)
override val transitionState: StateFlow<ObservableTransitionState> =
@@ -48,4 +46,13 @@ class FakeCommunalSceneRepository(
override fun setTransitionState(transitionState: Flow<ObservableTransitionState>?) {
_transitionState.value = transitionState
}
+
+ private val _communalContainerOrientation =
+ MutableStateFlow(Configuration.ORIENTATION_UNDEFINED)
+ override val communalContainerOrientation: StateFlow<Int> =
+ _communalContainerOrientation.asStateFlow()
+
+ override fun setCommunalContainerOrientation(orientation: Int) {
+ _communalContainerOrientation.value = orientation
+ }
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalSceneInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalSceneInteractorKosmos.kt
index 209d1636e380..6b629e4c0472 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalSceneInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalSceneInteractorKosmos.kt
@@ -20,14 +20,18 @@ import com.android.systemui.communal.data.repository.communalSceneRepository
import com.android.systemui.communal.shared.log.communalSceneLogger
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.kosmos.testDispatcher
import com.android.systemui.scene.domain.interactor.sceneInteractor
+import com.android.systemui.statusbar.policy.keyguardStateController
val Kosmos.communalSceneInteractor: CommunalSceneInteractor by
Kosmos.Fixture {
CommunalSceneInteractor(
applicationScope = applicationCoroutineScope,
+ mainImmediateDispatcher = testDispatcher,
repository = communalSceneRepository,
logger = communalSceneLogger,
sceneInteractor = sceneInteractor,
+ keyguardStateController = keyguardStateController,
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractorKosmos.kt
index 75c4b6f5366b..e90758715ad8 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractorKosmos.kt
@@ -22,6 +22,7 @@ import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.kosmos.testDispatcher
import com.android.systemui.power.domain.interactor.powerInteractor
val Kosmos.communalSceneTransitionInteractor: CommunalSceneTransitionInteractor by
@@ -35,5 +36,6 @@ val Kosmos.communalSceneTransitionInteractor: CommunalSceneTransitionInteractor
repository = communalSceneTransitionRepository,
keyguardInteractor = keyguardInteractor,
powerInteractor = powerInteractor,
+ mainImmediateDispatcher = testDispatcher,
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/util/UserTouchActivityNotifierKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/util/UserTouchActivityNotifierKosmos.kt
new file mode 100644
index 000000000000..3452d097d3da
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/util/UserTouchActivityNotifierKosmos.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.communal.util
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.backgroundScope
+import com.android.systemui.power.domain.interactor.powerInteractor
+
+val Kosmos.userTouchActivityNotifier by
+ Kosmos.Fixture {
+ UserTouchActivityNotifier(backgroundScope, powerInteractor, USER_TOUCH_ACTIVITY_RATE_LIMIT)
+ }
+
+const val USER_TOUCH_ACTIVITY_RATE_LIMIT = 100
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/data/ui/viewmodel/UdfpsAccessibilityOverlayViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/data/ui/viewmodel/UdfpsAccessibilityOverlayViewModelKosmos.kt
index 2a46437ed33e..2a3bd335bf98 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/data/ui/viewmodel/UdfpsAccessibilityOverlayViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/data/ui/viewmodel/UdfpsAccessibilityOverlayViewModelKosmos.kt
@@ -18,6 +18,8 @@ package com.android.systemui.deviceentry.data.ui.viewmodel
import com.android.systemui.accessibility.domain.interactor.accessibilityInteractor
import com.android.systemui.biometrics.domain.interactor.udfpsOverlayInteractor
+import com.android.systemui.biometrics.udfpsUtils
+import com.android.systemui.deviceentry.ui.viewmodel.AlternateBouncerUdfpsAccessibilityOverlayViewModel
import com.android.systemui.deviceentry.ui.viewmodel.DeviceEntryUdfpsAccessibilityOverlayViewModel
import com.android.systemui.keyguard.ui.viewmodel.deviceEntryForegroundIconViewModel
import com.android.systemui.keyguard.ui.viewmodel.deviceEntryIconViewModel
@@ -30,5 +32,15 @@ val Kosmos.deviceEntryUdfpsAccessibilityOverlayViewModel by
accessibilityInteractor = accessibilityInteractor,
deviceEntryIconViewModel = deviceEntryIconViewModel,
deviceEntryFgIconViewModel = deviceEntryForegroundIconViewModel,
+ udfpsUtils = udfpsUtils,
+ )
+ }
+
+val Kosmos.alternateBouncerUdfpsAccessibilityOverlayViewModel by
+ Kosmos.Fixture {
+ AlternateBouncerUdfpsAccessibilityOverlayViewModel(
+ udfpsOverlayInteractor = udfpsOverlayInteractor,
+ accessibilityInteractor = accessibilityInteractor,
+ udfpsUtils = udfpsUtils,
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/dreams/WakeGestureMonitorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/dreams/WakeGestureMonitorKosmos.kt
new file mode 100644
index 000000000000..7a433281cc7b
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/dreams/WakeGestureMonitorKosmos.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.dreams
+
+import android.hardware.display.ambientDisplayConfiguration
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.backgroundCoroutineContext
+import com.android.systemui.user.domain.interactor.selectedUserInteractor
+import com.android.systemui.util.sensors.asyncSensorManager
+import com.android.systemui.util.settings.fakeSettings
+
+val Kosmos.wakeGestureMonitor by
+ Kosmos.Fixture {
+ WakeGestureMonitor(
+ ambientDisplayConfiguration = ambientDisplayConfiguration,
+ asyncSensorManager = asyncSensorManager,
+ bgContext = backgroundCoroutineContext,
+ secureSettings = fakeSettings,
+ selectedUserInteractor = selectedUserInteractor,
+ )
+ }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractorKosmos.kt
index 1698a5078038..3d684b893e35 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractorKosmos.kt
@@ -17,6 +17,7 @@
package com.android.systemui.keyguard.domain.interactor
import com.android.systemui.communal.domain.interactor.communalSceneInteractor
+import com.android.systemui.communal.domain.interactor.communalSettingsInteractor
import com.android.systemui.keyguard.data.repository.keyguardTransitionRepository
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.applicationCoroutineScope
@@ -38,5 +39,6 @@ val Kosmos.fromGoneTransitionInteractor by
communalSceneInteractor = communalSceneInteractor,
keyguardOcclusionInteractor = keyguardOcclusionInteractor,
keyguardShowWhileAwakeInteractor = keyguardShowWhileAwakeInteractor,
+ communalSettingsInteractor = communalSettingsInteractor,
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AccessibilityActionsViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AccessibilityActionsViewModelKosmos.kt
index bc35dc8052ec..be5431c3d0d7 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AccessibilityActionsViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AccessibilityActionsViewModelKosmos.kt
@@ -16,6 +16,7 @@
package com.android.systemui.keyguard.ui.viewmodel
+import com.android.systemui.biometrics.domain.interactor.udfpsOverlayInteractor
import com.android.systemui.communal.domain.interactor.communalInteractor
import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
@@ -27,5 +28,6 @@ val Kosmos.accessibilityActionsViewModelKosmos by Fixture {
communalInteractor = communalInteractor,
keyguardTransitionInteractor = keyguardTransitionInteractor,
keyguardInteractor = keyguardInteractor,
+ udfpsOverlayInteractor = udfpsOverlayInteractor,
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToPrimaryBouncerViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToPrimaryBouncerViewModelKosmos.kt
new file mode 100644
index 000000000000..625e751c8b65
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToPrimaryBouncerViewModelKosmos.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.ui.viewmodel
+
+import com.android.systemui.keyguard.ui.keyguardTransitionAnimationFlow
+import com.android.systemui.keyguard.ui.transitions.blurConfig
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+
+val Kosmos.dreamingToPrimaryBouncerViewModel by Fixture {
+ DreamingToPrimaryBouncerTransitionViewModel(
+ animationFlow = keyguardTransitionAnimationFlow,
+ blurConfig = blurConfig,
+ )
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModelKosmos.kt
index 530981c489e8..02e63a42d87d 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModelKosmos.kt
@@ -17,15 +17,21 @@
package com.android.systemui.keyguard.ui.viewmodel
import com.android.systemui.common.ui.domain.interactor.configurationInteractor
+import com.android.systemui.communal.domain.interactor.communalSceneInteractor
+import com.android.systemui.communal.domain.interactor.communalSettingsInteractor
import com.android.systemui.keyguard.ui.glanceableHubBlurComponentFactory
import com.android.systemui.keyguard.ui.keyguardTransitionAnimationFlow
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.kosmos.applicationCoroutineScope
val Kosmos.glanceableHubToLockscreenTransitionViewModel by Fixture {
GlanceableHubToLockscreenTransitionViewModel(
+ applicationScope = applicationCoroutineScope,
configurationInteractor = configurationInteractor,
animationFlow = keyguardTransitionAnimationFlow,
+ communalSceneInteractor = communalSceneInteractor,
+ communalSettingsInteractor = communalSettingsInteractor,
blurFactory = glanceableHubBlurComponentFactory,
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/GoneToGlanceableHubTransitionViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/GoneToGlanceableHubTransitionViewModelKosmos.kt
new file mode 100644
index 000000000000..0198214de0a9
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/GoneToGlanceableHubTransitionViewModelKosmos.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.ui.viewmodel
+
+import com.android.systemui.keyguard.ui.keyguardTransitionAnimationFlow
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+
+var Kosmos.goneToGlanceableHubTransitionViewModel by Fixture {
+ GoneToGlanceableHubTransitionViewModel(animationFlow = keyguardTransitionAnimationFlow)
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt
index a9aa8cd5a7f9..d0f6ae39cc43 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt
@@ -71,6 +71,7 @@ val Kosmos.keyguardRootViewModel by Fixture {
goneToDozingTransitionViewModel = goneToDozingTransitionViewModel,
goneToDreamingTransitionViewModel = goneToDreamingTransitionViewModel,
goneToLockscreenTransitionViewModel = goneToLockscreenTransitionViewModel,
+ goneToGlanceableHubTransitionViewModel = goneToGlanceableHubTransitionViewModel,
lockscreenToAodTransitionViewModel = lockscreenToAodTransitionViewModel,
lockscreenToDozingTransitionViewModel = lockscreenToDozingTransitionViewModel,
lockscreenToDreamingTransitionViewModel = lockscreenToDreamingTransitionViewModel,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToDreamingTransitionViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToDreamingTransitionViewModelKosmos.kt
new file mode 100644
index 000000000000..c53bf9556aca
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToDreamingTransitionViewModelKosmos.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.ui.viewmodel
+
+import com.android.systemui.keyguard.ui.keyguardTransitionAnimationFlow
+import com.android.systemui.keyguard.ui.transitions.blurConfig
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+
+val Kosmos.primaryBouncerToDreamingTransitionViewModel by Fixture {
+ PrimaryBouncerToDreamingTransitionViewModel(
+ animationFlow = keyguardTransitionAnimationFlow,
+ blurConfig = blurConfig,
+ )
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/data/repository/MediaDataRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/data/repository/MediaDataRepositoryKosmos.kt
index 5c17cb95de84..13ee8a4c6edb 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/data/repository/MediaDataRepositoryKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/data/repository/MediaDataRepositoryKosmos.kt
@@ -19,8 +19,5 @@ package com.android.systemui.media.controls.data.repository
import com.android.systemui.dump.dumpManager
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.Kosmos.Fixture
-import com.android.systemui.media.controls.util.mediaFlags
-val Kosmos.mediaDataRepository by Fixture {
- MediaDataRepository(mediaFlags = mediaFlags, dumpManager = dumpManager)
-}
+val Kosmos.mediaDataRepository by Fixture { MediaDataRepository(dumpManager = dumpManager) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterKosmos.kt
index dae51190c136..02222108cae2 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterKosmos.kt
@@ -16,33 +16,21 @@
package com.android.systemui.media.controls.domain.pipeline
-import android.content.applicationContext
-import com.android.systemui.broadcast.BroadcastSender
import com.android.systemui.concurrency.fakeExecutor
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.media.controls.data.repository.mediaFilterRepository
import com.android.systemui.media.controls.shared.mediaLogger
-import com.android.systemui.media.controls.util.mediaUiEventLogger
import com.android.systemui.settings.userTracker
import com.android.systemui.statusbar.notificationLockscreenUserManager
import com.android.systemui.util.time.fakeSystemClock
-import com.android.systemui.util.wakelock.WakeLockFake
val Kosmos.mediaDataFilter by
Kosmos.Fixture {
MediaDataFilterImpl(
- context = applicationContext,
userTracker = userTracker,
- broadcastSender =
- BroadcastSender(
- applicationContext,
- WakeLockFake.Builder(applicationContext),
- fakeExecutor,
- ),
lockscreenUserManager = notificationLockscreenUserManager,
executor = fakeExecutor,
systemClock = fakeSystemClock,
- logger = mediaUiEventLogger,
mediaFilterRepository = mediaFilterRepository,
mediaLogger = mediaLogger,
)
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorKosmos.kt
index fcaad6bb28ea..8e64e1e81e8a 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorKosmos.kt
@@ -16,7 +16,6 @@
package com.android.systemui.media.controls.domain.pipeline
-import android.app.smartspace.SmartspaceManager
import android.content.applicationContext
import com.android.keyguard.keyguardUpdateMonitor
import com.android.systemui.broadcast.broadcastDispatcher
@@ -27,7 +26,6 @@ import com.android.systemui.kosmos.applicationCoroutineScope
import com.android.systemui.kosmos.testDispatcher
import com.android.systemui.media.controls.data.repository.mediaDataRepository
import com.android.systemui.media.controls.shared.mediaLogger
-import com.android.systemui.media.controls.shared.model.SmartspaceMediaDataProvider
import com.android.systemui.media.controls.util.fakeMediaControllerFactory
import com.android.systemui.media.controls.util.mediaFlags
import com.android.systemui.media.controls.util.mediaUiEventLogger
@@ -47,13 +45,11 @@ val Kosmos.mediaDataProcessor by
mediaControllerFactory = fakeMediaControllerFactory,
broadcastDispatcher = broadcastDispatcher,
dumpManager = dumpManager,
- smartspaceMediaDataProvider = SmartspaceMediaDataProvider(),
useMediaResumption = Utils.useMediaResumption(applicationContext),
useQsMediaPlayer = Utils.useQsMediaPlayer(applicationContext),
systemClock = systemClock,
mediaFlags = mediaFlags,
logger = mediaUiEventLogger,
- smartspaceManager = SmartspaceManager(applicationContext),
keyguardUpdateMonitor = keyguardUpdateMonitor,
mediaDataRepository = mediaDataRepository,
mediaDataLoader = { mediaDataLoader },
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListenerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListenerKosmos.kt
index b33edf97bd55..7889ab20433c 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListenerKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListenerKosmos.kt
@@ -20,7 +20,6 @@ import com.android.systemui.concurrency.fakeExecutor
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.log.logcatLogBuffer
import com.android.systemui.media.controls.util.fakeMediaControllerFactory
-import com.android.systemui.media.controls.util.mediaFlags
import com.android.systemui.plugins.statusbar.statusBarStateController
import com.android.systemui.util.time.systemClock
@@ -34,6 +33,5 @@ val Kosmos.mediaTimeoutListener by
logger = MediaTimeoutLogger(logcatLogBuffer("MediaTimeoutLogBuffer")),
statusBarStateController = statusBarStateController,
systemClock = systemClock,
- mediaFlags = mediaFlags,
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractorKosmos.kt
index 1397d974cbc5..20d3682c6964 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractorKosmos.kt
@@ -21,6 +21,7 @@ import android.window.WindowContext
import com.android.systemui.common.ui.data.repository.configurationRepository
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.testScope
+import com.android.systemui.log.logcatLogBuffer
import com.android.systemui.shade.ShadeDisplayChangeLatencyTracker
import com.android.systemui.shade.ShadeWindowLayoutParams
import com.android.systemui.shade.data.repository.fakeShadeDisplaysRepository
@@ -60,5 +61,6 @@ val Kosmos.shadeDisplaysInteractor by
notificationRebindingTracker,
notificationStackRebindingHider,
configurationController,
+ logcatLogBuffer("ShadeDisplaysInteractor"),
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeOverlayContentViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeOverlayContentViewModelKosmos.kt
index 23251d27cff9..90e23290e9e9 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeOverlayContentViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeOverlayContentViewModelKosmos.kt
@@ -22,9 +22,7 @@ import com.android.systemui.media.controls.domain.pipeline.interactor.mediaCarou
import com.android.systemui.notifications.ui.viewmodel.NotificationsShadeOverlayContentViewModel
import com.android.systemui.scene.domain.interactor.sceneInteractor
import com.android.systemui.shade.domain.interactor.shadeInteractor
-import com.android.systemui.shade.domain.interactor.shadeModeInteractor
import com.android.systemui.statusbar.disableflags.domain.interactor.disableFlagsInteractor
-import com.android.systemui.statusbar.notification.domain.interactor.activeNotificationsInteractor
import com.android.systemui.statusbar.notification.stack.ui.viewmodel.notificationsPlaceholderViewModelFactory
val Kosmos.notificationsShadeOverlayContentViewModel:
@@ -34,9 +32,7 @@ val Kosmos.notificationsShadeOverlayContentViewModel:
notificationsPlaceholderViewModelFactory = notificationsPlaceholderViewModelFactory,
sceneInteractor = sceneInteractor,
shadeInteractor = shadeInteractor,
- shadeModeInteractor = shadeModeInteractor,
disableFlagsInteractor = disableFlagsInteractor,
mediaCarouselInteractor = mediaCarouselInteractor,
- activeNotificationsInteractor = activeNotificationsInteractor,
)
}
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 c4542c4e709b..00b26c944b90 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,7 +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.NotificationLockscreenUserManager.REDACTION_TYPE_PUBLIC
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
@@ -40,7 +40,7 @@ fun Kosmos.setPromotedContent(entry: NotificationEntry) {
promotedNotificationContentExtractor.extractContent(
entry,
Notification.Builder.recoverBuilder(applicationContext, entry.sbn.notification),
- REDACTION_TYPE_NONE,
+ REDACTION_TYPE_PUBLIC,
RowImageInflater.newInstance(previousIndex = null, reinflating = false)
.useForContentModel(),
)
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/AODPromotedNotificationInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/AODPromotedNotificationInteractorKosmos.kt
index fcd484353011..ea459a95728a 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/AODPromotedNotificationInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/AODPromotedNotificationInteractorKosmos.kt
@@ -17,12 +17,16 @@
package com.android.systemui.statusbar.notification.promoted.domain.interactor
import com.android.systemui.dump.dumpManager
+import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.statusbar.policy.domain.interactor.sensitiveNotificationProtectionInteractor
val Kosmos.aodPromotedNotificationInteractor by
Kosmos.Fixture {
AODPromotedNotificationInteractor(
promotedNotificationsInteractor = promotedNotificationsInteractor,
+ keyguardInteractor = keyguardInteractor,
+ sensitiveNotificationProtectionInteractor = sensitiveNotificationProtectionInteractor,
dumpManager = dumpManager,
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt
index 8fa82cad5c32..72f9d550eb4d 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt
@@ -26,7 +26,6 @@ import com.android.systemui.log.table.TableLogBuffer
import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel
import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy
import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.StateFlow
class FakeMobileIconsInteractor(
mobileMappings: MobileMappingsProxy,
@@ -76,7 +75,7 @@ class FakeMobileIconsInteractor(
override val icons: MutableStateFlow<List<MobileIconInteractor>> = MutableStateFlow(emptyList())
- override val isStackable: StateFlow<Boolean> = MutableStateFlow(false)
+ override val isStackable: MutableStateFlow<Boolean> = MutableStateFlow(false)
private val _defaultMobileIconMapping = MutableStateFlow(TEST_MAPPING)
override val defaultMobileIconMapping = _defaultMobileIconMapping
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/domain/interactor/SensitiveNotificationProtectionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/domain/interactor/SensitiveNotificationProtectionInteractorKosmos.kt
new file mode 100644
index 000000000000..ba4410b51b75
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/domain/interactor/SensitiveNotificationProtectionInteractorKosmos.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.policy.domain.interactor
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.statusbar.policy.sensitiveNotificationProtectionController
+
+var Kosmos.sensitiveNotificationProtectionInteractor: SensitiveNotificationProtectionInteractor by
+ Kosmos.Fixture {
+ SensitiveNotificationProtectionInteractor(sensitiveNotificationProtectionController)
+ }
diff --git a/ravenwood/README.md b/ravenwood/README.md
index 9c4fda7a50a6..62f2ffae56ba 100644
--- a/ravenwood/README.md
+++ b/ravenwood/README.md
@@ -4,25 +4,4 @@ Ravenwood is an officially-supported lightweight unit testing environment for An
Ravenwood’s focus on Android platform use-cases, improved maintainability, and device consistency distinguishes it from Robolectric, which remains a popular choice for app testing.
-## Background
-
-Executing tests on a typical Android device has substantial overhead, such as flashing the build, waiting for the boot to complete, and retrying tests that fail due to general flakiness.
-
-In contrast, defining a lightweight unit testing environment mitigates these issues by running directly from build artifacts (no flashing required), runs immediately (no booting required), and runs in an isolated environment (less flakiness).
-
-## Guiding principles
-Here’s a summary of the guiding principles for Ravenwood, aimed at addressing Robolectric design concerns and better supporting Android platform developers:
-
-* **API support for Ravenwood is opt-in.** Teams that own APIs decide exactly what, and how, they support their API functionality being available to tests. When an API hasn’t opted-in, the API signatures remain available for tests to compile against and/or mock, but they throw when called under a Ravenwood environment.
- * _Contrasted with Robolectric which attempts to run API implementations as-is, causing maintenance pains as teams maintain or redesign their API internals._
-* **API support and customizations for Ravenwood appear directly inline with relevant code.** This improves maintenance of APIs by providing awareness of what code runs under Ravenwood, including the ability to replace code at a per-method level when Ravenwood-specific customization is needed.
- * _Contrasted with Robolectric which maintains customized behavior in separate “Shadow” classes that are difficult for maintainers to be aware of._
-* **APIs supported under Ravenwood are tested to remain consistent with physical devices.** As teams progressively opt-in supporting APIs under Ravenwood, we’re requiring they bring along “bivalent” tests (such as the relevant CTS) to validate that Ravenwood behaves just like a physical device.
- * _Contrasted with Robolectric, which has limited (and forked) testing of their environment, increasing their risk of accidental divergence over time and misleading “passing” signals._
-* **Ravenwood aims to support more “real” code.** As API owners progressively opt-in their code, they have the freedom to provide either a limited “fake” that is a faithful emulation of how a device behaves, or they can bring more “real” code that runs on physical devices.
- * _Contrasted with Robolectric, where support for “real” code ends at the app process boundary, such as a call into `system_server`._
-
-## More details
-
-* [Ravenwood for Test Authors](test-authors.md)
-* [Ravenwood for API Maintainers](api-maintainers.md)
+Documents have been moved to go/ravenwood.
diff --git a/ravenwood/api-maintainers.md b/ravenwood/api-maintainers.md
deleted file mode 100644
index 4b2f96804c97..000000000000
--- a/ravenwood/api-maintainers.md
+++ /dev/null
@@ -1,94 +0,0 @@
-# Ravenwood for API Maintainers
-
-By default, Android APIs aren’t opted-in to Ravenwood, and they default to throwing when called under the Ravenwood environment.
-
-To opt-in to supporting an API under Ravenwood, you can use the inline annotations documented below to customize your API behavior when running under Ravenwood. Because these annotations are inline in the relevant platform source code, they serve as valuable reminders to future API maintainers of Ravenwood support expectations.
-
-> **Note:** to ensure that API teams are well-supported during early Ravenwood onboarding, the Ravenwood team is manually maintaining an allow-list of classes that are able to use Ravenwood annotations. Please reach out to ravenwood@ so we can offer design advice and allow-list your APIs.
-
-These Ravenwood-specific annotations have no bearing on the status of an API being public, `@SystemApi`, `@TestApi`, `@hide`, etc. Ravenwood annotations are an orthogonal concept that are only consumed by the internal `hoststubgen` tool during a post-processing step that generates the Ravenwood runtime environment. Teams that own APIs can continue to refactor opted-in `@hide` implementation details, as long as the test-visible behavior continues passing.
-
-As described in our Guiding Principles, when a team opts-in an API, we’re requiring that they bring along “bivalent” tests (such as the relevant CTS) to validate that Ravenwood behaves just like a physical device. At the moment this means adding the bivalent tests to relevant `TEST_MAPPING` files to ensure they remain consistently passing over time. These bivalent tests are important because they progressively provide the foundation on which higher-level unit tests place their trust.
-
-## Opt-in to supporting a single method while other methods remained opt-out
-
-```
-@RavenwoodKeepPartialClass
-public class MyManager {
- @RavenwoodKeep
- public static String modeToString(int mode) {
- // This method implementation runs as-is on both devices and Ravenwood
- }
-
- public static void doComplex() {
- // This method implementation runs as-is on devices, but because there
- // is no method-level annotation, and the class-level default is
- // “keep partial”, this method is not supported under Ravenwood and
- // will throw
- }
-}
-```
-
-## Opt-in an entire class with opt-out of specific methods
-
-```
-@RavenwoodKeepWholeClass
-public class MyStruct {
- public void doSimple() {
- // This method implementation runs as-is on both devices and Ravenwood,
- // implicitly inheriting the class-level annotation
- }
-
- @RavenwoodThrow
- public void doComplex() {
- // This method implementation runs as-is on devices, but the
- // method-level annotation overrides the class-level annotation, so
- // this method is not supported under Ravenwood and will throw
- }
-}
-```
-
-## Replace a complex method when under Ravenwood
-
-```
-@RavenwoodKeepWholeClass
-public class MyStruct {
- @RavenwoodReplace
- public void doComplex() {
- // This method implementation runs as-is on devices, but the
- // implementation is replaced/substituted by the
- // doComplex$ravenwood() method implementation under Ravenwood
- }
-
- public void doComplex$ravenwood() {
- // This method implementation only runs under Ravenwood
- }
-}
-```
-
-## General strategies for side-stepping tricky dependencies
-
-The “replace” strategy described above is quite powerful, and can be used in creative ways to sidestep tricky underlying dependencies that aren’t ready yet.
-
-For example, consider a constructor or static initializer that relies on unsupported functionality from another team. By factoring the unsupported logic into a dedicated method, that method can then be replaced under Ravenwood to offer baseline functionality.
-
-## Strategies for JNI
-
-At the moment, JNI isn't yet supported under Ravenwood, but you may still want to support APIs that are partially implemented with JNI. The current approach is to use the “replace” strategy to offer a pure-Java alternative implementation for any JNI-provided logic.
-
-Since this approach requires potentially complex re-implementation, it should only be considered for core infrastructure that is critical to unblocking widespread testing use-cases. Other less-common usages of JNI should instead wait for offical JNI support in the Ravenwood environment.
-
-When a pure-Java implementation grows too large or complex to host within the original class, the `@RavenwoodNativeSubstitutionClass` annotation can be used to host it in a separate source file:
-
-```
-@RavenwoodKeepWholeClass
-@RavenwoodNativeSubstitutionClass("com.android.platform.test.ravenwood.nativesubstitution.MyComplexClass_host")
-public class MyComplexClass {
- private static native void nativeDoThing(long nativePtr);
-...
-
-public class MyComplexClass_host {
- public static void nativeDoThing(long nativePtr) {
- // ...
- }
-```
diff --git a/ravenwood/scripts/ravenwood-stats-collector.sh b/ravenwood/scripts/ravenwood-stats-collector.sh
index c2bf8d82e272..3b323411fd91 100755
--- a/ravenwood/scripts/ravenwood-stats-collector.sh
+++ b/ravenwood/scripts/ravenwood-stats-collector.sh
@@ -114,7 +114,7 @@ collect_apis() {
collect_stats $stats " (import it as 'ravenwood_stats')"
-collect_apis $apis " (import it as 'ravenwood_supported_apis')"
+collect_apis $apis " (import it as 'ravenwood_supported_apis2')"
cp *keep_all.txt $keep_all_dir
echo "Keep all files created at:"
@@ -122,4 +122,4 @@ find $keep_all_dir -type f
cp *dump.txt $dump_dir
echo "Dump files created at:"
-find $dump_dir -type f \ No newline at end of file
+find $dump_dir -type f
diff --git a/ravenwood/test-authors.md b/ravenwood/test-authors.md
deleted file mode 100644
index 6d82a744bc4f..000000000000
--- a/ravenwood/test-authors.md
+++ /dev/null
@@ -1,193 +0,0 @@
-# Ravenwood for Test Authors
-
-The Ravenwood testing environment runs inside a single Java process on the host side, and provides a limited yet growing set of Android API functionality.
-
-Ravenwood explicitly does not support “large” integration tests that expect a fully booted Android OS. Instead, it’s more suited for “small” and “medium” tests where your code-under-test has been factored to remove dependencies on a fully booted device.
-
-When writing tests under Ravenwood, all Android API symbols associated with your declared `sdk_version` are available to link against using, but unsupported APIs will throw an exception. This design choice enables mocking of unsupported APIs, and supports sharing of test code to build “bivalent” test suites that run against either Ravenwood or a traditional device.
-
-## Manually running tests
-
-To run all Ravenwood tests, use:
-
-```
-./frameworks/base/ravenwood/scripts/run-ravenwood-tests.sh
-```
-
-To run a specific test, use "atest" as normal, selecting the test from a Ravenwood suite such as:
-
-```
-atest CtsOsTestCasesRavenwood:ParcelTest\#testSetDataCapacityNegative
-```
-
-## Typical test structure
-
-Below are the typical steps needed to add a straightforward “small” unit test:
-
-* Define an `android_ravenwood_test` rule in your `Android.bp` file:
-
-```
-android_ravenwood_test {
- name: "MyTestsRavenwood",
- static_libs: [
- "androidx.annotation_annotation",
- "androidx.test.ext.junit",
- "androidx.test.rules",
- ],
- srcs: [
- "src/com/example/MyCode.java",
- "tests/src/com/example/MyCodeTest.java",
- ],
- sdk_version: "test_current",
- auto_gen_config: true,
-}
-```
-
-* Write your unit test just like you would for an Android device:
-
-```
-import android.platform.test.annotations.DisabledOnRavenwood;
-import android.platform.test.ravenwood.RavenwoodRule;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-@RunWith(AndroidJUnit4.class)
-public class MyCodeTest {
- @Test
- public void testSimple() {
- // ...
- }
-}
-```
-
-* APIs available under Ravenwood are stateless by default. If your test requires explicit states (such as defining the UID you’re running under, or requiring a main `Looper` thread), add a `RavenwoodRule` to declare that:
-
-```
-import android.platform.test.annotations.DisabledOnRavenwood;
-import android.platform.test.ravenwood.RavenwoodRule;
-
-import androidx.test.runner.AndroidJUnit4;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-@RunWith(AndroidJUnit4.class)
-public class MyCodeTest {
- @Rule
- public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder()
- .setProcessApp()
- .setProvideMainThread(true)
- .build();
-```
-
-Once you’ve defined your test, you can use typical commands to execute it locally:
-
-```
-$ atest --host MyTestsRavenwood
-```
-
-> **Note:** There's a known bug #312525698 where `atest` currently requires a connected device to run Ravenwood tests, but that device isn't used for testing. Using the `--host` argument above is a way to bypass this requirement until the bug is fixed.
-
-You can also run your new tests automatically via `TEST_MAPPING` rules like this:
-
-```
-{
- "ravenwood-presubmit": [
- {
- "name": "MyTestsRavenwood",
- "host": true
- }
- ]
-}
-```
-
-> **Note:** There's a known bug #308854804 where `TEST_MAPPING` is not being applied, so we're currently planning to run all Ravenwood tests unconditionally in presubmit for changes to `frameworks/base/` and `cts/` until there is a better path forward.
-
-## Strategies for migration/bivalent tests
-
-Ravenwood aims to support tests that are written in a “bivalent” way, where the same test code can be dual-compiled to run on both a real Android device and under a Ravenwood environment.
-
-In situations where a test method depends on API functionality not yet available under Ravenwood, we provide an annotation to quietly “ignore” that test under Ravenwood, while continuing to validate that test on real devices. The annotation can be applied to either individual methods or to an entire test class. Please note that your test class must declare a `RavenwoodRule` for the annotation to take effect.
-
-Test authors are encouraged to provide a `blockedBy` or `reason` argument to help future maintainers understand why a test is being ignored, and under what conditions it might be supported in the future.
-
-```
-@RunWith(AndroidJUnit4.class)
-public class MyCodeTest {
- @Rule
- public final RavenwoodRule mRavenwood = new RavenwoodRule();
-
- @Test
- public void testSimple() {
- // Simple test that runs on both devices and Ravenwood
- }
-
- @Test
- @DisabledOnRavenwood(blockedBy = PackageManager.class)
- public void testComplex() {
- // Complex test that runs on devices, but is ignored under Ravenwood
- }
-}
-```
-
-At the moment, the `android.content.res.Resources` subsystem isn't yet supported under Ravenwood, but you may still want to dual-compile test suites that depend on references to resources. Below is a strategy for supporting dual-compiliation, where you can "borrow" the generated resource symbols from your traditional `android_test` target:
-
-```
-android_test {
- name: "MyTestsDevice",
- resource_dirs: ["res"],
-...
-
-android_ravenwood_test {
- name: "MyTestsRavenwood",
- srcs: [
- ":MyTestsDevice{.aapt.srcjar}",
-...
-```
-
-## Strategies for unsupported APIs
-
-As you write tests against Ravenwood, you’ll likely discover API dependencies that aren’t supported yet. Here’s a few strategies that can help you make progress:
-
-* Your code-under-test may benefit from subtle dependency refactoring to reduce coupling. (For example, providing a specific `File` argument instead of deriving paths internally from a `Context` or `Environment`.)
- * One common use-case is providing a directory for your test to store temporary files, which can easily be accomplished using the `Files.createTempDirectory()` API which works on both physical devices and under Ravenwood:
-
-```
-import java.nio.file.Files;
-
-@RunWith(AndroidJUnit4.class)
-public class MyTest {
- @Before
- public void setUp() throws Exception {
- File tempDir = Files.createTempDirectory("MyTest").toFile();
-...
-```
-
-* Although mocking code that your team doesn’t own is a generally discouraged testing practice, it can be a valuable pressure relief valve when a dependency isn’t yet supported.
-
-## Strategies for debugging test development
-
-When writing tests you may encounter odd or hard to debug behaviors. One good place to start is at the beginning of the logs stored by atest:
-
-```
-$ atest MyTestsRavenwood
-...
-Test Logs have saved in /tmp/atest_result/20231128_094010_0e90t8v8/log
-Run 'atest --history' to review test result history.
-```
-
-The most useful logs are in the `isolated-java-logs` text file, which can typically be tab-completed by copy-pasting the logs path mentioned in the atest output:
-
-```
-$ less /tmp/atest_result/20231128_133105_h9al__79/log/i*/i*/isolated-java-logs*
-```
-
-Here are some common known issues and recommended workarounds:
-
-* Some code may unconditionally interact with unsupported APIs, such as via static initializers. One strategy is to shift the logic into `@Before` methods and make it conditional by testing `RavenwoodRule.isUnderRavenwood()`.
-* Some code may reference API symbols not yet present in the Ravenwood runtime, such as ART or ICU internals, or APIs from Mainline modules. One strategy is to refactor to avoid these internal dependencies, but Ravenwood aims to better support them soon.
- * This may also manifest as very odd behavior, such as test not being executed at all, tracked by bug #312517322
- * This may also manifest as an obscure Mockito error claiming “Mockito can only mock non-private & non-final classes”
diff --git a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/HostStubGenStats.kt b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/HostStubGenStats.kt
index ea8c25b6833c..4cfc205d5912 100644
--- a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/HostStubGenStats.kt
+++ b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/HostStubGenStats.kt
@@ -24,13 +24,9 @@ import org.objectweb.asm.Opcodes
import java.io.PrintWriter
/**
- * TODO This is for the legacy API coverage stats CSV that shows how many APIs are "supported"
- * in each class with some heuristics. We created [ApiDumper] later, which dumpps all methods
- * with the "supported" status. We should update the coverage dashboard to use the [ApiDumper]
- * output and remove this class, once we port all the heuristics to [ApiDumper] as well.
- * (For example, this class ignores non-public and/or abstract methods, but [ApiDumper] shows
- * all of them in the same way. We should probably mark them as "Boring" or maybe "Ignore"
- * for [ApiDumper])
+ * This class is no longer used. It was used for the old ravenwood dashboard. (b/402797626)
+ *
+ * TODO: Delete the class.
*/
open class HostStubGenStats(val classes: ClassNodes) {
data class Stats(
diff --git a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/asm/AsmUtils.kt b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/asm/AsmUtils.kt
index 112ef01e20cb..741abe3df638 100644
--- a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/asm/AsmUtils.kt
+++ b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/asm/AsmUtils.kt
@@ -377,6 +377,10 @@ fun MethodNode.isPublic(): Boolean {
return (this.access and Opcodes.ACC_PUBLIC) != 0
}
+fun MethodNode.isAbstract(): Boolean {
+ return (this.access and Opcodes.ACC_ABSTRACT) != 0
+}
+
fun MethodNode.isNative(): Boolean {
return (this.access and Opcodes.ACC_NATIVE) != 0
}
diff --git a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/dumper/ApiDumper.kt b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/dumper/ApiDumper.kt
index bb8cdccafaa6..6ece17ffa6c2 100644
--- a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/dumper/ApiDumper.kt
+++ b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/dumper/ApiDumper.kt
@@ -20,6 +20,8 @@ import com.android.hoststubgen.asm.CTOR_NAME
import com.android.hoststubgen.asm.ClassNodes
import com.android.hoststubgen.asm.getClassNameFromFullClassName
import com.android.hoststubgen.asm.getPackageNameFromFullClassName
+import com.android.hoststubgen.asm.isAbstract
+import com.android.hoststubgen.asm.isPublic
import com.android.hoststubgen.asm.toHumanReadableClassName
import com.android.hoststubgen.csvEscape
import com.android.hoststubgen.filters.FilterPolicy
@@ -27,8 +29,8 @@ import com.android.hoststubgen.filters.FilterPolicyWithReason
import com.android.hoststubgen.filters.OutputFilter
import com.android.hoststubgen.filters.StatsLabel
import com.android.hoststubgen.log
-import org.objectweb.asm.Type
import org.objectweb.asm.tree.ClassNode
+import org.objectweb.asm.tree.MethodNode
import java.io.PrintWriter
/**
@@ -45,19 +47,14 @@ class ApiDumper(
val descriptor: String,
)
- private val javaStandardApiPolicy = FilterPolicy.Keep.withReason(
- "Java standard API",
- StatsLabel.Supported,
- )
-
private val shownMethods = mutableSetOf<MethodKey>()
/**
* Do the dump.
*/
fun dump() {
- pw.printf("PackageName,ClassName,FromSubclass,DeclareClass,MethodName,MethodDesc" +
- ",Supported,Policy,Reason,SupportedLabel\n")
+ pw.printf("PackageName,ClassName,Inherited,DeclareClass,MethodName,MethodDesc" +
+ ",Supported,Policy,Reason,Boring\n")
classes.forEach { classNode ->
shownMethods.clear()
@@ -72,32 +69,21 @@ class ApiDumper(
methodClassName: String,
methodName: String,
methodDesc: String,
- classPolicy: FilterPolicyWithReason,
+ computedMethodLabel: StatsLabel,
methodPolicy: FilterPolicyWithReason,
) {
- if (methodPolicy.statsLabel == StatsLabel.Ignored) {
- return
- }
- // Label hack -- if the method is supported, but the class is boring, then the
- // method is boring too.
- var methodLabel = methodPolicy.statsLabel
- if (methodLabel == StatsLabel.SupportedButBoring
- && classPolicy.statsLabel == StatsLabel.SupportedButBoring) {
- methodLabel = classPolicy.statsLabel
- }
-
pw.printf(
- "%s,%s,%d,%s,%s,%s,%d,%s,%s,%s\n",
+ "%s,%s,%d,%s,%s,%s,%d,%s,%s,%d\n",
csvEscape(classPackage),
csvEscape(className),
if (isSuperClass) { 1 } else { 0 },
csvEscape(methodClassName),
csvEscape(methodName),
- csvEscape(methodDesc),
- methodLabel.statValue,
+ csvEscape(methodName + methodDesc),
+ if (computedMethodLabel.isSupported) { 1 } else { 0 },
methodPolicy.policy,
csvEscape(methodPolicy.reason),
- methodLabel,
+ if (computedMethodLabel == StatsLabel.SupportedButBoring) { 1 } else { 0 },
)
}
@@ -111,6 +97,42 @@ class ApiDumper(
return false
}
+ private fun getClassLabel(cn: ClassNode, classPolicy: FilterPolicyWithReason): StatsLabel {
+ if (!classPolicy.statsLabel.isSupported) {
+ return classPolicy.statsLabel
+ }
+ if (cn.name.endsWith("Proto")
+ || cn.name.endsWith("ProtoEnums")
+ || cn.name.endsWith("LogTags")
+ || cn.name.endsWith("StatsLog")) {
+ return StatsLabel.SupportedButBoring
+ }
+
+ return classPolicy.statsLabel
+ }
+
+ private fun resolveMethodLabel(
+ mn: MethodNode,
+ methodPolicy: FilterPolicyWithReason,
+ classLabel: StatsLabel,
+ ): StatsLabel {
+ // Class label will override the method label
+ if (!classLabel.isSupported) {
+ return classLabel
+ }
+ // If method isn't supported, just use it as-is.
+ if (!methodPolicy.statsLabel.isSupported) {
+ return methodPolicy.statsLabel
+ }
+
+ // Use heuristics to override the label.
+ if (!mn.isPublic() || mn.isAbstract()) {
+ return StatsLabel.SupportedButBoring
+ }
+
+ return methodPolicy.statsLabel
+ }
+
private fun dump(
dumpClass: ClassNode,
methodClass: ClassNode,
@@ -120,9 +142,11 @@ class ApiDumper(
return
}
log.d("Class ${dumpClass.name} -- policy $classPolicy")
+ val classLabel = getClassLabel(dumpClass, classPolicy)
- val pkg = getPackageNameFromFullClassName(dumpClass.name).toHumanReadableClassName()
- val cls = getClassNameFromFullClassName(dumpClass.name).toHumanReadableClassName()
+ val humanReadableClassName = dumpClass.name.toHumanReadableClassName()
+ val pkg = getPackageNameFromFullClassName(humanReadableClassName)
+ val cls = getClassNameFromFullClassName(humanReadableClassName)
val isSuperClass = dumpClass != methodClass
@@ -150,8 +174,12 @@ class ApiDumper(
val renameTo = filter.getRenameTo(methodClass.name, method.name, method.desc)
- dumpMethod(pkg, cls, isSuperClass, methodClass.name.toHumanReadableClassName(),
- renameTo ?: method.name, method.desc, classPolicy, methodPolicy)
+ val methodLabel = resolveMethodLabel(method, methodPolicy, classLabel)
+
+ if (methodLabel != StatsLabel.Ignored) {
+ dumpMethod(pkg, cls, isSuperClass, methodClass.name.toHumanReadableClassName(),
+ renameTo ?: method.name, method.desc, methodLabel, methodPolicy)
+ }
}
// Dump super class methods.
@@ -178,51 +206,6 @@ class ApiDumper(
dump(dumpClass, methodClass)
return
}
-
- // Dump overriding methods from Java standard classes, except for the Object methods,
- // which are obvious.
- if (methodClassName.startsWith("java/") || methodClassName.startsWith("javax/")) {
- if (methodClassName != "java/lang/Object") {
- dumpStandardClass(dumpClass, methodClassName)
- }
- return
- }
log.w("Super class or interface $methodClassName (used by ${dumpClass.name}) not found.")
}
-
- /**
- * Dump methods from Java standard classes.
- */
- private fun dumpStandardClass(
- dumpClass: ClassNode,
- methodClassName: String,
- ) {
- val pkg = getPackageNameFromFullClassName(dumpClass.name).toHumanReadableClassName()
- val cls = getClassNameFromFullClassName(dumpClass.name).toHumanReadableClassName()
-
- val methodClassName = methodClassName.toHumanReadableClassName()
-
- try {
- val clazz = Class.forName(methodClassName)
-
- // Method.getMethods() returns only public methods, but with inherited ones.
- // Method.getDeclaredMethods() returns private methods too, but no inherited methods.
- //
- // Since we're only interested in public ones, just use getMethods().
- clazz.methods.forEach { method ->
- val methodName = method.name
- val methodDesc = Type.getMethodDescriptor(method)
-
- // If we already printed the method from a subclass, don't print it.
- if (shownAlready(methodName, methodDesc)) {
- return@forEach
- }
-
- dumpMethod(pkg, cls, true, methodClassName,
- methodName, methodDesc, javaStandardApiPolicy, javaStandardApiPolicy)
- }
- } catch (e: ClassNotFoundException) {
- log.w("JVM type $methodClassName (used by ${dumpClass.name}) not found.")
- }
- }
}
diff --git a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/FilterPolicyWithReason.kt b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/FilterPolicyWithReason.kt
index e082bbb0a119..f135c60947b3 100644
--- a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/FilterPolicyWithReason.kt
+++ b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/FilterPolicyWithReason.kt
@@ -32,7 +32,15 @@ enum class StatsLabel(val statValue: Int, val label: String) {
SupportedButBoring(1, "Boring"),
/** Entry should be shown as "supported" */
- Supported(2, "Supported"),
+ Supported(2, "Supported");
+
+ val isSupported: Boolean
+ get() {
+ return when (this) {
+ SupportedButBoring, Supported -> true
+ else -> false
+ }
+ }
}
/**
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 60343e9e81e5..74397236847a 100644
--- a/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java
+++ b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java
@@ -81,10 +81,16 @@ import com.android.server.accessibility.Flags;
public class AutoclickController extends BaseEventStreamTransformation {
private static final String LOG_TAG = AutoclickController.class.getSimpleName();
+ // TODO(b/393559560): Finalize scroll amount.
+ private static final float SCROLL_AMOUNT = 1.0f;
private final AccessibilityTraceManager mTrace;
private final Context mContext;
private final int mUserId;
+ @VisibleForTesting
+ float mLastCursorX;
+ @VisibleForTesting
+ float mLastCursorY;
// Lazily created on the first mouse motion event.
@VisibleForTesting ClickScheduler mClickScheduler;
@@ -201,8 +207,8 @@ public class AutoclickController extends BaseEventStreamTransformation {
if (!isPaused()) {
handleMouseMotion(event, policyFlags);
}
- } else if (mClickScheduler != null) {
- mClickScheduler.cancel();
+ } else {
+ cancelPendingClick();
}
super.onMotionEvent(event, rawEvent, policyFlags);
@@ -232,7 +238,7 @@ public class AutoclickController extends BaseEventStreamTransformation {
if (KeyEvent.isModifierKey(event.getKeyCode())) {
mClickScheduler.updateMetaState(event.getMetaState());
} else {
- mClickScheduler.cancel();
+ cancelPendingClick();
}
}
@@ -241,8 +247,8 @@ public class AutoclickController extends BaseEventStreamTransformation {
@Override
public void clearEvents(int inputSource) {
- if (inputSource == InputDevice.SOURCE_MOUSE && mClickScheduler != null) {
- mClickScheduler.cancel();
+ if (inputSource == InputDevice.SOURCE_MOUSE) {
+ cancelPendingClick();
}
if (mAutoclickScrollPanel != null) {
@@ -282,7 +288,7 @@ public class AutoclickController extends BaseEventStreamTransformation {
if (event.getPointerCount() == 1) {
mClickScheduler.update(event, policyFlags);
} else {
- mClickScheduler.cancel();
+ cancelPendingClick();
}
} break;
// Ignore hover enter and exit.
@@ -290,7 +296,7 @@ public class AutoclickController extends BaseEventStreamTransformation {
case MotionEvent.ACTION_HOVER_EXIT:
break;
default:
- mClickScheduler.cancel();
+ cancelPendingClick();
}
}
@@ -315,8 +321,58 @@ public class AutoclickController extends BaseEventStreamTransformation {
/**
* Handles scroll operations in the specified direction.
*/
- public void handleScroll(@AutoclickScrollPanel.ScrollDirection int direction) {
- // TODO(b/388845721): Perform actual scroll.
+ private void handleScroll(@AutoclickScrollPanel.ScrollDirection int direction) {
+ final long now = SystemClock.uptimeMillis();
+
+ // Create pointer properties.
+ PointerProperties[] pointerProps = new PointerProperties[1];
+ pointerProps[0] = new PointerProperties();
+ pointerProps[0].id = 0;
+ pointerProps[0].toolType = MotionEvent.TOOL_TYPE_MOUSE;
+
+ // Create pointer coordinates at the last cursor position.
+ PointerCoords[] pointerCoords = new PointerCoords[1];
+ pointerCoords[0] = new PointerCoords();
+ pointerCoords[0].x = mLastCursorX;
+ pointerCoords[0].y = mLastCursorY;
+
+ // Set scroll values based on direction.
+ switch (direction) {
+ case AutoclickScrollPanel.DIRECTION_UP:
+ pointerCoords[0].setAxisValue(MotionEvent.AXIS_VSCROLL, SCROLL_AMOUNT);
+ break;
+ case AutoclickScrollPanel.DIRECTION_DOWN:
+ pointerCoords[0].setAxisValue(MotionEvent.AXIS_VSCROLL, -SCROLL_AMOUNT);
+ break;
+ case AutoclickScrollPanel.DIRECTION_LEFT:
+ pointerCoords[0].setAxisValue(MotionEvent.AXIS_HSCROLL, SCROLL_AMOUNT);
+ break;
+ case AutoclickScrollPanel.DIRECTION_RIGHT:
+ pointerCoords[0].setAxisValue(MotionEvent.AXIS_HSCROLL, -SCROLL_AMOUNT);
+ break;
+ case AutoclickScrollPanel.DIRECTION_EXIT:
+ case AutoclickScrollPanel.DIRECTION_NONE:
+ default:
+ return;
+ }
+
+ // Get device ID from last motion event if possible.
+ int deviceId = mClickScheduler != null && mClickScheduler.mLastMotionEvent != null
+ ? mClickScheduler.mLastMotionEvent.getDeviceId() : 0;
+
+ // Create a scroll event.
+ MotionEvent scrollEvent = MotionEvent.obtain(
+ /* downTime= */ now, /* eventTime= */ now,
+ MotionEvent.ACTION_SCROLL, /* pointerCount= */ 1, pointerProps,
+ pointerCoords, /* metaState= */ 0, /* actionButton= */ 0, /* xPrecision= */
+ 1.0f, /* yPrecision= */ 1.0f, deviceId, /* edgeFlags= */ 0,
+ InputDevice.SOURCE_MOUSE, /* flags= */ 0);
+
+ // Send the scroll event.
+ super.onMotionEvent(scrollEvent, scrollEvent, mClickScheduler.mEventPolicyFlags);
+
+ // Clean up.
+ scrollEvent.recycle();
}
/**
@@ -823,13 +879,19 @@ public class AutoclickController extends BaseEventStreamTransformation {
// If exit button is hovered, exit scroll mode after countdown and return early.
if (mHoveredDirection == AutoclickScrollPanel.DIRECTION_EXIT) {
exitScrollMode();
+ return;
}
- return;
}
// Handle scroll type specially, show scroll panel instead of sending click events.
if (mActiveClickType == AutoclickTypePanel.AUTOCLICK_TYPE_SCROLL) {
if (mAutoclickScrollPanel != null) {
+ // Save the last cursor position at the moment when sendClick() is called.
+ if (mClickScheduler != null && mClickScheduler.mLastMotionEvent != null) {
+ final int pointerIndex = mClickScheduler.mLastMotionEvent.getActionIndex();
+ mLastCursorX = mClickScheduler.mLastMotionEvent.getX(pointerIndex);
+ mLastCursorY = mClickScheduler.mLastMotionEvent.getY(pointerIndex);
+ }
mAutoclickScrollPanel.show();
}
return;
diff --git a/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickScrollPanel.java b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickScrollPanel.java
index c71443149687..025423078da1 100644
--- a/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickScrollPanel.java
+++ b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickScrollPanel.java
@@ -179,7 +179,7 @@ public class AutoclickScrollPanel {
private WindowManager.LayoutParams getLayoutParams() {
final WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams();
layoutParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
- layoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;
+ layoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
layoutParams.privateFlags |= WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS;
layoutParams.setFitInsetsTypes(WindowInsets.Type.statusBars());
layoutParams.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
diff --git a/services/art-profile-extra b/services/art-profile-extra
index 54362411e5ea..9cbc03903904 100644
--- a/services/art-profile-extra
+++ b/services/art-profile-extra
@@ -1 +1,9 @@
HSPLcom/android/server/am/ActivityManagerService$LocalService;->checkContentProviderAccess(Ljava/lang/String;I)Ljava/lang/String;
+HSPLcom/android/server/am/ActivityManagerService$LocalService;->updateDeviceIdleTempAllowlist([IIZJIILjava/lang/String;I)V
+HSPLcom/android/server/am/OomAdjuster;->setUidTempAllowlistStateLSP(IZ)V
+HSPLcom/android/server/am/BatteryStatsService;->setBatteryState(IIIIIIIIJ)V
+HSPLcom/android/server/pm/PackageManagerService$IPackageManagerImpl;->setComponentEnabledSetting(Landroid/content/ComponentName;IIILjava/lang/String;)V
+HSPLcom/android/server/am/ActiveServices;->bindServiceLocked(Landroid/app/IApplicationThread;Landroid/os/IBinder;Landroid/content/Intent;Ljava/lang/String;Landroid/app/IServiceConnection;JLjava/lang/String;ZILjava/lang/String;Landroid/app/IApplicationThread;Ljava/lang/String;I)I
+HSPLcom/android/server/accessibility/AccessibilityManagerService;->onServiceInfoChangedLocked(Lcom/android/server/accessibility/AccessibilityUserState;)V
+HSPLcom/android/server/clipboard/ClipboardService$ClipboardImpl;->checkAndSetPrimaryClip(Landroid/content/ClipData;Ljava/lang/String;Ljava/lang/String;IILjava/lang/String;)V
+HSPLcom/android/server/clipboard/ClipboardService$ClipboardImpl;->getPrimaryClip(Ljava/lang/String;Ljava/lang/String;II)Landroid/content/ClipData;
diff --git a/services/core/Android.bp b/services/core/Android.bp
index cf85dd957b3f..8a983f9e071d 100644
--- a/services/core/Android.bp
+++ b/services/core/Android.bp
@@ -263,6 +263,7 @@ java_library_static {
"profiling_flags_lib",
"android.adpf.sessionmanager_aidl-java",
"uprobestats_flags_java_lib",
+ "clipboard_flags_lib",
],
javac_shard_size: 50,
javacflags: [
diff --git a/services/core/java/com/android/server/BinaryTransparencyService.java b/services/core/java/com/android/server/BinaryTransparencyService.java
index 1d914c89c570..6ac2180176ce 100644
--- a/services/core/java/com/android/server/BinaryTransparencyService.java
+++ b/services/core/java/com/android/server/BinaryTransparencyService.java
@@ -85,6 +85,8 @@ import com.android.internal.os.IBinaryTransparencyService;
import com.android.internal.util.FrameworkStatsLog;
import com.android.modules.expresslog.Histogram;
import com.android.server.pm.ApexManager;
+import com.android.server.pm.BackgroundInstallControlCallbackHelper;
+import com.android.server.pm.BackgroundInstallControlService;
import com.android.server.pm.pkg.AndroidPackage;
import com.android.server.pm.pkg.AndroidPackageSplit;
import com.android.server.pm.pkg.PackageState;
@@ -101,9 +103,6 @@ import java.util.Map;
import java.util.concurrent.Executors;
import java.util.stream.Collectors;
-import com.android.server.pm.BackgroundInstallControlService;
-import com.android.server.pm.BackgroundInstallControlCallbackHelper;
-
/**
* @hide
*/
@@ -1577,19 +1576,17 @@ public class BinaryTransparencyService extends SystemService {
Slog.d(TAG, String.format("VBMeta Digest: %s", mVbmetaDigest));
FrameworkStatsLog.write(FrameworkStatsLog.VBMETA_DIGEST_REPORTED, mVbmetaDigest);
- if (android.security.Flags.binaryTransparencySepolicyHash()) {
- IoThread.getExecutor().execute(() -> {
- byte[] sepolicyHash = PackageUtils.computeSha256DigestForLargeFileAsBytes(
- "/sys/fs/selinux/policy", PackageUtils.createLargeFileBuffer());
- String sepolicyHashEncoded = null;
- if (sepolicyHash != null) {
- sepolicyHashEncoded = HexEncoding.encodeToString(sepolicyHash, false);
- Slog.d(TAG, "sepolicy hash: " + sepolicyHashEncoded);
- }
- FrameworkStatsLog.write(FrameworkStatsLog.BOOT_INTEGRITY_INFO_REPORTED,
- sepolicyHashEncoded, mVbmetaDigest);
- });
- }
+ IoThread.getExecutor().execute(() -> {
+ byte[] sepolicyHash = PackageUtils.computeSha256DigestForLargeFileAsBytes(
+ "/sys/fs/selinux/policy", PackageUtils.createLargeFileBuffer());
+ String sepolicyHashEncoded = null;
+ if (sepolicyHash != null) {
+ sepolicyHashEncoded = HexEncoding.encodeToString(sepolicyHash, false);
+ Slog.d(TAG, "sepolicy hash: " + sepolicyHashEncoded);
+ }
+ FrameworkStatsLog.write(FrameworkStatsLog.BOOT_INTEGRITY_INFO_REPORTED,
+ sepolicyHashEncoded, mVbmetaDigest);
+ });
}
/**
diff --git a/services/core/java/com/android/server/GestureLauncherService.java b/services/core/java/com/android/server/GestureLauncherService.java
index 87222a60d82d..28258ae47a65 100644
--- a/services/core/java/com/android/server/GestureLauncherService.java
+++ b/services/core/java/com/android/server/GestureLauncherService.java
@@ -19,7 +19,6 @@ package com.android.server;
import static android.service.quickaccesswallet.Flags.launchWalletOptionOnPowerDoubleTap;
import static android.service.quickaccesswallet.Flags.launchWalletViaSysuiCallbacks;
-import static com.android.hardware.input.Flags.overridePowerKeyBehaviorInFocusedWindow;
import static com.android.internal.R.integer.config_defaultMinEmergencyGestureTapDurationMillis;
import android.app.ActivityManager;
@@ -635,46 +634,6 @@ public class GestureLauncherService extends SystemService {
}
/**
- * Processes a power key event in GestureLauncherService without performing an action. This
- * method is called on every KEYCODE_POWER ACTION_DOWN event and ensures that, even if
- * KEYCODE_POWER events are passed to and handled by the app, the GestureLauncherService still
- * keeps track of all running KEYCODE_POWER events for its gesture detection and relevant
- * actions.
- */
- public void processPowerKeyDown(KeyEvent event) {
- if (mEmergencyGestureEnabled && mEmergencyGesturePowerButtonCooldownPeriodMs >= 0
- && event.getEventTime() - mLastEmergencyGestureTriggered
- < mEmergencyGesturePowerButtonCooldownPeriodMs) {
- return;
- }
- if (event.isLongPress()) {
- return;
- }
-
- final long powerTapInterval;
-
- synchronized (this) {
- powerTapInterval = event.getEventTime() - mLastPowerDown;
- mLastPowerDown = event.getEventTime();
- if (powerTapInterval >= POWER_SHORT_TAP_SEQUENCE_MAX_INTERVAL_MS) {
- // Tap too slow, reset consecutive tap counts.
- mFirstPowerDown = event.getEventTime();
- mPowerButtonConsecutiveTaps = 1;
- mPowerButtonSlowConsecutiveTaps = 1;
- } else if (powerTapInterval >= POWER_DOUBLE_TAP_MAX_TIME_MS) {
- // Tap too slow for shortcuts
- mFirstPowerDown = event.getEventTime();
- mPowerButtonConsecutiveTaps = 1;
- mPowerButtonSlowConsecutiveTaps++;
- } else if (!overridePowerKeyBehaviorInFocusedWindow() || powerTapInterval > 0) {
- // Fast consecutive tap
- mPowerButtonConsecutiveTaps++;
- mPowerButtonSlowConsecutiveTaps++;
- }
- }
- }
-
- /**
* Attempts to intercept power key down event by detecting certain gesture patterns
*
* @param interactive true if the event's policy contains {@code FLAG_INTERACTIVE}
@@ -721,7 +680,7 @@ public class GestureLauncherService extends SystemService {
mFirstPowerDown = event.getEventTime();
mPowerButtonConsecutiveTaps = 1;
mPowerButtonSlowConsecutiveTaps++;
- } else if (powerTapInterval > 0) {
+ } else {
// Fast consecutive tap
mPowerButtonConsecutiveTaps++;
mPowerButtonSlowConsecutiveTaps++;
diff --git a/services/core/java/com/android/server/am/BroadcastHistory.java b/services/core/java/com/android/server/am/BroadcastHistory.java
index 700cf9c8deb8..9f7e5cdb7900 100644
--- a/services/core/java/com/android/server/am/BroadcastHistory.java
+++ b/services/core/java/com/android/server/am/BroadcastHistory.java
@@ -20,6 +20,9 @@ import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Intent;
import android.os.Bundle;
+import android.os.Trace;
+import android.util.ArrayMap;
+import android.util.SparseArray;
import android.util.TimeUtils;
import android.util.proto.ProtoOutputStream;
@@ -83,20 +86,56 @@ public class BroadcastHistory {
final long[] mSummaryHistoryDispatchTime;
final long[] mSummaryHistoryFinishTime;
+ /**
+ * Map of uids to number of pending broadcasts it sent.
+ */
+ private final SparseArray<ArrayMap<String, Integer>> mPendingBroadcastCountsPerUid =
+ new SparseArray<>();
+
void onBroadcastFrozenLocked(@NonNull BroadcastRecord r) {
mFrozenBroadcasts.add(r);
}
void onBroadcastEnqueuedLocked(@NonNull BroadcastRecord r) {
mFrozenBroadcasts.remove(r);
- mPendingBroadcasts.add(r);
+ if (mPendingBroadcasts.add(r)) {
+ updatePendingBroadcastCounterAndLogToTrace(r, /* delta= */ 1);
+ }
}
void onBroadcastFinishedLocked(@NonNull BroadcastRecord r) {
- mPendingBroadcasts.remove(r);
+ if (mPendingBroadcasts.remove(r)) {
+ updatePendingBroadcastCounterAndLogToTrace(r, /* delta= */ -1);
+ }
addBroadcastToHistoryLocked(r);
}
+ private void updatePendingBroadcastCounterAndLogToTrace(@NonNull BroadcastRecord r,
+ int delta) {
+ ArrayMap<String, Integer> pendingBroadcastCounts =
+ mPendingBroadcastCountsPerUid.get(r.callingUid);
+ if (pendingBroadcastCounts == null) {
+ pendingBroadcastCounts = new ArrayMap<>();
+ mPendingBroadcastCountsPerUid.put(r.callingUid, pendingBroadcastCounts);
+ }
+ final String callerPackage = r.callerPackage == null ? "null" : r.callerPackage;
+ final Integer currentCount = pendingBroadcastCounts.get(callerPackage);
+ final int newCount = (currentCount == null ? 0 : currentCount) + delta;
+ if (newCount == 0) {
+ pendingBroadcastCounts.remove(callerPackage);
+ if (pendingBroadcastCounts.isEmpty()) {
+ mPendingBroadcastCountsPerUid.remove(r.callingUid);
+ }
+ } else {
+ pendingBroadcastCounts.put(callerPackage, newCount);
+ }
+
+ Trace.instantForTrack(Trace.TRACE_TAG_ACTIVITY_MANAGER, "Broadcasts pending per uid",
+ callerPackage + "/" + r.callingUid + ":" + newCount);
+ Trace.traceCounter(Trace.TRACE_TAG_ACTIVITY_MANAGER, "Broadcasts pending",
+ mPendingBroadcasts.size());
+ }
+
public void addBroadcastToHistoryLocked(@NonNull BroadcastRecord original) {
// Note sometimes (only for sticky broadcasts?) we reuse BroadcastRecords,
// So don't change the incoming record directly.
diff --git a/services/core/java/com/android/server/audio/HardeningEnforcer.java b/services/core/java/com/android/server/audio/HardeningEnforcer.java
index 9bb5160f108a..46693614e137 100644
--- a/services/core/java/com/android/server/audio/HardeningEnforcer.java
+++ b/services/core/java/com/android/server/audio/HardeningEnforcer.java
@@ -199,7 +199,9 @@ public class HardeningEnforcer {
if (packageName.isEmpty()) {
packageName = getPackNameForUid(callingUid);
}
-
+ // indicates would be blocked if audio capabilities were required
+ boolean blockedIfFull = !noteOp(AppOpsManager.OP_CONTROL_AUDIO,
+ callingUid, packageName, attributionTag);
boolean blocked = true;
// indicates the focus request was not blocked because of the SDK version
boolean unblockedBySdk = false;
@@ -213,22 +215,35 @@ public class HardeningEnforcer {
Slog.i(TAG, "blockFocusMethod pack:" + packageName + " NOT blocking due to sdk="
+ targetSdk);
}
- blocked = false;
unblockedBySdk = true;
}
- metricsLogFocusReq(blocked, focusReqType, callingUid, unblockedBySdk);
+ boolean enforced = mShouldEnableAllHardening.get() || !unblockedBySdk;
+ boolean enforcedFull = mShouldEnableAllHardening.get();
- if (!blocked) {
- return false;
- }
+ metricsLogFocusReq(blocked && enforced, focusReqType, callingUid, unblockedBySdk);
- String errorMssg = "Focus request DENIED for uid:" + callingUid
- + " clientId:" + clientId + " req:" + focusReqType
- + " procState:" + mActivityManager.getUidProcessState(callingUid);
- mEventLogger.enqueueAndSlog(errorMssg, EventLogger.Event.ALOGI, TAG);
+ if (blocked) {
+ String msg = "AudioHardening focus request for req "
+ + focusReqType
+ + (!enforced ? " would be " : " ")
+ + "ignored for "
+ + packageName + " (" + callingUid + "), "
+ + clientId
+ + ", level: partial";
+ mEventLogger.enqueueAndSlog(msg, EventLogger.Event.ALOGW, TAG);
+ } else if (blockedIfFull) {
+ String msg = "AudioHardening focus request for req "
+ + focusReqType
+ + (!enforcedFull ? " would be " : " ")
+ + "ignored for "
+ + packageName + " (" + callingUid + "), "
+ + clientId
+ + ", level: full";
+ mEventLogger.enqueueAndSlog(msg, EventLogger.Event.ALOGW, TAG);
+ }
- return true;
+ return blocked && enforced || (blockedIfFull && enforcedFull);
}
/**
diff --git a/services/core/java/com/android/server/biometrics/AuthenticationStats.java b/services/core/java/com/android/server/biometrics/AuthenticationStats.java
index 3c1cc006ffda..461ddf86ff71 100644
--- a/services/core/java/com/android/server/biometrics/AuthenticationStats.java
+++ b/services/core/java/com/android/server/biometrics/AuthenticationStats.java
@@ -32,14 +32,20 @@ public class AuthenticationStats {
private int mTotalAttempts;
private int mRejectedAttempts;
private int mEnrollmentNotifications;
+
+ private long mLastEnrollmentTime;
+ private long mLastFrrNotificationTime;
private final int mModality;
public AuthenticationStats(final int userId, int totalAttempts, int rejectedAttempts,
- int enrollmentNotifications, final int modality) {
+ int enrollmentNotifications, long lastEnrollmentTime, long lastFrrNotificationTime,
+ final int modality) {
mUserId = userId;
mTotalAttempts = totalAttempts;
mRejectedAttempts = rejectedAttempts;
mEnrollmentNotifications = enrollmentNotifications;
+ mLastEnrollmentTime = lastEnrollmentTime;
+ mLastFrrNotificationTime = lastFrrNotificationTime;
mModality = modality;
}
@@ -48,6 +54,8 @@ public class AuthenticationStats {
mTotalAttempts = 0;
mRejectedAttempts = 0;
mEnrollmentNotifications = 0;
+ mLastEnrollmentTime = 0;
+ mLastFrrNotificationTime = 0;
mModality = modality;
}
@@ -71,6 +79,14 @@ public class AuthenticationStats {
return mModality;
}
+ public long getLastEnrollmentTime() {
+ return mLastEnrollmentTime;
+ }
+
+ public long getLastFrrNotificationTime() {
+ return mLastFrrNotificationTime;
+ }
+
/** Calculate FRR. */
public float getFrr() {
if (mTotalAttempts > 0) {
@@ -100,6 +116,16 @@ public class AuthenticationStats {
mEnrollmentNotifications++;
}
+ /** Updates last enrollment time */
+ public void updateLastEnrollmentTime(long lastEnrollmentTime) {
+ mLastEnrollmentTime = lastEnrollmentTime;
+ }
+
+ /** Updates frr notification time */
+ public void updateLastFrrNotificationTime(long lastFrrNotificationTime) {
+ mLastFrrNotificationTime = lastFrrNotificationTime;
+ }
+
@Override
public boolean equals(Object obj) {
if (this == obj) {
@@ -118,6 +144,10 @@ public class AuthenticationStats {
== target.getRejectedAttempts()
&& this.getEnrollmentNotifications()
== target.getEnrollmentNotifications()
+ && this.getLastEnrollmentTime()
+ == target.getLastEnrollmentTime()
+ && this.getLastFrrNotificationTime()
+ == target.getLastFrrNotificationTime()
&& this.getModality() == target.getModality();
}
diff --git a/services/core/java/com/android/server/biometrics/AuthenticationStatsBroadcastReceiver.java b/services/core/java/com/android/server/biometrics/AuthenticationStatsBroadcastReceiver.java
index 832d73fd5d2d..54348403914a 100644
--- a/services/core/java/com/android/server/biometrics/AuthenticationStatsBroadcastReceiver.java
+++ b/services/core/java/com/android/server/biometrics/AuthenticationStatsBroadcastReceiver.java
@@ -27,6 +27,7 @@ import android.util.Slog;
import com.android.server.biometrics.sensors.BiometricNotificationImpl;
+import java.time.Clock;
import java.util.function.Consumer;
/**
@@ -62,7 +63,7 @@ public class AuthenticationStatsBroadcastReceiver extends BroadcastReceiver {
mCollectorConsumer.accept(
new AuthenticationStatsCollector(context, mModality,
- new BiometricNotificationImpl()));
+ new BiometricNotificationImpl(), Clock.systemUTC()));
context.unregisterReceiver(this);
}
diff --git a/services/core/java/com/android/server/biometrics/AuthenticationStatsCollector.java b/services/core/java/com/android/server/biometrics/AuthenticationStatsCollector.java
index 526264d67318..b79bab99f681 100644
--- a/services/core/java/com/android/server/biometrics/AuthenticationStatsCollector.java
+++ b/services/core/java/com/android/server/biometrics/AuthenticationStatsCollector.java
@@ -23,6 +23,7 @@ import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
+import android.hardware.biometrics.BiometricsProtoEnums;
import android.hardware.face.FaceManager;
import android.hardware.fingerprint.FingerprintManager;
import android.os.UserHandle;
@@ -32,6 +33,9 @@ import com.android.internal.R;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.biometrics.sensors.BiometricNotification;
+import java.time.Clock;
+import java.time.Duration;
+import java.time.Instant;
import java.util.HashMap;
import java.util.Map;
@@ -50,7 +54,12 @@ public class AuthenticationStatsCollector {
private static final int AUTHENTICATION_UPLOAD_INTERVAL = 50;
// The maximum number of eligible biometric enrollment notification can be sent.
@VisibleForTesting
- static final int MAXIMUM_ENROLLMENT_NOTIFICATIONS = 1;
+ static final int MAXIMUM_ENROLLMENT_NOTIFICATIONS = Flags.frrDialogImprovement() ? 3 : 1;
+ @VisibleForTesting
+ static final Duration FRR_MINIMAL_DURATION = Duration.ofDays(7);
+
+ public static final String ACTION_LAST_ENROLL_TIME_CHANGED = "last_enroll_time_changed";
+ public static final String EXTRA_MODALITY = "modality";
@NonNull private final Context mContext;
@NonNull private final PackageManager mPackageManager;
@@ -64,6 +73,7 @@ public class AuthenticationStatsCollector {
@NonNull private final Map<Integer, AuthenticationStats> mUserAuthenticationStatsMap;
@NonNull private AuthenticationStatsPersister mAuthenticationStatsPersister;
@NonNull private BiometricNotification mBiometricNotification;
+ @NonNull private final Clock mClock;
private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
@Override
@@ -78,8 +88,24 @@ public class AuthenticationStatsCollector {
}
};
+ private final BroadcastReceiver mEnrollTimeUpdatedReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(@NonNull Context context, @NonNull Intent intent) {
+ int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, UserHandle.USER_NULL);
+ int modality = intent.getIntExtra(EXTRA_MODALITY,
+ BiometricsProtoEnums.MODALITY_UNKNOWN);
+ if (modality == mModality) {
+ updateAuthenticationStatsMapIfNeeded(userId);
+ AuthenticationStats authenticationStats =
+ mUserAuthenticationStatsMap.get(userId);
+ Slog.d(TAG, "Update enroll time for user: " + userId);
+ authenticationStats.updateLastEnrollmentTime(mClock.millis());
+ }
+ }
+ };
+
public AuthenticationStatsCollector(@NonNull Context context, int modality,
- @NonNull BiometricNotification biometricNotification) {
+ @NonNull BiometricNotification biometricNotification, @NonNull Clock clock) {
mContext = context;
mEnabled = context.getResources().getBoolean(R.bool.config_biometricFrrNotificationEnabled);
mThreshold = context.getResources()
@@ -87,6 +113,7 @@ public class AuthenticationStatsCollector {
mUserAuthenticationStatsMap = new HashMap<>();
mModality = modality;
mBiometricNotification = biometricNotification;
+ mClock = clock;
mPackageManager = context.getPackageManager();
mFaceManager = mContext.getSystemService(FaceManager.class);
@@ -100,6 +127,11 @@ public class AuthenticationStatsCollector {
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(Intent.ACTION_USER_REMOVED);
context.registerReceiver(mBroadcastReceiver, intentFilter);
+
+ IntentFilter enrollTimeChangedFilter = new IntentFilter();
+ enrollTimeChangedFilter.addAction(ACTION_LAST_ENROLL_TIME_CHANGED);
+ context.registerReceiver(mEnrollTimeUpdatedReceiver, enrollTimeChangedFilter,
+ Context.RECEIVER_NOT_EXPORTED);
}
private void initializeUserAuthenticationStatsMap() {
@@ -123,19 +155,9 @@ public class AuthenticationStatsCollector {
return;
}
- // SharedPreference is not ready when starting system server, initialize
- // mUserAuthenticationStatsMap in authentication to ensure SharedPreference
- // is ready for application use.
- if (mUserAuthenticationStatsMap.isEmpty()) {
- initializeUserAuthenticationStatsMap();
- }
- // Check if this is a new user.
- if (!mUserAuthenticationStatsMap.containsKey(userId)) {
- mUserAuthenticationStatsMap.put(userId, new AuthenticationStats(userId, mModality));
- }
+ updateAuthenticationStatsMapIfNeeded(userId);
AuthenticationStats authenticationStats = mUserAuthenticationStatsMap.get(userId);
-
if (authenticationStats.getEnrollmentNotifications() >= MAXIMUM_ENROLLMENT_NOTIFICATIONS) {
return;
}
@@ -147,34 +169,91 @@ public class AuthenticationStatsCollector {
persistDataIfNeeded(userId);
}
+ private void updateAuthenticationStatsMapIfNeeded(int userId) {
+ // SharedPreference is not ready when starting system server, initialize
+ // mUserAuthenticationStatsMap in authentication to ensure SharedPreference
+ // is ready for application use.
+ if (mUserAuthenticationStatsMap.isEmpty()) {
+ initializeUserAuthenticationStatsMap();
+ }
+ // Check if this is a new user.
+ if (!mUserAuthenticationStatsMap.containsKey(userId)) {
+ mUserAuthenticationStatsMap.put(userId, new AuthenticationStats(userId, mModality));
+ }
+ }
+
/** Check if a notification should be sent after a calculation cycle. */
private void sendNotificationIfNeeded(int userId) {
AuthenticationStats authenticationStats = mUserAuthenticationStatsMap.get(userId);
if (authenticationStats.getTotalAttempts() < MINIMUM_ATTEMPTS) {
return;
}
+
+ boolean showFrr;
+ if (Flags.frrDialogImprovement()) {
+ long lastFrrOrEnrollTime = Math.max(authenticationStats.getLastEnrollmentTime(),
+ authenticationStats.getLastFrrNotificationTime());
+ showFrr = authenticationStats.getEnrollmentNotifications()
+ < MAXIMUM_ENROLLMENT_NOTIFICATIONS
+ && authenticationStats.getFrr() >= mThreshold
+ && isFrrMinimalDurationPassed(lastFrrOrEnrollTime);
+ } else {
+ showFrr = authenticationStats.getEnrollmentNotifications()
+ < MAXIMUM_ENROLLMENT_NOTIFICATIONS
+ && authenticationStats.getFrr() >= mThreshold;
+ }
+
// Don't send notification if FRR below the threshold.
- if (authenticationStats.getEnrollmentNotifications() >= MAXIMUM_ENROLLMENT_NOTIFICATIONS
- || authenticationStats.getFrr() < mThreshold) {
+ if (!showFrr) {
authenticationStats.resetData();
return;
}
-
authenticationStats.resetData();
+ if (Flags.frrDialogImprovement()
+ && mModality == BiometricsProtoEnums.MODALITY_FINGERPRINT) {
+ boolean sent = mBiometricNotification.sendCustomizeFpFrrNotification(mContext);
+ if (sent) {
+ authenticationStats.updateLastFrrNotificationTime(mClock.millis());
+ authenticationStats.updateNotificationCounter();
+ return;
+ }
+ }
+
final boolean hasEnrolledFace = hasEnrolledFace(userId);
final boolean hasEnrolledFingerprint = hasEnrolledFingerprint(userId);
if (hasEnrolledFace && !hasEnrolledFingerprint) {
mBiometricNotification.sendFpEnrollNotification(mContext);
+ authenticationStats.updateLastFrrNotificationTime(mClock.millis());
authenticationStats.updateNotificationCounter();
} else if (!hasEnrolledFace && hasEnrolledFingerprint) {
mBiometricNotification.sendFaceEnrollNotification(mContext);
+ authenticationStats.updateLastFrrNotificationTime(mClock.millis());
authenticationStats.updateNotificationCounter();
}
}
+ private boolean isFrrMinimalDurationPassed(long previousMillis) {
+ Instant previous = Instant.ofEpochMilli(previousMillis);
+ long nowMillis = mClock.millis();
+ Instant now = Instant.ofEpochMilli(nowMillis);
+
+ if (now.isAfter(previous)) {
+ Duration between = Duration.between(/* startInclusive= */ previous,
+ /* endExclusive= */ now);
+ if (between.compareTo(FRR_MINIMAL_DURATION) > 0) {
+ return true;
+ } else {
+ Slog.d(TAG, "isFrrMinimalDurationPassed, duration too short");
+ }
+ } else {
+ Slog.d(TAG, "isFrrMinimalDurationPassed, date not match");
+ }
+ return false;
+ }
+
private void persistDataIfNeeded(int userId) {
AuthenticationStats authenticationStats = mUserAuthenticationStatsMap.get(userId);
if (authenticationStats.getTotalAttempts() % AUTHENTICATION_UPLOAD_INTERVAL == 0) {
@@ -182,6 +261,8 @@ public class AuthenticationStatsCollector {
authenticationStats.getTotalAttempts(),
authenticationStats.getRejectedAttempts(),
authenticationStats.getEnrollmentNotifications(),
+ authenticationStats.getLastEnrollmentTime(),
+ authenticationStats.getLastFrrNotificationTime(),
authenticationStats.getModality());
}
}
diff --git a/services/core/java/com/android/server/biometrics/AuthenticationStatsPersister.java b/services/core/java/com/android/server/biometrics/AuthenticationStatsPersister.java
index 5625bfd21e76..746d00909900 100644
--- a/services/core/java/com/android/server/biometrics/AuthenticationStatsPersister.java
+++ b/services/core/java/com/android/server/biometrics/AuthenticationStatsPersister.java
@@ -48,8 +48,13 @@ public class AuthenticationStatsPersister {
private static final String USER_ID = "user_id";
private static final String FACE_ATTEMPTS = "face_attempts";
private static final String FACE_REJECTIONS = "face_rejections";
+ private static final String FACE_LAST_ENROLL_TIME = "face_last_enroll_time";
+ private static final String FACE_LAST_FRR_NOTIFICATION_TIME = "face_last_notification_time";
private static final String FINGERPRINT_ATTEMPTS = "fingerprint_attempts";
private static final String FINGERPRINT_REJECTIONS = "fingerprint_rejections";
+ private static final String FINGERPRINT_LAST_ENROLL_TIME = "fingerprint_last_enroll_time";
+ private static final String FINGERPRINT_LAST_FRR_NOTIFICATION_TIME =
+ "fingerprint_last_notification_time";
private static final String ENROLLMENT_NOTIFICATIONS = "enrollment_notifications";
private static final String KEY = "frr_stats";
private static final String THRESHOLD_KEY = "frr_threshold";
@@ -73,21 +78,10 @@ public class AuthenticationStatsPersister {
try {
JSONObject frrStatsJson = new JSONObject(frrStats);
if (modality == BiometricsProtoEnums.MODALITY_FACE) {
- authenticationStatsList.add(new AuthenticationStats(
- getIntValue(frrStatsJson, USER_ID,
- UserHandle.USER_NULL /* defaultValue */),
- getIntValue(frrStatsJson, FACE_ATTEMPTS),
- getIntValue(frrStatsJson, FACE_REJECTIONS),
- getIntValue(frrStatsJson, ENROLLMENT_NOTIFICATIONS),
- modality));
+ authenticationStatsList.add(getFaceAuthenticationStatsFromJson(frrStatsJson));
} else if (modality == BiometricsProtoEnums.MODALITY_FINGERPRINT) {
- authenticationStatsList.add(new AuthenticationStats(
- getIntValue(frrStatsJson, USER_ID,
- UserHandle.USER_NULL /* defaultValue */),
- getIntValue(frrStatsJson, FINGERPRINT_ATTEMPTS),
- getIntValue(frrStatsJson, FINGERPRINT_REJECTIONS),
- getIntValue(frrStatsJson, ENROLLMENT_NOTIFICATIONS),
- modality));
+ authenticationStatsList.add(
+ getFingerprintAuthenticationStatsFromJson(frrStatsJson));
}
} catch (JSONException e) {
Slog.w(TAG, String.format("Unable to resolve authentication stats JSON: %s",
@@ -97,6 +91,33 @@ public class AuthenticationStatsPersister {
return authenticationStatsList;
}
+ @NonNull
+ AuthenticationStats getFaceAuthenticationStatsFromJson(JSONObject json) throws JSONException {
+ return new AuthenticationStats(
+ /* userId */ getIntValue(json, USER_ID, UserHandle.USER_NULL),
+ /* totalAttempts */ getIntValue(json, FACE_ATTEMPTS),
+ /* rejectedAttempts */ getIntValue(json, FACE_REJECTIONS),
+ /* enrollmentNotifications */ getIntValue(json, ENROLLMENT_NOTIFICATIONS),
+ /* lastEnrollmentTime */ getLongValue(json, FACE_LAST_ENROLL_TIME),
+ /* lastFrrNotificationTime */getLongValue(json, FACE_LAST_FRR_NOTIFICATION_TIME),
+ /* modality */ BiometricsProtoEnums.MODALITY_FACE);
+ }
+
+ @NonNull
+ AuthenticationStats getFingerprintAuthenticationStatsFromJson(JSONObject json)
+ throws JSONException {
+ return new AuthenticationStats(
+ /* userId */ getIntValue(json, USER_ID, UserHandle.USER_NULL),
+ /* totalAttempts */ getIntValue(json, FINGERPRINT_ATTEMPTS),
+ /* rejectedAttempts */ getIntValue(json, FINGERPRINT_REJECTIONS),
+ /* enrollmentNotifications */ getIntValue(json, ENROLLMENT_NOTIFICATIONS),
+ /* lastEnrollmentTime */ getLongValue(json,
+ FINGERPRINT_LAST_ENROLL_TIME),
+ /* lastFrrNotificationTime */ getLongValue(json,
+ FINGERPRINT_LAST_FRR_NOTIFICATION_TIME),
+ /* modality */ BiometricsProtoEnums.MODALITY_FINGERPRINT);
+ }
+
/**
* Remove frr data for a specific user.
*/
@@ -124,7 +145,8 @@ public class AuthenticationStatsPersister {
* Persist frr data for a specific user.
*/
public void persistFrrStats(int userId, int totalAttempts, int rejectedAttempts,
- int enrollmentNotifications, int modality) {
+ int enrollmentNotifications, long lastEnrollmentTime, long lastFrrNotificationTime,
+ int modality) {
try {
// Copy into a new HashSet to allow modification.
Set<String> frrStatsSet = new HashSet<>(readFrrStats());
@@ -147,7 +169,8 @@ public class AuthenticationStatsPersister {
frrStatJson = new JSONObject().put(USER_ID, userId);
}
frrStatsSet.add(buildFrrStats(frrStatJson, totalAttempts, rejectedAttempts,
- enrollmentNotifications, modality));
+ enrollmentNotifications, lastEnrollmentTime, lastFrrNotificationTime,
+ modality));
Slog.d(TAG, "frrStatsSet to persist: " + frrStatsSet);
@@ -171,18 +194,24 @@ public class AuthenticationStatsPersister {
// Update frr stats for existing frrStats JSONObject and build the new string.
private String buildFrrStats(JSONObject frrStats, int totalAttempts, int rejectedAttempts,
- int enrollmentNotifications, int modality) throws JSONException {
+ int enrollmentNotifications, long lastEnrollmentTime, long lastFrrNotificationTime,
+ int modality)
+ throws JSONException {
if (modality == BiometricsProtoEnums.MODALITY_FACE) {
return frrStats
.put(FACE_ATTEMPTS, totalAttempts)
.put(FACE_REJECTIONS, rejectedAttempts)
.put(ENROLLMENT_NOTIFICATIONS, enrollmentNotifications)
+ .put(FACE_LAST_ENROLL_TIME, lastEnrollmentTime)
+ .put(FACE_LAST_FRR_NOTIFICATION_TIME, lastFrrNotificationTime)
.toString();
} else if (modality == BiometricsProtoEnums.MODALITY_FINGERPRINT) {
return frrStats
.put(FINGERPRINT_ATTEMPTS, totalAttempts)
.put(FINGERPRINT_REJECTIONS, rejectedAttempts)
.put(ENROLLMENT_NOTIFICATIONS, enrollmentNotifications)
+ .put(FINGERPRINT_LAST_ENROLL_TIME, lastEnrollmentTime)
+ .put(FINGERPRINT_LAST_FRR_NOTIFICATION_TIME, lastFrrNotificationTime)
.toString();
} else {
return frrStats.toString();
@@ -201,4 +230,13 @@ public class AuthenticationStatsPersister {
throws JSONException {
return jsonObject.has(key) ? jsonObject.getInt(key) : defaultValue;
}
+
+ private long getLongValue(JSONObject jsonObject, String key) throws JSONException {
+ return getLongValue(jsonObject, key, 0 /* defaultValue */);
+ }
+
+ private long getLongValue(JSONObject jsonObject, String key, long defaultValue)
+ throws JSONException {
+ return jsonObject.has(key) ? jsonObject.getLong(key) : defaultValue;
+ }
}
diff --git a/services/core/java/com/android/server/biometrics/sensors/BiometricNotification.java b/services/core/java/com/android/server/biometrics/sensors/BiometricNotification.java
index 90e18604d945..9fb25f429020 100644
--- a/services/core/java/com/android/server/biometrics/sensors/BiometricNotification.java
+++ b/services/core/java/com/android/server/biometrics/sensors/BiometricNotification.java
@@ -33,4 +33,9 @@ public interface BiometricNotification {
* Sends a fingerprint enrollment notification.
*/
void sendFpEnrollNotification(@NonNull Context context);
+
+ /**
+ * Sends a customized fingerprint frr notification.
+ */
+ boolean sendCustomizeFpFrrNotification(@NonNull Context context);
}
diff --git a/services/core/java/com/android/server/biometrics/sensors/BiometricNotificationImpl.java b/services/core/java/com/android/server/biometrics/sensors/BiometricNotificationImpl.java
index 7b420468f628..3ab157082c0b 100644
--- a/services/core/java/com/android/server/biometrics/sensors/BiometricNotificationImpl.java
+++ b/services/core/java/com/android/server/biometrics/sensors/BiometricNotificationImpl.java
@@ -35,4 +35,9 @@ public class BiometricNotificationImpl implements BiometricNotification {
public void sendFpEnrollNotification(@NonNull Context context) {
BiometricNotificationUtils.showFingerprintEnrollNotification(context);
}
+
+ @Override
+ public boolean sendCustomizeFpFrrNotification(@NonNull Context context) {
+ return BiometricNotificationUtils.showCustomizeFpFrrNotification(context);
+ }
}
diff --git a/services/core/java/com/android/server/biometrics/sensors/BiometricNotificationUtils.java b/services/core/java/com/android/server/biometrics/sensors/BiometricNotificationUtils.java
index 27f9cc88e28f..3bad3c2a3f8f 100644
--- a/services/core/java/com/android/server/biometrics/sensors/BiometricNotificationUtils.java
+++ b/services/core/java/com/android/server/biometrics/sensors/BiometricNotificationUtils.java
@@ -17,12 +17,16 @@
package com.android.server.biometrics.sensors;
import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
+import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
import android.hardware.biometrics.BiometricManager;
import android.hardware.biometrics.BiometricsProtoEnums;
import android.hardware.face.FaceEnrollOptions;
@@ -30,6 +34,7 @@ import android.hardware.fingerprint.FingerprintEnrollOptions;
import android.os.SystemClock;
import android.os.UserHandle;
import android.text.BidiFormatter;
+import android.text.TextUtils;
import android.util.Slog;
import com.android.internal.R;
@@ -56,6 +61,7 @@ public class BiometricNotificationUtils {
private static final String FACE_ENROLL_CHANNEL = "FaceEnrollNotificationChannel";
private static final String FACE_RE_ENROLL_CHANNEL = "FaceReEnrollNotificationChannel";
private static final String FINGERPRINT_ENROLL_CHANNEL = "FingerprintEnrollNotificationChannel";
+ private static final String FINGERPRINT_FRR_CHANNEL = "FingerprintFrrNotificationChannel";
private static final String FINGERPRINT_RE_ENROLL_CHANNEL =
"FingerprintReEnrollNotificationChannel";
private static final String FINGERPRINT_BAD_CALIBRATION_CHANNEL =
@@ -69,6 +75,7 @@ public class BiometricNotificationUtils {
public static final int NOTIFICATION_ID = 1;
public static final String FACE_ENROLL_NOTIFICATION_TAG = "FaceEnroll";
public static final String FINGERPRINT_ENROLL_NOTIFICATION_TAG = "FingerprintEnroll";
+ public static final String FINGERPRINT_FRR_NOTIFICATION_TAG = "FingerprintFrr";
/**
* Shows a face re-enrollment notification.
*/
@@ -151,6 +158,65 @@ public class BiometricNotificationUtils {
}
/**
+ * Shows a customized fingerprint frr notification.
+ *
+ * @return true if notification shows
+ */
+ public static boolean showCustomizeFpFrrNotification(@NonNull Context context) {
+ final String name =
+ context.getString(R.string.device_unlock_notification_name);
+ final String title =
+ context.getString(R.string.fingerprint_frr_notification_title);
+ final String content =
+ context.getString(R.string.fingerprint_frr_notification_msg);
+
+ Intent intent = getIntentFromFpFrrComponentNameStringRes(context);
+ Slog.d(TAG, "Showing Customize Fingerprint Frr Notification result:" + (intent != null));
+ if (intent == null) {
+ return false;
+ }
+
+ final PendingIntent pendingIntent = PendingIntent.getActivityAsUser(context,
+ 0 /* requestCode */, intent, PendingIntent.FLAG_IMMUTABLE /* flags */,
+ null /* options */, UserHandle.CURRENT);
+
+ showNotificationHelper(context, name, title, content, pendingIntent,
+ Notification.CATEGORY_RECOMMENDATION, FINGERPRINT_FRR_CHANNEL,
+ FINGERPRINT_FRR_NOTIFICATION_TAG, Notification.VISIBILITY_PUBLIC, true);
+
+ return true;
+ }
+
+ @Nullable
+ private static Intent getIntentFromFpFrrComponentNameStringRes(@NonNull Context context) {
+ String componentNameString = context.getResources().getString(
+ R.string.config_fingerprintFrrTargetComponent);
+ if (TextUtils.isEmpty(componentNameString)) {
+ return null;
+ }
+
+ ComponentName componentName = ComponentName.unflattenFromString(componentNameString);
+ if (componentName == null) {
+ return null;
+ }
+
+ PackageManager packageManager = context.getPackageManager();
+ Intent intent = new Intent();
+ intent.setComponent(componentName);
+ intent.setAction(Intent.ACTION_MAIN);
+ intent.addCategory(Intent.CATEGORY_LAUNCHER);
+
+ ResolveInfo resolveInfo = packageManager.resolveActivity(intent,
+ PackageManager.MATCH_DEFAULT_ONLY);
+ if (resolveInfo != null) {
+ return intent;
+ } else {
+ Slog.d(TAG, "Component for " + componentNameString + " not found");
+ return null;
+ }
+ }
+
+ /**
* Shows a fingerprint notification for loss of enrollment
*/
public static void showFingerprintLoeNotification(@NonNull Context context) {
diff --git a/services/core/java/com/android/server/biometrics/sensors/EnrollClient.java b/services/core/java/com/android/server/biometrics/sensors/EnrollClient.java
index 38bf99932838..1632e0d7ca6f 100644
--- a/services/core/java/com/android/server/biometrics/sensors/EnrollClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/EnrollClient.java
@@ -18,6 +18,7 @@ package com.android.server.biometrics.sensors;
import android.annotation.NonNull;
import android.content.Context;
+import android.content.Intent;
import android.hardware.biometrics.BiometricAuthenticator;
import android.hardware.biometrics.BiometricRequestConstants;
import android.hardware.face.FaceEnrollOptions;
@@ -26,6 +27,7 @@ import android.os.IBinder;
import android.os.RemoteException;
import android.util.Slog;
+import com.android.server.biometrics.AuthenticationStatsCollector;
import com.android.server.biometrics.BiometricsProto;
import com.android.server.biometrics.log.BiometricContext;
import com.android.server.biometrics.log.BiometricLogger;
@@ -159,4 +161,13 @@ public abstract class EnrollClient<T> extends AcquisitionClient<T> implements En
default -> BiometricRequestConstants.REASON_UNKNOWN;
};
}
+
+ protected void notifyLastEnrollmentTime(int modality) {
+ // Notify the last enrollment time to re-count authentication stats for frr.
+ final Intent intent = new Intent(
+ AuthenticationStatsCollector.ACTION_LAST_ENROLL_TIME_CHANGED);
+ intent.putExtra(Intent.EXTRA_USER_HANDLE, getTargetUserId());
+ intent.putExtra(AuthenticationStatsCollector.EXTRA_MODALITY, modality);
+ getContext().sendBroadcast(intent);
+ }
}
diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceEnrollClient.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceEnrollClient.java
index d4ec573e1667..e7b2d41024a4 100644
--- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceEnrollClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceEnrollClient.java
@@ -24,9 +24,11 @@ import static android.hardware.face.FaceManager.getErrorString;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
+import android.hardware.biometrics.BiometricAuthenticator;
import android.hardware.biometrics.BiometricConstants;
import android.hardware.biometrics.BiometricFaceConstants;
import android.hardware.biometrics.BiometricSourceType;
+import android.hardware.biometrics.BiometricsProtoEnums;
import android.hardware.biometrics.common.ICancellationSignal;
import android.hardware.biometrics.events.AuthenticationErrorInfo;
import android.hardware.biometrics.events.AuthenticationHelpInfo;
@@ -171,6 +173,14 @@ public class FaceEnrollClient extends EnrollClient<AidlSession> {
onAcquiredInternal(acquireInfo, vendorCode, shouldSend);
}
+ @Override
+ public void onEnrollResult(BiometricAuthenticator.Identifier identifier, int remaining) {
+ super.onEnrollResult(identifier, remaining);
+ if (remaining == 0) {
+ notifyLastEnrollmentTime(BiometricsProtoEnums.MODALITY_FACE);
+ }
+ }
+
/**
* Called each time a new frame is received during face enrollment.
*
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClient.java
index 993a68fd6ff8..776435d5abc8 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClient.java
@@ -30,6 +30,7 @@ import android.hardware.biometrics.BiometricFingerprintConstants;
import android.hardware.biometrics.BiometricFingerprintConstants.FingerprintAcquired;
import android.hardware.biometrics.BiometricSourceType;
import android.hardware.biometrics.BiometricStateListener;
+import android.hardware.biometrics.BiometricsProtoEnums;
import android.hardware.biometrics.common.ICancellationSignal;
import android.hardware.biometrics.events.AuthenticationAcquiredInfo;
import android.hardware.biometrics.events.AuthenticationErrorInfo;
@@ -156,8 +157,8 @@ public class FingerprintEnrollClient extends EnrollClient<AidlSession> implement
BiometricSourceType.FINGERPRINT,
getRequestReasonFromFingerprintEnrollReason(mEnrollReason)).build()
);
+ notifyLastEnrollmentTime(BiometricsProtoEnums.MODALITY_FINGERPRINT);
}
-
}
@Override
diff --git a/services/core/java/com/android/server/clipboard/Android.bp b/services/core/java/com/android/server/clipboard/Android.bp
new file mode 100644
index 000000000000..6905fc157a9a
--- /dev/null
+++ b/services/core/java/com/android/server/clipboard/Android.bp
@@ -0,0 +1,18 @@
+aconfig_declarations {
+ name: "clipboard_flags",
+ package: "com.android.server.clipboard",
+ container: "system",
+ srcs: ["*.aconfig"],
+}
+
+java_aconfig_library {
+ name: "clipboard_flags_lib",
+ aconfig_declarations: "clipboard_flags",
+}
+
+java_aconfig_library {
+ name: "clipboard_flags_host_lib",
+ host_supported: true,
+ libs: ["fake_device_config"],
+ aconfig_declarations: "clipboard_flags",
+}
diff --git a/services/core/java/com/android/server/clipboard/ClipboardService.java b/services/core/java/com/android/server/clipboard/ClipboardService.java
index 6122fdaafe77..40136c3e03ec 100644
--- a/services/core/java/com/android/server/clipboard/ClipboardService.java
+++ b/services/core/java/com/android/server/clipboard/ClipboardService.java
@@ -19,9 +19,27 @@ package com.android.server.clipboard;
import static android.app.ActivityManagerInternal.ALLOW_FULL_ONLY;
import static android.companion.virtual.VirtualDeviceParams.DEVICE_POLICY_CUSTOM;
import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_CLIPBOARD;
+import static android.content.ClipDescription.MIMETYPE_UNKNOWN;
+import static android.content.ClipDescription.MIMETYPE_TEXT_PLAIN;
+import static android.content.ClipDescription.MIMETYPE_TEXT_HTML;
+import static android.content.ClipDescription.MIMETYPE_TEXT_URILIST;
+import static android.content.ClipDescription.MIMETYPE_TEXT_INTENT;
+import static android.content.ClipDescription.MIMETYPE_APPLICATION_ACTIVITY;
+import static android.content.ClipDescription.MIMETYPE_APPLICATION_SHORTCUT;
+import static android.content.ClipDescription.MIMETYPE_APPLICATION_TASK;
import static android.content.Context.DEVICE_ID_DEFAULT;
import static android.content.Context.DEVICE_ID_INVALID;
+import static com.android.internal.util.FrameworkStatsLog.CLIPBOARD_GET_EVENT_REPORTED__CLIP_DATA_TYPE__MIMETYPE_UNKNOWN;
+import static com.android.internal.util.FrameworkStatsLog.CLIPBOARD_GET_EVENT_REPORTED__CLIP_DATA_TYPE__MIMETYPE_TEXT_PLAIN;
+import static com.android.internal.util.FrameworkStatsLog.CLIPBOARD_GET_EVENT_REPORTED__CLIP_DATA_TYPE__MIMETYPE_TEXT_HTML;
+import static com.android.internal.util.FrameworkStatsLog.CLIPBOARD_GET_EVENT_REPORTED__CLIP_DATA_TYPE__MIMETYPE_TEXT_URILIST;
+import static com.android.internal.util.FrameworkStatsLog.CLIPBOARD_GET_EVENT_REPORTED__CLIP_DATA_TYPE__MIMETYPE_TEXT_INTENT;
+import static com.android.internal.util.FrameworkStatsLog.CLIPBOARD_GET_EVENT_REPORTED__CLIP_DATA_TYPE__MIMETYPE_APPLICATION_ACTIVITY;
+import static com.android.internal.util.FrameworkStatsLog.CLIPBOARD_GET_EVENT_REPORTED__CLIP_DATA_TYPE__MIMETYPE_APPLICATION_SHORTCUT;
+import static com.android.internal.util.FrameworkStatsLog.CLIPBOARD_GET_EVENT_REPORTED__CLIP_DATA_TYPE__MIMETYPE_APPLICATION_TASK;
+import static com.android.server.clipboard.Flags.clipboardGetEventLogging;
+
import android.Manifest;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -70,6 +88,7 @@ import android.provider.Settings;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.ArraySet;
+import android.util.IntArray;
import android.util.Pair;
import android.util.SafetyProtectionUtils;
import android.util.Slog;
@@ -98,6 +117,7 @@ import com.android.server.wm.WindowManagerInternal;
import java.util.HashSet;
import java.util.List;
+import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
/**
@@ -132,6 +152,10 @@ public class ClipboardService extends SystemService {
private static final String PROPERTY_MAX_CLASSIFICATION_LENGTH = "max_classification_length";
private static final int DEFAULT_MAX_CLASSIFICATION_LENGTH = 400;
+ private static final int[] CLIP_DATA_TYPES_UNKNOWN = {
+ CLIPBOARD_GET_EVENT_REPORTED__CLIP_DATA_TYPE__MIMETYPE_UNKNOWN
+ };
+
private final ActivityManagerInternal mAmInternal;
private final IUriGrantsManager mUgm;
private final UriGrantsManagerInternal mUgmInternal;
@@ -657,6 +681,8 @@ public class ClipboardService extends SystemService {
pkg, intendingUid, intendingUserId, clipboard, deviceId);
notifyTextClassifierLocked(clipboard, pkg, intendingUid);
if (clipboard.primaryClip != null) {
+ scheduleWriteClipDataStats(clipboard.primaryClip,
+ clipboard.primaryClipUid, intendingUid);
scheduleAutoClear(userId, intendingUid, intendingDeviceId);
}
return clipboard.primaryClip;
@@ -1600,4 +1626,65 @@ public class ClipboardService extends SystemService {
Context context = getContext().createContextAsUser(UserHandle.of(userId), /* flags= */ 0);
return context.getSystemService(TextClassificationManager.class);
}
+
+ private static int mimeTypeToClipDataType(@NonNull String mimeType) {
+ switch (mimeType) {
+ case MIMETYPE_TEXT_PLAIN:
+ return CLIPBOARD_GET_EVENT_REPORTED__CLIP_DATA_TYPE__MIMETYPE_TEXT_PLAIN;
+ case MIMETYPE_TEXT_HTML:
+ return CLIPBOARD_GET_EVENT_REPORTED__CLIP_DATA_TYPE__MIMETYPE_TEXT_HTML;
+ case MIMETYPE_TEXT_URILIST:
+ return CLIPBOARD_GET_EVENT_REPORTED__CLIP_DATA_TYPE__MIMETYPE_TEXT_URILIST;
+ case MIMETYPE_TEXT_INTENT:
+ return CLIPBOARD_GET_EVENT_REPORTED__CLIP_DATA_TYPE__MIMETYPE_TEXT_INTENT;
+ case MIMETYPE_APPLICATION_ACTIVITY:
+ return CLIPBOARD_GET_EVENT_REPORTED__CLIP_DATA_TYPE__MIMETYPE_APPLICATION_ACTIVITY;
+ case MIMETYPE_APPLICATION_SHORTCUT:
+ return CLIPBOARD_GET_EVENT_REPORTED__CLIP_DATA_TYPE__MIMETYPE_APPLICATION_SHORTCUT;
+ case MIMETYPE_APPLICATION_TASK:
+ return CLIPBOARD_GET_EVENT_REPORTED__CLIP_DATA_TYPE__MIMETYPE_APPLICATION_TASK;
+ case MIMETYPE_UNKNOWN:
+ // fallthrough
+ default:
+ return CLIPBOARD_GET_EVENT_REPORTED__CLIP_DATA_TYPE__MIMETYPE_UNKNOWN;
+ }
+ }
+
+ private void scheduleWriteClipDataStats(@NonNull ClipData clipData,
+ int sourceUid, int intendingUid) {
+ if (!clipboardGetEventLogging()) {
+ return;
+ }
+ final ClipDescription description = clipData.getDescription();
+ if (description != null) {
+ final IntArray mimeTypes = new IntArray();
+ final int secondsSinceSet = (int) TimeUnit.MILLISECONDS.toSeconds(
+ System.currentTimeMillis() - description.getTimestamp());
+ for (int i = description.getMimeTypeCount() - 1; i >= 0; i--) {
+ final String mimeType = description.getMimeType(i);
+ if (mimeType != null) {
+ final int clipDataType = mimeTypeToClipDataType(mimeType);
+ if (!mimeTypes.contains(clipDataType)) {
+ mimeTypes.add(clipDataType);
+ }
+ }
+ }
+ // The getUidProcessState() will hit AMS lock which might be slow, while getting the
+ // clip data might be on the critical UI path. So post to the work thread.
+ // There could be race conditions where the UID state might have been changed
+ // between now and the work thread execution time, but this should be acceptable.
+ mWorkerHandler.post(() -> FrameworkStatsLog.write(
+ FrameworkStatsLog.CLIPBOARD_GET_EVENT_REPORTED,
+ sourceUid, intendingUid,
+ mAmInternal.getUidProcessState(intendingUid),
+ mimeTypes.toArray(),
+ secondsSinceSet));
+ } else {
+ mWorkerHandler.post(() -> FrameworkStatsLog.write(
+ FrameworkStatsLog.CLIPBOARD_GET_EVENT_REPORTED,
+ sourceUid, intendingUid,
+ mAmInternal.getUidProcessState(intendingUid),
+ CLIP_DATA_TYPES_UNKNOWN, 0));
+ }
+ }
}
diff --git a/services/core/java/com/android/server/clipboard/flags.aconfig b/services/core/java/com/android/server/clipboard/flags.aconfig
new file mode 100644
index 000000000000..964242d794a4
--- /dev/null
+++ b/services/core/java/com/android/server/clipboard/flags.aconfig
@@ -0,0 +1,9 @@
+package: "com.android.server.clipboard"
+container: "system"
+
+flag {
+ name: "clipboard_get_event_logging"
+ namespace: "backstage_power"
+ description: "Log the clipboard retrieval event in statsd"
+ bug: "402542624"
+}
diff --git a/services/core/java/com/android/server/connectivity/PacProxyService.java b/services/core/java/com/android/server/connectivity/PacProxyService.java
index 2e90a3d86161..c8c1eddd53e7 100644
--- a/services/core/java/com/android/server/connectivity/PacProxyService.java
+++ b/services/core/java/com/android/server/connectivity/PacProxyService.java
@@ -36,6 +36,7 @@ import android.net.Uri;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.IBinder;
+import android.os.IServiceManager;
import android.os.RemoteCallbackList;
import android.os.RemoteException;
import android.os.ServiceManager;
@@ -355,7 +356,9 @@ public class PacProxyService extends IPacProxyManager.Stub {
} catch (RemoteException e1) {
Log.e(TAG, "Remote Exception", e1);
}
- ServiceManager.addService(PAC_SERVICE_NAME, binder);
+ // Do not cache the service, otherwise it will crash com.android.pacprocessor
+ ServiceManager.addService(PAC_SERVICE_NAME, binder, /* allowIsolated */ false,
+ IServiceManager.FLAG_IS_LAZY_SERVICE);
mProxyService = IProxyService.Stub.asInterface(binder);
if (mProxyService == null) {
Log.e(TAG, "No proxy service");
diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java
index f2ededaa2a9c..76284fb81814 100644
--- a/services/core/java/com/android/server/input/InputManagerService.java
+++ b/services/core/java/com/android/server/input/InputManagerService.java
@@ -344,6 +344,9 @@ public class InputManagerService extends IInputManager.Stub
// Manages battery state for input devices.
private final BatteryController mBatteryController;
+ // Monitors any changes to the sysfs nodes when an input device is connected.
+ private final SysfsNodeMonitor mSysfsNodeMonitor;
+
@Nullable
private final TouchpadDebugViewController mTouchpadDebugViewController;
@@ -507,8 +510,7 @@ public class InputManagerService extends IInputManager.Stub
KeyboardBacklightControllerInterface getKeyboardBacklightController(
NativeInputManagerService nativeService) {
- return new KeyboardBacklightController(mContext, nativeService, mLooper,
- mUEventManager);
+ return new KeyboardBacklightController(mContext, nativeService, mLooper);
}
}
@@ -536,6 +538,8 @@ public class InputManagerService extends IInputManager.Stub
injector.getLooper(), this) : null;
mBatteryController = new BatteryController(mContext, mNative, injector.getLooper(),
injector.getUEventManager());
+ mSysfsNodeMonitor = new SysfsNodeMonitor(mContext, mNative, injector.getLooper(),
+ injector.getUEventManager());
mKeyboardBacklightController = injector.getKeyboardBacklightController(mNative);
mStickyModifierStateController = new StickyModifierStateController();
mInputDataStore = new InputDataStore();
@@ -665,6 +669,7 @@ public class InputManagerService extends IInputManager.Stub
mKeyboardLayoutManager.systemRunning();
mBatteryController.systemRunning();
+ mSysfsNodeMonitor.systemRunning();
mKeyboardBacklightController.systemRunning();
mKeyboardLedController.systemRunning();
mKeyRemapper.systemRunning();
diff --git a/services/core/java/com/android/server/input/KeyGestureController.java b/services/core/java/com/android/server/input/KeyGestureController.java
index 5de432e5849b..b069a87480ad 100644
--- a/services/core/java/com/android/server/input/KeyGestureController.java
+++ b/services/core/java/com/android/server/input/KeyGestureController.java
@@ -762,7 +762,7 @@ final class KeyGestureController {
if (!canceled) {
handleKeyGesture(deviceId, new int[]{keyCode},
/* modifierState = */0,
- KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_ALL_APPS,
+ KeyGestureEvent.KEY_GESTURE_TYPE_ALL_APPS,
KeyGestureEvent.ACTION_GESTURE_COMPLETE, displayId,
focusedToken, /* flags = */0, /* appLaunchData = */null);
}
diff --git a/services/core/java/com/android/server/input/KeyboardBacklightController.java b/services/core/java/com/android/server/input/KeyboardBacklightController.java
index 16368c7678d1..083c0006ad65 100644
--- a/services/core/java/com/android/server/input/KeyboardBacklightController.java
+++ b/services/core/java/com/android/server/input/KeyboardBacklightController.java
@@ -32,7 +32,6 @@ import android.os.Looper;
import android.os.Message;
import android.os.RemoteException;
import android.os.SystemClock;
-import android.os.UEventObserver;
import android.sysprop.InputProperties;
import android.text.TextUtils;
import android.util.IndentingPrintWriter;
@@ -83,8 +82,6 @@ final class KeyboardBacklightController implements
private static final long TRANSITION_ANIMATION_DURATION_MILLIS =
Duration.ofSeconds(1).toMillis();
- private static final String UEVENT_KEYBOARD_BACKLIGHT_TAG = "kbd_backlight";
-
@VisibleForTesting
static final int[] DEFAULT_BRIGHTNESS_VALUE_FOR_LEVEL =
new int[DEFAULT_NUM_BRIGHTNESS_CHANGE_STEPS + 1];
@@ -93,7 +90,6 @@ final class KeyboardBacklightController implements
private final NativeInputManagerService mNative;
private final Handler mHandler;
private final AnimatorFactory mAnimatorFactory;
- private final UEventManager mUEventManager;
// Always access on handler thread or need to lock this for synchronization.
private final SparseArray<KeyboardBacklightState> mKeyboardBacklights = new SparseArray<>(1);
// Maintains state if all backlights should be on or turned off
@@ -124,19 +120,18 @@ final class KeyboardBacklightController implements
}
KeyboardBacklightController(Context context, NativeInputManagerService nativeService,
- Looper looper, UEventManager uEventManager) {
- this(context, nativeService, looper, ValueAnimator::ofInt, uEventManager);
+ Looper looper) {
+ this(context, nativeService, looper, ValueAnimator::ofInt);
}
@VisibleForTesting
KeyboardBacklightController(Context context, NativeInputManagerService nativeService,
- Looper looper, AnimatorFactory animatorFactory, UEventManager uEventManager) {
+ Looper looper, AnimatorFactory animatorFactory) {
mContext = context;
mNative = nativeService;
mHandler = new Handler(looper, this::handleMessage);
mAnimatorFactory = animatorFactory;
mAmbientController = new AmbientKeyboardBacklightController(context, looper);
- mUEventManager = uEventManager;
Resources res = mContext.getResources();
mUserInactivityThresholdMs = res.getInteger(
com.android.internal.R.integer.config_keyboardBacklightTimeoutMs);
@@ -154,17 +149,6 @@ final class KeyboardBacklightController implements
inputManager.getInputDeviceIds());
mHandler.sendMessage(msg);
- // Observe UEvents for "kbd_backlight" sysfs nodes.
- // We want to observe creation of such LED nodes since they might be created after device
- // FD created and InputDevice creation logic doesn't initialize LED nodes which leads to
- // backlight not working.
- mUEventManager.addListener(new UEventManager.UEventListener() {
- @Override
- public void onUEvent(UEventObserver.UEvent event) {
- onKeyboardBacklightUEvent(event);
- }
- }, UEVENT_KEYBOARD_BACKLIGHT_TAG);
-
// Start ambient backlight controller
mAmbientController.systemRunning();
}
@@ -414,17 +398,6 @@ final class KeyboardBacklightController implements
}
}
- @VisibleForTesting
- public void onKeyboardBacklightUEvent(UEventObserver.UEvent event) {
- if ("ADD".equalsIgnoreCase(event.get("ACTION")) && "LEDS".equalsIgnoreCase(
- event.get("SUBSYSTEM"))) {
- final String devPath = event.get("DEVPATH");
- if (isValidBacklightNodePath(devPath)) {
- mNative.sysfsNodeChanged("/sys" + devPath);
- }
- }
- }
-
private void updateAmbientLightListener() {
boolean needToListenAmbientLightSensor = false;
for (int i = 0; i < mKeyboardBacklights.size(); i++) {
diff --git a/services/core/java/com/android/server/input/NativeInputManagerService.java b/services/core/java/com/android/server/input/NativeInputManagerService.java
index ccf1a2c90876..de54cd81aa43 100644
--- a/services/core/java/com/android/server/input/NativeInputManagerService.java
+++ b/services/core/java/com/android/server/input/NativeInputManagerService.java
@@ -272,6 +272,9 @@ interface NativeInputManagerService {
/** Set whether showing a pointer icon for styluses is enabled. */
void setStylusPointerIconEnabled(boolean enabled);
+ /** Get the sysfs root path of an input device if known, otherwise return null. */
+ @Nullable String getSysfsRootPath(int deviceId);
+
/**
* Report sysfs node changes. This may result in recreation of the corresponding InputDevice.
* The recreated device may contain new associated peripheral devices like Light, Battery, etc.
@@ -619,6 +622,9 @@ interface NativeInputManagerService {
public native void setStylusPointerIconEnabled(boolean enabled);
@Override
+ public native String getSysfsRootPath(int deviceId);
+
+ @Override
public native void sysfsNodeChanged(String sysfsNodePath);
@Override
diff --git a/services/core/java/com/android/server/input/SysfsNodeMonitor.java b/services/core/java/com/android/server/input/SysfsNodeMonitor.java
new file mode 100644
index 000000000000..e55e1284d03c
--- /dev/null
+++ b/services/core/java/com/android/server/input/SysfsNodeMonitor.java
@@ -0,0 +1,203 @@
+/*
+ * Copyright 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.input;
+
+import android.content.Context;
+import android.hardware.input.InputManager;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.UEventObserver;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.Slog;
+import android.util.SparseArray;
+
+import java.util.Objects;
+
+/**
+ * A thread-safe component of {@link InputManagerService} responsible for monitoring the addition
+ * of kernel sysfs nodes for newly connected input devices.
+ *
+ * This class uses the {@link UEventObserver} to monitor for changes to an input device's sysfs
+ * nodes, and is responsible for requesting the native code to refresh its sysfs nodes when there
+ * is a change. This is necessary because the sysfs nodes may only be configured after an input
+ * device is already added, with no way for the native code to detect any changes afterwards.
+ */
+final class SysfsNodeMonitor {
+ private static final String TAG = SysfsNodeMonitor.class.getSimpleName();
+
+ // To enable these logs, run:
+ // 'adb shell setprop log.tag.SysfsNodeMonitor DEBUG' (requires restart)
+ private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+
+ private static final long SYSFS_NODE_MONITORING_TIMEOUT_MS = 60_000; // 1 minute
+
+ private final Context mContext;
+ private final NativeInputManagerService mNative;
+ private final Handler mHandler;
+ private final UEventManager mUEventManager;
+
+ private InputManager mInputManager;
+
+ private final SparseArray<SysfsNodeAddedListener> mUEventListenersByDeviceId =
+ new SparseArray<>();
+
+ SysfsNodeMonitor(Context context, NativeInputManagerService nativeService, Looper looper,
+ UEventManager uEventManager) {
+ mContext = context;
+ mNative = nativeService;
+ mHandler = new Handler(looper);
+ mUEventManager = uEventManager;
+ }
+
+ public void systemRunning() {
+ mInputManager = Objects.requireNonNull(mContext.getSystemService(InputManager.class));
+ mInputManager.registerInputDeviceListener(mInputDeviceListener, mHandler);
+ for (int deviceId : mInputManager.getInputDeviceIds()) {
+ mInputDeviceListener.onInputDeviceAdded(deviceId);
+ }
+ }
+
+ private final InputManager.InputDeviceListener mInputDeviceListener =
+ new InputManager.InputDeviceListener() {
+ @Override
+ public void onInputDeviceAdded(int deviceId) {
+ startMonitoring(deviceId);
+ }
+
+ @Override
+ public void onInputDeviceRemoved(int deviceId) {
+ stopMonitoring(deviceId);
+ }
+
+ @Override
+ public void onInputDeviceChanged(int deviceId) {
+ }
+ };
+
+ private void startMonitoring(int deviceId) {
+ final var inputDevice = mInputManager.getInputDevice(deviceId);
+ if (inputDevice == null) {
+ return;
+ }
+ if (!inputDevice.isExternal()) {
+ if (DEBUG) {
+ Log.d(TAG, "Not listening to sysfs node changes for internal input device: "
+ + deviceId);
+ }
+ return;
+ }
+ final var sysfsRootPath = formatDevPath(mNative.getSysfsRootPath(deviceId));
+ if (sysfsRootPath == null) {
+ if (DEBUG) {
+ Log.d(TAG, "Sysfs node not found for external input device: " + deviceId);
+ }
+ return;
+ }
+ if (DEBUG) {
+ Log.d(TAG, "Start listening to sysfs node changes for input device: " + deviceId
+ + ", node: " + sysfsRootPath);
+ }
+ final var listener = new SysfsNodeAddedListener();
+ mUEventListenersByDeviceId.put(deviceId, listener);
+
+ // We must synchronously start monitoring for changes to this device's path.
+ // Once monitoring starts, we need to trigger a native refresh of the sysfs nodes to
+ // catch any changes that happened between the input device's creation and the UEvent
+ // listener being added.
+ // NOTE: This relies on the fact that the following `addListener` call is fully synchronous.
+ mUEventManager.addListener(listener, sysfsRootPath);
+ mNative.sysfsNodeChanged(sysfsRootPath);
+
+ // Always stop listening for new sysfs nodes after the timeout.
+ mHandler.postDelayed(() -> stopMonitoring(deviceId), SYSFS_NODE_MONITORING_TIMEOUT_MS);
+ }
+
+ private static String formatDevPath(String path) {
+ // Remove the "/sys" prefix if it has one.
+ return path != null && path.startsWith("/sys") ? path.substring(4) : path;
+ }
+
+ private void stopMonitoring(int deviceId) {
+ final var listener = mUEventListenersByDeviceId.removeReturnOld(deviceId);
+ if (listener == null) {
+ return;
+ }
+ if (DEBUG) {
+ Log.d(TAG, "Stop listening to sysfs node changes for input device: " + deviceId);
+ }
+ mUEventManager.removeListener(listener);
+ }
+
+ class SysfsNodeAddedListener extends UEventManager.UEventListener {
+
+ private boolean mHasReceivedRemovalNotification = false;
+ private boolean mHasReceivedPowerSupplyNotification = false;
+
+ @Override
+ public void onUEvent(UEventObserver.UEvent event) {
+ // This callback happens on the UEventObserver's thread.
+ // Ensure we are processing on the handler thread.
+ mHandler.post(() -> handleUEvent(event));
+ }
+
+ private void handleUEvent(UEventObserver.UEvent event) {
+ if (DEBUG) {
+ Slog.d(TAG, "UEventListener: Received UEvent: " + event);
+ }
+ final var subsystem = event.get("SUBSYSTEM");
+ final var devPath = "/sys" + Objects.requireNonNull(
+ TextUtils.nullIfEmpty(event.get("DEVPATH")));
+ final var action = event.get("ACTION");
+
+ // NOTE: We must be careful to avoid reconfiguring sysfs nodes during device removal,
+ // because it might result in the device getting re-opened in native code during
+ // removal, resulting in unexpected states. If we see any removal action for this node,
+ // ensure we stop responding altogether.
+ if (mHasReceivedRemovalNotification || "REMOVE".equalsIgnoreCase(action)) {
+ mHasReceivedRemovalNotification = true;
+ return;
+ }
+
+ if ("LEDS".equalsIgnoreCase(subsystem) && "ADD".equalsIgnoreCase(action)) {
+ // An LED node was added. Notify native code to reconfigure the sysfs node.
+ if (DEBUG) {
+ Slog.d(TAG,
+ "Reconfiguring sysfs node because 'leds' node was added: " + devPath);
+ }
+ mNative.sysfsNodeChanged(devPath);
+ return;
+ }
+
+ if ("POWER_SUPPLY".equalsIgnoreCase(subsystem)) {
+ if (mHasReceivedPowerSupplyNotification) {
+ return;
+ }
+ // This is the first notification we received from the power_supply subsystem.
+ // Notify native code that the battery node may have been added. The power_supply
+ // subsystem does not seem to be sending ADD events, so use use the first event
+ // with any action as a proxy for a new power_supply node being created.
+ if (DEBUG) {
+ Slog.d(TAG, "Reconfiguring sysfs node because 'power_supply' node had action '"
+ + action + "': " + devPath);
+ }
+ mHasReceivedPowerSupplyNotification = true;
+ mNative.sysfsNodeChanged(devPath);
+ }
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/input/debug/TouchpadDebugViewController.java b/services/core/java/com/android/server/input/debug/TouchpadDebugViewController.java
index 2c1d68e3dbda..8d664e848ef5 100644
--- a/services/core/java/com/android/server/input/debug/TouchpadDebugViewController.java
+++ b/services/core/java/com/android/server/input/debug/TouchpadDebugViewController.java
@@ -55,15 +55,17 @@ public class TouchpadDebugViewController implements InputManager.InputDeviceList
@Override
public void onInputDeviceAdded(int deviceId) {
+ if (!mTouchpadVisualizerEnabled) {
+ return;
+ }
final InputManager inputManager = Objects.requireNonNull(
mContext.getSystemService(InputManager.class));
InputDevice inputDevice = inputManager.getInputDevice(deviceId);
-
- if (Objects.requireNonNull(inputDevice).supportsSource(
- InputDevice.SOURCE_TOUCHPAD | InputDevice.SOURCE_MOUSE)
- && mTouchpadVisualizerEnabled) {
- showDebugView(deviceId);
+ if (inputDevice == null || !inputDevice.supportsSource(
+ InputDevice.SOURCE_TOUCHPAD | InputDevice.SOURCE_MOUSE)) {
+ return;
}
+ showDebugView(deviceId);
}
@Override
diff --git a/services/core/java/com/android/server/inputmethod/IInputMethodClientInvoker.java b/services/core/java/com/android/server/inputmethod/IInputMethodClientInvoker.java
index 9d889839879b..c2873e8ee28e 100644
--- a/services/core/java/com/android/server/inputmethod/IInputMethodClientInvoker.java
+++ b/services/core/java/com/android/server/inputmethod/IInputMethodClientInvoker.java
@@ -256,9 +256,11 @@ final class IInputMethodClientInvoker {
@AnyThread
private void setImeVisibilityInternal(boolean visible, @Nullable ImeTracker.Token statsToken) {
try {
+ ImeTracker.forLogging().onProgress(statsToken, ImeTracker.PHASE_SERVER_CLIENT_INVOKER);
mTarget.setImeVisibility(visible, statsToken);
} catch (RemoteException e) {
logRemoteException(e);
+ ImeTracker.forLogging().onFailed(statsToken, ImeTracker.PHASE_SERVER_CLIENT_INVOKER);
}
}
diff --git a/services/core/java/com/android/server/inputmethod/IInputMethodInvoker.java b/services/core/java/com/android/server/inputmethod/IInputMethodInvoker.java
index 0047ec20d691..3fcb6ce271e3 100644
--- a/services/core/java/com/android/server/inputmethod/IInputMethodInvoker.java
+++ b/services/core/java/com/android/server/inputmethod/IInputMethodInvoker.java
@@ -205,9 +205,11 @@ final class IInputMethodInvoker {
boolean showSoftInput(IBinder showInputToken, @NonNull ImeTracker.Token statsToken,
@InputMethod.ShowFlags int flags, ResultReceiver resultReceiver) {
try {
+ ImeTracker.forLogging().onProgress(statsToken, ImeTracker.PHASE_SERVER_IME_INVOKER);
mTarget.showSoftInput(showInputToken, statsToken, flags, resultReceiver);
} catch (RemoteException e) {
logRemoteException(e);
+ ImeTracker.forLogging().onFailed(statsToken, ImeTracker.PHASE_SERVER_IME_INVOKER);
return false;
}
return true;
@@ -218,9 +220,11 @@ final class IInputMethodInvoker {
boolean hideSoftInput(IBinder hideInputToken, @NonNull ImeTracker.Token statsToken,
int flags, ResultReceiver resultReceiver) {
try {
+ ImeTracker.forLogging().onProgress(statsToken, ImeTracker.PHASE_SERVER_IME_INVOKER);
mTarget.hideSoftInput(hideInputToken, statsToken, flags, resultReceiver);
} catch (RemoteException e) {
logRemoteException(e);
+ ImeTracker.forLogging().onFailed(statsToken, ImeTracker.PHASE_SERVER_IME_INVOKER);
return false;
}
return true;
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index 7ff41e309c55..5e3224d1012e 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -1826,14 +1826,8 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl.
@NonNull UserData userData) {
final int userId = userData.mUserId;
if (userData.mCurClient == client) {
- 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);
- }
+ hideCurrentInputLocked(userData.mImeBindingState.mFocusedWindow, 0 /* flags */,
+ SoftInputShowHideReason.HIDE_REMOVE_CLIENT, userId);
if (userData.mBoundToMethod) {
userData.mBoundToMethod = false;
final var userBindingController = userData.mBindingController;
@@ -2103,14 +2097,8 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl.
}
if (visibilityStateComputer.getImePolicy().isImeHiddenByDisplayPolicy()) {
- 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);
- }
+ hideCurrentInputLocked(userData.mImeBindingState.mFocusedWindow, 0 /* flags */,
+ SoftInputShowHideReason.HIDE_DISPLAY_IME_POLICY_HIDE, userId);
return InputBindResult.NO_IME;
}
@@ -3206,7 +3194,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl.
}
// TODO(b/353463205) check callers to see if we can make statsToken @NonNull
- boolean showCurrentInputInternal(IBinder windowToken, @Nullable ImeTracker.Token statsToken) {
+ boolean showCurrentInputInternal(IBinder windowToken, @NonNull ImeTracker.Token statsToken) {
Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "IMMS.showCurrentInputInternal");
ImeTracing.getInstance().triggerManagerServiceDump(
"InputMethodManagerService#showSoftInput", mDumper);
@@ -3226,7 +3214,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl.
}
// TODO(b/353463205) check callers to see if we can make statsToken @NonNull
- boolean hideCurrentInputInternal(IBinder windowToken, @Nullable ImeTracker.Token statsToken) {
+ boolean hideCurrentInputInternal(IBinder windowToken, @NonNull ImeTracker.Token statsToken) {
Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "IMMS.hideCurrentInputInternal");
ImeTracing.getInstance().triggerManagerServiceDump(
"InputMethodManagerService#hideSoftInput", mDumper);
@@ -3867,17 +3855,8 @@ 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.");
-
- 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);
- }
+ hideCurrentInputLocked(userData.mImeBindingState.mFocusedWindow,
+ 0 /* flags */, SoftInputShowHideReason.HIDE_INVALID_USER, userId);
return InputBindResult.INVALID_USER;
}
@@ -5014,6 +4993,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl.
setImeVisibilityOnFocusedWindowClient(false, userData,
null /* TODO(b/353463205) check statsToken */);
} else {
+
hideCurrentInputLocked(userData.mImeBindingState.mFocusedWindow,
0 /* flags */, reason, userId);
}
@@ -6709,9 +6689,8 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl.
final InputMethodSettings settings = InputMethodSettingsRepository.get(userId);
final var userData = getUserData(userId);
if (Flags.refactorInsetsController()) {
- final var statsToken = createStatsTokenForFocusedClient(false /* show */,
- SoftInputShowHideReason.HIDE_RESET_SHELL_COMMAND, userId);
- setImeVisibilityOnFocusedWindowClient(false, userData, statsToken);
+ setImeVisibilityOnFocusedWindowClient(false, userData,
+ null /* TODO(b329229469) initialize statsToken here? */);
} else {
hideCurrentInputLocked(userData.mImeBindingState.mFocusedWindow,
0 /* flags */,
diff --git a/services/core/java/com/android/server/location/contexthub/ContextHubEndpointManager.java b/services/core/java/com/android/server/location/contexthub/ContextHubEndpointManager.java
index 0dc1b832f5a4..47e19089de92 100644
--- a/services/core/java/com/android/server/location/contexthub/ContextHubEndpointManager.java
+++ b/services/core/java/com/android/server/location/contexthub/ContextHubEndpointManager.java
@@ -192,9 +192,10 @@ import java.util.function.Consumer;
* This is separate from the constructor so that this may be passed into the callback registered
* with the HAL.
*
- * @throws InstantiationException on any failure
+ * @throws InstantiationException on unexpected failure
+ * @throws UnsupportedOperationException if not supported by the HAL
*/
- /* package */ void init() throws InstantiationException {
+ /* package */ void init() throws InstantiationException, UnsupportedOperationException {
if (mSessionIdsValid) {
throw new IllegalStateException("Already initialized");
}
@@ -214,12 +215,11 @@ import java.util.function.Consumer;
if (mHubInterface == null) {
throw new IllegalStateException("Received null IEndpointCommunication");
}
- } catch (RemoteException | IllegalStateException | ServiceSpecificException
- | UnsupportedOperationException e) {
+ } catch (RemoteException | IllegalStateException | ServiceSpecificException e) {
String error = "Failed to register ContextHubService as message hub";
Log.e(TAG, error, e);
throw new InstantiationException(error);
- }
+ } // Forward UnsupportedOperationException to caller
int[] range = null;
try {
diff --git a/services/core/java/com/android/server/location/contexthub/ContextHubService.java b/services/core/java/com/android/server/location/contexthub/ContextHubService.java
index 2c0c55bd8df4..44996350e3f3 100644
--- a/services/core/java/com/android/server/location/contexthub/ContextHubService.java
+++ b/services/core/java/com/android/server/location/contexthub/ContextHubService.java
@@ -342,17 +342,19 @@ public class ContextHubService extends IContextHubService.Stub {
new ContextHubEndpointManager(
mContext, mContextHubWrapper, registry, mTransactionManager);
mEndpointManager.init();
- Log.i(TAG, "Enabling generic offload API");
- } catch (InstantiationException e) {
+ Log.d(TAG, "Enabling generic offload API");
+ } catch (InstantiationException | UnsupportedOperationException e) {
mEndpointManager = null;
registry = null;
- Log.w(TAG, "Generic offload API not supported, disabling");
+ if (e instanceof UnsupportedOperationException) {
+ Log.d(TAG, "Generic offload API not supported by HAL");
+ }
}
mHubInfoRegistry = registry;
} else {
mHubInfoRegistry = null;
mEndpointManager = null;
- Log.i(TAG, "Disabling generic offload API");
+ Log.d(TAG, "Disabling generic offload API due to flag config");
}
initDefaultClientMap();
diff --git a/services/core/java/com/android/server/location/injector/SystemEmergencyHelper.java b/services/core/java/com/android/server/location/injector/SystemEmergencyHelper.java
index 3f75b11befc2..ea4b3d426346 100644
--- a/services/core/java/com/android/server/location/injector/SystemEmergencyHelper.java
+++ b/services/core/java/com/android/server/location/injector/SystemEmergencyHelper.java
@@ -23,6 +23,7 @@ import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
+import android.location.flags.Flags;
import android.os.SystemClock;
import android.telephony.TelephonyCallback;
import android.telephony.TelephonyManager;
@@ -71,13 +72,25 @@ public class SystemEmergencyHelper extends EmergencyHelper {
return;
}
- synchronized (SystemEmergencyHelper.this) {
+ if (Flags.fixIsInEmergencyAnr()) {
try {
- mIsInEmergencyCall = mTelephonyManager.isEmergencyNumber(
+ boolean isInEmergency = mTelephonyManager.isEmergencyNumber(
intent.getStringExtra(Intent.EXTRA_PHONE_NUMBER));
+ synchronized (SystemEmergencyHelper.this) {
+ mIsInEmergencyCall = isInEmergency;
+ }
} catch (IllegalStateException | UnsupportedOperationException e) {
Log.w(TAG, "Failed to call TelephonyManager.isEmergencyNumber().", e);
}
+ } else {
+ synchronized (SystemEmergencyHelper.this) {
+ try {
+ mIsInEmergencyCall = mTelephonyManager.isEmergencyNumber(
+ intent.getStringExtra(Intent.EXTRA_PHONE_NUMBER));
+ } catch (IllegalStateException | UnsupportedOperationException e) {
+ Log.w(TAG, "Failed to call TelephonyManager.isEmergencyNumber().", e);
+ }
+ }
}
dispatchEmergencyStateChanged();
@@ -98,27 +111,55 @@ public class SystemEmergencyHelper extends EmergencyHelper {
}
@Override
- public synchronized boolean isInEmergency(long extensionTimeMs) {
- if (mTelephonyManager == null) {
- return false;
- }
+ public boolean isInEmergency(long extensionTimeMs) {
+ if (Flags.fixIsInEmergencyAnr()) {
+ if (mTelephonyManager == null) {
+ return false;
+ }
+ boolean emergencyCallbackMode = false;
+ boolean emergencySmsMode = false;
+ PackageManager pm = mContext.getPackageManager();
+ if (pm.hasSystemFeature(PackageManager.FEATURE_TELEPHONY_CALLING)) {
+ emergencyCallbackMode = mTelephonyManager.getEmergencyCallbackMode();
+ }
+ if (pm.hasSystemFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING)) {
+ emergencySmsMode = mTelephonyManager.isInEmergencySmsMode();
+ }
+ boolean isInExtensionTime;
+ synchronized (this) {
+ isInExtensionTime = mEmergencyCallEndRealtimeMs != Long.MIN_VALUE
+ && (SystemClock.elapsedRealtime() - mEmergencyCallEndRealtimeMs)
+ < extensionTimeMs;
+ return mIsInEmergencyCall
+ || isInExtensionTime
+ || emergencyCallbackMode
+ || emergencySmsMode;
+ }
+ } else {
+ synchronized (this) {
+ if (mTelephonyManager == null) {
+ return false;
+ }
- boolean isInExtensionTime = mEmergencyCallEndRealtimeMs != Long.MIN_VALUE
- && (SystemClock.elapsedRealtime() - mEmergencyCallEndRealtimeMs) < extensionTimeMs;
+ boolean isInExtensionTime = mEmergencyCallEndRealtimeMs != Long.MIN_VALUE
+ && (SystemClock.elapsedRealtime() - mEmergencyCallEndRealtimeMs)
+ < extensionTimeMs;
- boolean emergencyCallbackMode = false;
- boolean emergencySmsMode = false;
- PackageManager pm = mContext.getPackageManager();
- if (pm.hasSystemFeature(PackageManager.FEATURE_TELEPHONY_CALLING)) {
- emergencyCallbackMode = mTelephonyManager.getEmergencyCallbackMode();
- }
- if (pm.hasSystemFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING)) {
- emergencySmsMode = mTelephonyManager.isInEmergencySmsMode();
+ boolean emergencyCallbackMode = false;
+ boolean emergencySmsMode = false;
+ PackageManager pm = mContext.getPackageManager();
+ if (pm.hasSystemFeature(PackageManager.FEATURE_TELEPHONY_CALLING)) {
+ emergencyCallbackMode = mTelephonyManager.getEmergencyCallbackMode();
+ }
+ if (pm.hasSystemFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING)) {
+ emergencySmsMode = mTelephonyManager.isInEmergencySmsMode();
+ }
+ return mIsInEmergencyCall
+ || isInExtensionTime
+ || emergencyCallbackMode
+ || emergencySmsMode;
+ }
}
- return mIsInEmergencyCall
- || isInExtensionTime
- || emergencyCallbackMode
- || emergencySmsMode;
}
private class EmergencyCallTelephonyCallback extends TelephonyCallback implements
diff --git a/services/core/java/com/android/server/locksettings/LockSettingsService.java b/services/core/java/com/android/server/locksettings/LockSettingsService.java
index 07d1a65850e3..da01d510ab8e 100644
--- a/services/core/java/com/android/server/locksettings/LockSettingsService.java
+++ b/services/core/java/com/android/server/locksettings/LockSettingsService.java
@@ -183,7 +183,6 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.Enumeration;
-import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
@@ -340,8 +339,6 @@ public class LockSettingsService extends ILockSettings.Stub {
private static final int[] SYSTEM_CREDENTIAL_UIDS = {
Process.VPN_UID, Process.ROOT_UID, Process.SYSTEM_UID};
- private HashMap<UserHandle, UserManager> mUserManagerCache = new HashMap<>();
-
private final CopyOnWriteArrayList<LockSettingsStateListener> mLockSettingsStateListeners =
new CopyOnWriteArrayList<>();
@@ -455,7 +452,7 @@ public class LockSettingsService extends ILockSettings.Stub {
private void tieProfileLockIfNecessary(int profileUserId,
LockscreenCredential profileUserPassword) {
// Only for profiles that shares credential with parent
- if (!isCredentialSharableWithParent(profileUserId)) {
+ if (!isCredentialShareableWithParent(profileUserId)) {
return;
}
// Do not tie profile when separate challenge is enabled
@@ -850,7 +847,7 @@ public class LockSettingsService extends ILockSettings.Stub {
if (android.os.Flags.allowPrivateProfile()
&& android.multiuser.Flags.enableBiometricsToUnlockPrivateSpace()
&& android.multiuser.Flags.enablePrivateSpaceFeatures()) {
- UserProperties userProperties = mUserManager.getUserProperties(UserHandle.of(userId));
+ UserProperties userProperties = getUserProperties(userId);
if (userProperties != null && userProperties.getAllowStoppingUserWithDelayedLocking()) {
return;
}
@@ -963,18 +960,12 @@ public class LockSettingsService extends ILockSettings.Stub {
&& android.multiuser.Flags.enablePrivateSpaceFeatures()
&& android.multiuser.Flags.enableBiometricsToUnlockPrivateSpace()) {
mHandler.post(() -> {
- try {
- UserProperties userProperties =
- mUserManager.getUserProperties(UserHandle.of(userId));
- if (userProperties != null && userProperties
- .getAllowStoppingUserWithDelayedLocking()) {
- int strongAuthRequired = LockPatternUtils.StrongAuthTracker
- .getDefaultFlags(mContext);
- requireStrongAuth(strongAuthRequired, userId);
- }
- } catch (IllegalArgumentException e) {
- Slogf.d(TAG, "User %d does not exist or has been removed",
- userId);
+ UserProperties userProperties = getUserProperties(userId);
+ if (userProperties != null && userProperties
+ .getAllowStoppingUserWithDelayedLocking()) {
+ int strongAuthRequired = LockPatternUtils.StrongAuthTracker
+ .getDefaultFlags(mContext);
+ requireStrongAuth(strongAuthRequired, userId);
}
});
}
@@ -1070,7 +1061,7 @@ public class LockSettingsService extends ILockSettings.Stub {
final int userCount = users.size();
for (int i = 0; i < userCount; i++) {
UserInfo user = users.get(i);
- if (isCredentialSharableWithParent(user.id)
+ if (isCredentialShareableWithParent(user.id)
&& !getSeparateProfileChallengeEnabledInternal(user.id)) {
success &= SyntheticPasswordCrypto.migrateLockSettingsKey(
PROFILE_KEY_NAME_ENCRYPT + user.id);
@@ -1639,7 +1630,7 @@ public class LockSettingsService extends ILockSettings.Stub {
Thread.currentThread().interrupt();
}
- if (isCredentialSharableWithParent(userId)) {
+ if (isCredentialShareableWithParent(userId)) {
if (!hasUnifiedChallenge(userId)) {
mBiometricDeferredQueue.processPendingLockoutResets();
}
@@ -1648,7 +1639,7 @@ public class LockSettingsService extends ILockSettings.Stub {
for (UserInfo profile : mUserManager.getProfiles(userId)) {
if (profile.id == userId) continue;
- if (!isCredentialSharableWithParent(profile.id)) continue;
+ if (!isCredentialShareableWithParent(profile.id)) continue;
if (hasUnifiedChallenge(profile.id)) {
if (mUserManager.isUserRunning(profile.id)) {
@@ -1685,7 +1676,7 @@ public class LockSettingsService extends ILockSettings.Stub {
}
private Map<Integer, LockscreenCredential> getDecryptedPasswordsForAllTiedProfiles(int userId) {
- if (isCredentialSharableWithParent(userId)) {
+ if (isCredentialShareableWithParent(userId)) {
return null;
}
Map<Integer, LockscreenCredential> result = new ArrayMap<>();
@@ -1693,7 +1684,7 @@ public class LockSettingsService extends ILockSettings.Stub {
final int size = profiles.size();
for (int i = 0; i < size; i++) {
final UserInfo profile = profiles.get(i);
- if (!isCredentialSharableWithParent(profile.id)) {
+ if (!isCredentialShareableWithParent(profile.id)) {
continue;
}
final int profileUserId = profile.id;
@@ -1728,7 +1719,7 @@ public class LockSettingsService extends ILockSettings.Stub {
*/
private void synchronizeUnifiedChallengeForProfiles(int userId,
Map<Integer, LockscreenCredential> profilePasswordMap) {
- if (isCredentialSharableWithParent(userId)) {
+ if (isCredentialShareableWithParent(userId)) {
return;
}
final boolean isSecure = isUserSecure(userId);
@@ -1737,7 +1728,7 @@ public class LockSettingsService extends ILockSettings.Stub {
for (int i = 0; i < size; i++) {
final UserInfo profile = profiles.get(i);
final int profileUserId = profile.id;
- if (isCredentialSharableWithParent(profileUserId)) {
+ if (isCredentialShareableWithParent(profileUserId)) {
if (getSeparateProfileChallengeEnabledInternal(profileUserId)) {
continue;
}
@@ -1764,7 +1755,7 @@ public class LockSettingsService extends ILockSettings.Stub {
}
private boolean isProfileWithUnifiedLock(int userId) {
- return isCredentialSharableWithParent(userId)
+ return isCredentialShareableWithParent(userId)
&& !getSeparateProfileChallengeEnabledInternal(userId);
}
@@ -1886,7 +1877,7 @@ public class LockSettingsService extends ILockSettings.Stub {
setSeparateProfileChallengeEnabledLocked(userId, true, /* unused */ null);
notifyPasswordChanged(credential, userId);
}
- if (isCredentialSharableWithParent(userId)) {
+ if (isCredentialShareableWithParent(userId)) {
// Make sure the profile doesn't get locked straight after setting challenge.
setDeviceUnlockedForUser(userId);
}
@@ -1921,9 +1912,8 @@ public class LockSettingsService extends ILockSettings.Stub {
* setting a new credential where there was none, updates the strong auth state for
* {@param userId} to <tt>STRONG_AUTH_NOT_REQUIRED</tt>.
*
- * @param savedCredential if the user is a profile with
- * {@link UserManager#isCredentialSharableWithParent()} with unified challenge and
- * savedCredential is empty, LSS will try to re-derive the profile password internally.
+ * @param savedCredential if the user is a profile with unified challenge and savedCredential is
+ * empty, LSS will try to re-derive the profile password internally.
* TODO (b/80170828): Fix this so profile password is always passed in.
* @param isLockTiedToParent is {@code true} if {@code userId} is a profile and its new
* credentials are being tied to its parent's credentials.
@@ -2051,25 +2041,14 @@ public class LockSettingsService extends ILockSettings.Stub {
return mInjector.getDevicePolicyManager().getPasswordHistoryLength(null, userId);
}
- private UserManager getUserManagerFromCache(int userId) {
- UserHandle userHandle = UserHandle.of(userId);
- if (mUserManagerCache.containsKey(userHandle)) {
- return mUserManagerCache.get(userHandle);
- }
-
- try {
- Context userContext = mContext.createPackageContextAsUser("system", 0, userHandle);
- UserManager userManager = userContext.getSystemService(UserManager.class);
- mUserManagerCache.put(userHandle, userManager);
- return userManager;
- } catch (PackageManager.NameNotFoundException e) {
- throw new RuntimeException("Failed to create context for user " + userHandle, e);
- }
+ private @Nullable UserProperties getUserProperties(int userId) {
+ return mInjector.getUserManagerInternal().getUserProperties(userId);
}
@VisibleForTesting /** Note: this method is overridden in unit tests */
- protected boolean isCredentialSharableWithParent(int userId) {
- return getUserManagerFromCache(userId).isCredentialSharableWithParent();
+ protected boolean isCredentialShareableWithParent(int userId) {
+ UserProperties props = getUserProperties(userId);
+ return props != null && props.isCredentialShareableWithParent();
}
/** Register the given WeakEscrowTokenRemovedListener. */
@@ -2305,7 +2284,7 @@ public class LockSettingsService extends ILockSettings.Stub {
final List<UserInfo> profiles = mUserManager.getProfiles(userId);
for (UserInfo pi : profiles) {
// Unlock profile which shares credential with parent with unified lock
- if (isCredentialSharableWithParent(pi.id)
+ if (isCredentialShareableWithParent(pi.id)
&& !getSeparateProfileChallengeEnabledInternal(pi.id)
&& mStorage.hasChildProfileLock(pi.id)) {
try {
@@ -3087,7 +3066,7 @@ public class LockSettingsService extends ILockSettings.Stub {
activateEscrowTokens(sp, userId);
- if (isCredentialSharableWithParent(userId)) {
+ if (isCredentialShareableWithParent(userId)) {
if (getSeparateProfileChallengeEnabledInternal(userId)) {
setDeviceUnlockedForUser(userId);
} else {
@@ -3260,8 +3239,7 @@ public class LockSettingsService extends ILockSettings.Stub {
* Returns a fixed pseudorandom byte string derived from the user's synthetic password.
* This is used to salt the password history hash to protect the hash against offline
* bruteforcing, since rederiving this value requires a successful authentication.
- * If user is a profile with {@link UserManager#isCredentialSharableWithParent()} true and with
- * unified challenge, currentCredential is ignored.
+ * If user is a profile with unified challenge, currentCredential is ignored.
*/
@Override
public byte[] getHashFactor(LockscreenCredential currentCredential, int userId) {
diff --git a/services/core/java/com/android/server/media/LegacyDeviceRouteController.java b/services/core/java/com/android/server/media/LegacyDeviceRouteController.java
index 65b0ad0d61a0..1e8ebca7f336 100644
--- a/services/core/java/com/android/server/media/LegacyDeviceRouteController.java
+++ b/services/core/java/com/android/server/media/LegacyDeviceRouteController.java
@@ -39,6 +39,7 @@ import android.os.UserHandle;
import android.util.Slog;
import com.android.internal.R;
+import com.android.media.flags.Flags;
import java.util.Collections;
import java.util.List;
@@ -123,7 +124,9 @@ import java.util.Objects;
@Override
public synchronized List<MediaRoute2Info> getAvailableRoutes() {
- return Collections.emptyList();
+ return Flags.enableFixForEmptySystemRoutesCrash()
+ ? List.of(mDeviceRoute)
+ : Collections.emptyList();
}
@Override
diff --git a/services/core/java/com/android/server/media/MediaSessionService.java b/services/core/java/com/android/server/media/MediaSessionService.java
index c174451e8f5b..5d571de2ce54 100644
--- a/services/core/java/com/android/server/media/MediaSessionService.java
+++ b/services/core/java/com/android/server/media/MediaSessionService.java
@@ -3269,13 +3269,21 @@ public class MediaSessionService extends SystemService implements Monitor {
if (!postedNotification.isMediaNotification()) {
return;
}
+ if ((postedNotification.flags & Notification.FLAG_FOREGROUND_SERVICE) == 0) {
+ // Ignore notifications posted without a foreground service.
+ return;
+ }
synchronized (mLock) {
Map<String, StatusBarNotification> notifications = mMediaNotifications.get(uid);
if (notifications == null) {
notifications = new HashMap<>();
mMediaNotifications.put(uid, notifications);
}
- notifications.put(sbn.getKey(), sbn);
+ StatusBarNotification previousSbn = notifications.put(sbn.getKey(), sbn);
+ if (previousSbn != null) {
+ // Only act on the first notification update.
+ return;
+ }
MediaSessionRecordImpl userEngagedRecord =
getUserEngagedMediaSessionRecordForNotification(uid, postedNotification);
if (userEngagedRecord != null) {
diff --git a/services/core/java/com/android/server/media/quality/MediaQualityService.java b/services/core/java/com/android/server/media/quality/MediaQualityService.java
index 0202b554b8aa..a3d9c66c2668 100644
--- a/services/core/java/com/android/server/media/quality/MediaQualityService.java
+++ b/services/core/java/com/android/server/media/quality/MediaQualityService.java
@@ -40,10 +40,13 @@ import android.hardware.tv.mediaquality.PictureParameter;
import android.hardware.tv.mediaquality.PictureParameters;
import android.hardware.tv.mediaquality.SoundParameter;
import android.hardware.tv.mediaquality.SoundParameters;
+import android.hardware.tv.mediaquality.StreamStatus;
import android.hardware.tv.mediaquality.VendorParamCapability;
+import android.media.quality.ActiveProcessingPicture;
import android.media.quality.AmbientBacklightEvent;
import android.media.quality.AmbientBacklightMetadata;
import android.media.quality.AmbientBacklightSettings;
+import android.media.quality.IActiveProcessingPictureListener;
import android.media.quality.IAmbientBacklightCallback;
import android.media.quality.IMediaQualityManager;
import android.media.quality.IPictureProfileCallback;
@@ -71,6 +74,8 @@ import android.util.Log;
import android.util.Pair;
import android.util.Slog;
import android.util.SparseArray;
+import android.view.SurfaceControlActivePicture;
+import android.view.SurfaceControlActivePictureListener;
import com.android.internal.annotations.GuardedBy;
import com.android.server.SystemService;
@@ -104,6 +109,7 @@ public class MediaQualityService extends SystemService {
private final MediaQualityDbHelper mMediaQualityDbHelper;
private final BiMap<Long, String> mPictureProfileTempIdMap;
private final BiMap<Long, String> mSoundProfileTempIdMap;
+ private final Map<String, Long> mPackageDefaultPictureProfileHandleMap = new HashMap<>();
private IMediaQuality mMediaQuality;
private PictureProfileAdjustmentListenerImpl mPictureProfileAdjListener;
private SoundProfileAdjustmentListenerImpl mSoundProfileAdjListener;
@@ -119,6 +125,7 @@ public class MediaQualityService extends SystemService {
private MqManagerNotifier mMqManagerNotifier;
private MqDatabaseUtils mMqDatabaseUtils;
private Handler mHandler;
+ private SurfaceControlActivePictureListener mSurfaceControlActivePictureListener;
// A global lock for picture profile objects.
private final Object mPictureProfileLock = new Object();
@@ -129,6 +136,9 @@ public class MediaQualityService extends SystemService {
// A global lock for ambient backlight objects.
private final Object mAmbientBacklightLock = new Object();
+ private final Map<Long, PictureProfile> mHandleToPictureProfile = new HashMap<>();
+ private final BiMap<Long, Long> mCurrentPictureHandleToOriginal = new BiMap<>();
+
public MediaQualityService(Context context) {
super(context);
mContext = context;
@@ -160,6 +170,7 @@ public class MediaQualityService extends SystemService {
soundProfilePrefs, Context.MODE_PRIVATE);
}
+ @GuardedBy("mPictureProfileLock")
@Override
public void onStart() {
IBinder binder = ServiceManager.getService(IMediaQuality.DESCRIPTOR + "/default");
@@ -169,6 +180,14 @@ public class MediaQualityService extends SystemService {
}
Slogf.d(TAG, "Binder is not null");
+ mSurfaceControlActivePictureListener = new SurfaceControlActivePictureListener() {
+ @Override
+ public void onActivePicturesChanged(SurfaceControlActivePicture[] activePictures) {
+ handleOnActivePicturesChanged(activePictures);
+ }
+ };
+ mSurfaceControlActivePictureListener.startListening(); // TODO: stop listening
+
mMediaQuality = IMediaQuality.Stub.asInterface(binder);
if (mMediaQuality != null) {
try {
@@ -180,6 +199,22 @@ public class MediaQualityService extends SystemService {
mMediaQuality.setPictureProfileAdjustmentListener(mPictureProfileAdjListener);
mMediaQuality.setSoundProfileAdjustmentListener(mSoundProfileAdjListener);
+ synchronized (mPictureProfileLock) {
+ String selection = BaseParameters.PARAMETER_TYPE + " = ? AND "
+ + BaseParameters.PARAMETER_NAME + " = ?";
+ String[] selectionArguments = {
+ Integer.toString(PictureProfile.TYPE_SYSTEM),
+ PictureProfile.NAME_DEFAULT
+ };
+ List<PictureProfile> packageDefaultPictureProfiles =
+ mMqDatabaseUtils.getPictureProfilesBasedOnConditions(MediaQualityUtils
+ .getMediaProfileColumns(false), selection, selectionArguments);
+ mPackageDefaultPictureProfileHandleMap.clear();
+ for (PictureProfile profile : packageDefaultPictureProfiles) {
+ mPackageDefaultPictureProfileHandleMap.put(
+ profile.getPackageName(), profile.getHandle().getId());
+ }
+ }
} catch (RemoteException e) {
Slog.e(TAG, "Failed to set ambient backlight detector callback", e);
}
@@ -188,6 +223,54 @@ public class MediaQualityService extends SystemService {
publishBinderService(Context.MEDIA_QUALITY_SERVICE, new BinderService());
}
+ private void handleOnActivePicturesChanged(SurfaceControlActivePicture[] scActivePictures) {
+ if (DEBUG) {
+ Slog.d(TAG, "handleOnActivePicturesChanged");
+ }
+ synchronized (mPictureProfileLock) {
+ // TODO handle other users
+ UserState userState = getOrCreateUserState(UserHandle.USER_SYSTEM);
+ int n = userState.mActiveProcessingPictureCallbackList.beginBroadcast();
+ for (int i = 0; i < n; ++i) {
+ try {
+ IActiveProcessingPictureListener l = userState
+ .mActiveProcessingPictureCallbackList
+ .getBroadcastItem(i);
+ ActiveProcessingPictureListenerInfo info =
+ userState.mActiveProcessingPictureListenerMap.get(l);
+ if (info == null) {
+ continue;
+ }
+ int uid = info.mUid;
+ boolean hasGlobalPermission = mContext.checkPermission(
+ android.Manifest.permission.MANAGE_GLOBAL_PICTURE_QUALITY_SERVICE,
+ info.mPid, uid)
+ == PackageManager.PERMISSION_GRANTED;
+ List<ActiveProcessingPicture> aps = new ArrayList<>();
+ for (SurfaceControlActivePicture scap : scActivePictures) {
+ if (!hasGlobalPermission && scap.getOwnerUid() != uid) {
+ // should not receive the event
+ continue;
+ }
+ String profileId = mPictureProfileTempIdMap.getValue(
+ scap.getPictureProfileHandle().getId());
+ if (profileId == null) {
+ continue;
+ }
+ aps.add(new ActiveProcessingPicture(
+ scap.getLayerId(), profileId, scap.getOwnerUid() != uid));
+
+ }
+
+ l.onActiveProcessingPicturesChanged(aps);
+ } catch (RemoteException e) {
+ Slog.e(TAG, "failed to report added AD service to callback", e);
+ }
+ }
+ userState.mActiveProcessingPictureCallbackList.finishBroadcast();
+ }
+ }
+
private final class BinderService extends IMediaQualityManager.Stub {
@GuardedBy("mPictureProfileLock")
@@ -222,6 +305,10 @@ public class MediaQualityService extends SystemService {
pp.setProfileId(value);
mMqManagerNotifier.notifyOnPictureProfileAdded(value, pp,
Binder.getCallingUid(), Binder.getCallingPid());
+ if (isPackageDefaultPictureProfile(pp)) {
+ mPackageDefaultPictureProfileHandleMap.put(
+ pp.getPackageName(), pp.getHandle().getId());
+ }
}
}
);
@@ -230,21 +317,28 @@ public class MediaQualityService extends SystemService {
@GuardedBy("mPictureProfileLock")
@Override
public void updatePictureProfile(String id, PictureProfile pp, int userId) {
- Long dbId = mPictureProfileTempIdMap.getKey(id);
- if (!hasPermissionToUpdatePictureProfile(dbId, pp)) {
- mMqManagerNotifier.notifyOnPictureProfileError(id,
- PictureProfile.ERROR_NO_PERMISSION,
- Binder.getCallingUid(), Binder.getCallingPid());
- }
- synchronized (mPictureProfileLock) {
- ContentValues values = MediaQualityUtils.getContentValues(dbId,
- pp.getProfileType(),
- pp.getName(),
- pp.getPackageName(),
- pp.getInputId(),
- pp.getParameters());
- updateDatabaseOnPictureProfileAndNotifyManagerAndHal(values, pp.getParameters());
- }
+ mHandler.post(() -> {
+ Long dbId = mPictureProfileTempIdMap.getKey(id);
+ if (!hasPermissionToUpdatePictureProfile(dbId, pp)) {
+ mMqManagerNotifier.notifyOnPictureProfileError(id,
+ PictureProfile.ERROR_NO_PERMISSION,
+ Binder.getCallingUid(), Binder.getCallingPid());
+ }
+ synchronized (mPictureProfileLock) {
+ ContentValues values = MediaQualityUtils.getContentValues(dbId,
+ pp.getProfileType(),
+ pp.getName(),
+ pp.getPackageName(),
+ pp.getInputId(),
+ pp.getParameters());
+ updateDatabaseOnPictureProfileAndNotifyManagerAndHal(values,
+ pp.getParameters());
+ if (isPackageDefaultPictureProfile(pp)) {
+ mPackageDefaultPictureProfileHandleMap.put(
+ pp.getPackageName(), pp.getHandle().getId());
+ }
+ }
+ });
}
private boolean hasPermissionToUpdatePictureProfile(Long dbId, PictureProfile toUpdate) {
@@ -258,35 +352,42 @@ public class MediaQualityService extends SystemService {
@GuardedBy("mPictureProfileLock")
@Override
public void removePictureProfile(String id, int userId) {
- synchronized (mPictureProfileLock) {
- Long dbId = mPictureProfileTempIdMap.getKey(id);
+ mHandler.post(() -> {
+ synchronized (mPictureProfileLock) {
+ Long dbId = mPictureProfileTempIdMap.getKey(id);
- PictureProfile toDelete = mMqDatabaseUtils.getPictureProfile(dbId);
- if (!hasPermissionToRemovePictureProfile(toDelete)) {
- mMqManagerNotifier.notifyOnPictureProfileError(id,
- PictureProfile.ERROR_NO_PERMISSION,
- Binder.getCallingUid(), Binder.getCallingPid());
- }
-
- if (dbId != null) {
- SQLiteDatabase db = mMediaQualityDbHelper.getWritableDatabase();
- String selection = BaseParameters.PARAMETER_ID + " = ?";
- String[] selectionArgs = {Long.toString(dbId)};
- int result = db.delete(mMediaQualityDbHelper.PICTURE_QUALITY_TABLE_NAME,
- selection, selectionArgs);
- if (result == 0) {
+ PictureProfile toDelete = mMqDatabaseUtils.getPictureProfile(dbId);
+ if (!hasPermissionToRemovePictureProfile(toDelete)) {
mMqManagerNotifier.notifyOnPictureProfileError(id,
- PictureProfile.ERROR_INVALID_ARGUMENT,
+ PictureProfile.ERROR_NO_PERMISSION,
Binder.getCallingUid(), Binder.getCallingPid());
- } else {
- mMqManagerNotifier.notifyOnPictureProfileRemoved(
- mPictureProfileTempIdMap.getValue(dbId), toDelete,
- Binder.getCallingUid(), Binder.getCallingPid());
- mPictureProfileTempIdMap.remove(dbId);
- mHalNotifier.notifyHalOnPictureProfileChange(dbId, null);
+ }
+
+ if (dbId != null) {
+ SQLiteDatabase db = mMediaQualityDbHelper.getWritableDatabase();
+ String selection = BaseParameters.PARAMETER_ID + " = ?";
+ String[] selectionArgs = {Long.toString(dbId)};
+ int result = db.delete(mMediaQualityDbHelper.PICTURE_QUALITY_TABLE_NAME,
+ selection, selectionArgs);
+ if (result == 0) {
+ mMqManagerNotifier.notifyOnPictureProfileError(id,
+ PictureProfile.ERROR_INVALID_ARGUMENT,
+ Binder.getCallingUid(), Binder.getCallingPid());
+ } else {
+ mMqManagerNotifier.notifyOnPictureProfileRemoved(
+ mPictureProfileTempIdMap.getValue(dbId), toDelete,
+ Binder.getCallingUid(), Binder.getCallingPid());
+ mPictureProfileTempIdMap.remove(dbId);
+ mHalNotifier.notifyHalOnPictureProfileChange(dbId, null);
+ }
+ }
+
+ if (isPackageDefaultPictureProfile(toDelete)) {
+ mPackageDefaultPictureProfileHandleMap.remove(
+ toDelete.getPackageName());
}
}
- }
+ });
}
private boolean hasPermissionToRemovePictureProfile(PictureProfile toDelete) {
@@ -368,13 +469,19 @@ public class MediaQualityService extends SystemService {
Binder.getCallingUid(), Binder.getCallingPid());
}
- PictureProfile pictureProfile = mMqDatabaseUtils.getPictureProfile(
- mPictureProfileTempIdMap.getKey(profileId));
+ Long longId = mPictureProfileTempIdMap.getKey(profileId);
+ if (longId == null) {
+ return false;
+ }
+ PictureProfile pictureProfile = mMqDatabaseUtils.getPictureProfile(longId);
PersistableBundle params = pictureProfile.getParameters();
try {
if (mMediaQuality != null) {
PictureParameters pp = new PictureParameters();
+ // put ID in params for profile update in HAL
+ // TODO: update HAL API for this case
+ params.putLong(BaseParameters.PARAMETER_ID, longId);
PictureParameter[] pictureParameters = MediaQualityUtils
.convertPersistableBundleToPictureParameterList(params);
@@ -429,6 +536,72 @@ public class MediaQualityService extends SystemService {
return toReturn;
}
+ @GuardedBy("mPictureProfileLock")
+ @Override
+ public long getPictureProfileHandleValue(String id, int userId) {
+ synchronized (mPictureProfileLock) {
+ Long value = mPictureProfileTempIdMap.getKey(id);
+ return value != null ? value : -1;
+ }
+ }
+
+ @GuardedBy("mPictureProfileLock")
+ @Override
+ public long getDefaultPictureProfileHandleValue(int userId) {
+ synchronized (mPictureProfileLock) {
+ String packageName = getPackageOfCallingUid();
+ Long value = null;
+ if (packageName != null) {
+ value = mPackageDefaultPictureProfileHandleMap.get(packageName);
+ }
+ return value != null ? value : -1;
+ }
+ }
+
+ @GuardedBy("mPictureProfileLock")
+ @Override
+ public void notifyPictureProfileHandleSelection(long handle, int userId) {
+ PictureProfile profile = mMqDatabaseUtils.getPictureProfile(handle);
+ if (profile != null) {
+ mHalNotifier.notifyHalOnPictureProfileChange(handle, profile.getParameters());
+ }
+ }
+
+ public long getPictureProfileForTvInput(String inputId, int userId) {
+ // TODO: cache profiles
+ if (!hasGlobalPictureQualityServicePermission()) {
+ mMqManagerNotifier.notifyOnPictureProfileError(null,
+ PictureProfile.ERROR_NO_PERMISSION,
+ Binder.getCallingUid(), Binder.getCallingPid());
+ }
+ String[] columns = {BaseParameters.PARAMETER_ID};
+ String selection = BaseParameters.PARAMETER_TYPE + " = ? AND "
+ + BaseParameters.PARAMETER_NAME + " = ? AND "
+ + BaseParameters.PARAMETER_INPUT_ID + " = ?";
+ String[] selectionArguments = {
+ Integer.toString(PictureProfile.TYPE_SYSTEM),
+ PictureProfile.NAME_DEFAULT,
+ inputId
+ };
+ synchronized (mPictureProfileLock) {
+ try (Cursor cursor = mMqDatabaseUtils.getCursorAfterQuerying(
+ mMediaQualityDbHelper.PICTURE_QUALITY_TABLE_NAME,
+ columns, selection, selectionArguments)) {
+ int count = cursor.getCount();
+ if (count == 0) {
+ return -1;
+ }
+ long handle = -1;
+ cursor.moveToFirst();
+ int colIndex = cursor.getColumnIndex(BaseParameters.PARAMETER_ID);
+ if (colIndex != -1) {
+ handle = cursor.getLong(colIndex);
+ }
+ return handle;
+ }
+ }
+ }
+
@GuardedBy("mSoundProfileLock")
@Override
public List<SoundProfileHandle> getSoundProfileHandle(String[] ids, int userId) {
@@ -448,56 +621,60 @@ public class MediaQualityService extends SystemService {
@GuardedBy("mSoundProfileLock")
@Override
- public SoundProfile createSoundProfile(SoundProfile sp, int userId) {
- if ((sp.getPackageName() != null && !sp.getPackageName().isEmpty()
- && !incomingPackageEqualsCallingUidPackage(sp.getPackageName()))
- && !hasGlobalSoundQualityServicePermission()) {
- mMqManagerNotifier.notifyOnSoundProfileError(null, SoundProfile.ERROR_NO_PERMISSION,
- Binder.getCallingUid(), Binder.getCallingPid());
- }
-
- synchronized (mSoundProfileLock) {
- SQLiteDatabase db = mMediaQualityDbHelper.getWritableDatabase();
+ public void createSoundProfile(SoundProfile sp, int userId) {
+ mHandler.post(() -> {
+ if ((sp.getPackageName() != null && !sp.getPackageName().isEmpty()
+ && !incomingPackageEqualsCallingUidPackage(sp.getPackageName()))
+ && !hasGlobalSoundQualityServicePermission()) {
+ mMqManagerNotifier.notifyOnSoundProfileError(null, SoundProfile.ERROR_NO_PERMISSION,
+ Binder.getCallingUid(), Binder.getCallingPid());
+ }
- ContentValues values = MediaQualityUtils.getContentValues(null,
- sp.getProfileType(),
- sp.getName(),
- sp.getPackageName() == null || sp.getPackageName().isEmpty()
- ? getPackageOfCallingUid() : sp.getPackageName(),
- sp.getInputId(),
- sp.getParameters());
+ synchronized (mSoundProfileLock) {
+ SQLiteDatabase db = mMediaQualityDbHelper.getWritableDatabase();
- // id is auto-generated by SQLite upon successful insertion of row
- Long id = db.insert(mMediaQualityDbHelper.SOUND_QUALITY_TABLE_NAME,
- null, values);
- MediaQualityUtils.populateTempIdMap(mSoundProfileTempIdMap, id);
- String value = mSoundProfileTempIdMap.getValue(id);
- sp.setProfileId(value);
- mMqManagerNotifier.notifyOnSoundProfileAdded(value, sp, Binder.getCallingUid(),
- Binder.getCallingPid());
- return sp;
- }
+ ContentValues values = MediaQualityUtils.getContentValues(null,
+ sp.getProfileType(),
+ sp.getName(),
+ sp.getPackageName() == null || sp.getPackageName().isEmpty()
+ ? getPackageOfCallingUid() : sp.getPackageName(),
+ sp.getInputId(),
+ sp.getParameters());
+
+ // id is auto-generated by SQLite upon successful insertion of row
+ Long id = db.insert(mMediaQualityDbHelper.SOUND_QUALITY_TABLE_NAME,
+ null, values);
+ MediaQualityUtils.populateTempIdMap(mSoundProfileTempIdMap, id);
+ String value = mSoundProfileTempIdMap.getValue(id);
+ sp.setProfileId(value);
+ mMqManagerNotifier.notifyOnSoundProfileAdded(value, sp, Binder.getCallingUid(),
+ Binder.getCallingPid());
+ }
+ });
}
@GuardedBy("mSoundProfileLock")
@Override
public void updateSoundProfile(String id, SoundProfile sp, int userId) {
- Long dbId = mSoundProfileTempIdMap.getKey(id);
- if (!hasPermissionToUpdateSoundProfile(dbId, sp)) {
- mMqManagerNotifier.notifyOnSoundProfileError(id, SoundProfile.ERROR_NO_PERMISSION,
- Binder.getCallingUid(), Binder.getCallingPid());
- }
+ mHandler.post(() -> {
+ Long dbId = mSoundProfileTempIdMap.getKey(id);
+ if (!hasPermissionToUpdateSoundProfile(dbId, sp)) {
+ mMqManagerNotifier.notifyOnSoundProfileError(id,
+ SoundProfile.ERROR_NO_PERMISSION,
+ Binder.getCallingUid(), Binder.getCallingPid());
+ }
- synchronized (mSoundProfileLock) {
- ContentValues values = MediaQualityUtils.getContentValues(dbId,
- sp.getProfileType(),
- sp.getName(),
- sp.getPackageName(),
- sp.getInputId(),
- sp.getParameters());
+ synchronized (mSoundProfileLock) {
+ ContentValues values = MediaQualityUtils.getContentValues(dbId,
+ sp.getProfileType(),
+ sp.getName(),
+ sp.getPackageName(),
+ sp.getInputId(),
+ sp.getParameters());
- updateDatabaseOnSoundProfileAndNotifyManagerAndHal(values, sp.getParameters());
- }
+ updateDatabaseOnSoundProfileAndNotifyManagerAndHal(values, sp.getParameters());
+ }
+ });
}
private boolean hasPermissionToUpdateSoundProfile(Long dbId, SoundProfile sp) {
@@ -511,34 +688,36 @@ public class MediaQualityService extends SystemService {
@GuardedBy("mSoundProfileLock")
@Override
public void removeSoundProfile(String id, int userId) {
- synchronized (mSoundProfileLock) {
- Long dbId = mSoundProfileTempIdMap.getKey(id);
- SoundProfile toDelete = mMqDatabaseUtils.getSoundProfile(dbId);
- if (!hasPermissionToRemoveSoundProfile(toDelete)) {
- mMqManagerNotifier.notifyOnSoundProfileError(id,
- SoundProfile.ERROR_NO_PERMISSION,
- Binder.getCallingUid(), Binder.getCallingPid());
- }
- if (dbId != null) {
- SQLiteDatabase db = mMediaQualityDbHelper.getWritableDatabase();
- String selection = BaseParameters.PARAMETER_ID + " = ?";
- String[] selectionArgs = {Long.toString(dbId)};
- int result = db.delete(mMediaQualityDbHelper.SOUND_QUALITY_TABLE_NAME,
- selection,
- selectionArgs);
- if (result == 0) {
+ mHandler.post(() -> {
+ synchronized (mSoundProfileLock) {
+ Long dbId = mSoundProfileTempIdMap.getKey(id);
+ SoundProfile toDelete = mMqDatabaseUtils.getSoundProfile(dbId);
+ if (!hasPermissionToRemoveSoundProfile(toDelete)) {
mMqManagerNotifier.notifyOnSoundProfileError(id,
- SoundProfile.ERROR_INVALID_ARGUMENT,
- Binder.getCallingUid(), Binder.getCallingPid());
- } else {
- mMqManagerNotifier.notifyOnSoundProfileRemoved(
- mSoundProfileTempIdMap.getValue(dbId), toDelete,
+ SoundProfile.ERROR_NO_PERMISSION,
Binder.getCallingUid(), Binder.getCallingPid());
- mSoundProfileTempIdMap.remove(dbId);
- mHalNotifier.notifyHalOnSoundProfileChange(dbId, null);
+ }
+ if (dbId != null) {
+ SQLiteDatabase db = mMediaQualityDbHelper.getWritableDatabase();
+ String selection = BaseParameters.PARAMETER_ID + " = ?";
+ String[] selectionArgs = {Long.toString(dbId)};
+ int result = db.delete(mMediaQualityDbHelper.SOUND_QUALITY_TABLE_NAME,
+ selection,
+ selectionArgs);
+ if (result == 0) {
+ mMqManagerNotifier.notifyOnSoundProfileError(id,
+ SoundProfile.ERROR_INVALID_ARGUMENT,
+ Binder.getCallingUid(), Binder.getCallingPid());
+ } else {
+ mMqManagerNotifier.notifyOnSoundProfileRemoved(
+ mSoundProfileTempIdMap.getValue(dbId), toDelete,
+ Binder.getCallingUid(), Binder.getCallingPid());
+ mSoundProfileTempIdMap.remove(dbId);
+ mHalNotifier.notifyHalOnSoundProfileChange(dbId, null);
+ }
}
}
- }
+ });
}
private boolean hasPermissionToRemoveSoundProfile(SoundProfile toDelete) {
@@ -619,16 +798,25 @@ public class MediaQualityService extends SystemService {
Binder.getCallingUid(), Binder.getCallingPid());
}
- SoundProfile soundProfile =
- mMqDatabaseUtils.getSoundProfile(mSoundProfileTempIdMap.getKey(profileId));
+ Long longId = mSoundProfileTempIdMap.getKey(profileId);
+ if (longId == null) {
+ return false;
+ }
+
+ SoundProfile soundProfile = mMqDatabaseUtils.getSoundProfile(longId);
PersistableBundle params = soundProfile.getParameters();
try {
if (mMediaQuality != null) {
+ SoundParameters sp = new SoundParameters();
+ // put ID in params for profile update in HAL
+ // TODO: update HAL API for this case
+ params.putLong(BaseParameters.PARAMETER_ID, longId);
SoundParameter[] soundParameters =
MediaQualityUtils.convertPersistableBundleToSoundParameterList(params);
- SoundParameters sp = new SoundParameters();
+ Parcel parcel = Parcel.obtain();
+ setVendorSoundParameters(sp, parcel, params);
sp.soundParameters = soundParameters;
mMediaQuality.sendDefaultSoundParameters(sp);
@@ -674,21 +862,21 @@ public class MediaQualityService extends SystemService {
}
private boolean hasGlobalPictureQualityServicePermission() {
- return mPackageManager.checkPermission(android.Manifest.permission
- .MANAGE_GLOBAL_PICTURE_QUALITY_SERVICE,
- mContext.getPackageName()) == mPackageManager.PERMISSION_GRANTED;
+ return mContext.checkCallingPermission(
+ android.Manifest.permission.MANAGE_GLOBAL_PICTURE_QUALITY_SERVICE)
+ == PackageManager.PERMISSION_GRANTED;
}
private boolean hasGlobalSoundQualityServicePermission() {
- return mPackageManager.checkPermission(android.Manifest.permission
- .MANAGE_GLOBAL_SOUND_QUALITY_SERVICE,
- mContext.getPackageName()) == mPackageManager.PERMISSION_GRANTED;
+ return mContext.checkCallingPermission(
+ android.Manifest.permission.MANAGE_GLOBAL_SOUND_QUALITY_SERVICE)
+ == PackageManager.PERMISSION_GRANTED;
}
private boolean hasReadColorZonesPermission() {
- return mPackageManager.checkPermission(android.Manifest.permission
- .READ_COLOR_ZONES,
- mContext.getPackageName()) == mPackageManager.PERMISSION_GRANTED;
+ return mContext.checkCallingPermission(
+ android.Manifest.permission.READ_COLOR_ZONES)
+ == PackageManager.PERMISSION_GRANTED;
}
@Override
@@ -712,6 +900,18 @@ public class MediaQualityService extends SystemService {
}
@Override
+ public void registerActiveProcessingPictureListener(
+ final IActiveProcessingPictureListener l) {
+ int callingPid = Binder.getCallingPid();
+ int callingUid = Binder.getCallingUid();
+
+ UserState userState = getOrCreateUserState(Binder.getCallingUid());
+ String packageName = getPackageOfCallingUid();
+ userState.mActiveProcessingPictureListenerMap.put(l,
+ new ActiveProcessingPictureListenerInfo(callingUid, callingPid, packageName));
+ }
+
+ @Override
public void registerAmbientBacklightCallback(IAmbientBacklightCallback callback) {
if (DEBUG) {
Slogf.d(TAG, "registerAmbientBacklightCallback");
@@ -738,6 +938,26 @@ public class MediaQualityService extends SystemService {
}
}
+ public void unregisterAmbientBacklightCallback(IAmbientBacklightCallback callback) {
+ if (DEBUG) {
+ Slogf.d(TAG, "unregisterAmbientBacklightCallback");
+ }
+
+ if (!hasReadColorZonesPermission()) {
+ //TODO: error handling
+ }
+
+ synchronized (mCallbackRecords) {
+ for (AmbientBacklightCallbackRecord record : mCallbackRecords.values()) {
+ if (record.mCallback.asBinder().equals(callback.asBinder())) {
+ record.release();
+ mCallbackRecords.remove(record.mPackageName);
+ return;
+ }
+ }
+ }
+ }
+
@GuardedBy("mAmbientBacklightLock")
@Override
public void setAmbientBacklightSettings(
@@ -849,14 +1069,16 @@ public class MediaQualityService extends SystemService {
@GuardedBy("mPictureProfileLock")
@Override
public void setPictureProfileAllowList(List<String> packages, int userId) {
- if (!hasGlobalPictureQualityServicePermission()) {
- mMqManagerNotifier.notifyOnPictureProfileError(null,
- PictureProfile.ERROR_NO_PERMISSION,
- Binder.getCallingUid(), Binder.getCallingPid());
- }
- SharedPreferences.Editor editor = mPictureProfileSharedPreference.edit();
- editor.putString(ALLOWLIST, String.join(COMMA_DELIMITER, packages));
- editor.commit();
+ mHandler.post(() -> {
+ if (!hasGlobalPictureQualityServicePermission()) {
+ mMqManagerNotifier.notifyOnPictureProfileError(null,
+ PictureProfile.ERROR_NO_PERMISSION,
+ Binder.getCallingUid(), Binder.getCallingPid());
+ }
+ SharedPreferences.Editor editor = mPictureProfileSharedPreference.edit();
+ editor.putString(ALLOWLIST, String.join(COMMA_DELIMITER, packages));
+ editor.commit();
+ });
}
@GuardedBy("mSoundProfileLock")
@@ -877,13 +1099,16 @@ public class MediaQualityService extends SystemService {
@GuardedBy("mSoundProfileLock")
@Override
public void setSoundProfileAllowList(List<String> packages, int userId) {
- if (!hasGlobalSoundQualityServicePermission()) {
- mMqManagerNotifier.notifyOnSoundProfileError(null, SoundProfile.ERROR_NO_PERMISSION,
- Binder.getCallingUid(), Binder.getCallingPid());
- }
- SharedPreferences.Editor editor = mSoundProfileSharedPreference.edit();
- editor.putString(ALLOWLIST, String.join(COMMA_DELIMITER, packages));
- editor.commit();
+ mHandler.post(() -> {
+ if (!hasGlobalSoundQualityServicePermission()) {
+ mMqManagerNotifier.notifyOnSoundProfileError(null,
+ SoundProfile.ERROR_NO_PERMISSION,
+ Binder.getCallingUid(), Binder.getCallingPid());
+ }
+ SharedPreferences.Editor editor = mSoundProfileSharedPreference.edit();
+ editor.putString(ALLOWLIST, String.join(COMMA_DELIMITER, packages));
+ editor.commit();
+ });
}
@Override
@@ -894,22 +1119,24 @@ public class MediaQualityService extends SystemService {
@GuardedBy("mPictureProfileLock")
@Override
public void setAutoPictureQualityEnabled(boolean enabled, int userId) {
- if (!hasGlobalPictureQualityServicePermission()) {
- mMqManagerNotifier.notifyOnPictureProfileError(null,
- PictureProfile.ERROR_NO_PERMISSION,
- Binder.getCallingUid(), Binder.getCallingPid());
- }
- synchronized (mPictureProfileLock) {
- try {
- if (mMediaQuality != null) {
- if (mMediaQuality.isAutoPqSupported()) {
- mMediaQuality.setAutoPqEnabled(enabled);
+ mHandler.post(() -> {
+ if (!hasGlobalPictureQualityServicePermission()) {
+ mMqManagerNotifier.notifyOnPictureProfileError(null,
+ PictureProfile.ERROR_NO_PERMISSION,
+ Binder.getCallingUid(), Binder.getCallingPid());
+ }
+ synchronized (mPictureProfileLock) {
+ try {
+ if (mMediaQuality != null) {
+ if (mMediaQuality.isAutoPqSupported()) {
+ mMediaQuality.setAutoPqEnabled(enabled);
+ }
}
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Failed to set auto picture quality", e);
}
- } catch (RemoteException e) {
- Slog.e(TAG, "Failed to set auto picture quality", e);
}
- }
+ });
}
@GuardedBy("mPictureProfileLock")
@@ -932,22 +1159,24 @@ public class MediaQualityService extends SystemService {
@GuardedBy("mPictureProfileLock")
@Override
public void setSuperResolutionEnabled(boolean enabled, int userId) {
- if (!hasGlobalPictureQualityServicePermission()) {
- mMqManagerNotifier.notifyOnPictureProfileError(null,
- PictureProfile.ERROR_NO_PERMISSION,
- Binder.getCallingUid(), Binder.getCallingPid());
- }
- synchronized (mPictureProfileLock) {
- try {
- if (mMediaQuality != null) {
- if (mMediaQuality.isAutoSrSupported()) {
- mMediaQuality.setAutoSrEnabled(enabled);
+ mHandler.post(() -> {
+ if (!hasGlobalPictureQualityServicePermission()) {
+ mMqManagerNotifier.notifyOnPictureProfileError(null,
+ PictureProfile.ERROR_NO_PERMISSION,
+ Binder.getCallingUid(), Binder.getCallingPid());
+ }
+ synchronized (mPictureProfileLock) {
+ try {
+ if (mMediaQuality != null) {
+ if (mMediaQuality.isAutoSrSupported()) {
+ mMediaQuality.setAutoSrEnabled(enabled);
+ }
}
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Failed to set super resolution", e);
}
- } catch (RemoteException e) {
- Slog.e(TAG, "Failed to set super resolution", e);
}
- }
+ });
}
@GuardedBy("mPictureProfileLock")
@@ -970,22 +1199,25 @@ public class MediaQualityService extends SystemService {
@GuardedBy("mSoundProfileLock")
@Override
public void setAutoSoundQualityEnabled(boolean enabled, int userId) {
- if (!hasGlobalSoundQualityServicePermission()) {
- mMqManagerNotifier.notifyOnSoundProfileError(null, SoundProfile.ERROR_NO_PERMISSION,
- Binder.getCallingUid(), Binder.getCallingPid());
- }
+ mHandler.post(() -> {
+ if (!hasGlobalSoundQualityServicePermission()) {
+ mMqManagerNotifier.notifyOnSoundProfileError(null,
+ SoundProfile.ERROR_NO_PERMISSION,
+ Binder.getCallingUid(), Binder.getCallingPid());
+ }
- synchronized (mSoundProfileLock) {
- try {
- if (mMediaQuality != null) {
- if (mMediaQuality.isAutoAqSupported()) {
- mMediaQuality.setAutoAqEnabled(enabled);
+ synchronized (mSoundProfileLock) {
+ try {
+ if (mMediaQuality != null) {
+ if (mMediaQuality.isAutoAqSupported()) {
+ mMediaQuality.setAutoAqEnabled(enabled);
+ }
}
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Failed to set auto sound quality", e);
}
- } catch (RemoteException e) {
- Slog.e(TAG, "Failed to set auto sound quality", e);
}
- }
+ });
}
@GuardedBy("mSoundProfileLock")
@@ -1086,6 +1318,20 @@ public class MediaQualityService extends SystemService {
}
}
+ private class ActiveProcessingPictureCallbackList extends
+ RemoteCallbackList<IActiveProcessingPictureListener> {
+ @Override
+ public void onCallbackDied(IActiveProcessingPictureListener l) {
+ synchronized (mPictureProfileLock) {
+ for (int i = 0; i < mUserStates.size(); i++) {
+ int userId = mUserStates.keyAt(i);
+ UserState userState = getOrCreateUserState(userId);
+ userState.mActiveProcessingPictureListenerMap.remove(l);
+ }
+ }
+ }
+ }
+
private final class UserState {
// A list of callbacks.
private final MediaQualityManagerPictureProfileCallbackList mPictureProfileCallbacks =
@@ -1094,18 +1340,35 @@ public class MediaQualityService extends SystemService {
private final MediaQualityManagerSoundProfileCallbackList mSoundProfileCallbacks =
new MediaQualityManagerSoundProfileCallbackList();
+ private final ActiveProcessingPictureCallbackList mActiveProcessingPictureCallbackList =
+ new ActiveProcessingPictureCallbackList();
+
private final Map<IPictureProfileCallback, Pair<Integer, Integer>>
mPictureProfileCallbackPidUidMap = new HashMap<>();
private final Map<ISoundProfileCallback, Pair<Integer, Integer>>
mSoundProfileCallbackPidUidMap = new HashMap<>();
+ private final Map<IActiveProcessingPictureListener, ActiveProcessingPictureListenerInfo>
+ mActiveProcessingPictureListenerMap = new HashMap<>();
+
private UserState(Context context, int userId) {
}
}
- @GuardedBy("mUserStateLock")
+ private final class ActiveProcessingPictureListenerInfo {
+ private int mUid;
+ private int mPid;
+ private String mPackageName;
+
+ ActiveProcessingPictureListenerInfo(int uid, int pid, String packageName) {
+ mUid = uid;
+ mPid = pid;
+ mPackageName = packageName;
+ }
+ }
+
private UserState getOrCreateUserState(int userId) {
UserState userState = getUserState(userId);
if (userState == null) {
@@ -1127,15 +1390,17 @@ public class MediaQualityService extends SystemService {
private final class MqDatabaseUtils {
private PictureProfile getPictureProfile(Long dbId) {
+ return getPictureProfile(dbId, false);
+ }
+
+ private PictureProfile getPictureProfile(Long dbId, boolean includeParams) {
String selection = BaseParameters.PARAMETER_ID + " = ?";
String[] selectionArguments = {Long.toString(dbId)};
- try (
- Cursor cursor = getCursorAfterQuerying(
- mMediaQualityDbHelper.PICTURE_QUALITY_TABLE_NAME,
- MediaQualityUtils.getMediaProfileColumns(false), selection,
- selectionArguments)
- ) {
+ try (Cursor cursor = getCursorAfterQuerying(
+ mMediaQualityDbHelper.PICTURE_QUALITY_TABLE_NAME,
+ MediaQualityUtils.getMediaProfileColumns(includeParams), selection,
+ selectionArguments)) {
int count = cursor.getCount();
if (count == 0) {
return null;
@@ -1154,11 +1419,9 @@ public class MediaQualityService extends SystemService {
private List<PictureProfile> getPictureProfilesBasedOnConditions(String[] columns,
String selection, String[] selectionArguments) {
- try (
- Cursor cursor = getCursorAfterQuerying(
- mMediaQualityDbHelper.PICTURE_QUALITY_TABLE_NAME, columns, selection,
- selectionArguments)
- ) {
+ try (Cursor cursor = getCursorAfterQuerying(
+ mMediaQualityDbHelper.PICTURE_QUALITY_TABLE_NAME, columns, selection,
+ selectionArguments)) {
List<PictureProfile> pictureProfiles = new ArrayList<>();
while (cursor.moveToNext()) {
pictureProfiles.add(MediaQualityUtils.convertCursorToPictureProfileWithTempId(
@@ -1172,12 +1435,10 @@ public class MediaQualityService extends SystemService {
String selection = BaseParameters.PARAMETER_ID + " = ?";
String[] selectionArguments = {Long.toString(dbId)};
- try (
- Cursor cursor = mMqDatabaseUtils.getCursorAfterQuerying(
- mMediaQualityDbHelper.SOUND_QUALITY_TABLE_NAME,
- MediaQualityUtils.getMediaProfileColumns(false), selection,
- selectionArguments)
- ) {
+ try (Cursor cursor = mMqDatabaseUtils.getCursorAfterQuerying(
+ mMediaQualityDbHelper.SOUND_QUALITY_TABLE_NAME,
+ MediaQualityUtils.getMediaProfileColumns(false), selection,
+ selectionArguments)) {
int count = cursor.getCount();
if (count == 0) {
return null;
@@ -1196,11 +1457,9 @@ public class MediaQualityService extends SystemService {
private List<SoundProfile> getSoundProfilesBasedOnConditions(String[] columns,
String selection, String[] selectionArguments) {
- try (
- Cursor cursor = mMqDatabaseUtils.getCursorAfterQuerying(
- mMediaQualityDbHelper.SOUND_QUALITY_TABLE_NAME, columns, selection,
- selectionArguments)
- ) {
+ try (Cursor cursor = mMqDatabaseUtils.getCursorAfterQuerying(
+ mMediaQualityDbHelper.SOUND_QUALITY_TABLE_NAME, columns, selection,
+ selectionArguments)) {
List<SoundProfile> soundProfiles = new ArrayList<>();
while (cursor.moveToNext()) {
soundProfiles.add(MediaQualityUtils.convertCursorToSoundProfileWithTempId(
@@ -1416,8 +1675,19 @@ public class MediaQualityService extends SystemService {
private void notifyHalOnPictureProfileChange(Long dbId, PersistableBundle params) {
// TODO: only notify HAL when the profile is active / being used
if (mPpChangedListener != null) {
+ Long currentHandle = mCurrentPictureHandleToOriginal.getKey(dbId);
+ if (currentHandle != null) {
+ // this handle maps to another current profile, skip
+ return;
+ }
try {
- mPpChangedListener.onPictureProfileChanged(convertToHalPictureProfile(dbId,
+ Long idForHal = dbId;
+ Long originalHandle = mCurrentPictureHandleToOriginal.getValue(dbId);
+ if (originalHandle != null) {
+ // the original id is used in HAL because of status change
+ idForHal = originalHandle;
+ }
+ mPpChangedListener.onPictureProfileChanged(convertToHalPictureProfile(idForHal,
params));
} catch (RemoteException e) {
Slog.e(TAG, "Failed to notify HAL on picture profile change.", e);
@@ -1464,6 +1734,11 @@ public class MediaQualityService extends SystemService {
soundParameters.soundParameters =
MediaQualityUtils.convertPersistableBundleToSoundParameterList(params);
+ Parcel parcel = Parcel.obtain();
+ if (params != null) {
+ setVendorSoundParameters(soundParameters, parcel, params);
+ }
+
android.hardware.tv.mediaquality.SoundProfile toReturn =
new android.hardware.tv.mediaquality.SoundProfile();
toReturn.soundProfileId = id;
@@ -1546,9 +1821,116 @@ public class MediaQualityService extends SystemService {
}
@Override
- public void onStreamStatusChanged(long pictureProfileId, byte status)
+ public void onStreamStatusChanged(long profileHandle, byte status)
throws RemoteException {
- // TODO
+ mHandler.post(() -> {
+ synchronized (mPictureProfileLock) {
+ // get from map if exists
+ PictureProfile previous = mHandleToPictureProfile.get(profileHandle);
+ if (previous == null) {
+ // get from DB if not exists
+ previous = mMqDatabaseUtils.getPictureProfile(profileHandle);
+ if (previous == null) {
+ return;
+ }
+ }
+ String[] arr = splitNameAndStatus(previous.getName());
+ String profileName = arr[0];
+ String profileStatus = arr[1];
+ if (status == StreamStatus.HDR10) {
+ if (isHdr(profileStatus)) {
+ // already HDR
+ return;
+ }
+ if (isSdr(profileStatus)) {
+ // SDR to HDR
+ String selection = BaseParameters.PARAMETER_TYPE + " = ? AND "
+ + BaseParameters.PARAMETER_PACKAGE + " = ? AND "
+ + BaseParameters.PARAMETER_NAME + " = ?";
+ String[] selectionArguments = {
+ Integer.toString(previous.getProfileType()),
+ previous.getPackageName(),
+ profileName + "/" + PictureProfile.STATUS_HDR
+ };
+ List<PictureProfile> list =
+ mMqDatabaseUtils.getPictureProfilesBasedOnConditions(
+ MediaQualityUtils.getMediaProfileColumns(true),
+ selection,
+ selectionArguments);
+ if (list.isEmpty()) {
+ // HDR profile not found
+ return;
+ }
+ PictureProfile current = list.get(0);
+ mHandleToPictureProfile.put(profileHandle, current);
+ mCurrentPictureHandleToOriginal.put(
+ current.getHandle().getId(), profileHandle);
+
+ mHalNotifier.notifyHalOnPictureProfileChange(profileHandle,
+ current.getParameters());
+
+ }
+ } else if (status == StreamStatus.SDR) {
+ if (isSdr(profileStatus)) {
+ // already SDR
+ return;
+ }
+ if (isHdr(profileStatus)) {
+ // HDR to SDR
+ String selection = BaseParameters.PARAMETER_TYPE + " = ? AND "
+ + BaseParameters.PARAMETER_PACKAGE + " = ? AND ("
+ + BaseParameters.PARAMETER_NAME + " = ? OR "
+ + BaseParameters.PARAMETER_NAME + " = ?)";
+ String[] selectionArguments = {
+ Integer.toString(previous.getProfileType()),
+ previous.getPackageName(),
+ profileName,
+ profileName + "/" + PictureProfile.STATUS_SDR
+ };
+ List<PictureProfile> list =
+ mMqDatabaseUtils.getPictureProfilesBasedOnConditions(
+ MediaQualityUtils.getMediaProfileColumns(true),
+ selection,
+ selectionArguments);
+ if (list.isEmpty()) {
+ // SDR profile not found
+ return;
+ }
+ PictureProfile current = list.get(0);
+ mHandleToPictureProfile.put(profileHandle, current);
+ mCurrentPictureHandleToOriginal.put(
+ current.getHandle().getId(), profileHandle);
+
+ mHalNotifier.notifyHalOnPictureProfileChange(profileHandle,
+ current.getParameters());
+ }
+ }
+ }
+ });
+
+ }
+
+ @NonNull
+ private String[] splitNameAndStatus(@NonNull String nameAndStatus) {
+ int index = nameAndStatus.lastIndexOf('/');
+ if (index == -1 || index == nameAndStatus.length() - 1) {
+ // no status in the original name
+ return new String[] {nameAndStatus, ""};
+ }
+ return new String[] {
+ nameAndStatus.substring(0, index),
+ nameAndStatus.substring(index + 1)
+ };
+
+ }
+
+ private boolean isSdr(@NonNull String profileStatus) {
+ return profileStatus.equals(PictureProfile.STATUS_SDR)
+ || profileStatus.isEmpty();
+ }
+
+ private boolean isHdr(@NonNull String profileStatus) {
+ return profileStatus.equals(PictureProfile.STATUS_HDR);
}
@Override
@@ -1760,4 +2142,21 @@ public class MediaQualityService extends SystemService {
vendorBundleToByteArray, vendorBundleToByteArray.length);
pictureParameters.vendorPictureParameters.setParcelable(defaultExtension);
}
+
+ private void setVendorSoundParameters(
+ SoundParameters soundParameters,
+ Parcel parcel,
+ PersistableBundle vendorSoundParameters) {
+ vendorSoundParameters.writeToParcel(parcel, 0);
+ byte[] vendorBundleToByteArray = parcel.marshall();
+ DefaultExtension defaultExtension = new DefaultExtension();
+ defaultExtension.bytes = Arrays.copyOf(
+ vendorBundleToByteArray, vendorBundleToByteArray.length);
+ soundParameters.vendorSoundParameters.setParcelable(defaultExtension);
+ }
+
+ private boolean isPackageDefaultPictureProfile(PictureProfile pp) {
+ return pp != null && pp.getProfileType() == PictureProfile.TYPE_SYSTEM &&
+ pp.getName().equals(PictureProfile.NAME_DEFAULT);
+ }
}
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 05aac5587c2c..f58bc982373b 100644
--- a/services/core/java/com/android/server/media/quality/MediaQualityUtils.java
+++ b/services/core/java/com/android/server/media/quality/MediaQualityUtils.java
@@ -1073,80 +1073,127 @@ public final class MediaQualityUtils {
if (params.containsKey(SoundQuality.PARAMETER_BALANCE)) {
soundParams.add(SoundParameter.balance(params.getInt(
SoundQuality.PARAMETER_BALANCE)));
+ params.remove(SoundQuality.PARAMETER_BALANCE);
}
if (params.containsKey(SoundQuality.PARAMETER_BASS)) {
soundParams.add(SoundParameter.bass(params.getInt(SoundQuality.PARAMETER_BASS)));
+ params.remove(SoundQuality.PARAMETER_BASS);
}
if (params.containsKey(SoundQuality.PARAMETER_TREBLE)) {
soundParams.add(SoundParameter.treble(params.getInt(
SoundQuality.PARAMETER_TREBLE)));
+ params.remove(SoundQuality.PARAMETER_TREBLE);
}
if (params.containsKey(SoundQuality.PARAMETER_SURROUND_SOUND)) {
soundParams.add(SoundParameter.surroundSoundEnabled(params.getBoolean(
SoundQuality.PARAMETER_SURROUND_SOUND)));
+ params.remove(SoundQuality.PARAMETER_SURROUND_SOUND);
}
if (params.containsKey(SoundQuality.PARAMETER_SPEAKERS)) {
soundParams.add(SoundParameter.speakersEnabled(params.getBoolean(
SoundQuality.PARAMETER_SPEAKERS)));
+ params.remove(SoundQuality.PARAMETER_SPEAKERS);
}
if (params.containsKey(SoundQuality.PARAMETER_SPEAKERS_DELAY_MILLIS)) {
soundParams.add(SoundParameter.speakersDelayMs(params.getInt(
SoundQuality.PARAMETER_SPEAKERS_DELAY_MILLIS)));
+ params.remove(SoundQuality.PARAMETER_SPEAKERS_DELAY_MILLIS);
}
if (params.containsKey(SoundQuality.PARAMETER_AUTO_VOLUME_CONTROL)) {
soundParams.add(SoundParameter.autoVolumeControl(params.getBoolean(
SoundQuality.PARAMETER_AUTO_VOLUME_CONTROL)));
+ params.remove(SoundQuality.PARAMETER_AUTO_VOLUME_CONTROL);
}
if (params.containsKey(SoundQuality.PARAMETER_DTS_DRC)) {
soundParams.add(SoundParameter.dtsDrc(params.getBoolean(
SoundQuality.PARAMETER_DTS_DRC)));
+ params.remove(SoundQuality.PARAMETER_DTS_DRC);
}
if (params.containsKey(SoundQuality.PARAMETER_DIGITAL_OUTPUT_DELAY_MILLIS)) {
soundParams.add(SoundParameter.surroundSoundEnabled(params.getBoolean(
SoundQuality.PARAMETER_DIGITAL_OUTPUT_DELAY_MILLIS)));
+ params.remove(SoundQuality.PARAMETER_DIGITAL_OUTPUT_DELAY_MILLIS);
}
if (params.containsKey(SoundQuality.PARAMETER_EARC)) {
soundParams.add(SoundParameter.enhancedAudioReturnChannelEnabled(params.getBoolean(
SoundQuality.PARAMETER_EARC)));
+ params.remove(SoundQuality.PARAMETER_EARC);
}
if (params.containsKey(SoundQuality.PARAMETER_DOWN_MIX_MODE)) {
soundParams.add(SoundParameter.downmixMode((byte) params.getInt(
SoundQuality.PARAMETER_DOWN_MIX_MODE)));
+ params.remove(SoundQuality.PARAMETER_DOWN_MIX_MODE);
}
if (params.containsKey(SoundQuality.PARAMETER_SOUND_STYLE)) {
soundParams.add(SoundParameter.soundStyle((byte) params.getInt(
SoundQuality.PARAMETER_SOUND_STYLE)));
+ params.remove(SoundQuality.PARAMETER_SOUND_STYLE);
}
if (params.containsKey(SoundQuality.PARAMETER_DIGITAL_OUTPUT_MODE)) {
soundParams.add(SoundParameter.digitalOutput((byte) params.getInt(
SoundQuality.PARAMETER_DIGITAL_OUTPUT_MODE)));
+ params.remove(SoundQuality.PARAMETER_DIGITAL_OUTPUT_MODE);
}
if (params.containsKey(SoundQuality.PARAMETER_DIALOGUE_ENHANCER)) {
soundParams.add(SoundParameter.dolbyDialogueEnhancer((byte) params.getInt(
SoundQuality.PARAMETER_DIALOGUE_ENHANCER)));
+ params.remove(SoundQuality.PARAMETER_DIALOGUE_ENHANCER);
}
DolbyAudioProcessing dab = new DolbyAudioProcessing();
- dab.soundMode =
- (byte) params.getInt(SoundQuality.PARAMETER_DOLBY_AUDIO_PROCESSING_SOUND_MODE);
- dab.volumeLeveler =
- params.getBoolean(SoundQuality.PARAMETER_DOLBY_AUDIO_PROCESSING_VOLUME_LEVELER);
- dab.surroundVirtualizer = params.getBoolean(
- SoundQuality.PARAMETER_DOLBY_AUDIO_PROCESSING_SURROUND_VIRTUALIZER);
- dab.dolbyAtmos =
- params.getBoolean(SoundQuality.PARAMETER_DOLBY_AUDIO_PROCESSING_DOLBY_ATMOS);
+ if (params.containsKey(SoundQuality.PARAMETER_DOLBY_AUDIO_PROCESSING_SOUND_MODE)) {
+ dab.soundMode =
+ (byte) params.getInt(SoundQuality.PARAMETER_DOLBY_AUDIO_PROCESSING_SOUND_MODE);
+ params.remove(SoundQuality.PARAMETER_DOLBY_AUDIO_PROCESSING_SOUND_MODE);
+ }
+ if (params.containsKey(SoundQuality.PARAMETER_DOLBY_AUDIO_PROCESSING_VOLUME_LEVELER)) {
+ dab.volumeLeveler =
+ params.getBoolean(SoundQuality.PARAMETER_DOLBY_AUDIO_PROCESSING_VOLUME_LEVELER);
+ params.remove(SoundQuality.PARAMETER_DOLBY_AUDIO_PROCESSING_VOLUME_LEVELER);
+ }
+ if (params.containsKey(
+ SoundQuality.PARAMETER_DOLBY_AUDIO_PROCESSING_SURROUND_VIRTUALIZER)) {
+ dab.surroundVirtualizer = params.getBoolean(
+ SoundQuality.PARAMETER_DOLBY_AUDIO_PROCESSING_SURROUND_VIRTUALIZER);
+ params.remove(SoundQuality.PARAMETER_DOLBY_AUDIO_PROCESSING_SURROUND_VIRTUALIZER);
+ }
+ if (params.containsKey(SoundQuality.PARAMETER_DOLBY_AUDIO_PROCESSING_DOLBY_ATMOS)) {
+ dab.dolbyAtmos =
+ params.getBoolean(SoundQuality.PARAMETER_DOLBY_AUDIO_PROCESSING_DOLBY_ATMOS);
+ params.remove(SoundQuality.PARAMETER_DOLBY_AUDIO_PROCESSING_DOLBY_ATMOS);
+ }
soundParams.add(SoundParameter.dolbyAudioProcessing(dab));
DtsVirtualX dts = new DtsVirtualX();
- dts.tbHdx = params.getBoolean(SoundQuality.PARAMETER_DTS_VIRTUAL_X_TBHDX);
- dts.limiter = params.getBoolean(SoundQuality.PARAMETER_DTS_VIRTUAL_X_LIMITER);
- dts.truSurroundX = params.getBoolean(
- SoundQuality.PARAMETER_DTS_VIRTUAL_X_TRU_SURROUND_X);
- dts.truVolumeHd = params.getBoolean(SoundQuality.PARAMETER_DTS_VIRTUAL_X_TRU_VOLUME_HD);
- dts.dialogClarity = params.getBoolean(
- SoundQuality.PARAMETER_DTS_VIRTUAL_X_DIALOG_CLARITY);
- dts.definition = params.getBoolean(SoundQuality.PARAMETER_DTS_VIRTUAL_X_DEFINITION);
- dts.height = params.getBoolean(SoundQuality.PARAMETER_DTS_VIRTUAL_X_HEIGHT);
+ if (params.containsKey(SoundQuality.PARAMETER_DTS_VIRTUAL_X_TBHDX)) {
+ dts.tbHdx = params.getBoolean(SoundQuality.PARAMETER_DTS_VIRTUAL_X_TBHDX);
+ params.remove(SoundQuality.PARAMETER_DTS_VIRTUAL_X_TBHDX);
+ }
+
+ if (params.containsKey(SoundQuality.PARAMETER_DTS_VIRTUAL_X_LIMITER)) {
+ dts.tbHdx = params.getBoolean(SoundQuality.PARAMETER_DTS_VIRTUAL_X_LIMITER);
+ params.remove(SoundQuality.PARAMETER_DTS_VIRTUAL_X_LIMITER);
+ }
+ if (params.containsKey(SoundQuality.PARAMETER_DTS_VIRTUAL_X_TRU_SURROUND_X)) {
+ dts.tbHdx = params.getBoolean(SoundQuality.PARAMETER_DTS_VIRTUAL_X_TRU_SURROUND_X);
+ params.remove(SoundQuality.PARAMETER_DTS_VIRTUAL_X_TRU_SURROUND_X);
+ }
+ if (params.containsKey(SoundQuality.PARAMETER_DTS_VIRTUAL_X_TRU_VOLUME_HD)) {
+ dts.tbHdx = params.getBoolean(SoundQuality.PARAMETER_DTS_VIRTUAL_X_TRU_VOLUME_HD);
+ params.remove(SoundQuality.PARAMETER_DTS_VIRTUAL_X_TRU_VOLUME_HD);
+ }
+ if (params.containsKey(SoundQuality.PARAMETER_DTS_VIRTUAL_X_DIALOG_CLARITY)) {
+ dts.tbHdx = params.getBoolean(SoundQuality.PARAMETER_DTS_VIRTUAL_X_DIALOG_CLARITY);
+ params.remove(SoundQuality.PARAMETER_DTS_VIRTUAL_X_DIALOG_CLARITY);
+ }
+ if (params.containsKey(SoundQuality.PARAMETER_DTS_VIRTUAL_X_DEFINITION)) {
+ dts.tbHdx = params.getBoolean(SoundQuality.PARAMETER_DTS_VIRTUAL_X_DEFINITION);
+ params.remove(SoundQuality.PARAMETER_DTS_VIRTUAL_X_DEFINITION);
+ }
+ if (params.containsKey(SoundQuality.PARAMETER_DTS_VIRTUAL_X_HEIGHT)) {
+ dts.tbHdx = params.getBoolean(SoundQuality.PARAMETER_DTS_VIRTUAL_X_HEIGHT);
+ params.remove(SoundQuality.PARAMETER_DTS_VIRTUAL_X_HEIGHT);
+ }
soundParams.add(SoundParameter.dtsVirtualX(dts));
return soundParams.toArray(new SoundParameter[0]);
@@ -1273,14 +1320,18 @@ public final class MediaQualityUtils {
*/
public static PictureProfile convertCursorToPictureProfileWithTempId(Cursor cursor,
BiMap<Long, String> map) {
+ String tmpId = getTempId(map, cursor);
+ Long dbId = map.getKey(tmpId);
+ PictureProfileHandle handle = dbId == null
+ ? PictureProfileHandle.NONE : new PictureProfileHandle(dbId);
return new PictureProfile(
- getTempId(map, cursor),
+ tmpId,
getType(cursor),
getName(cursor),
getInputId(cursor),
getPackageName(cursor),
jsonToPersistableBundle(getSettingsString(cursor)),
- PictureProfileHandle.NONE
+ handle
);
}
diff --git a/services/core/java/com/android/server/pm/ComputerEngine.java b/services/core/java/com/android/server/pm/ComputerEngine.java
index 7e5ada54c953..38ac0473d1a3 100644
--- a/services/core/java/com/android/server/pm/ComputerEngine.java
+++ b/services/core/java/com/android/server/pm/ComputerEngine.java
@@ -5484,6 +5484,9 @@ public class ComputerEngine implements Computer {
// For update or already installed case, leverage the existing visibility rule.
if (targetAppId != INVALID_UID) {
final Object targetSetting = mSettings.getSettingBase(targetAppId);
+ if (targetSetting == null) {
+ return false;
+ }
if (targetSetting instanceof PackageSetting) {
return !shouldFilterApplication(
(PackageSetting) targetSetting, callingUid, userId);
diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java
index 0ea9af4b9c38..e1e8fc231dda 100644
--- a/services/core/java/com/android/server/pm/UserManagerService.java
+++ b/services/core/java/com/android/server/pm/UserManagerService.java
@@ -1712,15 +1712,19 @@ public class UserManagerService extends IUserManager.Stub {
@Override
public int getCredentialOwnerProfile(@UserIdInt int userId) {
checkManageUsersPermission("get the credential owner");
- if (!mLockPatternUtils.isSeparateProfileChallengeEnabled(userId)) {
- synchronized (mUsersLock) {
- UserInfo profileParent = getProfileParentLU(userId);
- if (profileParent != null) {
- return profileParent.id;
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ if (!mLockPatternUtils.isSeparateProfileChallengeEnabled(userId)) {
+ synchronized (mUsersLock) {
+ UserInfo profileParent = getProfileParentLU(userId);
+ if (profileParent != null) {
+ return profileParent.id;
+ }
}
}
+ } finally {
+ Binder.restoreCallingIdentity(identity);
}
-
return userId;
}
diff --git a/services/core/java/com/android/server/pm/permission/PermissionManagerService.java b/services/core/java/com/android/server/pm/permission/PermissionManagerService.java
index ac19ea12c6a4..fbf81b9accad 100644
--- a/services/core/java/com/android/server/pm/permission/PermissionManagerService.java
+++ b/services/core/java/com/android/server/pm/permission/PermissionManagerService.java
@@ -1541,8 +1541,19 @@ public class PermissionManagerService extends IPermissionManager.Stub {
}
final AttributionSource resolvedAttributionSource =
accessorSource.withPackageName(resolvedAccessorPackageName);
- final int opMode = appOpsManager.unsafeCheckOpRawNoThrow(op,
- resolvedAttributionSource);
+ // Avoid checking the first attr in the chain in some cases for consistency with
+ // checks for data delivery.
+ // In particular, for chains of 2 or more, when skipProxyOperation is true, the
+ // for data delivery implementation does not actually check the first link in the
+ // chain. If the attribution is just a singleReceiverFromDatasource, this
+ // exemption does not apply, since it does not go through proxyOp flow, and the top
+ // of the chain is actually removed above.
+ // Skipping the check avoids situations where preflight checks fail since the data
+ // source itself does not have the op (e.g. audioserver).
+ final int opMode = (skipProxyOperation && !singleReceiverFromDatasource) ?
+ AppOpsManager.MODE_ALLOWED :
+ appOpsManager.unsafeCheckOpRawNoThrow(op, resolvedAttributionSource);
+
final AttributionSource next = accessorSource.getNext();
if (!selfAccess && opMode == AppOpsManager.MODE_ALLOWED && next != null) {
final String resolvedNextPackageName = resolvePackageName(context, next);
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index 8cf0481b1dc3..d3aa0469435c 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -520,32 +520,6 @@ public class PhoneWindowManager implements WindowManagerPolicy {
private WindowWakeUpPolicy mWindowWakeUpPolicy;
- /**
- * The three variables below are used for custom power key gesture detection in
- * PhoneWindowManager. They are used to detect when the power button has been double pressed
- * and, when it does happen, makes the behavior overrideable by the app.
- *
- * We cannot use the {@link PowerKeyRule} for this because multi-press power gesture detection
- * and behaviors are handled by {@link com.android.server.GestureLauncherService}, and the
- * {@link PowerKeyRule} only handles single and long-presses of the power button. As a result,
- * overriding the double tap behavior requires custom gesture detection here that mimics the
- * logic in {@link com.android.server.GestureLauncherService}.
- *
- * Long-term, it would be beneficial to move all power gesture detection to
- * {@link PowerKeyRule} so that this custom logic isn't required.
- */
- // Time of last power down event.
- private long mLastPowerDown;
-
- // Number of power button events consecutively triggered (within a specific timeout threshold).
- private int mPowerButtonConsecutiveTaps = 0;
-
- // Whether a double tap of the power button has been detected.
- volatile boolean mDoubleTapPowerDetected;
-
- // Runnable that is queued on a delay when the first power keyDown event is sent to the app.
- private Runnable mPowerKeyDelayedRunnable = null;
-
boolean mSafeMode;
// Whether to allow dock apps with METADATA_DOCK_HOME to temporarily take over the Home key.
@@ -1135,10 +1109,6 @@ public class PhoneWindowManager implements WindowManagerPolicy {
|| handledByPowerManager || isKeyGestureTriggered
|| mKeyCombinationManager.isPowerKeyIntercepted();
- if (overridePowerKeyBehaviorInFocusedWindow()) {
- mPowerKeyHandled |= mDoubleTapPowerDetected;
- }
-
if (!mPowerKeyHandled) {
if (!interactive) {
wakeUpFromWakeKey(event);
@@ -2785,18 +2755,7 @@ public class PhoneWindowManager implements WindowManagerPolicy {
if (mShouldEarlyShortPressOnPower) {
return;
}
- // TODO(b/380433365): Remove deferring single power press action when refactoring.
- if (overridePowerKeyBehaviorInFocusedWindow()) {
- mDeferredKeyActionExecutor.cancelQueuedAction(KEYCODE_POWER);
- mDeferredKeyActionExecutor.queueKeyAction(
- KEYCODE_POWER,
- downTime,
- () -> {
- powerPress(downTime, 1 /*count*/, displayId);
- });
- } else {
- powerPress(downTime, 1 /*count*/, displayId);
- }
+ powerPress(downTime, 1 /*count*/, displayId);
}
@Override
@@ -2827,17 +2786,7 @@ public class PhoneWindowManager implements WindowManagerPolicy {
@Override
void onMultiPress(long downTime, int count, int displayId) {
- if (overridePowerKeyBehaviorInFocusedWindow()) {
- mDeferredKeyActionExecutor.cancelQueuedAction(KEYCODE_POWER);
- mDeferredKeyActionExecutor.queueKeyAction(
- KEYCODE_POWER,
- downTime,
- () -> {
- powerPress(downTime, count, displayId);
- });
- } else {
- powerPress(downTime, count, displayId);
- }
+ powerPress(downTime, count, displayId);
}
@Override
@@ -3614,12 +3563,6 @@ public class PhoneWindowManager implements WindowManagerPolicy {
}
}
- if (overridePowerKeyBehaviorInFocusedWindow() && event.getKeyCode() == KEYCODE_POWER
- && event.getAction() == KeyEvent.ACTION_UP
- && mDoubleTapPowerDetected) {
- mDoubleTapPowerDetected = false;
- }
-
return needToConsumeKey ? keyConsumed : keyNotConsumed;
}
@@ -4117,8 +4060,6 @@ public class PhoneWindowManager implements WindowManagerPolicy {
sendSystemKeyToStatusBarAsync(event);
return true;
}
- case KeyEvent.KEYCODE_POWER:
- return interceptPowerKeyBeforeDispatching(focusedToken, event);
case KeyEvent.KEYCODE_SCREENSHOT:
if (firstDown) {
interceptScreenshotChord(SCREENSHOT_KEY_OTHER, 0 /*pressDelay*/);
@@ -4174,8 +4115,6 @@ public class PhoneWindowManager implements WindowManagerPolicy {
sendSystemKeyToStatusBarAsync(event);
return true;
}
- case KeyEvent.KEYCODE_POWER:
- return interceptPowerKeyBeforeDispatching(focusedToken, event);
}
if (isValidGlobalKey(keyCode)
&& mGlobalKeyManager.handleGlobalKey(mContext, keyCode, event)) {
@@ -4193,90 +4132,6 @@ public class PhoneWindowManager implements WindowManagerPolicy {
return (metaState & KeyEvent.META_META_ON) != 0;
}
- /**
- * Called by interceptKeyBeforeDispatching to handle interception logic for KEYCODE_POWER
- * KeyEvents.
- *
- * @return true if intercepting the key, false if sending to app.
- */
- private boolean interceptPowerKeyBeforeDispatching(IBinder focusedToken, KeyEvent event) {
- if (!overridePowerKeyBehaviorInFocusedWindow()) {
- //Flag disabled: intercept the power key and do not send to app.
- return true;
- }
- if (event.getKeyCode() != KEYCODE_POWER) {
- Log.wtf(TAG, "interceptPowerKeyBeforeDispatching received a non-power KeyEvent "
- + "with key code: " + event.getKeyCode());
- return false;
- }
-
- // Intercept keys (don't send to app) for 3x, 4x, 5x gestures)
- if (mPowerButtonConsecutiveTaps > DOUBLE_POWER_TAP_COUNT_THRESHOLD) {
- setDeferredKeyActionsExecutableAsync(KEYCODE_POWER, event.getDownTime());
- return true;
- }
-
- // UP key; just reuse the original decision.
- if (event.getAction() == KeyEvent.ACTION_UP) {
- final Set<Integer> consumedKeys = mConsumedKeysForDevice.get(event.getDeviceId());
- return consumedKeys != null
- && consumedKeys.contains(event.getKeyCode());
- }
-
- KeyInterceptionInfo info =
- mWindowManagerInternal.getKeyInterceptionInfoFromToken(focusedToken);
-
- if (info == null || !mButtonOverridePermissionChecker.canWindowOverridePowerKey(mContext,
- info.windowOwnerUid, info.inputFeaturesFlags)) {
- // The focused window does not have the permission to override power key behavior.
- if (DEBUG_INPUT) {
- String interceptReason = "";
- if (info == null) {
- interceptReason = "Window is null";
- } else if (!mButtonOverridePermissionChecker.canAppOverrideSystemKey(mContext,
- info.windowOwnerUid)) {
- interceptReason = "Application does not have "
- + "OVERRIDE_SYSTEM_KEY_BEHAVIOR_IN_FOCUSED_WINDOW permission";
- } else {
- interceptReason = "Window does not have inputFeatureFlag set";
- }
-
- Log.d(TAG, TextUtils.formatSimple("Intercepting KEYCODE_POWER event. action=%d, "
- + "eventTime=%d to window=%s. interceptReason=%s. "
- + "mDoubleTapPowerDetected=%b",
- event.getAction(), event.getEventTime(), (info != null)
- ? info.windowTitle : "null", interceptReason,
- mDoubleTapPowerDetected));
- }
- // Intercept the key (i.e. do not send to app)
- setDeferredKeyActionsExecutableAsync(KEYCODE_POWER, event.getDownTime());
- return true;
- }
-
- if (DEBUG_INPUT) {
- Log.d(TAG, TextUtils.formatSimple("Sending KEYCODE_POWER to app. action=%d, "
- + "eventTime=%d to window=%s. mDoubleTapPowerDetected=%b",
- event.getAction(), event.getEventTime(), info.windowTitle,
- mDoubleTapPowerDetected));
- }
-
- if (!mDoubleTapPowerDetected) {
- //Single press: post a delayed runnable for the single press power action that will be
- // called if it's not cancelled by a double press.
- final var downTime = event.getDownTime();
- mPowerKeyDelayedRunnable = () ->
- setDeferredKeyActionsExecutableAsync(KEYCODE_POWER, downTime);
- mHandler.postDelayed(mPowerKeyDelayedRunnable, POWER_MULTI_PRESS_TIMEOUT_MILLIS);
- } else if (mPowerKeyDelayedRunnable != null) {
- //Double press detected: cancel the single press runnable.
- mHandler.removeCallbacks(mPowerKeyDelayedRunnable);
- mPowerKeyDelayedRunnable = null;
- }
-
- // Focused window has permission. Send to app.
- return false;
- }
-
@SuppressLint("MissingPermission")
private void initKeyGestures() {
if (!useKeyGestureEventHandler()) {
@@ -4303,7 +4158,6 @@ public class PhoneWindowManager implements WindowManagerPolicy {
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,
@@ -4456,7 +4310,6 @@ public class PhoneWindowManager implements WindowManagerPolicy {
}
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();
@@ -4764,11 +4617,6 @@ public class PhoneWindowManager implements WindowManagerPolicy {
return true;
}
- if (overridePowerKeyBehaviorInFocusedWindow() && keyCode == KEYCODE_POWER) {
- handleUnhandledSystemKey(event);
- return true;
- }
-
if (useKeyGestureEventHandler()) {
return false;
}
@@ -5595,12 +5443,8 @@ public class PhoneWindowManager implements WindowManagerPolicy {
KeyEvent.actionToString(event.getAction()),
mPowerKeyHandled ? 1 : 0,
mSingleKeyGestureDetector.getKeyPressCounter(KeyEvent.KEYCODE_POWER));
- if (overridePowerKeyBehaviorInFocusedWindow()) {
- result |= ACTION_PASS_TO_USER;
- } else {
- // Any activity on the power button stops the accessibility shortcut
- result &= ~ACTION_PASS_TO_USER;
- }
+ // Any activity on the power button stops the accessibility shortcut
+ result &= ~ACTION_PASS_TO_USER;
isWakeKey = false; // wake-up will be handled separately
if (down) {
interceptPowerKeyDown(event, interactiveAndAwake, isKeyGestureTriggered);
@@ -5862,35 +5706,6 @@ public class PhoneWindowManager implements WindowManagerPolicy {
}
if (event.getKeyCode() == KEYCODE_POWER && event.getAction() == KeyEvent.ACTION_DOWN) {
- if (overridePowerKeyBehaviorInFocusedWindow()) {
- if (event.getRepeatCount() > 0 && !mHasFeatureWatch) {
- return;
- }
- if (mGestureLauncherService != null) {
- mGestureLauncherService.processPowerKeyDown(event);
- }
-
- if (detectDoubleTapPower(event)) {
- mDoubleTapPowerDetected = true;
-
- // Copy of the event for handler in case the original event gets recycled.
- KeyEvent eventCopy = KeyEvent.obtain(event);
- mDeferredKeyActionExecutor.queueKeyAction(
- KeyEvent.KEYCODE_POWER,
- eventCopy.getEventTime(),
- () -> {
- if (!handleCameraGesture(eventCopy, interactive)) {
- mSingleKeyGestureDetector.interceptKey(
- eventCopy, interactive, defaultDisplayOn);
- } else {
- mSingleKeyGestureDetector.reset();
- }
- eventCopy.recycle();
- });
- return;
- }
- }
-
mPowerKeyHandled = handleCameraGesture(event, interactive);
if (mPowerKeyHandled) {
// handled by camera gesture.
@@ -5902,26 +5717,6 @@ public class PhoneWindowManager implements WindowManagerPolicy {
mSingleKeyGestureDetector.interceptKey(event, interactive, defaultDisplayOn);
}
- private boolean detectDoubleTapPower(KeyEvent event) {
- //Watches use the SingleKeyGestureDetector for detecting multi-press gestures.
- if (mHasFeatureWatch || event.getKeyCode() != KEYCODE_POWER
- || event.getAction() != KeyEvent.ACTION_DOWN || event.getRepeatCount() != 0) {
- return false;
- }
-
- final long powerTapInterval = event.getEventTime() - mLastPowerDown;
- mLastPowerDown = event.getEventTime();
- if (powerTapInterval >= POWER_MULTI_PRESS_TIMEOUT_MILLIS) {
- // Tap too slow for double press
- mPowerButtonConsecutiveTaps = 1;
- } else {
- mPowerButtonConsecutiveTaps++;
- }
-
- return powerTapInterval < POWER_MULTI_PRESS_TIMEOUT_MILLIS
- && mPowerButtonConsecutiveTaps == DOUBLE_POWER_TAP_COUNT_THRESHOLD;
- }
-
// The camera gesture will be detected by GestureLauncherService.
private boolean handleCameraGesture(KeyEvent event, boolean interactive) {
// camera gesture.
@@ -7779,12 +7574,6 @@ public class PhoneWindowManager implements WindowManagerPolicy {
null)
== PERMISSION_GRANTED;
}
-
- boolean canWindowOverridePowerKey(Context context, int uid, int inputFeaturesFlags) {
- return canAppOverrideSystemKey(context, uid)
- && (inputFeaturesFlags & WindowManager.LayoutParams
- .INPUT_FEATURE_RECEIVE_POWER_KEY_DOUBLE_PRESS) != 0;
- }
}
private int getTargetDisplayIdForKeyEvent(KeyEvent event) {
diff --git a/services/core/java/com/android/server/security/AttestationVerificationManagerService.java b/services/core/java/com/android/server/security/AttestationVerificationManagerService.java
index 22a359bced86..c1d1e2ba3e76 100644
--- a/services/core/java/com/android/server/security/AttestationVerificationManagerService.java
+++ b/services/core/java/com/android/server/security/AttestationVerificationManagerService.java
@@ -99,11 +99,6 @@ public class AttestationVerificationManagerService extends SystemService {
@Override
protected void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter writer,
@Nullable String[] args) {
- if (!android.security.Flags.dumpAttestationVerifications()) {
- super.dump(fd, writer, args);
- return;
- }
-
if (!DumpUtils.checkDumpAndUsageStatsPermission(getContext(), TAG, writer)) return;
final IndentingPrintWriter fout = new IndentingPrintWriter(writer, " ");
diff --git a/services/core/java/com/android/server/theming/ThemeSettingsManager.java b/services/core/java/com/android/server/theming/ThemeSettingsManager.java
new file mode 100644
index 000000000000..94094a6f9603
--- /dev/null
+++ b/services/core/java/com/android/server/theming/ThemeSettingsManager.java
@@ -0,0 +1,166 @@
+/*
+ * 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.theming;
+
+import android.annotation.FlaggedApi;
+import android.annotation.NonNull;
+import android.annotation.UserIdInt;
+import android.content.ContentResolver;
+import android.content.theming.ThemeSettings;
+import android.content.theming.ThemeSettingsField;
+import android.content.theming.ThemeSettingsUpdater;
+import android.provider.Settings;
+import android.telecom.Log;
+
+import com.android.internal.util.Preconditions;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.Iterator;
+
+/**
+ * Manages the loading and saving of theme settings. This class handles the persistence of theme
+ * settings to and from the system settings. It utilizes a collection of {@link ThemeSettingsField}
+ * objects to represent individual theme setting fields.
+ *
+ * @hide
+ */
+@FlaggedApi(android.server.Flags.FLAG_ENABLE_THEME_SERVICE)
+class ThemeSettingsManager {
+ private static final String TAG = ThemeSettingsManager.class.getSimpleName();
+ static final String TIMESTAMP_FIELD = "_applied_timestamp";
+ private final ThemeSettingsField<?, ?>[] mFields;
+ private final ThemeSettings mDefaults;
+
+ /**
+ * Constructs a new {@code ThemeSettingsManager} with the specified default settings.
+ *
+ * @param defaults The default theme settings to use.
+ */
+ ThemeSettingsManager(ThemeSettings defaults) {
+ mDefaults = defaults;
+ mFields = ThemeSettingsField.getFields(defaults);
+ }
+
+ /**
+ * Loads the theme settings for the specified user.
+ *
+ * @param userId The ID of the user.
+ * @param contentResolver The content resolver to use.
+ * @return The loaded {@link ThemeSettings}.
+ */
+ @NonNull
+ ThemeSettings loadSettings(@UserIdInt int userId, ContentResolver contentResolver) {
+ String jsonString = Settings.Secure.getStringForUser(contentResolver,
+ Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES, userId);
+
+ JSONObject userSettings;
+
+ try {
+ userSettings = new JSONObject(jsonString == null ? "" : jsonString);
+ } catch (JSONException e) {
+ userSettings = new JSONObject();
+ }
+
+ ThemeSettingsUpdater updater = ThemeSettings.updater();
+
+ for (ThemeSettingsField<?, ?> field : mFields) {
+ field.fromJSON(userSettings, updater);
+ }
+
+ return updater.toThemeSettings(mDefaults);
+ }
+
+ /**
+ * Saves the specified theme settings for the given user.
+ *
+ * @param userId The ID of the user.
+ * @param contentResolver The content resolver to use.
+ * @param newSettings The {@link ThemeSettings} to save.
+ */
+ void replaceSettings(@UserIdInt int userId, ContentResolver contentResolver,
+ ThemeSettings newSettings) throws RuntimeException {
+ Preconditions.checkArgument(newSettings != null, "Impossible to write empty settings");
+
+ JSONObject jsonSettings = new JSONObject();
+
+
+ for (ThemeSettingsField<?, ?> field : mFields) {
+ field.toJSON(newSettings, jsonSettings);
+ }
+
+ // user defined timestamp should be ignored. Storing new timestamp.
+ try {
+ jsonSettings.put(TIMESTAMP_FIELD, System.currentTimeMillis());
+ } catch (JSONException e) {
+ Log.w(TAG, "Error saving timestamp: " + e.getMessage());
+ }
+
+ String jsonString = jsonSettings.toString();
+
+ Settings.Secure.putStringForUser(contentResolver,
+ Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES, jsonString, userId);
+ }
+
+ /**
+ * Saves the specified theme settings for the given user, while preserving unrelated existing
+ * properties.
+ *
+ * @param userId The ID of the user.
+ * @param contentResolver The content resolver to use.
+ * @param newSettings The {@link ThemeSettings} to save.
+ */
+ void updateSettings(@UserIdInt int userId, ContentResolver contentResolver,
+ ThemeSettings newSettings) throws JSONException, RuntimeException {
+ Preconditions.checkArgument(newSettings != null, "Impossible to write empty settings");
+
+ String existingJsonString = Settings.Secure.getStringForUser(contentResolver,
+ Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES, userId);
+
+ JSONObject existingJson;
+ try {
+ existingJson = new JSONObject(existingJsonString == null ? "{}" : existingJsonString);
+ } catch (JSONException e) {
+ existingJson = new JSONObject();
+ }
+
+ JSONObject newJson = new JSONObject();
+ for (ThemeSettingsField<?, ?> field : mFields) {
+ field.toJSON(newSettings, newJson);
+ }
+
+ // user defined timestamp should be ignored. Storing new timestamp.
+ try {
+ newJson.put(TIMESTAMP_FIELD, System.currentTimeMillis());
+ } catch (JSONException e) {
+ Log.w(TAG, "Error saving timestamp: " + e.getMessage());
+ }
+
+ // Merge the new settings with the existing settings
+ Iterator<String> keys = newJson.keys();
+ while (keys.hasNext()) {
+ String key = keys.next();
+ existingJson.put(key, newJson.get(key));
+ }
+
+ String mergedJsonString = existingJson.toString();
+
+ Settings.Secure.putStringForUser(contentResolver,
+ Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES, mergedJsonString, userId);
+ }
+}
diff --git a/services/core/java/com/android/server/tv/TvInputHal.java b/services/core/java/com/android/server/tv/TvInputHal.java
index 87ebdbfbf4e8..8d02f2fc4f8b 100644
--- a/services/core/java/com/android/server/tv/TvInputHal.java
+++ b/services/core/java/com/android/server/tv/TvInputHal.java
@@ -68,6 +68,9 @@ final class TvInputHal implements Handler.Callback {
private static native int nativeSetTvMessageEnabled(long ptr, int deviceId, int streamId,
int type, boolean enabled);
+ private static native int nativeSetPictureProfile(
+ long ptr, int deviceId, int streamId, long profileHandle);
+
private final Object mLock = new Object();
private long mPtr = 0;
private final Callback mCallback;
@@ -122,6 +125,24 @@ final class TvInputHal implements Handler.Callback {
}
}
+ public int setPictureProfile(int deviceId, TvStreamConfig streamConfig, long profileHandle) {
+ synchronized (mLock) {
+ if (mPtr == 0) {
+ return ERROR_NO_INIT;
+ }
+ int generation = mStreamConfigGenerations.get(deviceId, 0);
+ if (generation != streamConfig.getGeneration()) {
+ return ERROR_STALE_CONFIG;
+ }
+ if (nativeSetPictureProfile(mPtr, deviceId, streamConfig.getStreamId(), profileHandle)
+ == 0) {
+ return SUCCESS;
+ } else {
+ return ERROR_UNKNOWN;
+ }
+ }
+ }
+
public int removeStream(int deviceId, TvStreamConfig streamConfig) {
synchronized (mLock) {
if (mPtr == 0) {
diff --git a/services/core/java/com/android/server/tv/TvInputHardwareManager.java b/services/core/java/com/android/server/tv/TvInputHardwareManager.java
index 92b57645b9a3..d3e3257fe384 100644
--- a/services/core/java/com/android/server/tv/TvInputHardwareManager.java
+++ b/services/core/java/com/android/server/tv/TvInputHardwareManager.java
@@ -596,6 +596,26 @@ class TvInputHardwareManager implements TvInputHal.Callback {
}
}
+ public boolean setPictureProfile(String inputId, long profileHandle) {
+ synchronized (mLock) {
+ int deviceId = findDeviceIdForInputIdLocked(inputId);
+ if (deviceId < 0) {
+ Slog.e(TAG, "Invalid inputId : " + inputId);
+ return false;
+ }
+
+ Connection connection = mConnections.get(deviceId);
+ boolean success = true;
+ for (TvStreamConfig config : connection.getConfigsLocked()) {
+ success = success
+ && mHal.setPictureProfile(deviceId, config, profileHandle)
+ == TvInputHal.SUCCESS;
+ }
+
+ return success;
+ }
+ }
+
/**
* Take a snapshot of the given TV input into the provided Surface.
*/
@@ -844,7 +864,6 @@ class TvInputHardwareManager implements TvInputHal.Callback {
return mCallback;
}
- @GuardedBy("mLock")
public TvStreamConfig[] getConfigsLocked() {
return mConfigs;
}
diff --git a/services/core/java/com/android/server/tv/TvInputManagerService.java b/services/core/java/com/android/server/tv/TvInputManagerService.java
index 47d6879129ee..3c3bfeb75c85 100644
--- a/services/core/java/com/android/server/tv/TvInputManagerService.java
+++ b/services/core/java/com/android/server/tv/TvInputManagerService.java
@@ -51,6 +51,7 @@ import android.hardware.hdmi.HdmiDeviceInfo;
import android.hardware.hdmi.HdmiTvClient;
import android.media.AudioPresentation;
import android.media.PlaybackParams;
+import android.media.quality.MediaQualityManager;
import android.media.tv.AdBuffer;
import android.media.tv.AdRequest;
import android.media.tv.AdResponse;
@@ -193,6 +194,8 @@ public final class TvInputManagerService extends SystemService {
private HdmiControlManager mHdmiControlManager = null;
private HdmiTvClient mHdmiTvClient = null;
+ private MediaQualityManager mMediaQualityManager = null;
+
public TvInputManagerService(Context context) {
super(context);
@@ -919,7 +922,7 @@ public final class TvInputManagerService extends SystemService {
sendSessionTokenToClientLocked(sessionState.client,
sessionState.inputId, null, null, sessionState.seq);
}
- if (!serviceState.isHardware) {
+ if (!serviceState.isHardware || serviceState.reconnecting) {
updateServiceConnectionLocked(serviceState.component, userId);
} else {
updateHardwareServiceConnectionDelayed(userId);
@@ -3702,10 +3705,24 @@ public final class TvInputManagerService extends SystemService {
TvInputInfo inputInfo, ComponentName component, int userId) {
ServiceState serviceState = getServiceStateLocked(component, userId);
serviceState.hardwareInputMap.put(inputInfo.getId(), inputInfo);
+ setPictureProfileLocked(inputInfo.getId());
buildTvInputListLocked(userId, null);
}
@GuardedBy("mLock")
+ private void setPictureProfileLocked(String inputId) {
+ if (mMediaQualityManager == null) {
+ mMediaQualityManager = (MediaQualityManager) getContext()
+ .getSystemService(Context.MEDIA_QUALITY_SERVICE);
+ if (mMediaQualityManager == null) {
+ return;
+ }
+ }
+ long profileHandle = mMediaQualityManager.getPictureProfileForTvInput(inputId);
+ mTvInputHardwareManager.setPictureProfile(inputId, profileHandle);
+ }
+
+ @GuardedBy("mLock")
private void removeHardwareInputLocked(String inputId, int userId) {
if (!mTvInputHardwareManager.getInputMap().containsKey(inputId)) {
return;
diff --git a/services/core/java/com/android/server/updates/CertPinInstallReceiver.java b/services/core/java/com/android/server/updates/CertPinInstallReceiver.java
index 250e99b47b1a..c8e7a8dea5c3 100644
--- a/services/core/java/com/android/server/updates/CertPinInstallReceiver.java
+++ b/services/core/java/com/android/server/updates/CertPinInstallReceiver.java
@@ -19,7 +19,10 @@ package com.android.server.updates;
import android.content.Context;
import android.content.Intent;
+import java.io.File;
+
public class CertPinInstallReceiver extends ConfigUpdateInstallReceiver {
+ private static final String KEYCHAIN_DIR = "/data/misc/keychain/";
public CertPinInstallReceiver() {
super("/data/misc/keychain/", "pins", "metadata/", "version");
@@ -27,7 +30,22 @@ public class CertPinInstallReceiver extends ConfigUpdateInstallReceiver {
@Override
public void onReceive(final Context context, final Intent intent) {
- if (!com.android.server.flags.Flags.certpininstallerRemoval()) {
+ if (Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction())) {
+ if (com.android.server.flags.Flags.certpininstallerRemoval()) {
+ File pins = new File(KEYCHAIN_DIR + "pins");
+ if (pins.exists()) {
+ pins.delete();
+ }
+ File version = new File(KEYCHAIN_DIR + "metadata/version");
+ if (version.exists()) {
+ version.delete();
+ }
+ File metadata = new File(KEYCHAIN_DIR + "metadata");
+ if (metadata.exists()) {
+ metadata.delete();
+ }
+ }
+ } else if (!com.android.server.flags.Flags.certpininstallerRemoval()) {
super.onReceive(context, intent);
}
}
diff --git a/services/core/java/com/android/server/wm/ActivityClientController.java b/services/core/java/com/android/server/wm/ActivityClientController.java
index 7da4beb95114..fd4e38e9813d 100644
--- a/services/core/java/com/android/server/wm/ActivityClientController.java
+++ b/services/core/java/com/android/server/wm/ActivityClientController.java
@@ -24,10 +24,12 @@ import static android.app.ActivityTaskManager.INVALID_TASK_ID;
import static android.app.ActivityTaskManager.INVALID_WINDOWING_MODE;
import static android.app.FullscreenRequestHandler.REMOTE_CALLBACK_RESULT_KEY;
import static android.app.FullscreenRequestHandler.RESULT_APPROVED;
+import static android.app.FullscreenRequestHandler.RESULT_FAILED_ALREADY_FULLY_EXPANDED;
import static android.app.FullscreenRequestHandler.RESULT_FAILED_NOT_IN_FULLSCREEN_WITH_HISTORY;
import static android.app.FullscreenRequestHandler.RESULT_FAILED_NOT_TOP_FOCUSED;
import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
import static android.content.pm.PackageManager.PERMISSION_DENIED;
@@ -95,6 +97,7 @@ import android.os.UserHandle;
import android.service.voice.VoiceInteractionManagerInternal;
import android.util.Slog;
import android.view.RemoteAnimationDefinition;
+import android.window.DesktopModeFlags;
import android.window.SizeConfigurationBuckets;
import android.window.TransitionInfo;
@@ -1188,17 +1191,25 @@ class ActivityClientController extends IActivityClientController.Stub {
if (requesterActivity.getWindowingMode() == WINDOWING_MODE_PINNED) {
return RESULT_APPROVED;
}
+ final int taskWindowingMode = topFocusedRootTask.getWindowingMode();
// If this is not coming from the currently top-most activity, reject the request.
if (requesterActivity != topFocusedRootTask.getTopMostActivity()) {
return RESULT_FAILED_NOT_TOP_FOCUSED;
}
if (fullscreenRequest == FULLSCREEN_MODE_REQUEST_EXIT) {
- if (topFocusedRootTask.getWindowingMode() != WINDOWING_MODE_FULLSCREEN) {
+ if (taskWindowingMode != WINDOWING_MODE_FULLSCREEN) {
return RESULT_FAILED_NOT_IN_FULLSCREEN_WITH_HISTORY;
}
if (topFocusedRootTask.mMultiWindowRestoreWindowingMode == INVALID_WINDOWING_MODE) {
return RESULT_FAILED_NOT_IN_FULLSCREEN_WITH_HISTORY;
}
+ return RESULT_APPROVED;
+ }
+
+ if (DesktopModeFlags.ENABLE_REQUEST_FULLSCREEN_BUGFIX.isTrue()
+ && (taskWindowingMode == WINDOWING_MODE_FULLSCREEN
+ || taskWindowingMode == WINDOWING_MODE_MULTI_WINDOW)) {
+ return RESULT_FAILED_ALREADY_FULLY_EXPANDED;
}
return RESULT_APPROVED;
}
@@ -1281,7 +1292,7 @@ class ActivityClientController extends IActivityClientController.Stub {
}
}
- private static void executeMultiWindowFullscreenRequest(int fullscreenRequest, Task requester) {
+ private void executeMultiWindowFullscreenRequest(int fullscreenRequest, Task requester) {
final int targetWindowingMode;
if (fullscreenRequest == FULLSCREEN_MODE_REQUEST_ENTER) {
final int restoreWindowingMode = requester.getRequestedOverrideWindowingMode();
@@ -1294,7 +1305,13 @@ class ActivityClientController extends IActivityClientController.Stub {
requester.getParent().mRemoteToken.toWindowContainerToken();
} else {
targetWindowingMode = requester.mMultiWindowRestoreWindowingMode;
- requester.restoreWindowingMode();
+ if (DesktopModeFlags.ENABLE_REQUEST_FULLSCREEN_BUGFIX.isTrue()
+ && targetWindowingMode == WINDOWING_MODE_PINNED) {
+ final ActivityRecord r = requester.topRunningActivity();
+ enterPictureInPictureMode(r.token, r.pictureInPictureArgs);
+ } else {
+ requester.restoreWindowingMode();
+ }
}
if (targetWindowingMode == WINDOWING_MODE_FULLSCREEN) {
requester.setBounds(null);
diff --git a/services/core/java/com/android/server/wm/DisplayAreaPolicy.java b/services/core/java/com/android/server/wm/DisplayAreaPolicy.java
index 9d9c5ceb57d6..11fb229bb93d 100644
--- a/services/core/java/com/android/server/wm/DisplayAreaPolicy.java
+++ b/services/core/java/com/android/server/wm/DisplayAreaPolicy.java
@@ -26,6 +26,7 @@ import static android.view.WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL;
import static android.view.WindowManager.LayoutParams.TYPE_NOTIFICATION_SHADE;
import static android.view.WindowManager.LayoutParams.TYPE_SECURE_SYSTEM_OVERLAY;
import static android.view.WindowManager.LayoutParams.TYPE_STATUS_BAR;
+import static android.view.WindowManager.LayoutParams.TYPE_VOLUME_OVERLAY;
import static android.view.WindowManager.LayoutParams.TYPE_WALLPAPER;
import static android.window.DisplayAreaOrganizer.FEATURE_APP_ZOOM_OUT;
import static android.window.DisplayAreaOrganizer.FEATURE_DEFAULT_TASK_CONTAINER;
@@ -166,7 +167,7 @@ public abstract class DisplayAreaPolicy {
.all()
.except(TYPE_NAVIGATION_BAR, TYPE_NAVIGATION_BAR_PANEL,
TYPE_STATUS_BAR, TYPE_NOTIFICATION_SHADE,
- TYPE_KEYGUARD_DIALOG, TYPE_WALLPAPER)
+ TYPE_KEYGUARD_DIALOG, TYPE_WALLPAPER, TYPE_VOLUME_OVERLAY)
.build());
}
if (USE_DISPLAY_AREA_FOR_FULLSCREEN_MAGNIFICATION) {
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index 16caec81f5f8..deee44dd7f61 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -47,7 +47,6 @@ import static android.view.Display.FLAG_PRIVATE;
import static android.view.Display.FLAG_SHOULD_SHOW_SYSTEM_DECORATIONS;
import static android.view.Display.INVALID_DISPLAY;
import static android.view.Display.STATE_UNKNOWN;
-import static android.view.Display.TYPE_EXTERNAL;
import static android.view.Display.isSuspendedState;
import static android.view.InsetsSource.ID_IME;
import static android.view.Surface.ROTATION_0;
@@ -433,9 +432,9 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp
/**
* Ratio between overridden display density for current user and the initial display density,
- * used only for external displays.
+ * used for updating the base density when resolution change happens to preserve display size.
*/
- float mExternalDisplayForcedDensityRatio = 0.0f;
+ float mForcedDisplayDensityRatio = 0.0f;
boolean mIsDensityForced = false;
/**
@@ -3120,6 +3119,12 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp
mBaseRoundedCorners = loadRoundedCorners(baseWidth, baseHeight);
}
+ // Update the base density if there is a forced density ratio.
+ if (DesktopExperienceFlags.ENABLE_PERSISTING_DISPLAY_SIZE_FOR_CONNECTED_DISPLAYS.isTrue()
+ && mForcedDisplayDensityRatio != 0.0f) {
+ mBaseDisplayDensity = getBaseDensityFromRatio();
+ }
+
if (mMaxUiWidth > 0 && mBaseDisplayWidth > mMaxUiWidth) {
final float ratio = mMaxUiWidth / (float) mBaseDisplayWidth;
mBaseDisplayHeight = (int) (mBaseDisplayHeight * ratio);
@@ -3137,18 +3142,22 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp
+ mBaseDisplayHeight + " on display:" + getDisplayId());
}
}
- // Update the base density if there is a forced density ratio.
- if (DesktopExperienceFlags.ENABLE_PERSISTING_DISPLAY_SIZE_FOR_CONNECTED_DISPLAYS.isTrue()
- && mIsDensityForced && mExternalDisplayForcedDensityRatio != 0.0f) {
- mBaseDisplayDensity = (int)
- (mInitialDisplayDensity * mExternalDisplayForcedDensityRatio + 0.5);
- }
if (mDisplayReady && !mDisplayPolicy.shouldKeepCurrentDecorInsets()) {
mDisplayPolicy.mDecorInsets.invalidate();
}
}
/**
+ * Returns the forced density from forcedDensityRatio if the ratio is valid by rounding the
+ * density down to an even number. Returns the initial density if the ratio is 0.
+ */
+ private int getBaseDensityFromRatio() {
+ return (mForcedDisplayDensityRatio != 0.0f)
+ ? ((int) (mInitialDisplayDensity * mForcedDisplayDensityRatio)) & ~1
+ : mInitialDisplayDensity;
+ }
+
+ /**
* Forces this display to use the specified density.
*
* @param density The density in DPI to use. If the value equals to initial density, the setting
@@ -3172,15 +3181,19 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp
if (density == getInitialDisplayDensity()) {
density = 0;
}
- // Save the new density ratio to settings for external displays.
- if (DesktopExperienceFlags.ENABLE_PERSISTING_DISPLAY_SIZE_FOR_CONNECTED_DISPLAYS.isTrue()
- && mDisplayInfo.type == TYPE_EXTERNAL) {
- mExternalDisplayForcedDensityRatio = (float)
- mBaseDisplayDensity / getInitialDisplayDensity();
+ mWmService.mDisplayWindowSettings.setForcedDensity(getDisplayInfo(), density, userId);
+ }
+
+ void setForcedDensityRatio(float ratio, int userId) {
+ // Save the new density ratio to settings and update forced density with the ratio.
+ if (DesktopExperienceFlags.ENABLE_PERSISTING_DISPLAY_SIZE_FOR_CONNECTED_DISPLAYS.isTrue()) {
+ mForcedDisplayDensityRatio = ratio;
mWmService.mDisplayWindowSettings.setForcedDensityRatio(getDisplayInfo(),
- mExternalDisplayForcedDensityRatio);
+ mForcedDisplayDensityRatio);
+
+ // Set forced density from ratio.
+ setForcedDensity(getBaseDensityFromRatio(), userId);
}
- mWmService.mDisplayWindowSettings.setForcedDensity(getDisplayInfo(), density, userId);
}
/** @param mode {@link #FORCE_SCALING_MODE_AUTO} or {@link #FORCE_SCALING_MODE_DISABLED}. */
@@ -3403,6 +3416,10 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp
void removeImmediately() {
mDeferredRemoval = false;
try {
+ if (DesktopExperienceFlags.ENABLE_DISPLAY_CONTENT_MODE_MANAGEMENT.isTrue()
+ && mWmService.mDisplayWindowSettings.shouldShowSystemDecorsLocked(this)) {
+ mDisplayPolicy.notifyDisplayRemoveSystemDecorations();
+ }
mUnknownAppVisibilityController.clear();
mTransitionController.unregisterLegacyListener(mFixedRotationTransitionListener);
mDeviceStateController.unregisterDeviceStateCallback(mDeviceStateConsumer);
diff --git a/services/core/java/com/android/server/wm/DisplayWindowSettings.java b/services/core/java/com/android/server/wm/DisplayWindowSettings.java
index 56579206566f..2818d79a40ad 100644
--- a/services/core/java/com/android/server/wm/DisplayWindowSettings.java
+++ b/services/core/java/com/android/server/wm/DisplayWindowSettings.java
@@ -391,7 +391,7 @@ class DisplayWindowSettings {
final int density = hasDensityOverride ? settings.mForcedDensity
: dc.getInitialDisplayDensity();
if (hasDensityOverrideRatio) {
- dc.mExternalDisplayForcedDensityRatio = settings.mForcedDensityRatio;
+ dc.mForcedDisplayDensityRatio = settings.mForcedDensityRatio;
}
dc.updateBaseDisplayMetrics(width, height, density, dc.mBaseDisplayPhysicalXDpi,
diff --git a/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java b/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java
index 53681f950c8e..f2bc909bd64b 100644
--- a/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java
+++ b/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java
@@ -445,14 +445,21 @@ final class ImeInsetsSourceProvider extends InsetsSourceProvider {
if (controlTarget != null) {
final boolean imeAnimating = Flags.reportAnimatingInsetsTypes()
&& (controlTarget.getAnimatingTypes() & WindowInsets.Type.ime()) != 0;
- ImeTracker.forLogging().onProgress(statsToken,
+ final boolean imeVisible =
+ controlTarget.isRequestedVisible(WindowInsets.Type.ime()) || imeAnimating;
+ final var finalStatsToken = statsToken != null ? statsToken
+ : ImeTracker.forLogging().onStart(
+ imeVisible ? ImeTracker.TYPE_SHOW : ImeTracker.TYPE_HIDE,
+ ImeTracker.ORIGIN_SERVER,
+ SoftInputShowHideReason.IME_REQUESTED_CHANGED_LISTENER,
+ false /* fromUser */);
+ ImeTracker.forLogging().onProgress(finalStatsToken,
ImeTracker.PHASE_WM_POSTING_CHANGED_IME_VISIBILITY);
mDisplayContent.mWmService.mH.post(() -> {
- ImeTracker.forLogging().onProgress(statsToken,
+ ImeTracker.forLogging().onProgress(finalStatsToken,
ImeTracker.PHASE_WM_INVOKING_IME_REQUESTED_LISTENER);
- imeListener.onImeRequestedChanged(controlTarget.getWindowToken(),
- controlTarget.isRequestedVisible(WindowInsets.Type.ime())
- || imeAnimating, statsToken);
+ imeListener.onImeRequestedChanged(controlTarget.getWindowToken(), imeVisible,
+ finalStatsToken);
});
} else {
ImeTracker.forLogging().onFailed(statsToken,
diff --git a/services/core/java/com/android/server/wm/RecentTasks.java b/services/core/java/com/android/server/wm/RecentTasks.java
index b9550feeab8a..24b5f618e32b 100644
--- a/services/core/java/com/android/server/wm/RecentTasks.java
+++ b/services/core/java/com/android/server/wm/RecentTasks.java
@@ -49,6 +49,7 @@ import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_ATM;
import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_WITH_CLASS_NAME;
import static com.android.server.wm.ActivityTaskSupervisor.REMOVE_FROM_RECENTS;
+import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.ActivityManager;
import android.app.ActivityTaskManager;
@@ -83,6 +84,7 @@ import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.protolog.ProtoLog;
import com.android.internal.util.function.pooled.PooledLambda;
import com.android.server.am.ActivityManagerService;
+import com.android.window.flags.Flags;
import com.google.android.collect.Sets;
@@ -1452,9 +1454,10 @@ class RecentTasks {
* @return whether the given active task should be presented to the user through SystemUI.
*/
@VisibleForTesting
- boolean isVisibleRecentTask(Task task) {
+ boolean isVisibleRecentTask(@NonNull Task task) {
if (DEBUG_RECENTS_TRIM_TASKS) {
Slog.d(TAG, "isVisibleRecentTask: task=" + task
+ + " isForceExcludedFromRecents=" + task.isForceExcludedFromRecents()
+ " minVis=" + mMinNumVisibleTasks + " maxVis=" + mMaxNumVisibleTasks
+ " sessionDuration=" + mActiveTasksSessionDurationMs
+ " inactiveDuration=" + task.getInactiveDuration()
@@ -1464,6 +1467,11 @@ class RecentTasks {
+ " intentFlags=" + task.getBaseIntent().getFlags());
}
+ // Ignore the task if it is force excluded from recents.
+ if (Flags.excludeTaskFromRecents() && task.isForceExcludedFromRecents()) {
+ return false;
+ }
+
switch (task.getActivityType()) {
case ACTIVITY_TYPE_HOME:
case ACTIVITY_TYPE_RECENTS:
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index 3cce17242648..89634707995a 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -625,6 +625,9 @@ class Task extends TaskFragment {
boolean mAlignActivityLocaleWithTask = false;
+ /** @see #isForceExcludedFromRecents() */
+ private boolean mForceExcludedFromRecents;
+
private Task(ActivityTaskManagerService atmService, int _taskId, Intent _intent,
Intent _affinityIntent, String _affinity, String _rootAffinity,
ComponentName _realActivity, ComponentName _origActivity, boolean _rootWasReset,
@@ -3842,7 +3845,8 @@ class Task extends TaskFragment {
pw.print(prefix); pw.print("lastActiveTime="); pw.print(lastActiveTime);
pw.println(" (inactive for " + (getInactiveDuration() / 1000) + "s)");
pw.print(prefix); pw.print("isTrimmable=" + mIsTrimmableFromRecents);
- pw.print(" isForceHidden="); pw.println(isForceHidden());
+ pw.print(" isForceHidden="); pw.print(isForceHidden());
+ pw.print(" isForceExcludedFromRecents="); pw.println(isForceExcludedFromRecents());
if (mLaunchAdjacentDisabled) {
pw.println(prefix + "mLaunchAdjacentDisabled=true");
}
@@ -4555,11 +4559,45 @@ class Task extends TaskFragment {
/**
* @return whether this task is always on top without taking visibility into account.
+ * @deprecated b/388630258 replace hidden bubble tasks with reordering.
+ * {@link RecentTasks#isVisibleRecentTask} now checks {@link #isForceExcludedFromRecents}.
*/
- public boolean isAlwaysOnTopWhenVisible() {
+ @Deprecated
+ boolean isAlwaysOnTopWhenVisible() {
return super.isAlwaysOnTop();
}
+ /**
+ * Returns whether this task is forcibly excluded from the Recents list.
+ *
+ * <p>This flag is used by {@link RecentTasks#isVisibleRecentTask} to determine
+ * if the task should be presented to the user through SystemUI. If this method
+ * returns {@code true}, the task will not be shown in Recents, regardless of other
+ * visibility criteria.
+ *
+ * @return {@code true} if the task is excluded, {@code false} otherwise.
+ */
+ boolean isForceExcludedFromRecents() {
+ return mForceExcludedFromRecents;
+ }
+
+ /**
+ * Sets whether this task should be forcibly excluded from the Recents list.
+ *
+ * <p>This method is intended to be used in conjunction with
+ * {@link android.window.WindowContainerTransaction#setTaskForceExcludedFromRecents} to modify the
+ * task's exclusion state.
+ *
+ * @param excluded {@code true} to exclude the task, {@code false} otherwise.
+ */
+ void setForceExcludedFromRecents(boolean excluded) {
+ if (!Flags.excludeTaskFromRecents()) {
+ Slog.w(TAG, "Flag " + Flags.FLAG_EXCLUDE_TASK_FROM_RECENTS + " is not enabled");
+ return;
+ }
+ mForceExcludedFromRecents = excluded;
+ }
+
boolean isForceHiddenForPinnedTask() {
return (mForceHiddenFlags & FLAG_FORCE_HIDDEN_FOR_PINNED_TASK) != 0;
}
diff --git a/services/core/java/com/android/server/wm/TaskChangeNotificationController.java b/services/core/java/com/android/server/wm/TaskChangeNotificationController.java
index c3649fe98056..709f491a3bdc 100644
--- a/services/core/java/com/android/server/wm/TaskChangeNotificationController.java
+++ b/services/core/java/com/android/server/wm/TaskChangeNotificationController.java
@@ -72,7 +72,6 @@ class TaskChangeNotificationController {
private final Handler mHandler;
// Task stack change listeners in a remote process.
- @GuardedBy("mRemoteTaskStackListeners")
private final RemoteCallbackList<ITaskStackListener> mRemoteTaskStackListeners =
new RemoteCallbackList<>();
@@ -311,9 +310,7 @@ class TaskChangeNotificationController {
}
}
} else if (listener != null) {
- synchronized (mRemoteTaskStackListeners) {
- mRemoteTaskStackListeners.register(listener);
- }
+ mRemoteTaskStackListeners.register(listener);
}
}
@@ -323,24 +320,20 @@ class TaskChangeNotificationController {
mLocalTaskStackListeners.remove(listener);
}
} else if (listener != null) {
- synchronized (mRemoteTaskStackListeners) {
- mRemoteTaskStackListeners.unregister(listener);
- }
+ mRemoteTaskStackListeners.unregister(listener);
}
}
private void forAllRemoteListeners(TaskStackConsumer callback, Message message) {
- synchronized (mRemoteTaskStackListeners) {
- for (int i = mRemoteTaskStackListeners.beginBroadcast() - 1; i >= 0; i--) {
- try {
- // Make a one-way callback to the listener
- callback.accept(mRemoteTaskStackListeners.getBroadcastItem(i), message);
- } catch (RemoteException e) {
- // Handled by the RemoteCallbackList.
- }
+ for (int i = mRemoteTaskStackListeners.beginBroadcast() - 1; i >= 0; i--) {
+ try {
+ // Make a one-way callback to the listener
+ callback.accept(mRemoteTaskStackListeners.getBroadcastItem(i), message);
+ } catch (RemoteException e) {
+ // Handled by the RemoteCallbackList.
}
- mRemoteTaskStackListeners.finishBroadcast();
}
+ mRemoteTaskStackListeners.finishBroadcast();
}
private void forAllLocalListeners(TaskStackConsumer callback, Message message) {
diff --git a/services/core/java/com/android/server/wm/TaskSystemBarsListenerController.java b/services/core/java/com/android/server/wm/TaskSystemBarsListenerController.java
index acb6061de93f..dc6b70d839e4 100644
--- a/services/core/java/com/android/server/wm/TaskSystemBarsListenerController.java
+++ b/services/core/java/com/android/server/wm/TaskSystemBarsListenerController.java
@@ -48,8 +48,10 @@ final class TaskSystemBarsListenerController {
int taskId,
boolean visible,
boolean wereRevealedFromSwipeOnSystemBar) {
- HashSet<TaskSystemBarsListener> localListeners;
- localListeners = new HashSet<>(mListeners);
+ if (mListeners.isEmpty()) {
+ return;
+ }
+ final HashSet<TaskSystemBarsListener> localListeners = new HashSet<>(mListeners);
mBackgroundExecutor.execute(() -> {
for (TaskSystemBarsListener listener : localListeners) {
diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java
index b1422c20e516..247a51d9fcb3 100644
--- a/services/core/java/com/android/server/wm/WindowContainer.java
+++ b/services/core/java/com/android/server/wm/WindowContainer.java
@@ -3203,7 +3203,9 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer<
mLocalInsetsSources.valueAt(i).dump(childPrefix, pw);
}
}
- pw.println(prefix + mSafeRegionBounds + " SafeRegionBounds");
+ if (mSafeRegionBounds != null) {
+ pw.println(prefix + "mSafeRegionBounds=" + mSafeRegionBounds);
+ }
}
final void updateSurfacePositionNonOrganized() {
diff --git a/services/core/java/com/android/server/wm/WindowManagerInternal.java b/services/core/java/com/android/server/wm/WindowManagerInternal.java
index 5f2a2ad7f0eb..8ed93276d646 100644
--- a/services/core/java/com/android/server/wm/WindowManagerInternal.java
+++ b/services/core/java/com/android/server/wm/WindowManagerInternal.java
@@ -371,7 +371,7 @@ public abstract class WindowManagerInternal {
* @param statsToken the token tracking the current IME request.
*/
void onImeRequestedChanged(IBinder windowToken, boolean imeVisible,
- @Nullable ImeTracker.Token statsToken);
+ @NonNull ImeTracker.Token statsToken);
}
/**
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index 00a437cc31f9..aa5eb33d8069 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -67,7 +67,6 @@ import static android.view.WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER;
import static android.view.WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED;
import static android.view.WindowManager.LayoutParams.FLAG_SLIPPERY;
import static android.view.WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL;
-import static android.view.WindowManager.LayoutParams.INPUT_FEATURE_RECEIVE_POWER_KEY_DOUBLE_PRESS;
import static android.view.WindowManager.LayoutParams.INPUT_FEATURE_SENSITIVE_FOR_PRIVACY;
import static android.view.WindowManager.LayoutParams.INPUT_FEATURE_SPY;
import static android.view.WindowManager.LayoutParams.INVALID_WINDOW_TYPE;
@@ -99,7 +98,6 @@ import static android.view.displayhash.DisplayHashResultCallback.DISPLAY_HASH_ER
import static android.view.flags.Flags.sensitiveContentAppProtection;
import static android.window.WindowProviderService.isWindowProviderService;
-import static com.android.hardware.input.Flags.overridePowerKeyBehaviorInFocusedWindow;
import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_ADD_REMOVE;
import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_ANIM;
import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_BOOT;
@@ -3163,6 +3161,11 @@ public class WindowManagerService extends IWindowManager.Stub
// Reparent the window created for this window context.
dc.reParentWindowToken(token);
hideUntilNextDraw(token);
+ // Prevent a race condition where VRI temporarily reverts the context display ID
+ // before the onDisplayMoved callback arrives. This caused incorrect display IDs
+ // during configuration changes, breaking SysUI layouts dependent on it.
+ // Forcing a resize report ensures VRI has the correct ID before the update.
+ forceReportResizing(token);
// This makes sure there is a traversal scheduled that will eventually report
// the window resize to the client.
dc.setLayoutNeeded();
@@ -3184,6 +3187,14 @@ public class WindowManagerService extends IWindowManager.Stub
}
}
+ private void forceReportResizing(@NonNull WindowContainer<?> wc) {
+ wc.forAllWindows(w -> {
+ if (!mResizingWindows.contains(w)) {
+ mResizingWindows.add(w);
+ }
+ }, true /* traverseTopToBottom */);
+ }
+
private void hideUntilNextDraw(@NonNull WindowToken token) {
final WindowState topChild = token.getTopChild();
if (topChild != null) {
@@ -6233,6 +6244,10 @@ public class WindowManagerService extends IWindowManager.Stub
final long ident = Binder.clearCallingIdentity();
try {
synchronized (mGlobalLock) {
+ // Clear forced display density ratio
+ setForcedDisplayDensityRatioInternal(displayId, 0.0f, userId);
+
+ // Clear forced display density
final DisplayContent displayContent = mRoot.getDisplayContent(displayId);
if (displayContent != null) {
displayContent.setForcedDensity(displayContent.getInitialDisplayDensity(),
@@ -6257,6 +6272,37 @@ public class WindowManagerService extends IWindowManager.Stub
@EnforcePermission(android.Manifest.permission.WRITE_SECURE_SETTINGS)
@Override
+ public void setForcedDisplayDensityRatio(int displayId, float ratio, int userId) {
+ setForcedDisplayDensityRatio_enforcePermission();
+ final long ident = Binder.clearCallingIdentity();
+ try {
+ synchronized (mGlobalLock) {
+ setForcedDisplayDensityRatioInternal(displayId, ratio, userId);
+ }
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
+ }
+
+ private void setForcedDisplayDensityRatioInternal(
+ int displayId, float ratio, int userId) {
+ final DisplayContent displayContent = mRoot.getDisplayContent(displayId);
+ if (displayContent != null) {
+ displayContent.setForcedDensityRatio(ratio, userId);
+ return;
+ }
+
+ final DisplayInfo info = mDisplayManagerInternal.getDisplayInfo(displayId);
+ if (info == null) {
+ ProtoLog.e(WM_ERROR, "Failed to get information about logical display %d. "
+ + "Skip setting forced display density.", displayId);
+ return;
+ }
+ mDisplayWindowSettings.setForcedDensityRatio(info, ratio);
+ }
+
+ @EnforcePermission(android.Manifest.permission.WRITE_SECURE_SETTINGS)
+ @Override
public void setConfigurationChangeSettingsForUser(
@NonNull List<ConfigurationChangeSetting> settings, int userId) {
setConfigurationChangeSettingsForUser_enforcePermission();
@@ -9241,25 +9287,6 @@ public class WindowManagerService extends IWindowManager.Stub
+ "' because it isn't a trusted overlay");
return inputFeatures & ~INPUT_FEATURE_SENSITIVE_FOR_PRIVACY;
}
-
- // You need OVERRIDE_SYSTEM_KEY_BEHAVIOR_IN_FOCUSED_WINDOW permission to be able
- // to set INPUT_FEATURE_RECEIVE_POWER_KEY_DOUBLE_PRESS.
- if (overridePowerKeyBehaviorInFocusedWindow()
- && (inputFeatures
- & INPUT_FEATURE_RECEIVE_POWER_KEY_DOUBLE_PRESS)
- != 0) {
- final int powerPermissionResult =
- mContext.checkPermission(
- permission.OVERRIDE_SYSTEM_KEY_BEHAVIOR_IN_FOCUSED_WINDOW,
- callingPid,
- callingUid);
- if (powerPermissionResult != PackageManager.PERMISSION_GRANTED) {
- throw new IllegalArgumentException(
- "Cannot use INPUT_FEATURE_RECEIVE_POWER_KEY_DOUBLE_PRESS from" + windowName
- + " because it doesn't have the"
- + " OVERRIDE_SYSTEM_KEY_BEHAVIOR_IN_FOCUSED_WINDOW permission");
- }
- }
return inputFeatures;
}
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index a03b765cae6a..93876f5eeed4 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -169,6 +169,7 @@ import static com.android.server.wm.WindowStateProto.IS_READY_FOR_DISPLAY;
import static com.android.server.wm.WindowStateProto.IS_VISIBLE;
import static com.android.server.wm.WindowStateProto.KEEP_CLEAR_AREAS;
import static com.android.server.wm.WindowStateProto.MERGED_LOCAL_INSETS_SOURCES;
+import static com.android.server.wm.WindowStateProto.PREPARE_SYNC_SEQ_ID;
import static com.android.server.wm.WindowStateProto.REMOVED;
import static com.android.server.wm.WindowStateProto.REMOVE_ON_EXIT;
import static com.android.server.wm.WindowStateProto.REQUESTED_HEIGHT;
@@ -177,6 +178,7 @@ import static com.android.server.wm.WindowStateProto.REQUESTED_WIDTH;
import static com.android.server.wm.WindowStateProto.STACK_ID;
import static com.android.server.wm.WindowStateProto.SURFACE_INSETS;
import static com.android.server.wm.WindowStateProto.SURFACE_POSITION;
+import static com.android.server.wm.WindowStateProto.SYNC_SEQ_ID;
import static com.android.server.wm.WindowStateProto.UNRESTRICTED_KEEP_CLEAR_AREAS;
import static com.android.server.wm.WindowStateProto.VIEW_VISIBILITY;
import static com.android.server.wm.WindowStateProto.WINDOW_CONTAINER;
@@ -3945,6 +3947,8 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
dimBounds.dumpDebug(proto, DIM_BOUNDS);
}
}
+ proto.write(SYNC_SEQ_ID, mSyncSeqId);
+ proto.write(PREPARE_SYNC_SEQ_ID, mPrepareSyncSeqId);
proto.end(token);
}
@@ -5507,10 +5511,9 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
|| mKeyInterceptionInfo.layoutParamsPrivateFlags != mAttrs.privateFlags
|| mKeyInterceptionInfo.layoutParamsType != mAttrs.type
|| mKeyInterceptionInfo.windowTitle != getWindowTag()
- || mKeyInterceptionInfo.windowOwnerUid != getOwningUid()
- || mKeyInterceptionInfo.inputFeaturesFlags != mAttrs.inputFeatures) {
+ || mKeyInterceptionInfo.windowOwnerUid != getOwningUid()) {
mKeyInterceptionInfo = new KeyInterceptionInfo(mAttrs.type, mAttrs.privateFlags,
- getWindowTag().toString(), getOwningUid(), mAttrs.inputFeatures);
+ getWindowTag().toString(), getOwningUid());
}
return mKeyInterceptionInfo;
}
diff --git a/services/core/java/com/android/server/wm/WindowToken.java b/services/core/java/com/android/server/wm/WindowToken.java
index cca73c574951..7823b92e4057 100644
--- a/services/core/java/com/android/server/wm/WindowToken.java
+++ b/services/core/java/com/android/server/wm/WindowToken.java
@@ -134,15 +134,6 @@ class WindowToken extends WindowContainer<WindowState> {
}
/**
- * Transforms the window container from the next rotation to the current rotation for
- * showing the window in a display with different rotation.
- */
- void transform(WindowContainer<?> container) {
- // The default implementation assumes shell transition is enabled, so the transform
- // is done by getOrCreateFixedRotationLeash().
- }
-
- /**
* Resets the transformation of the window containers which have been rotated. This should
* be called when the window has the same rotation as display.
*/
@@ -158,45 +149,6 @@ class WindowToken extends WindowContainer<WindowState> {
}
}
- private static class FixedRotationTransformStateLegacy extends FixedRotationTransformState {
- final SeamlessRotator mRotator;
- final ArrayList<WindowContainer<?>> mRotatedContainers = new ArrayList<>(3);
-
- FixedRotationTransformStateLegacy(DisplayInfo rotatedDisplayInfo,
- DisplayFrames rotatedDisplayFrames, Configuration rotatedConfig,
- int currentRotation) {
- super(rotatedDisplayInfo, rotatedDisplayFrames, rotatedConfig);
- // This will use unrotate as rotate, so the new and old rotation are inverted.
- mRotator = new SeamlessRotator(rotatedDisplayInfo.rotation, currentRotation,
- rotatedDisplayInfo, true /* applyFixedTransformationHint */);
- }
-
- @Override
- void transform(WindowContainer<?> container) {
- mRotator.unrotate(container.getPendingTransaction(), container);
- if (!mRotatedContainers.contains(container)) {
- mRotatedContainers.add(container);
- }
- }
-
- @Override
- void resetTransform() {
- for (int i = mRotatedContainers.size() - 1; i >= 0; i--) {
- final WindowContainer<?> c = mRotatedContainers.get(i);
- // If the window is detached (no parent), its surface may have been released.
- if (c.getParent() != null) {
- mRotator.finish(c.getPendingTransaction(), c);
- }
- }
- }
-
- @Override
- void disassociate(WindowToken token) {
- super.disassociate(token);
- mRotatedContainers.remove(token);
- }
- }
-
/**
* Compares two child window of this token and returns -1 if the first is lesser than the
* second in terms of z-order and 1 otherwise.
@@ -494,10 +446,7 @@ class WindowToken extends WindowContainer<WindowState> {
mFixedRotationTransformState.disassociate(this);
}
config = new Configuration(config);
- mFixedRotationTransformState = mTransitionController.isShellTransitionsEnabled()
- ? new FixedRotationTransformState(info, displayFrames, config)
- : new FixedRotationTransformStateLegacy(info, displayFrames, config,
- mDisplayContent.getRotation());
+ mFixedRotationTransformState = new FixedRotationTransformState(info, displayFrames, config);
mFixedRotationTransformState.mAssociatedTokens.add(this);
mDisplayContent.getDisplayPolicy().simulateLayoutDisplay(displayFrames);
onFixedRotationStatePrepared();
@@ -699,15 +648,6 @@ class WindowToken extends WindowContainer<WindowState> {
return;
}
super.updateSurfacePosition(t);
- if (!mTransitionController.isShellTransitionsEnabled() && isFixedRotationTransforming()) {
- final Task rootTask = r != null ? r.getRootTask() : null;
- // Don't transform the activity in PiP because the PiP task organizer will handle it.
- if (rootTask == null || !rootTask.inPinnedWindowingMode()) {
- // The window is laid out in a simulated rotated display but the real display hasn't
- // rotated, so here transforms its surface to fit in the real display.
- mFixedRotationTransformState.transform(this);
- }
- }
}
@Override
diff --git a/services/core/jni/Android.bp b/services/core/jni/Android.bp
index adfabe1e54fd..e49e60632d0e 100644
--- a/services/core/jni/Android.bp
+++ b/services/core/jni/Android.bp
@@ -191,7 +191,7 @@ cc_defaults {
"android.hardware.thermal@1.0",
"android.hardware.thermal-V3-ndk",
"android.hardware.tv.input@1.0",
- "android.hardware.tv.input-V2-ndk",
+ "android.hardware.tv.input-V3-ndk",
"android.hardware.vibrator-V3-ndk",
"android.hardware.vr@1.0",
"android.hidl.token@1.0-utils",
diff --git a/services/core/jni/com_android_server_input_InputManagerService.cpp b/services/core/jni/com_android_server_input_InputManagerService.cpp
index e29511564cea..ee7c9368f897 100644
--- a/services/core/jni/com_android_server_input_InputManagerService.cpp
+++ b/services/core/jni/com_android_server_input_InputManagerService.cpp
@@ -2926,6 +2926,12 @@ static void nativeReloadDeviceAliases(JNIEnv* env, jobject nativeImplObj) {
InputReaderConfiguration::Change::DEVICE_ALIAS);
}
+static jstring nativeGetSysfsRootPath(JNIEnv* env, jobject nativeImplObj, jint deviceId) {
+ NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
+ const auto path = im->getInputManager()->getReader().getSysfsRootPath(deviceId);
+ return path.empty() ? nullptr : env->NewStringUTF(path.c_str());
+}
+
static void nativeSysfsNodeChanged(JNIEnv* env, jobject nativeImplObj, jstring path) {
ScopedUtfChars sysfsNodePathChars(env, path);
const std::string sysfsNodePath = sysfsNodePathChars.c_str();
@@ -3388,6 +3394,7 @@ static const JNINativeMethod gInputManagerMethods[] = {
{"getBatteryDevicePath", "(I)Ljava/lang/String;", (void*)nativeGetBatteryDevicePath},
{"reloadKeyboardLayouts", "()V", (void*)nativeReloadKeyboardLayouts},
{"reloadDeviceAliases", "()V", (void*)nativeReloadDeviceAliases},
+ {"getSysfsRootPath", "(I)Ljava/lang/String;", (void*)nativeGetSysfsRootPath},
{"sysfsNodeChanged", "(Ljava/lang/String;)V", (void*)nativeSysfsNodeChanged},
{"dump", "()Ljava/lang/String;", (void*)nativeDump},
{"monitor", "()V", (void*)nativeMonitor},
diff --git a/services/core/jni/com_android_server_tv_TvInputHal.cpp b/services/core/jni/com_android_server_tv_TvInputHal.cpp
index 1e6384031f9a..def95daea92d 100644
--- a/services/core/jni/com_android_server_tv_TvInputHal.cpp
+++ b/services/core/jni/com_android_server_tv_TvInputHal.cpp
@@ -91,6 +91,12 @@ static int nativeSetTvMessageEnabled(JNIEnv* env, jclass clazz, jlong ptr, jint
return tvInputHal->setTvMessageEnabled(deviceId, streamId, type, enabled);
}
+static int nativeSetPictureProfile(JNIEnv* env, jclass clazz, jlong ptr, jint deviceId,
+ jint streamId, jlong profileHandle) {
+ JTvInputHal* tvInputHal = (JTvInputHal*)ptr;
+ return tvInputHal->setPictureProfileId(deviceId, streamId, profileHandle);
+}
+
static void nativeClose(JNIEnv* env, jclass clazz, jlong ptr) {
JTvInputHal* tvInputHal = (JTvInputHal*)ptr;
delete tvInputHal;
@@ -104,6 +110,7 @@ static const JNINativeMethod gTvInputHalMethods[] = {
{"nativeGetStreamConfigs", "(JII)[Landroid/media/tv/TvStreamConfig;",
(void*)nativeGetStreamConfigs},
{"nativeSetTvMessageEnabled", "(JIIIZ)I", (void*)nativeSetTvMessageEnabled},
+ {"nativeSetPictureProfile", "(JIIJ)I", (void*)nativeSetPictureProfile},
{"nativeClose", "(J)V", (void*)nativeClose},
};
diff --git a/services/core/jni/gnss/GnssAssistance.cpp b/services/core/jni/gnss/GnssAssistance.cpp
index fff396ea126a..e97c7c340e40 100644
--- a/services/core/jni/gnss/GnssAssistance.cpp
+++ b/services/core/jni/gnss/GnssAssistance.cpp
@@ -206,7 +206,7 @@ jmethodID method_beidouSatelliteClockModelGetTgd2;
jmethodID method_beidouSatelliteClockModelGetTimeOfClockSeconds;
jmethodID method_beidouSatelliteHealthGetSatH1;
jmethodID method_beidouSatelliteHealthGetSvAccur;
-jmethodID method_beidouSatelliteEphemerisTimeGetIode;
+jmethodID method_beidouSatelliteEphemerisTimeGetAode;
jmethodID method_beidouSatelliteEphemerisTimeGetBeidouWeekNumber;
jmethodID method_beidouSatelliteEphemerisTimeGetToeSeconds;
@@ -710,8 +710,8 @@ void GnssAssistance_class_init_once(JNIEnv* env, jclass clazz) {
// Get the methods of BeidouSatelliteEphemerisTime
jclass beidouSatelliteEphemerisTimeClass = env->FindClass(
"android/location/BeidouSatelliteEphemeris$BeidouSatelliteEphemerisTime");
- method_beidouSatelliteEphemerisTimeGetIode =
- env->GetMethodID(beidouSatelliteEphemerisTimeClass, "getIode", "()I");
+ method_beidouSatelliteEphemerisTimeGetAode =
+ env->GetMethodID(beidouSatelliteEphemerisTimeClass, "getAode", "()I");
method_beidouSatelliteEphemerisTimeGetBeidouWeekNumber =
env->GetMethodID(beidouSatelliteEphemerisTimeClass, "getBeidouWeekNumber", "()I");
method_beidouSatelliteEphemerisTimeGetToeSeconds =
@@ -723,7 +723,7 @@ void GnssAssistance_class_init_once(JNIEnv* env, jclass clazz) {
"()Landroid/location/GnssAlmanac;");
method_galileoAssistanceGetIonosphericModel =
env->GetMethodID(galileoAssistanceClass, "getIonosphericModel",
- "()Landroid/location/KlobucharIonosphericModel;");
+ "()Landroid/location/GalileoIonosphericModel;");
method_galileoAssistanceGetUtcModel = env->GetMethodID(galileoAssistanceClass, "getUtcModel",
"()Landroid/location/UtcModel;");
method_galileoAssistanceGetLeapSecondsModel =
@@ -1244,8 +1244,7 @@ void GnssAssistanceUtil::setGalileoAssistance(JNIEnv* env, jobject galileoAssist
env->CallObjectMethod(galileoAssistanceObj,
method_galileoAssistanceGetSatelliteCorrections);
setGnssAlmanac(env, galileoAlmanacObj, galileoAssistance.almanac);
- setGaliloKlobucharIonosphericModel(env, ionosphericModelObj,
- galileoAssistance.ionosphericModel);
+ setGalileoIonosphericModel(env, ionosphericModelObj, galileoAssistance.ionosphericModel);
setUtcModel(env, utcModelObj, galileoAssistance.utcModel);
setLeapSecondsModel(env, leapSecondsModelObj, galileoAssistance.leapSecondsModel);
setTimeModels(env, timeModelsObj, galileoAssistance.timeModels);
@@ -1263,9 +1262,8 @@ void GnssAssistanceUtil::setGalileoAssistance(JNIEnv* env, jobject galileoAssist
env->DeleteLocalRef(satelliteCorrectionsObj);
}
-void GnssAssistanceUtil::setGaliloKlobucharIonosphericModel(
- JNIEnv* env, jobject galileoIonosphericModelObj,
- GalileoIonosphericModel& ionosphericModel) {
+void GnssAssistanceUtil::setGalileoIonosphericModel(JNIEnv* env, jobject galileoIonosphericModelObj,
+ GalileoIonosphericModel& ionosphericModel) {
if (galileoIonosphericModelObj == nullptr) return;
jdouble ai0 =
env->CallDoubleMethod(galileoIonosphericModelObj, method_galileoIonosphericModelGetAi0);
@@ -1500,14 +1498,14 @@ void GnssAssistanceUtil::setBeidouSatelliteEphemeris(
jobject satelliteEphemerisTimeObj =
env->CallObjectMethod(beidouSatelliteEphemerisObj,
method_beidouSatelliteEphemerisGetSatelliteEphemerisTime);
- jint iode = env->CallIntMethod(satelliteEphemerisTimeObj,
- method_beidouSatelliteEphemerisTimeGetIode);
+ jint aode = env->CallIntMethod(satelliteEphemerisTimeObj,
+ method_beidouSatelliteEphemerisTimeGetAode);
jint beidouWeekNumber =
env->CallIntMethod(satelliteEphemerisTimeObj,
method_beidouSatelliteEphemerisTimeGetBeidouWeekNumber);
jint toeSeconds = env->CallDoubleMethod(satelliteEphemerisTimeObj,
method_beidouSatelliteEphemerisTimeGetToeSeconds);
- beidouSatelliteEphemeris.satelliteEphemerisTime.aode = static_cast<int32_t>(iode);
+ beidouSatelliteEphemeris.satelliteEphemerisTime.aode = static_cast<int32_t>(aode);
beidouSatelliteEphemeris.satelliteEphemerisTime.weekNumber =
static_cast<int32_t>(beidouWeekNumber);
beidouSatelliteEphemeris.satelliteEphemerisTime.toeSeconds =
diff --git a/services/core/jni/gnss/GnssAssistance.h b/services/core/jni/gnss/GnssAssistance.h
index ee97e19371f8..968e661fbaed 100644
--- a/services/core/jni/gnss/GnssAssistance.h
+++ b/services/core/jni/gnss/GnssAssistance.h
@@ -94,8 +94,8 @@ struct GnssAssistanceUtil {
static void setGalileoSatelliteEphemeris(
JNIEnv* env, jobject galileoSatelliteEphemerisObj,
std::vector<GalileoSatelliteEphemeris>& galileoSatelliteEphemerisList);
- static void setGaliloKlobucharIonosphericModel(JNIEnv* env, jobject galileoIonosphericModelObj,
- GalileoIonosphericModel& ionosphericModel);
+ static void setGalileoIonosphericModel(JNIEnv* env, jobject galileoIonosphericModelObj,
+ GalileoIonosphericModel& ionosphericModel);
static void setGnssAssistance(JNIEnv* env, jobject gnssAssistanceObj,
GnssAssistance& gnssAssistance);
static void setGpsAssistance(JNIEnv* env, jobject gpsAssistanceObj,
diff --git a/services/core/jni/tvinput/JTvInputHal.cpp b/services/core/jni/tvinput/JTvInputHal.cpp
index 505421e81d3d..e4821299eee9 100644
--- a/services/core/jni/tvinput/JTvInputHal.cpp
+++ b/services/core/jni/tvinput/JTvInputHal.cpp
@@ -156,6 +156,15 @@ int JTvInputHal::setTvMessageEnabled(int deviceId, int streamId, int type, bool
return NO_ERROR;
}
+int JTvInputHal::setPictureProfileId(int deviceId, int streamId, long profileHandle) {
+ ::ndk::ScopedAStatus status = mTvInput->setPictureProfileId(deviceId, streamId, profileHandle);
+ if (!status.isOk()) {
+ ALOGE("Error in setPictureProfileId. device id:%d stream id:%d", deviceId, streamId);
+ return status.getStatus();
+ }
+ return NO_ERROR;
+}
+
const std::vector<AidlTvStreamConfig> JTvInputHal::getStreamConfigs(int deviceId) {
std::vector<AidlTvStreamConfig> list;
::ndk::ScopedAStatus status = mTvInput->getStreamConfigurations(deviceId, &list);
@@ -551,6 +560,16 @@ JTvInputHal::ITvInputWrapper::ITvInputWrapper(std::shared_ptr<AidlITvInput>& aid
}
}
+::ndk::ScopedAStatus JTvInputHal::ITvInputWrapper::setPictureProfileId(int32_t deviceId,
+ int32_t streamId,
+ long profileHandle) {
+ if (mIsHidl) {
+ return ::ndk::ScopedAStatus::fromExceptionCode(EX_UNSUPPORTED_OPERATION);
+ } else {
+ return mAidlTvInput->setPictureProfileId(deviceId, streamId, profileHandle);
+ }
+}
+
::ndk::ScopedAStatus JTvInputHal::ITvInputWrapper::getTvMessageQueueDesc(
MQDescriptor<int8_t, SynchronizedReadWrite>* out_queue, int32_t in_deviceId,
int32_t in_streamId) {
diff --git a/services/core/jni/tvinput/JTvInputHal.h b/services/core/jni/tvinput/JTvInputHal.h
index 2ef94ac4a3b0..4481f1d37c2b 100644
--- a/services/core/jni/tvinput/JTvInputHal.h
+++ b/services/core/jni/tvinput/JTvInputHal.h
@@ -85,6 +85,7 @@ public:
int addOrUpdateStream(int deviceId, int streamId, const sp<Surface>& surface);
int setTvMessageEnabled(int deviceId, int streamId, int type, bool enabled);
+ int setPictureProfileId(int deviceId, int streamId, long profileHandle);
int removeStream(int deviceId, int streamId);
const std::vector<AidlTvStreamConfig> getStreamConfigs(int deviceId);
@@ -208,6 +209,7 @@ private:
::ndk::ScopedAStatus closeStream(int32_t in_deviceId, int32_t in_streamId);
::ndk::ScopedAStatus setTvMessageEnabled(int32_t deviceId, int32_t streamId,
TvMessageEventType in_type, bool enabled);
+ ::ndk::ScopedAStatus setPictureProfileId(int deviceId, int streamId, long profileHandle);
::ndk::ScopedAStatus getTvMessageQueueDesc(
MQDescriptor<int8_t, SynchronizedReadWrite>* out_queue, int32_t in_deviceId,
int32_t in_streamId);
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 3d740e531e14..a2d6510007bc 100644
--- a/services/credentials/java/com/android/server/credentials/metrics/RequestSessionMetric.java
+++ b/services/credentials/java/com/android/server/credentials/metrics/RequestSessionMetric.java
@@ -268,7 +268,7 @@ public class RequestSessionMetric {
* @param createOrCredentialType the string type to collect when an entry is tapped by the user
*/
public void collectChosenClassType(String createOrCredentialType) {
- String truncatedType = generateMetricKey(createOrCredentialType, DELTA_EXCEPTION_CUT);
+ String truncatedType = generateMetricKey(createOrCredentialType, DELTA_RESPONSES_CUT);
try {
mChosenProviderFinalPhaseMetric.setChosenClassType(truncatedType);
} catch (Exception e) {
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index d984fcc599cb..964826d1ee73 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -23903,10 +23903,9 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub {
UserHandle.USER_ALL);
synchronized (getLockObject()) {
- final EnforcingAdmin admin = enforcePermissionAndGetEnforcingAdmin(null,
- MANAGE_DEVICE_POLICY_MTE, callerPackageName, caller.getUserId());
- final Integer policyFromAdmin = mDevicePolicyEngine.getGlobalPolicySetByAdmin(
- PolicyDefinition.MEMORY_TAGGING, admin);
+ final Integer policyFromAdmin = mDevicePolicyEngine.getResolvedPolicy(
+ PolicyDefinition.MEMORY_TAGGING, UserHandle.USER_ALL);
+
return (policyFromAdmin != null ? policyFromAdmin
: DevicePolicyManager.MTE_NOT_CONTROLLED_BY_POLICY);
}
diff --git a/services/java/com/android/server/flags.aconfig b/services/java/com/android/server/flags.aconfig
index 7a6bd75e5893..f864b6b8c768 100644
--- a/services/java/com/android/server/flags.aconfig
+++ b/services/java/com/android/server/flags.aconfig
@@ -31,6 +31,13 @@ flag {
}
flag {
+ namespace: "system_performance"
+ name: "enable_theme_service"
+ description: "Switches from SystemUi's ThemeOverlayController to Server's ThemeService."
+ bug: "333694176"
+}
+
+flag {
name: "allow_removing_vpn_service"
namespace: "wear_frameworks"
description: "Allow removing VpnManagerService"
diff --git a/services/permission/java/com/android/server/permission/access/AccessPolicy.kt b/services/permission/java/com/android/server/permission/access/AccessPolicy.kt
index 1bb395441587..410539c8c5d0 100644
--- a/services/permission/java/com/android/server/permission/access/AccessPolicy.kt
+++ b/services/permission/java/com/android/server/permission/access/AccessPolicy.kt
@@ -431,7 +431,7 @@ private constructor(
companion object {
private val LOG_TAG = AccessPolicy::class.java.simpleName
- internal const val VERSION_LATEST = 16
+ internal const val VERSION_LATEST = 17
private const val TAG_ACCESS = "access"
private const val TAG_DEFAULT_PERMISSION_GRANT = "default-permission-grant"
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 662e0c06f261..f4d7a8ec5484 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
@@ -21,6 +21,7 @@ import android.content.pm.PackageManager
import android.content.pm.PermissionGroupInfo
import android.content.pm.PermissionInfo
import android.content.pm.SigningDetails
+import android.health.connect.HealthPermissions
import android.os.Build
import android.permission.flags.Flags
import android.util.Slog
@@ -112,7 +113,6 @@ class AppIdPermissionPolicy : SchemePolicy() {
addPermissions(packageState, changedPermissionNames)
trimPermissions(packageState.packageName, changedPermissionNames)
trimPermissionStates(packageState.appId)
- revokePermissionsOnPackageUpdate(packageState.appId)
}
changedPermissionNames.forEachIndexed { _, permissionName ->
evaluatePermissionStateForAllPackages(permissionName, null)
@@ -130,6 +130,7 @@ class AppIdPermissionPolicy : SchemePolicy() {
newState.externalState.userIds.forEachIndexed { _, userId ->
inheritImplicitPermissionStates(packageState.appId, userId)
}
+ revokePermissionsOnPackageUpdate(packageState.appId)
}
}
@@ -140,7 +141,6 @@ class AppIdPermissionPolicy : SchemePolicy() {
addPermissions(packageState, changedPermissionNames)
trimPermissions(packageState.packageName, changedPermissionNames)
trimPermissionStates(packageState.appId)
- revokePermissionsOnPackageUpdate(packageState.appId)
changedPermissionNames.forEachIndexed { _, permissionName ->
evaluatePermissionStateForAllPackages(permissionName, null)
}
@@ -148,6 +148,7 @@ class AppIdPermissionPolicy : SchemePolicy() {
newState.externalState.userIds.forEachIndexed { _, userId ->
inheritImplicitPermissionStates(packageState.appId, userId)
}
+ revokePermissionsOnPackageUpdate(packageState.appId)
}
override fun MutateStateScope.onPackageRemoved(packageName: String, appId: Int) {
@@ -700,6 +701,11 @@ class AppIdPermissionPolicy : SchemePolicy() {
}
private fun MutateStateScope.revokePermissionsOnPackageUpdate(appId: Int) {
+ revokeStorageAndMediaPermissionsOnPackageUpdate(appId)
+ revokeHeartRatePermissionsOnPackageUpdate(appId)
+ }
+
+ private fun MutateStateScope.revokeStorageAndMediaPermissionsOnPackageUpdate(appId: Int) {
val hasOldPackage =
appId in oldState.externalState.appIdPackageNames &&
anyPackageInAppId(appId, oldState) { true }
@@ -747,23 +753,154 @@ class AppIdPermissionPolicy : SchemePolicy() {
// SYSTEM_FIXED. Otherwise the user cannot grant back the permission.
if (
permissionName in STORAGE_AND_MEDIA_PERMISSIONS &&
- oldFlags.hasBits(PermissionFlags.RUNTIME_GRANTED) &&
- !oldFlags.hasAnyBit(SYSTEM_OR_POLICY_FIXED_MASK)
+ oldFlags.hasBits(PermissionFlags.RUNTIME_GRANTED)
) {
- Slog.v(
- LOG_TAG,
- "Revoking storage permission: $permissionName for appId: " +
- " $appId and user: $userId",
+ revokeRuntimePermission(appId, userId, permissionName)
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * If the app is updated, the legacy BODY_SENSOR and READ_HEART_RATE permissions may go out of
+ * sync (for example, when the app eventually requests the implicit new permission). If this
+ * occurs, revoke both permissions to force a re-prompt.
+ */
+ private fun MutateStateScope.revokeHeartRatePermissionsOnPackageUpdate(appId: Int) {
+ val targetSdkVersion = getAppIdTargetSdkVersion(appId, null)
+ // Apps targeting BAKLAVA and above shouldn't be using BODY_SENSORS.
+ if (targetSdkVersion >= Build.VERSION_CODES.BAKLAVA) {
+ return
+ }
+
+ val isBodySensorsRequested =
+ anyPackageInAppId(appId, newState) {
+ Manifest.permission.BODY_SENSORS in it.androidPackage!!.requestedPermissions
+ }
+ val isReadHeartRateRequested =
+ anyPackageInAppId(appId, newState) {
+ HealthPermissions.READ_HEART_RATE in it.androidPackage!!.requestedPermissions
+ }
+ val isBodySensorsBackgroundRequested =
+ anyPackageInAppId(appId, newState) {
+ Manifest.permission.BODY_SENSORS_BACKGROUND in
+ it.androidPackage!!.requestedPermissions
+ }
+ val isReadHealthDataInBackgroundRequested =
+ anyPackageInAppId(appId, newState) {
+ HealthPermissions.READ_HEALTH_DATA_IN_BACKGROUND in
+ it.androidPackage!!.requestedPermissions
+ }
+
+ // Walk the list of user IDs and revoke states as needed.
+ newState.userStates.forEachIndexed { _, userId, _ ->
+ // First sync BODY_SENSORS and READ_HEART_RATE, if required.
+ var isBodySensorsGranted =
+ isRuntimePermissionGranted(appId, userId, Manifest.permission.BODY_SENSORS)
+ if (isBodySensorsRequested && isReadHeartRateRequested) {
+ val isReadHeartRateGranted =
+ isRuntimePermissionGranted(appId, userId, HealthPermissions.READ_HEART_RATE)
+ if (isBodySensorsGranted != isReadHeartRateGranted) {
+ if (isBodySensorsGranted) {
+ if (
+ revokeRuntimePermission(appId, userId, Manifest.permission.BODY_SENSORS)
+ ) {
+ isBodySensorsGranted = false
+ }
+ }
+ if (isReadHeartRateGranted) {
+ revokeRuntimePermission(appId, userId, HealthPermissions.READ_HEART_RATE)
+ }
+ }
+ }
+
+ // Then check to ensure we haven't put the background/foreground permissions out of
+ // sync.
+ var isBodySensorsBackgroundGranted =
+ isRuntimePermissionGranted(
+ appId,
+ userId,
+ Manifest.permission.BODY_SENSORS_BACKGROUND,
+ )
+ if (isBodySensorsBackgroundGranted && !isBodySensorsGranted) {
+ if (
+ revokeRuntimePermission(
+ appId,
+ userId,
+ Manifest.permission.BODY_SENSORS_BACKGROUND,
+ )
+ ) {
+ isBodySensorsBackgroundGranted = false
+ }
+ }
+
+ // Finally sync BODY_SENSORS_BACKGROUND and READ_HEALTH_DATA_IN_BACKGROUND, if required.
+ if (isBodySensorsBackgroundRequested && isReadHealthDataInBackgroundRequested) {
+ val isReadHealthDataInBackgroundGranted =
+ isRuntimePermissionGranted(
+ appId,
+ userId,
+ HealthPermissions.READ_HEALTH_DATA_IN_BACKGROUND,
+ )
+ if (isBodySensorsBackgroundGranted != isReadHealthDataInBackgroundGranted) {
+ if (isBodySensorsBackgroundGranted) {
+ revokeRuntimePermission(
+ appId,
+ userId,
+ Manifest.permission.BODY_SENSORS_BACKGROUND,
+ )
+ }
+ if (isReadHealthDataInBackgroundGranted) {
+ revokeRuntimePermission(
+ appId,
+ userId,
+ HealthPermissions.READ_HEALTH_DATA_IN_BACKGROUND,
)
- val newFlags =
- oldFlags andInv (PermissionFlags.RUNTIME_GRANTED or USER_SETTABLE_MASK)
- setPermissionFlags(appId, userId, permissionName, newFlags)
}
}
}
}
}
+ private fun GetStateScope.isRuntimePermissionGranted(
+ appId: Int,
+ userId: Int,
+ permissionName: String,
+ ): Boolean {
+ val flags = getPermissionFlags(appId, userId, permissionName)
+ return PermissionFlags.isAppOpGranted(flags)
+ }
+
+ fun MutateStateScope.revokeRuntimePermission(
+ appId: Int,
+ userId: Int,
+ permissionName: String,
+ ): Boolean {
+ Slog.v(
+ LOG_TAG,
+ "Revoking runtime permission for appId: $appId, " +
+ "permission: $permissionName, userId: $userId",
+ )
+ var flags = getPermissionFlags(appId, userId, permissionName)
+ if (flags.hasAnyBit(SYSTEM_OR_POLICY_FIXED_MASK)) {
+ Slog.v(
+ LOG_TAG,
+ "Not allowed to revoke $permissionName for appId: $appId, userId: $userId",
+ )
+ return false
+ }
+
+ flags =
+ flags andInv
+ (PermissionFlags.RUNTIME_GRANTED or
+ USER_SETTABLE_MASK or
+ PermissionFlags.PREGRANT or
+ PermissionFlags.ROLE)
+ setPermissionFlags(appId, userId, permissionName, flags)
+ return true
+ }
+
private fun MutateStateScope.evaluatePermissionStateForAllPackages(
permissionName: String,
installedPackageState: PackageState?,
@@ -1751,13 +1888,6 @@ 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/permission/java/com/android/server/permission/access/permission/AppIdPermissionUpgrade.kt b/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionUpgrade.kt
index a4546aebef21..e3e965de4559 100644
--- a/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionUpgrade.kt
+++ b/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionUpgrade.kt
@@ -17,8 +17,10 @@
package com.android.server.permission.access.permission
import android.Manifest
+import android.health.connect.HealthPermissions
import android.os.Build
import android.util.Slog
+import com.android.server.permission.access.GetStateScope
import com.android.server.permission.access.MutateStateScope
import com.android.server.permission.access.immutable.* // ktlint-disable no-wildcard-imports
import com.android.server.permission.access.util.andInv
@@ -36,14 +38,14 @@ class AppIdPermissionUpgrade(private val policy: AppIdPermissionPolicy) {
fun MutateStateScope.upgradePackageState(
packageState: PackageState,
userId: Int,
- version: Int
+ version: Int,
) {
val packageName = packageState.packageName
if (version <= 3) {
Slog.v(
LOG_TAG,
"Allowlisting and upgrading background location permission for " +
- "package: $packageName, version: $version, user:$userId"
+ "package: $packageName, version: $version, user:$userId",
)
allowlistRestrictedPermissions(packageState, userId)
upgradeBackgroundLocationPermission(packageState, userId)
@@ -52,7 +54,7 @@ class AppIdPermissionUpgrade(private val policy: AppIdPermissionPolicy) {
Slog.v(
LOG_TAG,
"Upgrading access media location permission for package: $packageName" +
- ", version: $version, user: $userId"
+ ", version: $version, user: $userId",
)
upgradeAccessMediaLocationPermission(packageState, userId)
}
@@ -61,27 +63,37 @@ class AppIdPermissionUpgrade(private val policy: AppIdPermissionPolicy) {
Slog.v(
LOG_TAG,
"Upgrading scoped media and body sensor permissions for package: $packageName" +
- ", version: $version, user: $userId"
+ ", version: $version, user: $userId",
)
upgradeAuralVisualMediaPermissions(packageState, userId)
- upgradeBodySensorPermissions(packageState, userId)
+ upgradeBodySensorBackgroundPermissions(packageState, userId)
}
// TODO Enable isAtLeastU check, when moving subsystem to mainline.
if (version <= 14 /*&& SdkLevel.isAtLeastU()*/) {
Slog.v(
LOG_TAG,
"Upgrading visual media permission for package: $packageName" +
- ", version: $version, user: $userId"
+ ", version: $version, user: $userId",
)
upgradeUserSelectedVisualMediaPermission(packageState, userId)
}
+ // TODO Enable isAtLeastB check, when moving subsystem to mainline.
+ if (version <= 16 /*&& SdkLevel.isAtLeastB()*/) {
+ Slog.v(
+ LOG_TAG,
+ "Upgrading body sensor / read heart rate permissions for package: $packageName" +
+ ", version: $version, user: $userId",
+ )
+ upgradeBodySensorReadHeartRatePermissions(packageState, userId)
+ }
+
// Add a new upgrade step: if (packageVersion <= LATEST_VERSION) { .... }
// Also increase LATEST_VERSION
}
private fun MutateStateScope.allowlistRestrictedPermissions(
packageState: PackageState,
- userId: Int
+ userId: Int,
) {
packageState.androidPackage!!.requestedPermissions.forEach { permissionName ->
if (permissionName in LEGACY_RESTRICTED_PERMISSIONS) {
@@ -91,7 +103,7 @@ class AppIdPermissionUpgrade(private val policy: AppIdPermissionPolicy) {
userId,
permissionName,
PermissionFlags.UPGRADE_EXEMPT,
- PermissionFlags.UPGRADE_EXEMPT
+ PermissionFlags.UPGRADE_EXEMPT,
)
}
}
@@ -100,7 +112,7 @@ class AppIdPermissionUpgrade(private val policy: AppIdPermissionPolicy) {
private fun MutateStateScope.upgradeBackgroundLocationPermission(
packageState: PackageState,
- userId: Int
+ userId: Int,
) {
if (
Manifest.permission.ACCESS_BACKGROUND_LOCATION in
@@ -122,7 +134,7 @@ class AppIdPermissionUpgrade(private val policy: AppIdPermissionPolicy) {
grantRuntimePermission(
packageState,
userId,
- Manifest.permission.ACCESS_BACKGROUND_LOCATION
+ Manifest.permission.ACCESS_BACKGROUND_LOCATION,
)
}
}
@@ -130,7 +142,7 @@ class AppIdPermissionUpgrade(private val policy: AppIdPermissionPolicy) {
private fun MutateStateScope.upgradeAccessMediaLocationPermission(
packageState: PackageState,
- userId: Int
+ userId: Int,
) {
if (
Manifest.permission.ACCESS_MEDIA_LOCATION in
@@ -141,14 +153,14 @@ class AppIdPermissionUpgrade(private val policy: AppIdPermissionPolicy) {
getPermissionFlags(
packageState.appId,
userId,
- Manifest.permission.READ_EXTERNAL_STORAGE
+ Manifest.permission.READ_EXTERNAL_STORAGE,
)
}
if (PermissionFlags.isAppOpGranted(flags)) {
grantRuntimePermission(
packageState,
userId,
- Manifest.permission.ACCESS_MEDIA_LOCATION
+ Manifest.permission.ACCESS_MEDIA_LOCATION,
)
}
}
@@ -157,7 +169,7 @@ class AppIdPermissionUpgrade(private val policy: AppIdPermissionPolicy) {
/** Upgrade permissions based on storage permissions grant */
private fun MutateStateScope.upgradeAuralVisualMediaPermissions(
packageState: PackageState,
- userId: Int
+ userId: Int,
) {
val androidPackage = packageState.androidPackage!!
if (androidPackage.targetSdkVersion < Build.VERSION_CODES.TIRAMISU) {
@@ -182,9 +194,9 @@ class AppIdPermissionUpgrade(private val policy: AppIdPermissionPolicy) {
}
}
- private fun MutateStateScope.upgradeBodySensorPermissions(
+ private fun MutateStateScope.upgradeBodySensorBackgroundPermissions(
packageState: PackageState,
- userId: Int
+ userId: Int,
) {
if (
Manifest.permission.BODY_SENSORS_BACKGROUND !in
@@ -221,7 +233,7 @@ class AppIdPermissionUpgrade(private val policy: AppIdPermissionPolicy) {
grantRuntimePermission(
packageState,
userId,
- Manifest.permission.BODY_SENSORS_BACKGROUND
+ Manifest.permission.BODY_SENSORS_BACKGROUND,
)
}
}
@@ -229,7 +241,7 @@ class AppIdPermissionUpgrade(private val policy: AppIdPermissionPolicy) {
/** Upgrade permission based on the grant in [Manifest.permission_group.READ_MEDIA_VISUAL] */
private fun MutateStateScope.upgradeUserSelectedVisualMediaPermission(
packageState: PackageState,
- userId: Int
+ userId: Int,
) {
val androidPackage = packageState.androidPackage!!
if (androidPackage.targetSdkVersion < Build.VERSION_CODES.TIRAMISU) {
@@ -250,21 +262,131 @@ class AppIdPermissionUpgrade(private val policy: AppIdPermissionPolicy) {
grantRuntimePermission(
packageState,
userId,
- Manifest.permission.READ_MEDIA_VISUAL_USER_SELECTED
+ Manifest.permission.READ_MEDIA_VISUAL_USER_SELECTED,
+ )
+ }
+ }
+ }
+
+ /**
+ * Upgrade permissions based on the body sensors and health permissions status.
+ *
+ * Starting in BAKLAVA, the BODY_SENSORS and BODY_SENSORS_BACKGROUND permissions are being
+ * replaced by the READ_HEART_RATE and READ_HEALTH_DATA_IN_BACKGROUND permissions respectively.
+ * To ensure that older apps can continue using BODY_SENSORS without breaking we need to keep
+ * their permission state in sync with the new health permissions.
+ *
+ * The approach we take is to be as conservative as possible. This means if either permission is
+ * not granted, then we want to ensure that both end up not granted to force the user to
+ * re-grant with the expanded scope.
+ */
+ private fun MutateStateScope.upgradeBodySensorReadHeartRatePermissions(
+ packageState: PackageState,
+ userId: Int,
+ ) {
+ val androidPackage = packageState.androidPackage!!
+ if (androidPackage.targetSdkVersion >= Build.VERSION_CODES.BAKLAVA) {
+ return
+ }
+
+ // First sync BODY_SENSORS and READ_HEART_RATE, if required.
+ val isBodySensorsRequested =
+ Manifest.permission.BODY_SENSORS in androidPackage.requestedPermissions
+ val isReadHeartRateRequested =
+ HealthPermissions.READ_HEART_RATE in androidPackage.requestedPermissions
+ var isBodySensorsGranted =
+ isRuntimePermissionGranted(packageState, userId, Manifest.permission.BODY_SENSORS)
+ if (isBodySensorsRequested && isReadHeartRateRequested) {
+ val isReadHeartRateGranted =
+ isRuntimePermissionGranted(packageState, userId, HealthPermissions.READ_HEART_RATE)
+ if (isBodySensorsGranted != isReadHeartRateGranted) {
+ if (isBodySensorsGranted) {
+ if (
+ revokeRuntimePermission(
+ packageState,
+ userId,
+ Manifest.permission.BODY_SENSORS,
+ )
+ ) {
+ isBodySensorsGranted = false
+ }
+ }
+ if (isReadHeartRateGranted) {
+ revokeRuntimePermission(packageState, userId, HealthPermissions.READ_HEART_RATE)
+ }
+ }
+ }
+
+ // Then check to ensure we haven't put the background/foreground permissions out of sync.
+ var isBodySensorsBackgroundGranted =
+ isRuntimePermissionGranted(
+ packageState,
+ userId,
+ Manifest.permission.BODY_SENSORS_BACKGROUND,
+ )
+ // Background permission should not be granted without the foreground permission.
+ if (!isBodySensorsGranted && isBodySensorsBackgroundGranted) {
+ if (
+ revokeRuntimePermission(
+ packageState,
+ userId,
+ Manifest.permission.BODY_SENSORS_BACKGROUND,
+ )
+ ) {
+ isBodySensorsBackgroundGranted = false
+ }
+ }
+
+ // Finally sync BODY_SENSORS_BACKGROUND and READ_HEALTH_DATA_IN_BACKGROUND, if required.
+ val isBodySensorsBackgroundRequested =
+ Manifest.permission.BODY_SENSORS_BACKGROUND in androidPackage.requestedPermissions
+ val isReadHealthDataInBackgroundRequested =
+ HealthPermissions.READ_HEALTH_DATA_IN_BACKGROUND in androidPackage.requestedPermissions
+ if (isBodySensorsBackgroundRequested && isReadHealthDataInBackgroundRequested) {
+ val isReadHealthDataInBackgroundGranted =
+ isRuntimePermissionGranted(
+ packageState,
+ userId,
+ HealthPermissions.READ_HEALTH_DATA_IN_BACKGROUND,
)
+ if (isBodySensorsBackgroundGranted != isReadHealthDataInBackgroundGranted) {
+ if (isBodySensorsBackgroundGranted) {
+ revokeRuntimePermission(
+ packageState,
+ userId,
+ Manifest.permission.BODY_SENSORS_BACKGROUND,
+ )
+ }
+ if (isReadHealthDataInBackgroundGranted) {
+ revokeRuntimePermission(
+ packageState,
+ userId,
+ HealthPermissions.READ_HEALTH_DATA_IN_BACKGROUND,
+ )
+ }
}
}
}
+ private fun GetStateScope.isRuntimePermissionGranted(
+ packageState: PackageState,
+ userId: Int,
+ permissionName: String,
+ ): Boolean {
+ val permissionFlags =
+ with(policy) { getPermissionFlags(packageState.appId, userId, permissionName) }
+ return PermissionFlags.isAppOpGranted(permissionFlags)
+ }
+
private fun MutateStateScope.grantRuntimePermission(
packageState: PackageState,
userId: Int,
- permissionName: String
+ permissionName: String,
) {
Slog.v(
LOG_TAG,
"Granting runtime permission for package: ${packageState.packageName}, " +
- "permission: $permissionName, userId: $userId"
+ "permission: $permissionName, userId: $userId",
)
val permission = newState.systemState.permissions[permissionName]!!
if (packageState.getUserStateOrDefault(userId).isInstantApp && !permission.isInstant) {
@@ -276,7 +398,7 @@ class AppIdPermissionUpgrade(private val policy: AppIdPermissionPolicy) {
if (flags.hasAnyBit(MASK_ANY_FIXED)) {
Slog.v(
LOG_TAG,
- "Not allowed to grant $permissionName to package ${packageState.packageName}"
+ "Not allowed to grant $permissionName to package ${packageState.packageName}",
)
return
}
@@ -292,6 +414,43 @@ class AppIdPermissionUpgrade(private val policy: AppIdPermissionPolicy) {
with(policy) { setPermissionFlags(appId, userId, permissionName, flags) }
}
+ /**
+ * Revoke a runtime permission for a given user from a given package.
+ *
+ * @return true if the permission was revoked, false otherwise.
+ */
+ private fun MutateStateScope.revokeRuntimePermission(
+ packageState: PackageState,
+ userId: Int,
+ permissionName: String,
+ ): Boolean {
+ Slog.v(
+ LOG_TAG,
+ "Revoking runtime permission for package: ${packageState.packageName}, " +
+ "permission: $permissionName, userId: $userId",
+ )
+
+ val appId = packageState.appId
+ var flags = with(policy) { getPermissionFlags(appId, userId, permissionName) }
+ if (flags.hasAnyBit(MASK_SYSTEM_OR_POLICY_FIXED)) {
+ Slog.v(
+ LOG_TAG,
+ "Cannot revoke fixed runtime permission from package: " +
+ "${packageState.packageName}, permission: $permissionName, userId: $userId",
+ )
+ return false
+ }
+
+ flags =
+ flags andInv
+ (PermissionFlags.RUNTIME_GRANTED or
+ MASK_USER_SETTABLE or
+ PermissionFlags.PREGRANT or
+ PermissionFlags.ROLE)
+ with(policy) { setPermissionFlags(appId, userId, permissionName, flags) }
+ return true
+ }
+
companion object {
private val LOG_TAG = AppIdPermissionUpgrade::class.java.simpleName
@@ -302,6 +461,17 @@ class AppIdPermissionUpgrade(private val policy: AppIdPermissionPolicy) {
PermissionFlags.POLICY_FIXED or
PermissionFlags.SYSTEM_FIXED
+ private const val MASK_SYSTEM_OR_POLICY_FIXED =
+ PermissionFlags.SYSTEM_FIXED or PermissionFlags.POLICY_FIXED
+
+ private const val MASK_USER_SETTABLE =
+ PermissionFlags.USER_SET or
+ PermissionFlags.USER_FIXED or
+ PermissionFlags.APP_OP_REVOKED or
+ PermissionFlags.ONE_TIME or
+ PermissionFlags.HIBERNATION or
+ PermissionFlags.USER_SELECTED
+
private val LEGACY_RESTRICTED_PERMISSIONS =
indexedSetOf(
Manifest.permission.ACCESS_BACKGROUND_LOCATION,
@@ -314,13 +484,13 @@ class AppIdPermissionUpgrade(private val policy: AppIdPermissionPolicy) {
Manifest.permission.READ_CELL_BROADCASTS,
Manifest.permission.READ_CALL_LOG,
Manifest.permission.WRITE_CALL_LOG,
- Manifest.permission.PROCESS_OUTGOING_CALLS
+ Manifest.permission.PROCESS_OUTGOING_CALLS,
)
private val STORAGE_PERMISSIONS =
indexedSetOf(
Manifest.permission.READ_EXTERNAL_STORAGE,
- Manifest.permission.WRITE_EXTERNAL_STORAGE
+ Manifest.permission.WRITE_EXTERNAL_STORAGE,
)
private val AURAL_VISUAL_MEDIA_PERMISSIONS =
indexedSetOf(
@@ -328,14 +498,14 @@ class AppIdPermissionUpgrade(private val policy: AppIdPermissionPolicy) {
Manifest.permission.READ_MEDIA_IMAGES,
Manifest.permission.READ_MEDIA_VIDEO,
Manifest.permission.ACCESS_MEDIA_LOCATION,
- Manifest.permission.READ_MEDIA_VISUAL_USER_SELECTED
+ Manifest.permission.READ_MEDIA_VISUAL_USER_SELECTED,
)
// Visual media permissions in T
private val VISUAL_MEDIA_PERMISSIONS =
indexedSetOf(
Manifest.permission.READ_MEDIA_IMAGES,
Manifest.permission.READ_MEDIA_VIDEO,
- Manifest.permission.ACCESS_MEDIA_LOCATION
+ Manifest.permission.ACCESS_MEDIA_LOCATION,
)
}
}
diff --git a/services/supervision/java/com/android/server/supervision/SupervisionService.java b/services/supervision/java/com/android/server/supervision/SupervisionService.java
index 0b5a95b0e888..c419fd2ecbd7 100644
--- a/services/supervision/java/com/android/server/supervision/SupervisionService.java
+++ b/services/supervision/java/com/android/server/supervision/SupervisionService.java
@@ -17,6 +17,7 @@
package com.android.server.supervision;
import static android.Manifest.permission.INTERACT_ACROSS_USERS;
+import static android.Manifest.permission.MANAGE_ROLE_HOLDERS;
import static android.Manifest.permission.MANAGE_USERS;
import static android.Manifest.permission.QUERY_USERS;
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
@@ -171,6 +172,44 @@ public class SupervisionService extends ISupervisionManager.Stub {
}
@Override
+ public boolean shouldAllowBypassingSupervisionRoleQualification() {
+ enforcePermission(MANAGE_ROLE_HOLDERS);
+
+ if (hasNonTestDefaultUsers()) {
+ return false;
+ }
+
+ synchronized (getLockObject()) {
+ for (int i = 0; i < mUserData.size(); i++) {
+ if (mUserData.valueAt(i).supervisionEnabled) {
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Returns true if there are any non-default non-test users.
+ *
+ * This excludes the system and main user(s) as those users are created by default.
+ */
+ private boolean hasNonTestDefaultUsers() {
+ List<UserInfo> users = mInjector.getUserManagerInternal().getUsers(true);
+ for (var user : users) {
+ if (!user.isForTesting() && !user.isMain() && !isSystemUser(user)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static boolean isSystemUser(UserInfo userInfo) {
+ return (userInfo.flags & UserInfo.FLAG_SYSTEM) == UserInfo.FLAG_SYSTEM;
+ }
+
+ @Override
public void onShellCommand(
@Nullable FileDescriptor in,
@Nullable FileDescriptor out,
diff --git a/services/tests/PermissionServiceMockingTests/src/com/android/server/permission/test/AppIdPermissionPolicyPermissionStatesTest.kt b/services/tests/PermissionServiceMockingTests/src/com/android/server/permission/test/AppIdPermissionPolicyPermissionStatesTest.kt
index bf9033981442..c0f0369d4774 100644
--- a/services/tests/PermissionServiceMockingTests/src/com/android/server/permission/test/AppIdPermissionPolicyPermissionStatesTest.kt
+++ b/services/tests/PermissionServiceMockingTests/src/com/android/server/permission/test/AppIdPermissionPolicyPermissionStatesTest.kt
@@ -29,6 +29,7 @@ import com.android.server.pm.pkg.PackageState
import com.android.server.testutils.mock
import com.android.server.testutils.whenever
import com.google.common.truth.Truth.assertWithMessage
+import org.junit.Assume.assumeTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -537,6 +538,442 @@ class AppIdPermissionPolicyPermissionStatesTest : BasePermissionPolicyTest() {
.isEqualTo(expectedNewFlags)
}
+ /** Setup: BODY_SENSORS: granted, READ_HEART_RATE: not granted Result: BODY_SENSORS: revoked */
+ @Test
+ fun testEvaluatePermissionState_bodySensorReadHrOutOfSync_revokesGrantedBodySensor() {
+ assumeTrue(action != Action.ON_USER_ADDED)
+ val oldFlags = PermissionFlags.RUNTIME_GRANTED
+ testEvaluatePermissionState(
+ oldFlags,
+ PermissionInfo.PROTECTION_DANGEROUS,
+ permissionName = PERMISSION_BODY_SENSORS,
+ requestedPermissions = setOf(PERMISSION_BODY_SENSORS, PERMISSION_READ_HEART_RATE),
+ ) {}
+
+ val actualFlags =
+ getPermissionFlags(APP_ID_1, getUserIdEvaluated(), PERMISSION_BODY_SENSORS)
+ val expectedNewFlags = 0
+ assertWithMessage(
+ "After $action is called for a package that requests a runtime body sensors" +
+ " permission that was granted while read hr was not, the actual permission" +
+ " flags $actualFlags should match the expected flags $expectedNewFlags"
+ )
+ .that(actualFlags)
+ .isEqualTo(expectedNewFlags)
+ }
+
+ /**
+ * Setup: BODY_SENSORS: not granted, READ_HEART_RATE: granted Result: READ_HEART_RATE: revoked
+ */
+ @Test
+ fun testEvaluatePermissionState_bodySensorReadHrOutOfSync_revokesGrantedReadHr() {
+ assumeTrue(action != Action.ON_USER_ADDED)
+ val oldFlags = 0
+ testEvaluatePermissionState(
+ oldFlags,
+ PermissionInfo.PROTECTION_DANGEROUS,
+ permissionName = PERMISSION_BODY_SENSORS,
+ requestedPermissions = setOf(PERMISSION_BODY_SENSORS, PERMISSION_READ_HEART_RATE),
+ ) {
+ setPermissionFlags(
+ APP_ID_1,
+ getUserIdEvaluated(),
+ PERMISSION_READ_HEART_RATE,
+ PermissionFlags.RUNTIME_GRANTED,
+ )
+ }
+
+ val actualFlags =
+ getPermissionFlags(APP_ID_1, getUserIdEvaluated(), PERMISSION_READ_HEART_RATE)
+ val expectedNewFlags = 0
+ assertWithMessage(
+ "After $action is called for a package that requests a runtime body sensors" +
+ " permission that was not granted while read hr was, the actual permission" +
+ "flags $actualFlags should match the expected flags $expectedNewFlags"
+ )
+ .that(actualFlags)
+ .isEqualTo(expectedNewFlags)
+ }
+
+ /**
+ * Setup: BODY_SENSORS: granted, READ_HEART_RATE: not granted Result: nothing revoked since the
+ * targetSdk is Baklava
+ */
+ @Test
+ fun testEvaluatePermissionState_bodySensorReadHrOutOfSync_baklavaTargetSdk_nothingRevoked() {
+ assumeTrue(action != Action.ON_USER_ADDED)
+ val oldFlags = PermissionFlags.RUNTIME_GRANTED
+ testEvaluatePermissionState(
+ oldFlags,
+ PermissionInfo.PROTECTION_DANGEROUS,
+ permissionName = PERMISSION_BODY_SENSORS,
+ installedPackageTargetSdkVersion = Build.VERSION_CODES.BAKLAVA,
+ requestedPermissions = setOf(PERMISSION_BODY_SENSORS, PERMISSION_READ_HEART_RATE),
+ ) {}
+
+ val actualFlags =
+ getPermissionFlags(APP_ID_1, getUserIdEvaluated(), PERMISSION_BODY_SENSORS)
+ val expectedNewFlags = oldFlags
+ assertWithMessage(
+ "After $action is called for a package that requests a runtime body sensors" +
+ " permission that was granted while read hr was not targeting Baklava," +
+ " the actual permission flags $actualFlags should match the expected" +
+ " flags $expectedNewFlags"
+ )
+ .that(actualFlags)
+ .isEqualTo(expectedNewFlags)
+ }
+
+ /**
+ * Setup: BODY_SENSORS_BACKGROUND: granted, BODY_SENSORS: not granted Result:
+ * BODY_SENSORS_BACKGROUND: revoked
+ */
+ @Test
+ fun testEvaluatePermissionState_bodySensorBackgroundGrantMismatch_revokesBackground() {
+ assumeTrue(action != Action.ON_USER_ADDED)
+ val oldFlags = 0
+ testEvaluatePermissionState(
+ oldFlags,
+ PermissionInfo.PROTECTION_DANGEROUS,
+ permissionName = PERMISSION_BODY_SENSORS,
+ requestedPermissions =
+ setOf(PERMISSION_BODY_SENSORS, PERMISSION_BODY_SENSORS_BACKGROUND),
+ ) {
+ setPermissionFlags(
+ APP_ID_1,
+ getUserIdEvaluated(),
+ PERMISSION_BODY_SENSORS_BACKGROUND,
+ PermissionFlags.RUNTIME_GRANTED,
+ )
+ }
+
+ val actualFlags =
+ getPermissionFlags(APP_ID_1, getUserIdEvaluated(), PERMISSION_BODY_SENSORS_BACKGROUND)
+ val expectedNewFlags = 0
+ assertWithMessage(
+ "After $action is called for a package that requests a runtime body sensors" +
+ " permission that was not granted while body sensors background was," +
+ " the actual permission flags $actualFlags should match the expected" +
+ " flags $expectedNewFlags"
+ )
+ .that(actualFlags)
+ .isEqualTo(expectedNewFlags)
+ }
+
+ /**
+ * Setup: BODY_SENSORS_BACKGROUND: granted, BODY_SENSORS: not requested Result:
+ * BODY_SENSORS_BACKGROUND: revoked
+ */
+ @Test
+ fun testEvaluatePermissionState_bodySensorBackgroundMissingForeground_baklavaTargetSdk_revokesBackground() {
+ assumeTrue(action != Action.ON_USER_ADDED)
+ val oldFlags = PermissionFlags.RUNTIME_GRANTED
+ testEvaluatePermissionState(
+ oldFlags,
+ PermissionInfo.PROTECTION_DANGEROUS,
+ permissionName = PERMISSION_BODY_SENSORS_BACKGROUND,
+ requestedPermissions = setOf(PERMISSION_BODY_SENSORS_BACKGROUND),
+ ) {}
+
+ val actualFlags =
+ getPermissionFlags(APP_ID_1, getUserIdEvaluated(), PERMISSION_BODY_SENSORS_BACKGROUND)
+ val expectedNewFlags = 0
+ assertWithMessage(
+ "After $action is called for a package that has runtime body sensors background" +
+ " permission granted but is not requesting the body sensors foreground" +
+ " permission, the actual permission flags $actualFlags should match the" +
+ " expected flags $expectedNewFlags"
+ )
+ .that(actualFlags)
+ .isEqualTo(expectedNewFlags)
+ }
+
+ /**
+ * Setup: BODY_SENSORS_BACKGROUND: granted, BODY_SENSORS: granted, READ_HEART_RATE: not granted
+ * Result: BODY_SENSORS_BACKGROUND: revoked
+ */
+ @Test
+ fun testEvaluatePermissionState_bodySensorHeartRateOutOfSync_revokesGrantedBodySensorBackground() {
+ assumeTrue(action != Action.ON_USER_ADDED)
+ val oldFlags = PermissionFlags.RUNTIME_GRANTED
+ testEvaluatePermissionState(
+ oldFlags,
+ PermissionInfo.PROTECTION_DANGEROUS,
+ permissionName = PERMISSION_BODY_SENSORS,
+ installedPackageTargetSdkVersion = Build.VERSION_CODES.BAKLAVA,
+ requestedPermissions =
+ setOf(PERMISSION_BODY_SENSORS, PERMISSION_READ_HEART_RATE, PERMISSION_BODY_SENSORS),
+ ) {
+ setPermissionFlags(
+ APP_ID_1,
+ getUserIdEvaluated(),
+ PERMISSION_BODY_SENSORS_BACKGROUND,
+ PermissionFlags.RUNTIME_GRANTED,
+ )
+ }
+
+ val actualFlags =
+ getPermissionFlags(APP_ID_1, getUserIdEvaluated(), PERMISSION_BODY_SENSORS_BACKGROUND)
+ val expectedNewFlags = 0
+ assertWithMessage(
+ "After $action is called for a package that requests a runtime body sensors" +
+ " permission that was granted while read hr was not targeting Baklava," +
+ " the actual permission flags $actualFlags should match the expected" +
+ " flags $expectedNewFlags"
+ )
+ .that(actualFlags)
+ .isEqualTo(expectedNewFlags)
+ }
+
+ /**
+ * Setup: BODY_SENSORS_BACKGROUND: granted, READ_HEALTH_DATA_IN_BACKGROUND: not granted Result:
+ * BODY_SENSORS_BACKGROUND: revoked
+ */
+ @Test
+ fun testEvaluatePermissionState_bodySensorReadHealthBackgroundOutOfSync_revokesGrantedBodySensorBackground() {
+ assumeTrue(action != Action.ON_USER_ADDED)
+ val oldFlags = 0
+ testEvaluatePermissionState(
+ oldFlags,
+ PermissionInfo.PROTECTION_DANGEROUS,
+ permissionName = PERMISSION_BODY_SENSORS_BACKGROUND,
+ requestedPermissions =
+ setOf(PERMISSION_BODY_SENSORS_BACKGROUND, PERMISSION_READ_HEALTH_DATA_IN_BACKGROUND),
+ ) {
+ setPermissionFlags(
+ APP_ID_1,
+ getUserIdEvaluated(),
+ PERMISSION_READ_HEALTH_DATA_IN_BACKGROUND,
+ PermissionFlags.RUNTIME_GRANTED,
+ )
+ }
+
+ val actualFlags =
+ getPermissionFlags(
+ APP_ID_1,
+ getUserIdEvaluated(),
+ PERMISSION_READ_HEALTH_DATA_IN_BACKGROUND,
+ )
+ val expectedNewFlags = 0
+ assertWithMessage(
+ "After $action is called for a package that requests a runtime body sensors" +
+ " background permission that was not granted while read health data in" +
+ " background was, the actual permission flags $actualFlags should match" +
+ " the expected flags $expectedNewFlags"
+ )
+ .that(actualFlags)
+ .isEqualTo(expectedNewFlags)
+ }
+
+ /**
+ * Setup: BODY_SENSORS_BACKGROUND: not granted, READ_HEALTH_DATA_IN_BACKGROUND: granted Result:
+ * READ_HEALTH_DATA_IN_BACKGROUND: revoked
+ */
+ @Test
+ fun testEvaluatePermissionState_bodySensorReadHealthBackgroundOutOfSync_revokesGrantedReadHealthBackground() {
+ assumeTrue(action != Action.ON_USER_ADDED)
+ val oldFlags = PermissionFlags.RUNTIME_GRANTED
+ testEvaluatePermissionState(
+ oldFlags,
+ PermissionInfo.PROTECTION_DANGEROUS,
+ permissionName = PERMISSION_BODY_SENSORS_BACKGROUND,
+ requestedPermissions =
+ setOf(PERMISSION_BODY_SENSORS_BACKGROUND, PERMISSION_READ_HEALTH_DATA_IN_BACKGROUND),
+ ) {}
+
+ val actualFlags =
+ getPermissionFlags(APP_ID_1, getUserIdEvaluated(), PERMISSION_BODY_SENSORS_BACKGROUND)
+ val expectedNewFlags = 0
+ assertWithMessage(
+ "After $action is called for a package that requests a runtime body sensors" +
+ " background permission that was granted while read health data in" +
+ " background was not, the actual permission flags $actualFlags should match" +
+ " the expected flags $expectedNewFlags"
+ )
+ .that(actualFlags)
+ .isEqualTo(expectedNewFlags)
+ }
+
+ /**
+ * The sequence of events here is:
+ *
+ * Starting:
+ * - READ_HR=not granted
+ * - BODY_SENSORS=granted
+ * - BODY_SENSORS_BACKGROUND=granted,
+ * - READ_HEALTH_DATA_IN_BACKGROUND=granted
+ *
+ * Actions:
+ * - BODY_SENSORS->revoked (due to READ_HR mismatch)
+ * - BODY_SENSORS_BACKGROUND->revoked (due to new BODY_SENSORS mismatch)
+ * - READ_HEALTH_DATA_IN_BACKGROUND->revoked (due to new BODY_SENSORS_BACKGROUND mismatch)
+ *
+ * End result: All permissions revoked.
+ */
+ @Test
+ fun testEvaluatePermissionState_healthPermissionsSync_revocationChain() {
+ assumeTrue(action != Action.ON_USER_ADDED)
+ val oldFlags = 0
+ testEvaluatePermissionState(
+ oldFlags,
+ PermissionInfo.PROTECTION_DANGEROUS,
+ permissionName = PERMISSION_READ_HEART_RATE,
+ requestedPermissions =
+ setOf(
+ PERMISSION_READ_HEART_RATE,
+ PERMISSION_BODY_SENSORS,
+ PERMISSION_BODY_SENSORS_BACKGROUND,
+ PERMISSION_READ_HEALTH_DATA_IN_BACKGROUND,
+ ),
+ ) {
+ setPermissionFlags(
+ APP_ID_1,
+ getUserIdEvaluated(),
+ PERMISSION_BODY_SENSORS,
+ PermissionFlags.RUNTIME_GRANTED,
+ )
+ setPermissionFlags(
+ APP_ID_1,
+ getUserIdEvaluated(),
+ PERMISSION_BODY_SENSORS_BACKGROUND,
+ PermissionFlags.RUNTIME_GRANTED,
+ )
+ setPermissionFlags(
+ APP_ID_1,
+ getUserIdEvaluated(),
+ PERMISSION_READ_HEALTH_DATA_IN_BACKGROUND,
+ PermissionFlags.RUNTIME_GRANTED,
+ )
+ }
+
+ val bodySensorsFlags =
+ getPermissionFlags(APP_ID_1, getUserIdEvaluated(), PERMISSION_BODY_SENSORS)
+ val bodySensorsBackgroundFlags =
+ getPermissionFlags(APP_ID_1, getUserIdEvaluated(), PERMISSION_BODY_SENSORS_BACKGROUND)
+ val readHealthDataInBackgroundFlags =
+ getPermissionFlags(
+ APP_ID_1,
+ getUserIdEvaluated(),
+ PERMISSION_READ_HEALTH_DATA_IN_BACKGROUND,
+ )
+ val expectedNewFlags = 0
+ assertWithMessage(
+ "After $action is called for a package that has mismatching health permissions," +
+ " the actual permission flags for body sensors $bodySensorsFlags should" +
+ " match the expected flags $expectedNewFlags"
+ )
+ .that(bodySensorsFlags)
+ .isEqualTo(expectedNewFlags)
+ assertWithMessage(
+ "After $action is called for a package that has mismatching health permissions," +
+ " the actual permission flags for body sensors background" +
+ " $bodySensorsBackgroundFlags should match the expected flags" +
+ " $expectedNewFlags"
+ )
+ .that(bodySensorsBackgroundFlags)
+ .isEqualTo(expectedNewFlags)
+ assertWithMessage(
+ "After $action is called for a package that has mismatching health permissions," +
+ " the actual permission flags for read health data in background" +
+ " $readHealthDataInBackgroundFlags" +
+ " should match the expected flags $expectedNewFlags"
+ )
+ .that(readHealthDataInBackgroundFlags)
+ .isEqualTo(expectedNewFlags)
+ }
+
+ /**
+ * Similar to test case above but this time the READ_HR permission is going implicitly to
+ * explicitly granted (which causes it's grant to be revoked).
+ *
+ * Starting:
+ * - READ_HR=imlpicitly granted
+ * - BODY_SENSORS=granted
+ * - BODY_SENSORS_BACKGROUND=granted,
+ * - READ_HEALTH_DATA_IN_BACKGROUND=granted
+ *
+ * Actions:
+ * - READ_HR->revoked (due to implicit permission being explicitly requested)
+ * - BODY_SENSORS->revoked (due to READ_HR mismatch)
+ * - BODY_SENSORS_BACKGROUND->revoked (due to new BODY_SENSORS mismatch)
+ * - READ_HEALTH_DATA_IN_BACKGROUND->revoked (due to new BODY_SENSORS_BACKGROUND mismatch)
+ *
+ * End result: All permissions revoked.
+ */
+ @Test
+ fun testEvaluatePermissionState_implicitHealthPermissionRequested_causesRevocationChain() {
+ assumeTrue(action != Action.ON_USER_ADDED)
+ val oldFlags =
+ PermissionFlags.IMPLICIT or
+ PermissionFlags.RUNTIME_GRANTED or
+ PermissionFlags.USER_SET or
+ PermissionFlags.USER_FIXED
+ testEvaluatePermissionState(
+ oldFlags,
+ PermissionInfo.PROTECTION_DANGEROUS,
+ permissionName = PERMISSION_READ_HEART_RATE,
+ requestedPermissions =
+ setOf(
+ PERMISSION_READ_HEART_RATE,
+ PERMISSION_BODY_SENSORS,
+ PERMISSION_BODY_SENSORS_BACKGROUND,
+ PERMISSION_READ_HEALTH_DATA_IN_BACKGROUND,
+ ),
+ ) {
+ setPermissionFlags(
+ APP_ID_1,
+ getUserIdEvaluated(),
+ PERMISSION_BODY_SENSORS,
+ PermissionFlags.RUNTIME_GRANTED,
+ )
+ setPermissionFlags(
+ APP_ID_1,
+ getUserIdEvaluated(),
+ PERMISSION_BODY_SENSORS_BACKGROUND,
+ PermissionFlags.RUNTIME_GRANTED,
+ )
+ setPermissionFlags(
+ APP_ID_1,
+ getUserIdEvaluated(),
+ PERMISSION_READ_HEALTH_DATA_IN_BACKGROUND,
+ PermissionFlags.RUNTIME_GRANTED,
+ )
+ }
+
+ val bodySensorsFlags =
+ getPermissionFlags(APP_ID_1, getUserIdEvaluated(), PERMISSION_BODY_SENSORS)
+ val bodySensorsBackgroundFlags =
+ getPermissionFlags(APP_ID_1, getUserIdEvaluated(), PERMISSION_BODY_SENSORS_BACKGROUND)
+ val readHealthDataInBackgroundFlags =
+ getPermissionFlags(
+ APP_ID_1,
+ getUserIdEvaluated(),
+ PERMISSION_READ_HEALTH_DATA_IN_BACKGROUND,
+ )
+ val expectedNewFlags = 0
+ assertWithMessage(
+ "After $action is called for a package that has mismatching health permissions," +
+ "the actual permission flags for body sensors $bodySensorsFlags should match the" +
+ "expected flags $expectedNewFlags"
+ )
+ .that(bodySensorsFlags)
+ .isEqualTo(expectedNewFlags)
+ assertWithMessage(
+ "After $action is called for a package that has mismatching health permissions," +
+ "the actual permission flags for body sensors background $bodySensorsBackgroundFlags should" +
+ "match the expected flags $expectedNewFlags"
+ )
+ .that(bodySensorsBackgroundFlags)
+ .isEqualTo(expectedNewFlags)
+ assertWithMessage(
+ "After $action is called for a package that has mismatching health permissions," +
+ "the actual permission flags for read health data in background $readHealthDataInBackgroundFlags" +
+ "should match the expected flags $expectedNewFlags"
+ )
+ .that(readHealthDataInBackgroundFlags)
+ .isEqualTo(expectedNewFlags)
+ }
+
@Test
fun testEvaluatePermissionState_noLongerImplicitSystemOrPolicyFixedWasGranted_runtimeGranted() {
val oldFlags =
diff --git a/services/tests/PermissionServiceMockingTests/src/com/android/server/permission/test/BasePermissionPolicyTest.kt b/services/tests/PermissionServiceMockingTests/src/com/android/server/permission/test/BasePermissionPolicyTest.kt
index 207820cc3135..6dfd2611e0af 100644
--- a/services/tests/PermissionServiceMockingTests/src/com/android/server/permission/test/BasePermissionPolicyTest.kt
+++ b/services/tests/PermissionServiceMockingTests/src/com/android/server/permission/test/BasePermissionPolicyTest.kt
@@ -21,6 +21,7 @@ import android.content.pm.PackageManager
import android.content.pm.PermissionGroupInfo
import android.content.pm.PermissionInfo
import android.content.pm.SigningDetails
+import android.health.connect.HealthPermissions
import android.os.Build
import android.os.Bundle
import android.util.ArrayMap
@@ -390,6 +391,14 @@ abstract class BasePermissionPolicyTest {
Manifest.permission.ACCESS_BACKGROUND_LOCATION
@JvmStatic
protected val PERMISSION_ACCESS_MEDIA_LOCATION = Manifest.permission.ACCESS_MEDIA_LOCATION
+ @JvmStatic protected val PERMISSION_BODY_SENSORS = Manifest.permission.BODY_SENSORS
+ @JvmStatic
+ protected val PERMISSION_BODY_SENSORS_BACKGROUND =
+ Manifest.permission.BODY_SENSORS_BACKGROUND
+ @JvmStatic protected val PERMISSION_READ_HEART_RATE = HealthPermissions.READ_HEART_RATE
+ @JvmStatic
+ protected val PERMISSION_READ_HEALTH_DATA_IN_BACKGROUND =
+ HealthPermissions.READ_HEALTH_DATA_IN_BACKGROUND
@JvmStatic protected val USER_ID_0 = 0
@JvmStatic protected val USER_ID_NEW = 1
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsRule.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsRule.java
index 53a2522d299e..f29708496940 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsRule.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsRule.java
@@ -340,7 +340,7 @@ public class BatteryUsageStatsRule implements TestRule {
}
private void before() {
- BatteryUsageStats.DEBUG_INSTANCE_COUNT = true;
+ BatteryUsageStats.enableInstanceLeakDetection();
HandlerThread bgThread = new HandlerThread("bg thread");
bgThread.setUncaughtExceptionHandler((thread, throwable)-> {
mThrowable = throwable;
diff --git a/services/tests/servicestests/Android.bp b/services/tests/servicestests/Android.bp
index 64e6d323bdfd..a99e1b1f28e1 100644
--- a/services/tests/servicestests/Android.bp
+++ b/services/tests/servicestests/Android.bp
@@ -63,6 +63,7 @@ android_test {
"androidx.test.runner",
"androidx.test.rules",
"androidx.test.ext.junit",
+ "CtsAccessibilityCommon",
"cts-wm-util",
"platform-compat-test-rules",
"platform-parametric-runner-lib",
@@ -347,6 +348,17 @@ test_module_config {
include_filters: ["com.android.server.om."],
}
+test_module_config {
+ name: "FrameworksServicesTests_theme",
+ base: "FrameworksServicesTests",
+ test_suites: [
+ "device-tests",
+ "automotive-tests",
+ ],
+
+ include_filters: ["com.android.server.theming."],
+}
+
// Used by contexthub TEST_MAPPING
test_module_config {
name: "FrameworksServicesTests_contexthub_presubmit",
diff --git a/services/tests/servicestests/AndroidManifest.xml b/services/tests/servicestests/AndroidManifest.xml
index 4531b3948495..ef478e8ff1e1 100644
--- a/services/tests/servicestests/AndroidManifest.xml
+++ b/services/tests/servicestests/AndroidManifest.xml
@@ -129,6 +129,21 @@
<application android:testOnly="true" android:debuggable="true">
<uses-library android:name="android.test.runner"/>
+ <service
+ android:name="com.android.server.accessibility.integration.FullScreenMagnificationMouseFollowingTest$TestMagnificationAccessibilityService"
+ android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"
+ android:exported="true">
+ <intent-filter>
+ <action android:name="android.accessibilityservice.AccessibilityService" />
+ <category android:name="android.accessibilityservice.category.FEEDBACK_GENERIC" />
+ </intent-filter>
+
+ <meta-data
+ android:name="android.accessibilityservice"
+ android:resource="@xml/test_magnification_a11y_service" />
+ </service>
+ <activity android:name="com.android.server.accessibility.integration.FullScreenMagnificationMouseFollowingTest$TestActivity" />
+
<service android:name="com.android.server.accounts.TestAccountType1AuthenticatorService"
android:exported="false">
<intent-filter>
diff --git a/services/tests/servicestests/res/xml/test_magnification_a11y_service.xml b/services/tests/servicestests/res/xml/test_magnification_a11y_service.xml
new file mode 100644
index 000000000000..d28cdca6a26a
--- /dev/null
+++ b/services/tests/servicestests/res/xml/test_magnification_a11y_service.xml
@@ -0,0 +1,24 @@
+<?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.
+-->
+<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
+ android:accessibilityEventTypes="typeAllMask"
+ android:accessibilityFeedbackType="feedbackGeneric"
+ android:canRetrieveWindowContent="true"
+ android:canRequestTouchExplorationMode="true"
+ android:canRequestEnhancedWebAccessibility="true"
+ android:canRequestFilterKeyEvents="true"
+ android:canControlMagnification="true"
+ android:canPerformGestures="true"/> \ No newline at end of file
diff --git a/services/tests/servicestests/src/com/android/server/GestureLauncherServiceTest.java b/services/tests/servicestests/src/com/android/server/GestureLauncherServiceTest.java
index cc0d5e4710d2..73e5f8232faf 100644
--- a/services/tests/servicestests/src/com/android/server/GestureLauncherServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/GestureLauncherServiceTest.java
@@ -1787,46 +1787,6 @@ public class GestureLauncherServiceTest {
}
/**
- * If processPowerKeyDown is called instead of interceptPowerKeyDown (meaning the double tap
- * gesture isn't performed), the emergency gesture is still launched.
- */
- @Test
- public void testProcessPowerKeyDown_fiveInboundPresses_emergencyGestureLaunches() {
- enableCameraGesture();
- enableEmergencyGesture();
-
- // First event
- long eventTime = INITIAL_EVENT_TIME_MILLIS;
- sendPowerKeyDownToGestureLauncherServiceAndAssertValues(eventTime, false, false);
-
- //Second event; call processPowerKeyDown without calling interceptPowerKeyDown
- final long interval = POWER_DOUBLE_TAP_MAX_TIME_MS - 1;
- eventTime += interval;
- KeyEvent keyEvent =
- new KeyEvent(
- IGNORED_DOWN_TIME, eventTime, IGNORED_ACTION, IGNORED_CODE, IGNORED_REPEAT);
- mGestureLauncherService.processPowerKeyDown(keyEvent);
-
- verify(mMetricsLogger, never())
- .action(eq(MetricsEvent.ACTION_DOUBLE_TAP_POWER_CAMERA_GESTURE), anyInt());
- verify(mUiEventLogger, never()).log(any());
-
- // Presses 3 and 4 should not trigger any gesture
- for (int i = 0; i < 2; i++) {
- eventTime += interval;
- sendPowerKeyDownToGestureLauncherServiceAndAssertValues(eventTime, true, false);
- }
-
- // Fifth button press should still trigger the emergency flow
- eventTime += interval;
- sendPowerKeyDownToGestureLauncherServiceAndAssertValues(eventTime, true, true);
-
- verify(mUiEventLogger, times(1))
- .log(GestureLauncherService.GestureLauncherEvent.GESTURE_EMERGENCY_TAP_POWER);
- verify(mStatusBarManagerInternal).onEmergencyActionLaunchGestureDetected();
- }
-
- /**
* Helper method to trigger emergency gesture by pressing button for 5 times.
*
* @return last event time.
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 df77866b5e7f..900d5ad58719 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
@@ -88,6 +88,23 @@ public class AutoclickControllerTest {
}
}
+ public static class ScrollEventCaptor extends BaseEventStreamTransformation {
+ public MotionEvent scrollEvent;
+ public int eventCount = 0;
+
+ @Override
+ public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
+ if (event.getAction() == MotionEvent.ACTION_SCROLL) {
+ if (scrollEvent != null) {
+ scrollEvent.recycle();
+ }
+ scrollEvent = MotionEvent.obtain(event);
+ eventCount++;
+ }
+ super.onMotionEvent(event, rawEvent, policyFlags);
+ }
+ }
+
@Before
public void setUp() {
mTestableLooper = TestableLooper.get(this);
@@ -314,15 +331,7 @@ public class AutoclickControllerTest {
injectFakeMouseActionHoverMoveEvent();
// Send hover enter event.
- MotionEvent hoverEnter = MotionEvent.obtain(
- /* downTime= */ 0,
- /* eventTime= */ 100,
- /* action= */ MotionEvent.ACTION_HOVER_ENTER,
- /* x= */ 30f,
- /* y= */ 0f,
- /* metaState= */ 0);
- hoverEnter.setSource(InputDevice.SOURCE_MOUSE);
- mController.onMotionEvent(hoverEnter, hoverEnter, /* policyFlags= */ 0);
+ injectFakeMouseMoveEvent(/* x= */ 30f, /* y= */ 0, MotionEvent.ACTION_HOVER_ENTER);
// Verify there is no pending click.
assertThat(mController.mClickScheduler.getIsActiveForTesting()).isFalse();
@@ -334,15 +343,7 @@ public class AutoclickControllerTest {
injectFakeMouseActionHoverMoveEvent();
// Send hover move event.
- MotionEvent hoverMove = MotionEvent.obtain(
- /* downTime= */ 0,
- /* eventTime= */ 100,
- /* action= */ MotionEvent.ACTION_HOVER_MOVE,
- /* x= */ 30f,
- /* y= */ 0f,
- /* metaState= */ 0);
- hoverMove.setSource(InputDevice.SOURCE_MOUSE);
- mController.onMotionEvent(hoverMove, hoverMove, /* policyFlags= */ 0);
+ injectFakeMouseMoveEvent(/* x= */ 30f, /* y= */ 0, MotionEvent.ACTION_HOVER_MOVE);
// Verify there is a pending click.
assertThat(mController.mClickScheduler.getIsActiveForTesting()).isTrue();
@@ -351,39 +352,15 @@ public class AutoclickControllerTest {
@Test
public void smallJitteryMovement_doesNotTriggerClick() {
// Initial hover move to set an anchor point.
- MotionEvent initialHoverMove = MotionEvent.obtain(
- /* downTime= */ 0,
- /* eventTime= */ 100,
- /* action= */ MotionEvent.ACTION_HOVER_MOVE,
- /* x= */ 30f,
- /* y= */ 40f,
- /* metaState= */ 0);
- initialHoverMove.setSource(InputDevice.SOURCE_MOUSE);
- mController.onMotionEvent(initialHoverMove, initialHoverMove, /* policyFlags= */ 0);
+ injectFakeMouseMoveEvent(/* x= */ 30f, /* y= */ 40f, MotionEvent.ACTION_HOVER_MOVE);
// Get the initial scheduled click time.
long initialScheduledTime = mController.mClickScheduler.getScheduledClickTimeForTesting();
// Simulate small, jittery movements (all within the default slop).
- MotionEvent jitteryMove1 = MotionEvent.obtain(
- /* downTime= */ 0,
- /* eventTime= */ 150,
- /* action= */ MotionEvent.ACTION_HOVER_MOVE,
- /* x= */ 31f, // Small change in x
- /* y= */ 41f, // Small change in y
- /* metaState= */ 0);
- jitteryMove1.setSource(InputDevice.SOURCE_MOUSE);
- mController.onMotionEvent(jitteryMove1, jitteryMove1, /* policyFlags= */ 0);
+ injectFakeMouseMoveEvent(/* x= */ 31f, /* y= */ 41f, MotionEvent.ACTION_HOVER_MOVE);
- MotionEvent jitteryMove2 = MotionEvent.obtain(
- /* downTime= */ 0,
- /* eventTime= */ 200,
- /* action= */ MotionEvent.ACTION_HOVER_MOVE,
- /* x= */ 30.5f, // Small change in x
- /* y= */ 39.8f, // Small change in y
- /* metaState= */ 0);
- jitteryMove2.setSource(InputDevice.SOURCE_MOUSE);
- mController.onMotionEvent(jitteryMove2, jitteryMove2, /* policyFlags= */ 0);
+ injectFakeMouseMoveEvent(/* x= */ 30.5f, /* y= */ 39.8f, MotionEvent.ACTION_HOVER_MOVE);
// Verify that the scheduled click time has NOT changed.
assertThat(mController.mClickScheduler.getScheduledClickTimeForTesting())
@@ -393,29 +370,13 @@ public class AutoclickControllerTest {
@Test
public void singleSignificantMovement_triggersClick() {
// Initial hover move to set an anchor point.
- MotionEvent initialHoverMove = MotionEvent.obtain(
- /* downTime= */ 0,
- /* eventTime= */ 100,
- /* action= */ MotionEvent.ACTION_HOVER_MOVE,
- /* x= */ 30f,
- /* y= */ 40f,
- /* metaState= */ 0);
- initialHoverMove.setSource(InputDevice.SOURCE_MOUSE);
- mController.onMotionEvent(initialHoverMove, initialHoverMove, /* policyFlags= */ 0);
+ injectFakeMouseMoveEvent(/* x= */ 30f, /* y= */ 40f, MotionEvent.ACTION_HOVER_MOVE);
// Get the initial scheduled click time.
long initialScheduledTime = mController.mClickScheduler.getScheduledClickTimeForTesting();
- // Simulate a single, significant movement (greater than the default slop).
- MotionEvent significantMove = MotionEvent.obtain(
- /* downTime= */ 0,
- /* eventTime= */ 150,
- /* action= */ MotionEvent.ACTION_HOVER_MOVE,
- /* x= */ 60f, // Significant change in x (30f difference)
- /* y= */ 70f, // Significant change in y (30f difference)
- /* metaState= */ 0);
- significantMove.setSource(InputDevice.SOURCE_MOUSE);
- mController.onMotionEvent(significantMove, significantMove, /* policyFlags= */ 0);
+ // Significant change in x (30f difference) and y (30f difference)
+ injectFakeMouseMoveEvent(/* x= */ 60f, /* y= */ 70f, MotionEvent.ACTION_HOVER_MOVE);
// Verify that the scheduled click time has changed (click was rescheduled).
assertThat(mController.mClickScheduler.getScheduledClickTimeForTesting())
@@ -442,15 +403,7 @@ public class AutoclickControllerTest {
// Move the mouse down, less than customSize radius so a click is not triggered.
float moveDownY = customSize - 25;
- MotionEvent hoverMove = MotionEvent.obtain(
- /* downTime= */ 0,
- /* eventTime= */ 150,
- /* action= */ MotionEvent.ACTION_HOVER_MOVE,
- /* x= */ 0f,
- /* y= */ moveDownY,
- /* metaState= */ 0);
- hoverMove.setSource(InputDevice.SOURCE_MOUSE);
- mController.onMotionEvent(hoverMove, hoverMove, /* policyFlags= */ 0);
+ injectFakeMouseMoveEvent(/* x= */ 0, /* y= */ moveDownY, MotionEvent.ACTION_HOVER_MOVE);
assertThat(mController.mClickScheduler.getIsActiveForTesting()).isFalse();
}
@@ -474,15 +427,7 @@ public class AutoclickControllerTest {
// Move the mouse right, greater than customSize radius so a click is triggered.
float moveRightX = customSize + 100;
- MotionEvent hoverMove = MotionEvent.obtain(
- /* downTime= */ 0,
- /* eventTime= */ 200,
- /* action= */ MotionEvent.ACTION_HOVER_MOVE,
- /* x= */ moveRightX,
- /* y= */ 0,
- /* metaState= */ 0);
- hoverMove.setSource(InputDevice.SOURCE_MOUSE);
- mController.onMotionEvent(hoverMove, hoverMove, /* policyFlags= */ 0);
+ injectFakeMouseMoveEvent(/* x= */ moveRightX, /* y= */ 0, MotionEvent.ACTION_HOVER_MOVE);
assertThat(mController.mClickScheduler.getIsActiveForTesting()).isTrue();
}
@@ -505,15 +450,7 @@ public class AutoclickControllerTest {
// Move the mouse down less than customSize radius but ignore custom movement is not enabled
// so a click is triggered.
float moveDownY = customSize - 100;
- MotionEvent hoverMove = MotionEvent.obtain(
- /* downTime= */ 0,
- /* eventTime= */ 150,
- /* action= */ MotionEvent.ACTION_HOVER_MOVE,
- /* x= */ 0f,
- /* y= */ moveDownY,
- /* metaState= */ 0);
- hoverMove.setSource(InputDevice.SOURCE_MOUSE);
- mController.onMotionEvent(hoverMove, hoverMove, /* policyFlags= */ 0);
+ injectFakeMouseMoveEvent(/* x= */ 0, /* y= */ moveDownY, MotionEvent.ACTION_HOVER_MOVE);
assertThat(mController.mClickScheduler.getIsActiveForTesting()).isTrue();
}
@@ -537,15 +474,7 @@ public class AutoclickControllerTest {
// After enabling ignore custom movement, move the mouse right, less than customSize radius
// so a click won't be triggered.
float moveRightX = customSize - 100;
- MotionEvent hoverMove = MotionEvent.obtain(
- /* downTime= */ 0,
- /* eventTime= */ 200,
- /* action= */ MotionEvent.ACTION_HOVER_MOVE,
- /* x= */ moveRightX,
- /* y= */ 0,
- /* metaState= */ 0);
- hoverMove.setSource(InputDevice.SOURCE_MOUSE);
- mController.onMotionEvent(hoverMove, hoverMove, /* policyFlags= */ 0);
+ injectFakeMouseMoveEvent(/* x= */ moveRightX, /* y= */ 0, MotionEvent.ACTION_HOVER_MOVE);
assertThat(mController.mClickScheduler.getIsActiveForTesting()).isFalse();
}
@@ -583,15 +512,7 @@ public class AutoclickControllerTest {
mController.mAutoclickTypePanel = mockAutoclickTypePanel;
// Send hover move event.
- MotionEvent hoverMove = MotionEvent.obtain(
- /* downTime= */ 0,
- /* eventTime= */ 100,
- /* action= */ MotionEvent.ACTION_HOVER_MOVE,
- /* x= */ 30f,
- /* y= */ 0f,
- /* metaState= */ 0);
- hoverMove.setSource(InputDevice.SOURCE_MOUSE);
- mController.onMotionEvent(hoverMove, hoverMove, /* policyFlags= */ 0);
+ injectFakeMouseMoveEvent(/* x= */ 30f, /* y= */ 0, MotionEvent.ACTION_HOVER_MOVE);
// Verify there is not a pending click.
assertThat(mController.mClickScheduler.getIsActiveForTesting()).isFalse();
@@ -607,7 +528,7 @@ public class AutoclickControllerTest {
assertThat(mController.mClickScheduler.getScheduledClickTimeForTesting()).isEqualTo(-1);
// Send move again to trigger click and verify there is now a pending click.
- mController.onMotionEvent(hoverMove, hoverMove, /* policyFlags= */ 0);
+ injectFakeMouseMoveEvent(/* x= */ 30f, /* y= */ 0, MotionEvent.ACTION_HOVER_MOVE);
assertThat(mController.mClickScheduler.getIsActiveForTesting()).isTrue();
assertThat(mController.mClickScheduler.getScheduledClickTimeForTesting()).isNotEqualTo(-1);
}
@@ -624,15 +545,7 @@ public class AutoclickControllerTest {
mController.mAutoclickTypePanel = mockAutoclickTypePanel;
// Send hover move event.
- MotionEvent hoverMove = MotionEvent.obtain(
- /* downTime= */ 0,
- /* eventTime= */ 100,
- /* action= */ MotionEvent.ACTION_HOVER_MOVE,
- /* x= */ 30f,
- /* y= */ 0f,
- /* metaState= */ 0);
- hoverMove.setSource(InputDevice.SOURCE_MOUSE);
- mController.onMotionEvent(hoverMove, hoverMove, /* policyFlags= */ 0);
+ injectFakeMouseMoveEvent(/* x= */ 30f, /* y= */ 0, MotionEvent.ACTION_HOVER_MOVE);
// Verify click is not triggered.
assertThat(mController.mClickScheduler.getIsActiveForTesting()).isFalse();
@@ -651,15 +564,7 @@ public class AutoclickControllerTest {
mController.mAutoclickTypePanel = mockAutoclickTypePanel;
// Send hover move event.
- MotionEvent hoverMove = MotionEvent.obtain(
- /* downTime= */ 0,
- /* eventTime= */ 100,
- /* action= */ MotionEvent.ACTION_HOVER_MOVE,
- /* x= */ 30f,
- /* y= */ 0f,
- /* metaState= */ 0);
- hoverMove.setSource(InputDevice.SOURCE_MOUSE);
- mController.onMotionEvent(hoverMove, hoverMove, /* policyFlags= */ 0);
+ injectFakeMouseMoveEvent(/* x= */ 30f, /* y= */ 0, MotionEvent.ACTION_HOVER_MOVE);
// Verify click is triggered.
assertThat(mController.mClickScheduler.getIsActiveForTesting()).isTrue();
@@ -678,15 +583,7 @@ public class AutoclickControllerTest {
mController.mAutoclickTypePanel = mockAutoclickTypePanel;
// Send hover move event.
- MotionEvent hoverMove = MotionEvent.obtain(
- /* downTime= */ 0,
- /* eventTime= */ 100,
- /* action= */ MotionEvent.ACTION_HOVER_MOVE,
- /* x= */ 30f,
- /* y= */ 0f,
- /* metaState= */ 0);
- hoverMove.setSource(InputDevice.SOURCE_MOUSE);
- mController.onMotionEvent(hoverMove, hoverMove, /* policyFlags= */ 0);
+ injectFakeMouseMoveEvent(/* x= */ 30f, /* y= */ 0, MotionEvent.ACTION_HOVER_MOVE);
// Verify click is triggered.
assertThat(mController.mClickScheduler.getIsActiveForTesting()).isTrue();
@@ -764,15 +661,7 @@ public class AutoclickControllerTest {
mController.mClickScheduler.updateDelay(0);
// Send hover move event.
- MotionEvent hoverMove = MotionEvent.obtain(
- /* downTime= */ 0,
- /* eventTime= */ 100,
- /* action= */ MotionEvent.ACTION_HOVER_MOVE,
- /* x= */ 30f,
- /* y= */ 0f,
- /* metaState= */ 0);
- hoverMove.setSource(InputDevice.SOURCE_MOUSE);
- mController.onMotionEvent(hoverMove, hoverMove, /* policyFlags= */ 0);
+ injectFakeMouseMoveEvent(/* x= */ 30f, /* y= */ 0, MotionEvent.ACTION_HOVER_MOVE);
mTestableLooper.processAllMessages();
// Verify left click sent.
@@ -797,15 +686,7 @@ public class AutoclickControllerTest {
mController.mAutoclickTypePanel = mockAutoclickTypePanel;
// Send hover move event.
- MotionEvent hoverMove = MotionEvent.obtain(
- /* downTime= */ 0,
- /* eventTime= */ 100,
- /* action= */ MotionEvent.ACTION_HOVER_MOVE,
- /* x= */ 30f,
- /* y= */ 0f,
- /* metaState= */ 0);
- hoverMove.setSource(InputDevice.SOURCE_MOUSE);
- mController.onMotionEvent(hoverMove, hoverMove, /* policyFlags= */ 0);
+ injectFakeMouseMoveEvent(/* x= */ 30f, /* y= */ 0, MotionEvent.ACTION_HOVER_MOVE);
mTestableLooper.processAllMessages();
// Verify right click sent.
@@ -833,15 +714,7 @@ public class AutoclickControllerTest {
mController.mAutoclickScrollPanel = mockScrollPanel;
// First hover move event.
- MotionEvent hoverMove1 = MotionEvent.obtain(
- /* downTime= */ 0,
- /* eventTime= */ 100,
- /* action= */ MotionEvent.ACTION_HOVER_MOVE,
- /* x= */ 30f,
- /* y= */ 0f,
- /* metaState= */ 0);
- hoverMove1.setSource(InputDevice.SOURCE_MOUSE);
- mController.onMotionEvent(hoverMove1, hoverMove1, /* policyFlags= */ 0);
+ injectFakeMouseMoveEvent(/* x= */ 30f, /* y= */ 0, MotionEvent.ACTION_HOVER_MOVE);
mTestableLooper.processAllMessages();
// Verify scroll panel is shown once.
@@ -849,15 +722,7 @@ public class AutoclickControllerTest {
assertThat(motionEventCaptor.downEvent).isNull();
// Second significant hover move event to trigger another autoclick.
- MotionEvent hoverMove2 = MotionEvent.obtain(
- /* downTime= */ 0,
- /* eventTime= */ 200,
- /* action= */ MotionEvent.ACTION_HOVER_MOVE,
- /* x= */ 100f,
- /* y= */ 100f,
- /* metaState= */ 0);
- hoverMove2.setSource(InputDevice.SOURCE_MOUSE);
- mController.onMotionEvent(hoverMove2, hoverMove2, /* policyFlags= */ 0);
+ injectFakeMouseMoveEvent(/* x= */ 100f, /* y= */ 100f, MotionEvent.ACTION_HOVER_MOVE);
mTestableLooper.processAllMessages();
// Verify scroll panel is still only shown once (not called again).
@@ -901,15 +766,7 @@ public class AutoclickControllerTest {
mController.mAutoclickTypePanel = mockAutoclickTypePanel;
// Send hover move event.
- MotionEvent hoverMove = MotionEvent.obtain(
- /* downTime= */ 0,
- /* eventTime= */ 100,
- /* action= */ MotionEvent.ACTION_HOVER_MOVE,
- /* x= */ 30f,
- /* y= */ 0f,
- /* metaState= */ 0);
- hoverMove.setSource(InputDevice.SOURCE_MOUSE);
- mController.onMotionEvent(hoverMove, hoverMove, /* policyFlags= */ 0);
+ injectFakeMouseMoveEvent(/* x= */ 30f, /* y= */ 100f, MotionEvent.ACTION_HOVER_MOVE);
mTestableLooper.processAllMessages();
// Verify left click is sent due to the mouse hovering the panel.
@@ -918,27 +775,108 @@ public class AutoclickControllerTest {
MotionEvent.BUTTON_PRIMARY);
}
- private void injectFakeMouseActionHoverMoveEvent() {
- MotionEvent event = getFakeMotionHoverMoveEvent();
- event.setSource(InputDevice.SOURCE_MOUSE);
- mController.onMotionEvent(event, event, /* policyFlags= */ 0);
- }
+ @Test
+ @EnableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_AUTOCLICK_INDICATOR)
+ public void sendClick_updateLastCursorAndScrollAtThatLocation() {
+ // Set up event capturer to track scroll events.
+ ScrollEventCaptor scrollCaptor = new ScrollEventCaptor();
+ mController.setNext(scrollCaptor);
- private void injectFakeNonMouseActionHoverMoveEvent() {
- MotionEvent event = getFakeMotionHoverMoveEvent();
- event.setSource(InputDevice.SOURCE_KEYBOARD);
- mController.onMotionEvent(event, event, /* policyFlags= */ 0);
+ // Initialize controller with mouse event.
+ injectFakeMouseActionHoverMoveEvent();
+
+ // Mock the scroll panel.
+ AutoclickScrollPanel mockScrollPanel = mock(AutoclickScrollPanel.class);
+ mController.mAutoclickScrollPanel = mockScrollPanel;
+
+ // Set click type to scroll.
+ mController.clickPanelController.handleAutoclickTypeChange(
+ AutoclickTypePanel.AUTOCLICK_TYPE_SCROLL);
+
+ // Set cursor position.
+ float expectedX = 75f;
+ float expectedY = 125f;
+ mController.mLastCursorX = expectedX;
+ mController.mLastCursorY = expectedY;
+
+ // Trigger scroll action in up direction.
+ mController.mScrollPanelController.onHoverButtonChange(
+ AutoclickScrollPanel.DIRECTION_UP, true);
+
+ // Verify scroll event happens at last cursor location.
+ assertThat(scrollCaptor.scrollEvent).isNotNull();
+ assertThat(scrollCaptor.scrollEvent.getX()).isEqualTo(expectedX);
+ assertThat(scrollCaptor.scrollEvent.getY()).isEqualTo(expectedY);
}
- private void injectFakeKeyEvent(int keyCode, int modifiers) {
- KeyEvent keyEvent = new KeyEvent(
- /* downTime= */ 0,
- /* eventTime= */ 0,
- /* action= */ KeyEvent.ACTION_DOWN,
- /* code= */ keyCode,
- /* repeat= */ 0,
- /* metaState= */ modifiers);
- mController.onKeyEvent(keyEvent, /* policyFlags= */ 0);
+ @Test
+ @EnableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_AUTOCLICK_INDICATOR)
+ public void handleScroll_generatesCorrectScrollEvents() {
+ ScrollEventCaptor scrollCaptor = new ScrollEventCaptor();
+ mController.setNext(scrollCaptor);
+
+ // Initialize controller.
+ injectFakeMouseActionHoverMoveEvent();
+
+ // Set cursor position.
+ final float expectedX = 100f;
+ final float expectedY = 200f;
+ mController.mLastCursorX = expectedX;
+ mController.mLastCursorY = expectedY;
+
+ // Test UP direction.
+ mController.mScrollPanelController.onHoverButtonChange(
+ AutoclickScrollPanel.DIRECTION_UP, true);
+
+ // Verify basic event properties.
+ assertThat(scrollCaptor.eventCount).isEqualTo(1);
+ assertThat(scrollCaptor.scrollEvent).isNotNull();
+ assertThat(scrollCaptor.scrollEvent.getAction()).isEqualTo(MotionEvent.ACTION_SCROLL);
+ assertThat(scrollCaptor.scrollEvent.getX()).isEqualTo(expectedX);
+ assertThat(scrollCaptor.scrollEvent.getY()).isEqualTo(expectedY);
+
+ // Verify UP direction uses correct axis values.
+ float vScrollUp = scrollCaptor.scrollEvent.getAxisValue(MotionEvent.AXIS_VSCROLL);
+ float hScrollUp = scrollCaptor.scrollEvent.getAxisValue(MotionEvent.AXIS_HSCROLL);
+ assertThat(vScrollUp).isGreaterThan(0);
+ assertThat(hScrollUp).isEqualTo(0);
+
+ // Test DOWN direction.
+ mController.mScrollPanelController.onHoverButtonChange(
+ AutoclickScrollPanel.DIRECTION_DOWN, true);
+
+ // Verify DOWN direction uses correct axis values.
+ assertThat(scrollCaptor.eventCount).isEqualTo(2);
+ float vScrollDown = scrollCaptor.scrollEvent.getAxisValue(MotionEvent.AXIS_VSCROLL);
+ float hScrollDown = scrollCaptor.scrollEvent.getAxisValue(MotionEvent.AXIS_HSCROLL);
+ assertThat(vScrollDown).isLessThan(0);
+ assertThat(hScrollDown).isEqualTo(0);
+
+ // Test LEFT direction.
+ mController.mScrollPanelController.onHoverButtonChange(
+ AutoclickScrollPanel.DIRECTION_LEFT, true);
+
+ // Verify LEFT direction uses correct axis values.
+ assertThat(scrollCaptor.eventCount).isEqualTo(3);
+ float vScrollLeft = scrollCaptor.scrollEvent.getAxisValue(MotionEvent.AXIS_VSCROLL);
+ float hScrollLeft = scrollCaptor.scrollEvent.getAxisValue(MotionEvent.AXIS_HSCROLL);
+ assertThat(hScrollLeft).isGreaterThan(0);
+ assertThat(vScrollLeft).isEqualTo(0);
+
+ // Test RIGHT direction.
+ mController.mScrollPanelController.onHoverButtonChange(
+ AutoclickScrollPanel.DIRECTION_RIGHT, true);
+
+ // Verify RIGHT direction uses correct axis values.
+ assertThat(scrollCaptor.eventCount).isEqualTo(4);
+ float vScrollRight = scrollCaptor.scrollEvent.getAxisValue(MotionEvent.AXIS_VSCROLL);
+ float hScrollRight = scrollCaptor.scrollEvent.getAxisValue(MotionEvent.AXIS_HSCROLL);
+ assertThat(hScrollRight).isLessThan(0);
+ assertThat(vScrollRight).isEqualTo(0);
+
+ // Verify scroll cursor position is preserved.
+ assertThat(scrollCaptor.scrollEvent.getX()).isEqualTo(expectedX);
+ assertThat(scrollCaptor.scrollEvent.getY()).isEqualTo(expectedY);
}
@Test
@@ -958,15 +896,7 @@ public class AutoclickControllerTest {
mController.mAutoclickTypePanel = mockAutoclickTypePanel;
// Send hover move event.
- MotionEvent hoverMove = MotionEvent.obtain(
- /* downTime= */ 0,
- /* eventTime= */ 100,
- /* action= */ MotionEvent.ACTION_HOVER_MOVE,
- /* x= */ 30f,
- /* y= */ 0f,
- /* metaState= */ 0);
- hoverMove.setSource(InputDevice.SOURCE_MOUSE);
- mController.onMotionEvent(hoverMove, hoverMove, /* policyFlags= */ 0);
+ injectFakeMouseMoveEvent(/* x= */ 30f, /* y= */ 100f, MotionEvent.ACTION_HOVER_MOVE);
mTestableLooper.processAllMessages();
// Verify left click sent.
@@ -976,6 +906,45 @@ public class AutoclickControllerTest {
assertThat(motionEventCaptor.eventCount).isEqualTo(2);
}
+ /**
+ * =========================================================================
+ * Helper Functions
+ * =========================================================================
+ */
+
+ private void injectFakeMouseActionHoverMoveEvent() {
+ injectFakeMouseMoveEvent(0, 0, MotionEvent.ACTION_HOVER_MOVE);
+ }
+
+ private void injectFakeMouseMoveEvent(float x, float y, int action) {
+ MotionEvent event = MotionEvent.obtain(
+ /* downTime= */ 0,
+ /* eventTime= */ 0,
+ /* action= */ action,
+ /* x= */ x,
+ /* y= */ y,
+ /* metaState= */ 0);
+ event.setSource(InputDevice.SOURCE_MOUSE);
+ mController.onMotionEvent(event, event, /* policyFlags= */ 0);
+ }
+
+ private void injectFakeNonMouseActionHoverMoveEvent() {
+ MotionEvent event = getFakeMotionHoverMoveEvent();
+ event.setSource(InputDevice.SOURCE_KEYBOARD);
+ mController.onMotionEvent(event, event, /* policyFlags= */ 0);
+ }
+
+ private void injectFakeKeyEvent(int keyCode, int modifiers) {
+ KeyEvent keyEvent = new KeyEvent(
+ /* downTime= */ 0,
+ /* eventTime= */ 0,
+ /* action= */ KeyEvent.ACTION_DOWN,
+ /* code= */ keyCode,
+ /* repeat= */ 0,
+ /* metaState= */ modifiers);
+ mController.onKeyEvent(keyEvent, /* policyFlags= */ 0);
+ }
+
private MotionEvent getFakeMotionHoverMoveEvent() {
return MotionEvent.obtain(
/* downTime= */ 0,
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/integration/FullScreenMagnificationMouseFollowingTest.kt b/services/tests/servicestests/src/com/android/server/accessibility/integration/FullScreenMagnificationMouseFollowingTest.kt
new file mode 100644
index 000000000000..679bba4017fb
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/accessibility/integration/FullScreenMagnificationMouseFollowingTest.kt
@@ -0,0 +1,378 @@
+/*
+ * 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.accessibility.integration
+
+import android.Manifest
+import android.accessibility.cts.common.AccessibilityDumpOnFailureRule
+import android.accessibility.cts.common.InstrumentedAccessibilityService
+import android.accessibility.cts.common.InstrumentedAccessibilityServiceTestRule
+import android.accessibilityservice.AccessibilityService
+import android.accessibilityservice.AccessibilityServiceInfo
+import android.accessibilityservice.MagnificationConfig
+import android.app.Activity
+import android.app.Instrumentation
+import android.app.UiAutomation
+import android.companion.virtual.VirtualDeviceManager
+import android.graphics.PointF
+import android.hardware.display.DisplayManager
+import android.hardware.display.VirtualDisplay
+import android.hardware.input.InputManager
+import android.hardware.input.VirtualMouse
+import android.hardware.input.VirtualMouseConfig
+import android.hardware.input.VirtualMouseRelativeEvent
+import android.os.Handler
+import android.os.Looper
+import android.os.OutcomeReceiver
+import android.platform.test.annotations.RequiresFlagsEnabled
+import android.testing.PollingCheck
+import android.view.Display
+import android.view.InputDevice
+import android.view.MotionEvent
+import android.virtualdevice.cts.common.VirtualDeviceRule
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.server.accessibility.Flags
+import com.google.common.truth.Truth.assertThat
+import java.util.concurrent.CompletableFuture
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+import kotlin.math.abs
+import kotlin.time.Duration.Companion.milliseconds
+import kotlin.time.Duration.Companion.seconds
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.rules.RuleChain
+import org.junit.runner.RunWith
+
+// Convenient extension functions for float.
+private const val EPS = 0.00001f
+private fun Float.nearEq(other: Float) = abs(this - other) < EPS
+private fun PointF.nearEq(other: PointF) = this.x.nearEq(other.x) && this.y.nearEq(other.y)
+
+/** End-to-end tests for full screen magnification following mouse cursor. */
+@RunWith(AndroidJUnit4::class)
+@RequiresFlagsEnabled(Flags.FLAG_ENABLE_MAGNIFICATION_FOLLOWS_MOUSE_WITH_POINTER_MOTION_FILTER)
+class FullScreenMagnificationMouseFollowingTest {
+
+ private lateinit var instrumentation: Instrumentation
+ private lateinit var uiAutomation: UiAutomation
+
+ private val magnificationAccessibilityServiceRule =
+ InstrumentedAccessibilityServiceTestRule<TestMagnificationAccessibilityService>(
+ TestMagnificationAccessibilityService::class.java, false
+ )
+ private lateinit var service: TestMagnificationAccessibilityService
+
+ // virtualDeviceRule tears down `virtualDevice` and `virtualDisplay`.
+ // Note that CheckFlagsRule is a part of VirtualDeviceRule. See its javadoc.
+ val virtualDeviceRule: VirtualDeviceRule =
+ VirtualDeviceRule.withAdditionalPermissions(Manifest.permission.MANAGE_ACTIVITY_TASKS)
+ private lateinit var virtualDevice: VirtualDeviceManager.VirtualDevice
+ private lateinit var virtualDisplay: VirtualDisplay
+
+ // Once created, it's our responsibility to close the mouse.
+ private lateinit var virtualMouse: VirtualMouse
+
+ @get:Rule
+ val ruleChain: RuleChain =
+ RuleChain.outerRule(virtualDeviceRule)
+ .around(magnificationAccessibilityServiceRule)
+ .around(AccessibilityDumpOnFailureRule())
+
+ @Before
+ fun setUp() {
+ instrumentation = InstrumentationRegistry.getInstrumentation()
+ uiAutomation =
+ instrumentation.getUiAutomation(UiAutomation.FLAG_DONT_SUPPRESS_ACCESSIBILITY_SERVICES)
+ uiAutomation.serviceInfo =
+ uiAutomation.serviceInfo!!.apply {
+ flags = flags or AccessibilityServiceInfo.FLAG_RETRIEVE_INTERACTIVE_WINDOWS
+ }
+
+ prepareVirtualDevices()
+
+ launchTestActivityFullscreen(virtualDisplay.display.displayId)
+
+ service = magnificationAccessibilityServiceRule.enableService()
+ service.observingDisplayId = virtualDisplay.display.displayId
+ }
+
+ @After
+ fun cleanUp() {
+ if (this::virtualMouse.isInitialized) {
+ virtualMouse.close()
+ }
+ }
+
+ // Note on continuous movement:
+ // Assume that the entire display is magnified, and the zoom level is z.
+ // In continuous movement, mouse speed relative to the unscaled physical display is the same as
+ // unmagnified speed. While, when a cursor moves from the left edge to the right edge of the
+ // screen, the magnification center moves from the left bound to the right bound, which is
+ // (display width) * (z - 1) / z.
+ //
+ // Similarly, when the mouse cursor moves by d in unscaled, display coordinates,
+ // the magnification center moves by d * (z - 1) / z.
+
+ @Test
+ fun testContinuous_toBottomRight() {
+ ensureMouseAtCenter()
+
+ val controller = service.getMagnificationController(virtualDisplay.display.displayId)
+
+ scaleTo(controller, 2f)
+ assertMagnification(controller, scale = 2f, CENTER_X, CENTER_Y)
+
+ // Move cursor by (10, 15)
+ // This will move magnification center by (5, 7.5)
+ sendMouseMove(10f, 15f)
+ assertCursorLocation(CENTER_X + 10, CENTER_Y + 15)
+ assertMagnification(controller, scale = 2f, CENTER_X + 5, CENTER_Y + 7.5f)
+
+ // Move cursor to the rest of the way to the edge.
+ sendMouseMove(DISPLAY_WIDTH - 10, DISPLAY_HEIGHT - 15)
+ assertCursorLocation(DISPLAY_WIDTH - 1, DISPLAY_HEIGHT - 1)
+ assertMagnification(controller, scale = 2f, DISPLAY_WIDTH * 3 / 4, DISPLAY_HEIGHT * 3 / 4)
+
+ // Move cursor further won't move the magnification.
+ sendMouseMove(100f, 100f)
+ assertCursorLocation(DISPLAY_WIDTH - 1, DISPLAY_HEIGHT - 1)
+ }
+
+ @Test
+ fun testContinuous_toTopLeft() {
+ ensureMouseAtCenter()
+
+ val controller = service.getMagnificationController(virtualDisplay.display.displayId)
+
+ scaleTo(controller, 3f)
+ assertMagnification(controller, scale = 3f, CENTER_X, CENTER_Y)
+
+ // Move cursor by (-30, -15)
+ // This will move magnification center by (-20, -10)
+ sendMouseMove(-30f, -15f)
+ assertCursorLocation(CENTER_X - 30, CENTER_Y - 15)
+ assertMagnification(controller, scale = 3f, CENTER_X - 20, CENTER_Y - 10)
+
+ // Move cursor to the rest of the way to the edge.
+ sendMouseMove(-CENTER_X + 30, -CENTER_Y + 15)
+ assertCursorLocation(0f, 0f)
+ assertMagnification(controller, scale = 3f, DISPLAY_WIDTH / 6, DISPLAY_HEIGHT / 6)
+
+ // Move cursor further won't move the magnification.
+ sendMouseMove(-100f, -100f)
+ assertCursorLocation(0f, 0f)
+ assertMagnification(controller, scale = 3f, DISPLAY_WIDTH / 6, DISPLAY_HEIGHT / 6)
+ }
+
+ private fun ensureMouseAtCenter() {
+ val displayCenter = PointF(320f, 240f)
+ val cursorLocation = virtualMouse.cursorPosition
+ if (!cursorLocation.nearEq(displayCenter)) {
+ sendMouseMove(displayCenter.x - cursorLocation.x, displayCenter.y - cursorLocation.y)
+ assertCursorLocation(320f, 240f)
+ }
+ }
+
+ private fun sendMouseMove(dx: Float, dy: Float) {
+ virtualMouse.sendRelativeEvent(
+ VirtualMouseRelativeEvent.Builder().setRelativeX(dx).setRelativeY(dy).build()
+ )
+ }
+
+ /**
+ * Asserts that the cursor location is at the specified coordinates. The coordinates
+ * are in the non-scaled, display coordinates.
+ */
+ private fun assertCursorLocation(x: Float, y: Float) {
+ PollingCheck.check("Wait for the cursor at ($x, $y)", CURSOR_TIMEOUT.inWholeMilliseconds) {
+ service.lastObservedCursorLocation?.let { it.x.nearEq(x) && it.y.nearEq(y) } ?: false
+ }
+ }
+
+ private fun scaleTo(controller: AccessibilityService.MagnificationController, scale: Float) {
+ val config =
+ MagnificationConfig.Builder()
+ .setActivated(true)
+ .setMode(MagnificationConfig.MAGNIFICATION_MODE_FULLSCREEN)
+ .setScale(scale)
+ .build()
+ val setResult = BooleanArray(1)
+ service.runOnServiceSync { setResult[0] = controller.setMagnificationConfig(config, false) }
+ assertThat(setResult[0]).isTrue()
+ }
+
+ private fun assertMagnification(
+ controller: AccessibilityService.MagnificationController,
+ scale: Float = Float.NaN, centerX: Float = Float.NaN, centerY: Float = Float.NaN
+ ) {
+ PollingCheck.check(
+ "Wait for the magnification to scale=$scale, centerX=$centerX, centerY=$centerY",
+ MAGNIFICATION_TIMEOUT.inWholeMilliseconds
+ ) check@{
+ val actual = controller.getMagnificationConfig() ?: return@check false
+ actual.isActivated &&
+ (actual.mode == MagnificationConfig.MAGNIFICATION_MODE_FULLSCREEN) &&
+ (scale.isNaN() || scale.nearEq(actual.scale)) &&
+ (centerX.isNaN() || centerX.nearEq(actual.centerX)) &&
+ (centerY.isNaN() || centerY.nearEq(actual.centerY))
+ }
+ }
+
+ /**
+ * Sets up a virtual display and a virtual mouse for the test. The virtual mouse is associated
+ * with the virtual display.
+ */
+ private fun prepareVirtualDevices() {
+ val deviceLatch = CountDownLatch(1)
+ val im = instrumentation.context.getSystemService(InputManager::class.java)
+ val inputDeviceListener =
+ object : InputManager.InputDeviceListener {
+ override fun onInputDeviceAdded(deviceId: Int) {
+ onInputDeviceChanged(deviceId)
+ }
+
+ override fun onInputDeviceRemoved(deviceId: Int) {}
+
+ override fun onInputDeviceChanged(deviceId: Int) {
+ val device = im.getInputDevice(deviceId) ?: return
+ if (device.vendorId == VIRTUAL_MOUSE_VENDOR_ID &&
+ device.productId == VIRTUAL_MOUSE_PRODUCT_ID
+ ) {
+ deviceLatch.countDown()
+ }
+ }
+ }
+ im.registerInputDeviceListener(inputDeviceListener, Handler(Looper.getMainLooper()))
+
+ virtualDevice = virtualDeviceRule.createManagedVirtualDevice()
+ virtualDisplay =
+ virtualDeviceRule.createManagedVirtualDisplay(
+ virtualDevice,
+ VirtualDeviceRule
+ .createDefaultVirtualDisplayConfigBuilder(
+ DISPLAY_WIDTH.toInt(),
+ DISPLAY_HEIGHT.toInt()
+ )
+ .setFlags(
+ DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC
+ or DisplayManager.VIRTUAL_DISPLAY_FLAG_TRUSTED
+ or DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY
+ )
+ )!!
+ virtualMouse =
+ virtualDevice.createVirtualMouse(
+ VirtualMouseConfig.Builder()
+ .setVendorId(VIRTUAL_MOUSE_VENDOR_ID)
+ .setProductId(VIRTUAL_MOUSE_PRODUCT_ID)
+ .setAssociatedDisplayId(virtualDisplay.display.displayId)
+ .setInputDeviceName("VirtualMouse")
+ .build()
+ )
+
+ deviceLatch.await(UI_IDLE_GLOBAL_TIMEOUT.inWholeSeconds, TimeUnit.SECONDS)
+ im.unregisterInputDeviceListener(inputDeviceListener)
+ }
+
+ /**
+ * Launches a test (empty) activity and makes it fullscreen on the specified display. This
+ * ensures that system bars are hidden and the full screen magnification enlarges the entire
+ * display.
+ */
+ private fun launchTestActivityFullscreen(displayId: Int) {
+ val future = CompletableFuture<Void?>()
+ val fullscreenCallback =
+ object : OutcomeReceiver<Void, Throwable> {
+ override fun onResult(result: Void?) {
+ future.complete(null)
+ }
+
+ override fun onError(error: Throwable) {
+ future.completeExceptionally(error)
+ }
+ }
+
+ val activity =
+ virtualDeviceRule.startActivityOnDisplaySync<TestActivity>(
+ displayId,
+ TestActivity::class.java
+ )
+ instrumentation.runOnMainSync {
+ activity.requestFullscreenMode(
+ Activity.FULLSCREEN_MODE_REQUEST_ENTER,
+ fullscreenCallback
+ )
+ }
+ future.get(UI_IDLE_GLOBAL_TIMEOUT.inWholeSeconds, TimeUnit.SECONDS)
+
+ uiAutomation.waitForIdle(
+ UI_IDLE_TIMEOUT.inWholeMilliseconds, UI_IDLE_GLOBAL_TIMEOUT.inWholeMilliseconds
+ )
+ }
+
+ class TestMagnificationAccessibilityService : InstrumentedAccessibilityService() {
+ private val lock = Any()
+
+ var observingDisplayId = Display.INVALID_DISPLAY
+ set(v) {
+ synchronized(lock) { field = v }
+ }
+
+ var lastObservedCursorLocation: PointF? = null
+ private set
+ get() {
+ synchronized(lock) {
+ return field
+ }
+ }
+
+ override fun onServiceConnected() {
+ serviceInfo =
+ getServiceInfo()!!.apply { setMotionEventSources(InputDevice.SOURCE_MOUSE) }
+
+ super.onServiceConnected()
+ }
+
+ override fun onMotionEvent(event: MotionEvent) {
+ super.onMotionEvent(event)
+
+ synchronized(lock) {
+ if (event.displayId == observingDisplayId) {
+ lastObservedCursorLocation = PointF(event.x, event.y)
+ }
+ }
+ }
+ }
+
+ class TestActivity : Activity()
+
+ companion object {
+ private const val VIRTUAL_MOUSE_VENDOR_ID = 123
+ private const val VIRTUAL_MOUSE_PRODUCT_ID = 456
+
+ private val CURSOR_TIMEOUT = 1.seconds
+ private val MAGNIFICATION_TIMEOUT = 3.seconds
+ private val UI_IDLE_TIMEOUT = 500.milliseconds
+ private val UI_IDLE_GLOBAL_TIMEOUT = 5.seconds
+
+ private const val DISPLAY_WIDTH = 640.0f
+ private const val DISPLAY_HEIGHT = 480.0f
+ private const val CENTER_X = DISPLAY_WIDTH / 2f
+ private const val CENTER_Y = DISPLAY_HEIGHT / 2f
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/AuthenticationStatsCollectorTest.java b/services/tests/servicestests/src/com/android/server/biometrics/AuthenticationStatsCollectorTest.java
index 9eeb4f3f218f..579114bc6577 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/AuthenticationStatsCollectorTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/AuthenticationStatsCollectorTest.java
@@ -16,6 +16,7 @@
package com.android.server.biometrics;
+import static com.android.server.biometrics.AuthenticationStatsCollector.FRR_MINIMAL_DURATION;
import static com.android.server.biometrics.AuthenticationStatsCollector.MAXIMUM_ENROLLMENT_NOTIFICATIONS;
import static com.google.common.truth.Truth.assertThat;
@@ -37,9 +38,13 @@ import android.content.Context;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.content.res.Resources;
+import android.hardware.biometrics.BiometricsProtoEnums;
import android.hardware.face.FaceManager;
import android.hardware.fingerprint.FingerprintManager;
+import android.platform.test.annotations.DisableFlags;
+import android.platform.test.annotations.EnableFlags;
import android.platform.test.annotations.Presubmit;
+import android.platform.test.flag.junit.SetFlagsRule;
import androidx.test.filters.SmallTest;
@@ -54,6 +59,7 @@ import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import java.io.File;
+import java.time.Clock;
@Presubmit
@SmallTest
@@ -61,6 +67,8 @@ public class AuthenticationStatsCollectorTest {
@Rule
public MockitoRule mockitoRule = MockitoJUnit.rule();
+ @Rule
+ public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
private AuthenticationStatsCollector mAuthenticationStatsCollector;
private static final float FRR_THRESHOLD = 0.2f;
@@ -82,6 +90,8 @@ public class AuthenticationStatsCollectorTest {
private SharedPreferences.Editor mEditor;
@Mock
private BiometricNotification mBiometricNotification;
+ @Mock
+ private Clock mClock;
@Before
public void setUp() {
@@ -107,9 +117,12 @@ public class AuthenticationStatsCollectorTest {
when(mSharedPreferences.edit()).thenReturn(mEditor);
when(mEditor.putFloat(anyString(), anyFloat())).thenReturn(mEditor);
when(mEditor.putStringSet(anyString(), anySet())).thenReturn(mEditor);
+ when(mBiometricNotification.sendCustomizeFpFrrNotification(eq(mContext)))
+ .thenReturn(true);
+ when(mClock.millis()).thenReturn(Clock.systemUTC().millis());
mAuthenticationStatsCollector = new AuthenticationStatsCollector(mContext,
- 0 /* modality */, mBiometricNotification);
+ 0 /* modality */, mBiometricNotification, mClock);
}
@Test
@@ -130,6 +143,8 @@ public class AuthenticationStatsCollectorTest {
assertThat(authenticationStats.getTotalAttempts()).isEqualTo(1);
assertThat(authenticationStats.getRejectedAttempts()).isEqualTo(0);
assertThat(authenticationStats.getEnrollmentNotifications()).isEqualTo(0);
+ assertThat(authenticationStats.getLastEnrollmentTime()).isEqualTo(0L);
+ assertThat(authenticationStats.getLastFrrNotificationTime()).isEqualTo(0L);
}
@Test
@@ -151,6 +166,8 @@ public class AuthenticationStatsCollectorTest {
assertThat(authenticationStats.getTotalAttempts()).isEqualTo(1);
assertThat(authenticationStats.getRejectedAttempts()).isEqualTo(1);
assertThat(authenticationStats.getEnrollmentNotifications()).isEqualTo(0);
+ assertThat(authenticationStats.getLastEnrollmentTime()).isEqualTo(0L);
+ assertThat(authenticationStats.getLastFrrNotificationTime()).isEqualTo(0L);
}
/**
@@ -165,6 +182,7 @@ public class AuthenticationStatsCollectorTest {
new AuthenticationStats(USER_ID_1, 400 /* totalAttempts */,
40 /* rejectedAttempts */,
MAXIMUM_ENROLLMENT_NOTIFICATIONS /* enrollmentNotifications */,
+ 100L /* lastEnrollmentTime */, 200L /* lastFrrNotificationTime */,
0 /* modality */));
mAuthenticationStatsCollector.authenticate(USER_ID_1, false /* authenticated */);
@@ -178,14 +196,18 @@ public class AuthenticationStatsCollectorTest {
assertThat(authenticationStats.getRejectedAttempts()).isEqualTo(40);
assertThat(authenticationStats.getEnrollmentNotifications())
.isEqualTo(MAXIMUM_ENROLLMENT_NOTIFICATIONS);
+ assertThat(authenticationStats.getLastEnrollmentTime()).isEqualTo(100L);
+ assertThat(authenticationStats.getLastFrrNotificationTime()).isEqualTo(200L);
}
+ // TODO WIP
@Test
public void authenticate_frrNotExceeded_notificationNotExceeded_shouldNotSendNotification() {
mAuthenticationStatsCollector.setAuthenticationStatsForUser(USER_ID_1,
new AuthenticationStats(USER_ID_1, 500 /* totalAttempts */,
40 /* rejectedAttempts */, 0 /* enrollmentNotifications */,
+ 100L /* lastEnrollmentTime */, 200L /* lastFrrNotificationTime */,
0 /* modality */));
when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT))
@@ -196,6 +218,7 @@ public class AuthenticationStatsCollectorTest {
mAuthenticationStatsCollector.authenticate(USER_ID_1, false /* authenticated */);
// Assert that no notification should be sent.
+ verify(mBiometricNotification, never()).sendCustomizeFpFrrNotification(any());
verify(mBiometricNotification, never()).sendFaceEnrollNotification(any());
verify(mBiometricNotification, never()).sendFpEnrollNotification(any());
// Assert that data has been reset.
@@ -205,6 +228,9 @@ public class AuthenticationStatsCollectorTest {
assertThat(authenticationStats.getRejectedAttempts()).isEqualTo(0);
assertThat(authenticationStats.getEnrollmentNotifications()).isEqualTo(0);
assertThat(authenticationStats.getFrr()).isWithin(0f).of(-1.0f);
+ // lastEnrollmentTime and lastFrrNotificationTime shall be kept
+ assertThat(authenticationStats.getLastEnrollmentTime()).isEqualTo(100L);
+ assertThat(authenticationStats.getLastFrrNotificationTime()).isEqualTo(200L);
}
@Test
@@ -214,11 +240,13 @@ public class AuthenticationStatsCollectorTest {
new AuthenticationStats(USER_ID_1, 500 /* totalAttempts */,
400 /* rejectedAttempts */,
MAXIMUM_ENROLLMENT_NOTIFICATIONS /* enrollmentNotifications */,
+ 100L /* lastEnrollmentTime */, 200L /* lastFrrNotificationTime */,
0 /* modality */));
mAuthenticationStatsCollector.authenticate(USER_ID_1, false /* authenticated */);
// Assert that no notification should be sent.
+ verify(mBiometricNotification, never()).sendCustomizeFpFrrNotification(any());
verify(mBiometricNotification, never()).sendFaceEnrollNotification(any());
verify(mBiometricNotification, never()).sendFpEnrollNotification(any());
// Assert that data hasn't been reset.
@@ -228,15 +256,88 @@ public class AuthenticationStatsCollectorTest {
assertThat(authenticationStats.getRejectedAttempts()).isEqualTo(400);
assertThat(authenticationStats.getEnrollmentNotifications())
.isEqualTo(MAXIMUM_ENROLLMENT_NOTIFICATIONS);
+ assertThat(authenticationStats.getLastEnrollmentTime()).isEqualTo(100L);
+ assertThat(authenticationStats.getLastFrrNotificationTime()).isEqualTo(200L);
assertThat(authenticationStats.getFrr()).isWithin(0f).of(0.8f);
}
@Test
+ @DisableFlags(Flags.FLAG_FRR_DIALOG_IMPROVEMENT)
public void authenticate_frrExceeded_bothBiometricsEnrolled_shouldNotSendNotification() {
mAuthenticationStatsCollector.setAuthenticationStatsForUser(USER_ID_1,
new AuthenticationStats(USER_ID_1, 500 /* totalAttempts */,
400 /* rejectedAttempts */, 0 /* enrollmentNotifications */,
+ 100L /* lastEnrollmentTime */, 200L /* lastFrrNotificationTime */,
+ 0 /* modality */));
+
+ when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT))
+ .thenReturn(true);
+ when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_FACE)).thenReturn(true);
+ when(mFingerprintManager.hasEnrolledTemplates(anyInt())).thenReturn(true);
+ when(mFaceManager.hasEnrolledTemplates(anyInt())).thenReturn(true);
+
+ mAuthenticationStatsCollector.authenticate(USER_ID_1, false /* authenticated */);
+
+ // Assert that no notification should be sent.
+ verify(mBiometricNotification, never()).sendCustomizeFpFrrNotification(any());
+ verify(mBiometricNotification, never()).sendFaceEnrollNotification(any());
+ verify(mBiometricNotification, never()).sendFpEnrollNotification(any());
+ // Assert that data hasn't been reset.
+ AuthenticationStats authenticationStats = mAuthenticationStatsCollector
+ .getAuthenticationStatsForUser(USER_ID_1);
+ assertThat(authenticationStats.getTotalAttempts()).isEqualTo(500);
+ assertThat(authenticationStats.getRejectedAttempts()).isEqualTo(400);
+ assertThat(authenticationStats.getEnrollmentNotifications()).isEqualTo(0);
+ assertThat(authenticationStats.getFrr()).isWithin(0f).of(0.8f);
+ assertThat(authenticationStats.getLastEnrollmentTime()).isEqualTo(100L);
+ assertThat(authenticationStats.getLastFrrNotificationTime()).isEqualTo(200L);
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_FRR_DIALOG_IMPROVEMENT)
+ public void authenticate_enrollTimeNotPass_bothBiometricsEnrolled_shouldNotSendNotification() {
+
+ long lastEnrollmentTime = 60L * 60L * 1000L;
+ mAuthenticationStatsCollector.setAuthenticationStatsForUser(USER_ID_1,
+ new AuthenticationStats(USER_ID_1, 500 /* totalAttempts */,
+ 400 /* rejectedAttempts */, 0 /* enrollmentNotifications */,
+ lastEnrollmentTime, 0L /* lastFrrNotificationTime */,
+ 0 /* modality */));
+
+ when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT))
+ .thenReturn(true);
+ when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_FACE)).thenReturn(true);
+ when(mFingerprintManager.hasEnrolledTemplates(anyInt())).thenReturn(true);
+ when(mFaceManager.hasEnrolledTemplates(anyInt())).thenReturn(true);
+ when(mClock.millis()).thenReturn(lastEnrollmentTime + FRR_MINIMAL_DURATION.toMillis());
+
+ mAuthenticationStatsCollector.authenticate(USER_ID_1, false /* authenticated */);
+
+ // Assert that no notification should be sent.
+ verify(mBiometricNotification, never()).sendCustomizeFpFrrNotification(any());
+ verify(mBiometricNotification, never()).sendFaceEnrollNotification(any());
+ verify(mBiometricNotification, never()).sendFpEnrollNotification(any());
+ // Assert that data hasn't been reset.
+ AuthenticationStats authenticationStats = mAuthenticationStatsCollector
+ .getAuthenticationStatsForUser(USER_ID_1);
+ assertThat(authenticationStats.getTotalAttempts()).isEqualTo(500);
+ assertThat(authenticationStats.getRejectedAttempts()).isEqualTo(400);
+ assertThat(authenticationStats.getEnrollmentNotifications()).isEqualTo(0);
+ assertThat(authenticationStats.getFrr()).isWithin(0f).of(0.8f);
+ assertThat(authenticationStats.getLastEnrollmentTime()).isEqualTo(lastEnrollmentTime);
+ assertThat(authenticationStats.getLastFrrNotificationTime()).isEqualTo(0L);
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_FRR_DIALOG_IMPROVEMENT)
+ public void authenticate_lastFrrTimeNotPass_bothBiometricsEnrolled_shouldNotSendNotification() {
+
+ long lastFrrNotificationTime = 200L;
+ mAuthenticationStatsCollector.setAuthenticationStatsForUser(USER_ID_1,
+ new AuthenticationStats(USER_ID_1, 500 /* totalAttempts */,
+ 400 /* rejectedAttempts */, 0 /* enrollmentNotifications */,
+ 100L /* lastEnrollmentTime */, lastFrrNotificationTime,
0 /* modality */));
when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT))
@@ -244,10 +345,12 @@ public class AuthenticationStatsCollectorTest {
when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_FACE)).thenReturn(true);
when(mFingerprintManager.hasEnrolledTemplates(anyInt())).thenReturn(true);
when(mFaceManager.hasEnrolledTemplates(anyInt())).thenReturn(true);
+ when(mClock.millis()).thenReturn(lastFrrNotificationTime + FRR_MINIMAL_DURATION.toMillis());
mAuthenticationStatsCollector.authenticate(USER_ID_1, false /* authenticated */);
// Assert that no notification should be sent.
+ verify(mBiometricNotification, never()).sendCustomizeFpFrrNotification(any());
verify(mBiometricNotification, never()).sendFaceEnrollNotification(any());
verify(mBiometricNotification, never()).sendFpEnrollNotification(any());
// Assert that data hasn't been reset.
@@ -257,6 +360,9 @@ public class AuthenticationStatsCollectorTest {
assertThat(authenticationStats.getRejectedAttempts()).isEqualTo(400);
assertThat(authenticationStats.getEnrollmentNotifications()).isEqualTo(0);
assertThat(authenticationStats.getFrr()).isWithin(0f).of(0.8f);
+ assertThat(authenticationStats.getLastEnrollmentTime()).isEqualTo(100L);
+ assertThat(authenticationStats.getLastFrrNotificationTime()).isEqualTo(
+ lastFrrNotificationTime);
}
@Test
@@ -265,6 +371,7 @@ public class AuthenticationStatsCollectorTest {
mAuthenticationStatsCollector.setAuthenticationStatsForUser(USER_ID_1,
new AuthenticationStats(USER_ID_1, 500 /* totalAttempts */,
400 /* rejectedAttempts */, 0 /* enrollmentNotifications */,
+ 100L /* lastEnrollmentTime */, 200L /* lastFrrNotificationTime */,
0 /* modality */));
when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT))
@@ -276,6 +383,7 @@ public class AuthenticationStatsCollectorTest {
mAuthenticationStatsCollector.authenticate(USER_ID_1, false /* authenticated */);
// Assert that no notification should be sent.
+ verify(mBiometricNotification, never()).sendCustomizeFpFrrNotification(any());
verify(mBiometricNotification, never()).sendFaceEnrollNotification(any());
verify(mBiometricNotification, never()).sendFpEnrollNotification(any());
// Assert that data hasn't been reset.
@@ -285,26 +393,75 @@ public class AuthenticationStatsCollectorTest {
assertThat(authenticationStats.getRejectedAttempts()).isEqualTo(400);
assertThat(authenticationStats.getEnrollmentNotifications()).isEqualTo(0);
assertThat(authenticationStats.getFrr()).isWithin(0f).of(0.8f);
+ assertThat(authenticationStats.getLastEnrollmentTime()).isEqualTo(100L);
+ assertThat(authenticationStats.getLastFrrNotificationTime()).isEqualTo(200L);
}
@Test
+ @DisableFlags(Flags.FLAG_FRR_DIALOG_IMPROVEMENT)
public void authenticate_frrExceeded_faceEnrolled_shouldSendFpNotification() {
+ // Use correct modality
+ mAuthenticationStatsCollector = new AuthenticationStatsCollector(mContext,
+ BiometricsProtoEnums.MODALITY_FACE, mBiometricNotification, mClock);
+
mAuthenticationStatsCollector.setAuthenticationStatsForUser(USER_ID_1,
new AuthenticationStats(USER_ID_1, 500 /* totalAttempts */,
400 /* rejectedAttempts */, 0 /* enrollmentNotifications */,
- 0 /* modality */));
+ 100L /* lastEnrollmentTime */, 200L /* lastFrrNotificationTime */,
+ BiometricsProtoEnums.MODALITY_FACE /* modality */));
+
+ when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT))
+ .thenReturn(true);
+ when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_FACE)).thenReturn(true);
+ when(mFingerprintManager.hasEnrolledTemplates(anyInt())).thenReturn(false);
+ when(mFaceManager.hasEnrolledTemplates(anyInt())).thenReturn(true);
+ when(mClock.millis()).thenReturn(3344L);
+
+ mAuthenticationStatsCollector.authenticate(USER_ID_1, false /* authenticated */);
+
+ // Assert that fingerprint enrollment notification should be sent.
+ verify(mBiometricNotification, never()).sendCustomizeFpFrrNotification(any());
+ verify(mBiometricNotification, times(1)).sendFpEnrollNotification(mContext);
+ verify(mBiometricNotification, never()).sendFaceEnrollNotification(any());
+ // Assert that data has been reset.
+ AuthenticationStats authenticationStats = mAuthenticationStatsCollector
+ .getAuthenticationStatsForUser(USER_ID_1);
+ assertThat(authenticationStats.getTotalAttempts()).isEqualTo(0);
+ assertThat(authenticationStats.getRejectedAttempts()).isEqualTo(0);
+ assertThat(authenticationStats.getFrr()).isWithin(0f).of(-1.0f);
+ // Assert that notification count has been updated.
+ assertThat(authenticationStats.getEnrollmentNotifications()).isEqualTo(1);
+ assertThat(authenticationStats.getLastEnrollmentTime()).isEqualTo(100L);
+ // Assert that lastFrrNotificationTime has been updated.
+ assertThat(authenticationStats.getLastFrrNotificationTime()).isEqualTo(3344L);
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_FRR_DIALOG_IMPROVEMENT)
+ public void authenticate_frrExceeded_faceEnrolled_shouldSendFpNotification_withFrrFlag() {
+ // Use correct modality
+ mAuthenticationStatsCollector = new AuthenticationStatsCollector(mContext,
+ BiometricsProtoEnums.MODALITY_FACE, mBiometricNotification, mClock);
+
+ mAuthenticationStatsCollector.setAuthenticationStatsForUser(USER_ID_1,
+ new AuthenticationStats(USER_ID_1, 500 /* totalAttempts */,
+ 400 /* rejectedAttempts */, 0 /* enrollmentNotifications */,
+ 100L /* lastEnrollmentTime */, 200L /* lastFrrNotificationTime */,
+ BiometricsProtoEnums.MODALITY_FACE /* modality */));
when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT))
.thenReturn(true);
when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_FACE)).thenReturn(true);
when(mFingerprintManager.hasEnrolledTemplates(anyInt())).thenReturn(false);
when(mFaceManager.hasEnrolledTemplates(anyInt())).thenReturn(true);
+ long newLastFrrNotificationTime = 200L + FRR_MINIMAL_DURATION.toMillis() + 1;
+ when(mClock.millis()).thenReturn(newLastFrrNotificationTime);
mAuthenticationStatsCollector.authenticate(USER_ID_1, false /* authenticated */);
// Assert that fingerprint enrollment notification should be sent.
- verify(mBiometricNotification, times(1))
- .sendFpEnrollNotification(mContext);
+ verify(mBiometricNotification, never()).sendCustomizeFpFrrNotification(any());
+ verify(mBiometricNotification, times(1)).sendFpEnrollNotification(mContext);
verify(mBiometricNotification, never()).sendFaceEnrollNotification(any());
// Assert that data has been reset.
AuthenticationStats authenticationStats = mAuthenticationStatsCollector
@@ -314,26 +471,116 @@ public class AuthenticationStatsCollectorTest {
assertThat(authenticationStats.getFrr()).isWithin(0f).of(-1.0f);
// Assert that notification count has been updated.
assertThat(authenticationStats.getEnrollmentNotifications()).isEqualTo(1);
+ assertThat(authenticationStats.getLastEnrollmentTime()).isEqualTo(100L);
+ // Assert that lastFrrNotificationTime has been updated.
+ assertThat(authenticationStats.getLastFrrNotificationTime()).isEqualTo(
+ newLastFrrNotificationTime);
}
@Test
+ @DisableFlags(Flags.FLAG_FRR_DIALOG_IMPROVEMENT)
public void authenticate_frrExceeded_fpEnrolled_shouldSendFaceNotification() {
mAuthenticationStatsCollector.setAuthenticationStatsForUser(USER_ID_1,
new AuthenticationStats(USER_ID_1, 500 /* totalAttempts */,
400 /* rejectedAttempts */, 0 /* enrollmentNotifications */,
- 0 /* modality */));
+ 100L /* lastEnrollmentTime */, 200L /* lastFrrNotificationTime */,
+ BiometricsProtoEnums.MODALITY_FINGERPRINT /* modality */));
+
+ when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT))
+ .thenReturn(true);
+ when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_FACE)).thenReturn(true);
+ when(mFingerprintManager.hasEnrolledTemplates(anyInt())).thenReturn(true);
+ when(mFaceManager.hasEnrolledTemplates(anyInt())).thenReturn(false);
+ when(mClock.millis()).thenReturn(3344L);
+
+ mAuthenticationStatsCollector.authenticate(USER_ID_1, false /* authenticated */);
+
+ // Assert that fingerprint enrollment notification should be sent.
+ verify(mBiometricNotification, never()).sendCustomizeFpFrrNotification(any());
+ verify(mBiometricNotification, times(1)).sendFaceEnrollNotification(mContext);
+ verify(mBiometricNotification, never()).sendFpEnrollNotification(any());
+ // Assert that data has been reset.
+ AuthenticationStats authenticationStats = mAuthenticationStatsCollector
+ .getAuthenticationStatsForUser(USER_ID_1);
+ assertThat(authenticationStats.getTotalAttempts()).isEqualTo(0);
+ assertThat(authenticationStats.getRejectedAttempts()).isEqualTo(0);
+ assertThat(authenticationStats.getFrr()).isWithin(0f).of(-1.0f);
+ // Assert that notification count has been updated.
+ assertThat(authenticationStats.getEnrollmentNotifications()).isEqualTo(1);
+ assertThat(authenticationStats.getLastEnrollmentTime()).isEqualTo(100L);
+ // Assert that lastFrrNotificationTime has been updated.
+ assertThat(authenticationStats.getLastFrrNotificationTime()).isEqualTo(3344L);
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_FRR_DIALOG_IMPROVEMENT)
+ public void authenticate_frrExceeded_fpEnrolled_shouldSendCustNotification_withFrrFlag() {
+ // Use correct modality
+ mAuthenticationStatsCollector = new AuthenticationStatsCollector(mContext,
+ BiometricsProtoEnums.MODALITY_FINGERPRINT, mBiometricNotification, mClock);
+
+ mAuthenticationStatsCollector.setAuthenticationStatsForUser(USER_ID_1,
+ new AuthenticationStats(USER_ID_1, 500 /* totalAttempts */,
+ 400 /* rejectedAttempts */, 0 /* enrollmentNotifications */,
+ 100L /* lastEnrollmentTime */, 200L /* lastFrrNotificationTime */,
+ BiometricsProtoEnums.MODALITY_FINGERPRINT /* modality */));
+
+ when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT))
+ .thenReturn(true);
+ when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_FACE)).thenReturn(true);
+ when(mFingerprintManager.hasEnrolledTemplates(anyInt())).thenReturn(true);
+ when(mFaceManager.hasEnrolledTemplates(anyInt())).thenReturn(false);
+ long newFrrNotificationTime = 200L + FRR_MINIMAL_DURATION.toMillis() + 1;
+ when(mClock.millis()).thenReturn(newFrrNotificationTime);
+
+ mAuthenticationStatsCollector.authenticate(USER_ID_1, false /* authenticated */);
+
+ // Assert that fingerprint enrollment notification should be sent.
+ verify(mBiometricNotification, times(1)).sendCustomizeFpFrrNotification(mContext);
+ verify(mBiometricNotification, never()).sendFaceEnrollNotification(any());
+ verify(mBiometricNotification, never()).sendFpEnrollNotification(any());
+ // Assert that data has been reset.
+ AuthenticationStats authenticationStats = mAuthenticationStatsCollector
+ .getAuthenticationStatsForUser(USER_ID_1);
+ assertThat(authenticationStats.getTotalAttempts()).isEqualTo(0);
+ assertThat(authenticationStats.getRejectedAttempts()).isEqualTo(0);
+ assertThat(authenticationStats.getFrr()).isWithin(0f).of(-1.0f);
+ // Assert that notification count has been updated.
+ assertThat(authenticationStats.getEnrollmentNotifications()).isEqualTo(1);
+ assertThat(authenticationStats.getLastEnrollmentTime()).isEqualTo(100L);
+ // Assert that lastFrrNotificationTime has been updated.
+ assertThat(authenticationStats.getLastFrrNotificationTime()).isEqualTo(
+ newFrrNotificationTime);
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_FRR_DIALOG_IMPROVEMENT)
+ public void authenticate_frrExceeded_fpEnrolled_shouldSendFaceNotification_withFrrFlag() {
+ // Use correct modality
+ mAuthenticationStatsCollector = new AuthenticationStatsCollector(mContext,
+ BiometricsProtoEnums.MODALITY_FINGERPRINT, mBiometricNotification, mClock);
+
+ mAuthenticationStatsCollector.setAuthenticationStatsForUser(USER_ID_1,
+ new AuthenticationStats(USER_ID_1, 500 /* totalAttempts */,
+ 400 /* rejectedAttempts */, 0 /* enrollmentNotifications */,
+ 100L /* lastEnrollmentTime */, 200L /* lastFrrNotificationTime */,
+ BiometricsProtoEnums.MODALITY_FINGERPRINT /* modality */));
when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT))
.thenReturn(true);
when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_FACE)).thenReturn(true);
when(mFingerprintManager.hasEnrolledTemplates(anyInt())).thenReturn(true);
when(mFaceManager.hasEnrolledTemplates(anyInt())).thenReturn(false);
+ long newFrrNotificationTime = 200L + FRR_MINIMAL_DURATION.toMillis() + 1;
+ when(mClock.millis()).thenReturn(newFrrNotificationTime);
+ when(mBiometricNotification.sendCustomizeFpFrrNotification(eq(mContext)))
+ .thenReturn(false);
mAuthenticationStatsCollector.authenticate(USER_ID_1, false /* authenticated */);
// Assert that fingerprint enrollment notification should be sent.
- verify(mBiometricNotification, times(1))
- .sendFaceEnrollNotification(mContext);
+ verify(mBiometricNotification, times(1)).sendCustomizeFpFrrNotification(mContext);
+ verify(mBiometricNotification, times(1)).sendFaceEnrollNotification(mContext);
verify(mBiometricNotification, never()).sendFpEnrollNotification(any());
// Assert that data has been reset.
AuthenticationStats authenticationStats = mAuthenticationStatsCollector
@@ -343,6 +590,10 @@ public class AuthenticationStatsCollectorTest {
assertThat(authenticationStats.getFrr()).isWithin(0f).of(-1.0f);
// Assert that notification count has been updated.
assertThat(authenticationStats.getEnrollmentNotifications()).isEqualTo(1);
+ assertThat(authenticationStats.getLastEnrollmentTime()).isEqualTo(100L);
+ // Assert that lastFrrNotificationTime has been updated.
+ assertThat(authenticationStats.getLastFrrNotificationTime()).isEqualTo(
+ newFrrNotificationTime);
}
@Test
@@ -352,11 +603,12 @@ public class AuthenticationStatsCollectorTest {
.thenReturn(false);
AuthenticationStatsCollector authenticationStatsCollector =
new AuthenticationStatsCollector(mContext, 0 /* modality */,
- mBiometricNotification);
+ mBiometricNotification, Clock.systemUTC());
authenticationStatsCollector.setAuthenticationStatsForUser(USER_ID_1,
new AuthenticationStats(USER_ID_1, 500 /* totalAttempts */,
400 /* rejectedAttempts */, 0 /* enrollmentNotifications */,
+ 0L /* lastEnrollmentTime */, 0L /* lastFrrNotificationTime */,
0 /* modality */));
authenticationStatsCollector.authenticate(USER_ID_1, false /* authenticated */);
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/AuthenticationStatsPersisterTest.java b/services/tests/servicestests/src/com/android/server/biometrics/AuthenticationStatsPersisterTest.java
index 32c55ebcb674..67da3ed144fc 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/AuthenticationStatsPersisterTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/AuthenticationStatsPersisterTest.java
@@ -60,8 +60,13 @@ public class AuthenticationStatsPersisterTest {
private static final String USER_ID = "user_id";
private static final String FACE_ATTEMPTS = "face_attempts";
private static final String FACE_REJECTIONS = "face_rejections";
+ private static final String FACE_LAST_ENROLL_TIME = "face_last_enroll_time";
+ private static final String FACE_LAST_FRR_NOTIFICATION_TIME = "face_last_notification_time";
private static final String FINGERPRINT_ATTEMPTS = "fingerprint_attempts";
private static final String FINGERPRINT_REJECTIONS = "fingerprint_rejections";
+ private static final String FINGERPRINT_LAST_ENROLL_TIME = "fingerprint_last_enroll_time";
+ private static final String FINGERPRINT_LAST_FRR_NOTIFICATION_TIME =
+ "fingerprint_last_notification_time";
private static final String ENROLLMENT_NOTIFICATIONS = "enrollment_notifications";
private static final String KEY = "frr_stats";
private static final String THRESHOLD_KEY = "frr_threshold";
@@ -95,10 +100,12 @@ public class AuthenticationStatsPersisterTest {
public void getAllFrrStats_face_shouldListAllFrrStats() throws JSONException {
AuthenticationStats stats1 = new AuthenticationStats(USER_ID_1,
300 /* totalAttempts */, 10 /* rejectedAttempts */,
- 0 /* enrollmentNotifications */, BiometricsProtoEnums.MODALITY_FACE);
+ 0 /* enrollmentNotifications */, 100L /* lastEnrollmentTime */,
+ 200L /* lastFrrNotificationTime */, BiometricsProtoEnums.MODALITY_FACE);
AuthenticationStats stats2 = new AuthenticationStats(USER_ID_2,
200 /* totalAttempts */, 20 /* rejectedAttempts */,
- 0 /* enrollmentNotifications */, BiometricsProtoEnums.MODALITY_FINGERPRINT);
+ 0 /* enrollmentNotifications */, 200L /* lastEnrollmentTime */,
+ 300L /* lastFrrNotificationTime */, BiometricsProtoEnums.MODALITY_FINGERPRINT);
when(mSharedPreferences.getStringSet(eq(KEY), anySet())).thenReturn(
Set.of(buildFrrStats(stats1), buildFrrStats(stats2)));
@@ -108,7 +115,8 @@ public class AuthenticationStatsPersisterTest {
assertThat(authenticationStatsList.size()).isEqualTo(2);
AuthenticationStats expectedStats2 = new AuthenticationStats(USER_ID_2,
0 /* totalAttempts */, 0 /* rejectedAttempts */,
- 0 /* enrollmentNotifications */, BiometricsProtoEnums.MODALITY_FACE);
+ 0 /* enrollmentNotifications */, 0 /* lastEnrollmentTime */,
+ 0 /* lastFrrNotificationTime */, BiometricsProtoEnums.MODALITY_FACE);
assertThat(authenticationStatsList).contains(stats1);
assertThat(authenticationStatsList).contains(expectedStats2);
}
@@ -118,11 +126,13 @@ public class AuthenticationStatsPersisterTest {
// User 1 with fingerprint authentication stats.
AuthenticationStats stats1 = new AuthenticationStats(USER_ID_1,
200 /* totalAttempts */, 20 /* rejectedAttempts */,
- 0 /* enrollmentNotifications */, BiometricsProtoEnums.MODALITY_FINGERPRINT);
+ 0 /* enrollmentNotifications */, 100L /* lastEnrollmentTime */,
+ 200L /* lastFrrNotificationTime */, BiometricsProtoEnums.MODALITY_FINGERPRINT);
// User 2 without fingerprint authentication stats.
AuthenticationStats stats2 = new AuthenticationStats(USER_ID_2,
300 /* totalAttempts */, 10 /* rejectedAttempts */,
- 0 /* enrollmentNotifications */, BiometricsProtoEnums.MODALITY_FACE);
+ 0 /* enrollmentNotifications */, 200L /* lastEnrollmentTime */,
+ 300L /* lastFrrNotificationTime */, BiometricsProtoEnums.MODALITY_FACE);
when(mSharedPreferences.getStringSet(eq(KEY), anySet())).thenReturn(
Set.of(buildFrrStats(stats1), buildFrrStats(stats2)));
@@ -133,7 +143,8 @@ public class AuthenticationStatsPersisterTest {
assertThat(authenticationStatsList.size()).isEqualTo(2);
AuthenticationStats expectedStats2 = new AuthenticationStats(USER_ID_2,
0 /* totalAttempts */, 0 /* rejectedAttempts */,
- 0 /* enrollmentNotifications */, BiometricsProtoEnums.MODALITY_FINGERPRINT);
+ 0 /* enrollmentNotifications */, 0 /* lastEnrollmentTime */,
+ 0 /* lastFrrNotificationTime */, BiometricsProtoEnums.MODALITY_FINGERPRINT);
assertThat(authenticationStatsList).contains(stats1);
assertThat(authenticationStatsList).contains(expectedStats2);
}
@@ -142,12 +153,15 @@ public class AuthenticationStatsPersisterTest {
public void persistFrrStats_newUser_face_shouldSuccess() throws JSONException {
AuthenticationStats authenticationStats = new AuthenticationStats(USER_ID_1,
300 /* totalAttempts */, 10 /* rejectedAttempts */,
- 0 /* enrollmentNotifications */, BiometricsProtoEnums.MODALITY_FACE);
+ 0 /* enrollmentNotifications */, 200L /* lastEnrollmentTime */,
+ 300L /* lastFrrNotificationTime */, BiometricsProtoEnums.MODALITY_FACE);
mAuthenticationStatsPersister.persistFrrStats(authenticationStats.getUserId(),
authenticationStats.getTotalAttempts(),
authenticationStats.getRejectedAttempts(),
authenticationStats.getEnrollmentNotifications(),
+ authenticationStats.getLastEnrollmentTime(),
+ authenticationStats.getLastFrrNotificationTime(),
authenticationStats.getModality());
verify(mEditor).putStringSet(eq(KEY), mStringSetArgumentCaptor.capture());
@@ -159,12 +173,15 @@ public class AuthenticationStatsPersisterTest {
public void persistFrrStats_newUser_fingerprint_shouldSuccess() throws JSONException {
AuthenticationStats authenticationStats = new AuthenticationStats(USER_ID_1,
300 /* totalAttempts */, 10 /* rejectedAttempts */,
- 0 /* enrollmentNotifications */, BiometricsProtoEnums.MODALITY_FINGERPRINT);
+ 0 /* enrollmentNotifications */, 100L /* lastEnrollmentTime */,
+ 200L /* lastFrrNotificationTime */, BiometricsProtoEnums.MODALITY_FINGERPRINT);
mAuthenticationStatsPersister.persistFrrStats(authenticationStats.getUserId(),
authenticationStats.getTotalAttempts(),
authenticationStats.getRejectedAttempts(),
authenticationStats.getEnrollmentNotifications(),
+ authenticationStats.getLastEnrollmentTime(),
+ authenticationStats.getLastFrrNotificationTime(),
authenticationStats.getModality());
verify(mEditor).putStringSet(eq(KEY), mStringSetArgumentCaptor.capture());
@@ -176,10 +193,12 @@ public class AuthenticationStatsPersisterTest {
public void persistFrrStats_existingUser_shouldUpdateRecord() throws JSONException {
AuthenticationStats authenticationStats = new AuthenticationStats(USER_ID_1,
300 /* totalAttempts */, 10 /* rejectedAttempts */,
- 0 /* enrollmentNotifications */, BiometricsProtoEnums.MODALITY_FACE);
+ 0 /* enrollmentNotifications */, 100L /* lastEnrollmentTime */,
+ 200L /* lastFrrNotificationTime */, BiometricsProtoEnums.MODALITY_FACE);
AuthenticationStats newAuthenticationStats = new AuthenticationStats(USER_ID_1,
500 /* totalAttempts */, 30 /* rejectedAttempts */,
- 1 /* enrollmentNotifications */, BiometricsProtoEnums.MODALITY_FACE);
+ 1 /* enrollmentNotifications */, 200L /* lastEnrollmentTime */,
+ 300L /* lastFrrNotificationTime */, BiometricsProtoEnums.MODALITY_FACE);
when(mSharedPreferences.getStringSet(eq(KEY), anySet())).thenReturn(
Set.of(buildFrrStats(authenticationStats)));
@@ -187,6 +206,8 @@ public class AuthenticationStatsPersisterTest {
newAuthenticationStats.getTotalAttempts(),
newAuthenticationStats.getRejectedAttempts(),
newAuthenticationStats.getEnrollmentNotifications(),
+ newAuthenticationStats.getLastEnrollmentTime(),
+ newAuthenticationStats.getLastFrrNotificationTime(),
newAuthenticationStats.getModality());
verify(mEditor).putStringSet(eq(KEY), mStringSetArgumentCaptor.capture());
@@ -200,11 +221,13 @@ public class AuthenticationStatsPersisterTest {
// User with fingerprint authentication stats.
AuthenticationStats authenticationStats = new AuthenticationStats(USER_ID_1,
200 /* totalAttempts */, 20 /* rejectedAttempts */,
- 0 /* enrollmentNotifications */, BiometricsProtoEnums.MODALITY_FINGERPRINT);
+ 0 /* enrollmentNotifications */, 100L /* lastEnrollmentTime */,
+ 200L /* lastFrrNotificationTime */, BiometricsProtoEnums.MODALITY_FINGERPRINT);
// The same user with face authentication stats.
AuthenticationStats newAuthenticationStats = new AuthenticationStats(USER_ID_1,
500 /* totalAttempts */, 30 /* rejectedAttempts */,
- 1 /* enrollmentNotifications */, BiometricsProtoEnums.MODALITY_FACE);
+ 1 /* enrollmentNotifications */, 200L /* lastEnrollmentTime */,
+ 300L /* lastFrrNotificationTime */, BiometricsProtoEnums.MODALITY_FACE);
when(mSharedPreferences.getStringSet(eq(KEY), anySet())).thenReturn(
Set.of(buildFrrStats(authenticationStats)));
@@ -212,12 +235,18 @@ public class AuthenticationStatsPersisterTest {
newAuthenticationStats.getTotalAttempts(),
newAuthenticationStats.getRejectedAttempts(),
newAuthenticationStats.getEnrollmentNotifications(),
+ newAuthenticationStats.getLastEnrollmentTime(),
+ newAuthenticationStats.getLastFrrNotificationTime(),
newAuthenticationStats.getModality());
String expectedFrrStats = new JSONObject(buildFrrStats(authenticationStats))
.put(ENROLLMENT_NOTIFICATIONS, newAuthenticationStats.getEnrollmentNotifications())
.put(FACE_ATTEMPTS, newAuthenticationStats.getTotalAttempts())
- .put(FACE_REJECTIONS, newAuthenticationStats.getRejectedAttempts()).toString();
+ .put(FACE_REJECTIONS, newAuthenticationStats.getRejectedAttempts())
+ .put(FACE_LAST_ENROLL_TIME, newAuthenticationStats.getLastEnrollmentTime())
+ .put(FACE_LAST_FRR_NOTIFICATION_TIME,
+ newAuthenticationStats.getLastFrrNotificationTime())
+ .toString();
verify(mEditor).putStringSet(eq(KEY), mStringSetArgumentCaptor.capture());
assertThat(mStringSetArgumentCaptor.getValue()).contains(expectedFrrStats);
}
@@ -226,10 +255,12 @@ public class AuthenticationStatsPersisterTest {
public void persistFrrStats_multiUser_newUser_shouldUpdateRecord() throws JSONException {
AuthenticationStats authenticationStats1 = new AuthenticationStats(USER_ID_1,
300 /* totalAttempts */, 10 /* rejectedAttempts */,
- 0 /* enrollmentNotifications */, BiometricsProtoEnums.MODALITY_FACE);
+ 0 /* enrollmentNotifications */, 100L /* lastEnrollmentTime */,
+ 200L /* lastFrrNotificationTime */, BiometricsProtoEnums.MODALITY_FACE);
AuthenticationStats authenticationStats2 = new AuthenticationStats(USER_ID_2,
100 /* totalAttempts */, 5 /* rejectedAttempts */,
- 1 /* enrollmentNotifications */, BiometricsProtoEnums.MODALITY_FINGERPRINT);
+ 1 /* enrollmentNotifications */, 200L /* lastEnrollmentTime */,
+ 300L /* lastFrrNotificationTime */, BiometricsProtoEnums.MODALITY_FINGERPRINT);
// Sets up the shared preference with user 1 only.
when(mSharedPreferences.getStringSet(eq(KEY), anySet())).thenReturn(
@@ -240,6 +271,8 @@ public class AuthenticationStatsPersisterTest {
authenticationStats2.getTotalAttempts(),
authenticationStats2.getRejectedAttempts(),
authenticationStats2.getEnrollmentNotifications(),
+ authenticationStats2.getLastEnrollmentTime(),
+ authenticationStats2.getLastFrrNotificationTime(),
authenticationStats2.getModality());
verify(mEditor).putStringSet(eq(KEY), mStringSetArgumentCaptor.capture());
@@ -251,7 +284,8 @@ public class AuthenticationStatsPersisterTest {
public void removeFrrStats_existingUser_shouldUpdateRecord() throws JSONException {
AuthenticationStats authenticationStats = new AuthenticationStats(USER_ID_1,
300 /* totalAttempts */, 10 /* rejectedAttempts */,
- 0 /* enrollmentNotifications */, BiometricsProtoEnums.MODALITY_FACE);
+ 0 /* enrollmentNotifications */, 200L /* lastEnrollmentTime */,
+ 300L /* lastFrrNotificationTime */, BiometricsProtoEnums.MODALITY_FACE);
when(mSharedPreferences.getStringSet(eq(KEY), anySet())).thenReturn(
Set.of(buildFrrStats(authenticationStats)));
@@ -277,6 +311,9 @@ public class AuthenticationStatsPersisterTest {
.put(FACE_ATTEMPTS, authenticationStats.getTotalAttempts())
.put(FACE_REJECTIONS, authenticationStats.getRejectedAttempts())
.put(ENROLLMENT_NOTIFICATIONS, authenticationStats.getEnrollmentNotifications())
+ .put(FACE_LAST_ENROLL_TIME, authenticationStats.getLastEnrollmentTime())
+ .put(FACE_LAST_FRR_NOTIFICATION_TIME,
+ authenticationStats.getLastFrrNotificationTime())
.toString();
} else if (authenticationStats.getModality() == BiometricsProtoEnums.MODALITY_FINGERPRINT) {
return new JSONObject()
@@ -284,6 +321,9 @@ public class AuthenticationStatsPersisterTest {
.put(FINGERPRINT_ATTEMPTS, authenticationStats.getTotalAttempts())
.put(FINGERPRINT_REJECTIONS, authenticationStats.getRejectedAttempts())
.put(ENROLLMENT_NOTIFICATIONS, authenticationStats.getEnrollmentNotifications())
+ .put(FINGERPRINT_LAST_ENROLL_TIME, authenticationStats.getLastEnrollmentTime())
+ .put(FINGERPRINT_LAST_FRR_NOTIFICATION_TIME,
+ authenticationStats.getLastFrrNotificationTime())
.toString();
}
return "";
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/AuthenticationStatsTest.java b/services/tests/servicestests/src/com/android/server/biometrics/AuthenticationStatsTest.java
index e8e72cb81838..ca7b83caf20f 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/AuthenticationStatsTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/AuthenticationStatsTest.java
@@ -27,6 +27,7 @@ public class AuthenticationStatsTest {
AuthenticationStats authenticationStats =
new AuthenticationStats(1 /* userId */ , 0 /* totalAttempts */,
0 /* rejectedAttempts */, 0 /* enrollmentNotifications */,
+ 100L /* lastEnrollmentTime */, 200L /* lastFrrNotificationTime */,
0 /* modality */);
authenticationStats.authenticate(true /* authenticated */);
@@ -38,5 +39,8 @@ public class AuthenticationStatsTest {
assertEquals(authenticationStats.getTotalAttempts(), 2);
assertEquals(authenticationStats.getRejectedAttempts(), 1);
+
+ assertEquals(authenticationStats.getLastEnrollmentTime(), 100L);
+ assertEquals(authenticationStats.getLastFrrNotificationTime(), 200L);
}
}
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceEnrollClientTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceEnrollClientTest.java
index 276da39615af..c000f2f4c7a4 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceEnrollClientTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceEnrollClientTest.java
@@ -19,6 +19,8 @@ package com.android.server.biometrics.sensors.face.aidl;
import static android.hardware.biometrics.BiometricFaceConstants.FACE_ACQUIRED_START;
import static android.hardware.biometrics.BiometricFaceConstants.FACE_ACQUIRED_TOO_DARK;
+import static com.android.server.biometrics.AuthenticationStatsCollector.ACTION_LAST_ENROLL_TIME_CHANGED;
+
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
@@ -33,6 +35,10 @@ import static org.mockito.Mockito.same;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
import android.hardware.biometrics.BiometricFaceConstants;
import android.hardware.biometrics.BiometricRequestConstants;
import android.hardware.biometrics.BiometricSourceType;
@@ -56,6 +62,7 @@ import androidx.test.filters.SmallTest;
import androidx.test.platform.app.InstrumentationRegistry;
import com.android.internal.R;
+import com.android.server.biometrics.AuthenticationStatsCollector;
import com.android.server.biometrics.log.BiometricContext;
import com.android.server.biometrics.log.BiometricLogger;
import com.android.server.biometrics.log.OperationContextExt;
@@ -74,6 +81,8 @@ import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
@Presubmit
@@ -200,6 +209,26 @@ public class FaceEnrollClientTest {
eq(BiometricsProtoEnums.ENROLLMENT_SOURCE_SUW), eq(1));
}
+ @Test
+ public void testEnrollWithBroadcastEnrollTime() throws RemoteException, InterruptedException {
+ final FaceEnrollClient client = createClient(4);
+ final CountDownLatch countDownLatch = new CountDownLatch(1);
+ final EnrollmentTimeReceiver receiver = new EnrollmentTimeReceiver(countDownLatch);
+ mContext.registerReceiver(receiver, new IntentFilter(ACTION_LAST_ENROLL_TIME_CHANGED),
+ Context.RECEIVER_NOT_EXPORTED);
+
+ client.start(mCallback);
+ client.onEnrollResult(new Face("face", 1 /* faceId */, 20 /* deviceId */), 0);
+
+ assertThat(countDownLatch.await(2, TimeUnit.SECONDS)).isTrue();
+ final Intent intent = receiver.mIntent;
+ assertThat(intent).isNotNull();
+ assertThat(intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1)).isEqualTo(USER_ID);
+ assertThat(intent.getIntExtra(AuthenticationStatsCollector.EXTRA_MODALITY,
+ BiometricsProtoEnums.MODALITY_UNKNOWN))
+ .isEqualTo(BiometricsProtoEnums.MODALITY_FACE);
+ }
+
private FaceEnrollClient createClient() throws RemoteException {
return createClient(200 /* version */);
}
@@ -295,4 +324,18 @@ public class FaceEnrollClientTest {
);
}
+ static final class EnrollmentTimeReceiver extends BroadcastReceiver {
+ final CountDownLatch mLatch;
+ Intent mIntent;
+
+ EnrollmentTimeReceiver(CountDownLatch latch) {
+ mLatch = latch;
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ mIntent = intent;
+ mLatch.countDown();
+ }
+ }
}
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClientTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClientTest.java
index ea96d193c762..4d6fd7ef02c4 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClientTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClientTest.java
@@ -20,6 +20,8 @@ import static android.hardware.biometrics.BiometricFingerprintConstants.FINGERPR
import static android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_TOO_FAST;
import static android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ERROR_TIMEOUT;
+import static com.android.server.biometrics.AuthenticationStatsCollector.ACTION_LAST_ENROLL_TIME_CHANGED;
+
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.anyBoolean;
@@ -34,6 +36,10 @@ import static org.mockito.Mockito.same;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
import android.hardware.biometrics.BiometricRequestConstants;
import android.hardware.biometrics.BiometricSourceType;
import android.hardware.biometrics.BiometricsProtoEnums;
@@ -62,6 +68,7 @@ import androidx.test.filters.SmallTest;
import androidx.test.platform.app.InstrumentationRegistry;
import com.android.internal.R;
+import com.android.server.biometrics.AuthenticationStatsCollector;
import com.android.server.biometrics.log.BiometricContext;
import com.android.server.biometrics.log.BiometricLogger;
import com.android.server.biometrics.log.CallbackWithProbe;
@@ -83,6 +90,8 @@ import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
@Presubmit
@@ -312,6 +321,27 @@ public class FingerprintEnrollClientTest {
eq(BiometricsProtoEnums.ENROLLMENT_SOURCE_SUW), eq(1));
}
+ @Test
+ public void testEnrollWithBroadcastEnrollTime() throws RemoteException, InterruptedException {
+ final FingerprintEnrollClient client = createClient(4);
+ final CountDownLatch countDownLatch = new CountDownLatch(1);
+ final EnrollmentTimeReceiver receiver = new EnrollmentTimeReceiver(countDownLatch);
+ mContext.registerReceiver(receiver, new IntentFilter(ACTION_LAST_ENROLL_TIME_CHANGED),
+ Context.RECEIVER_NOT_EXPORTED);
+
+ client.start(mCallback);
+ client.onEnrollResult(new Fingerprint("fingerprint", 1 /* fingerId */, 20 /* deviceId */),
+ 0);
+
+ assertThat(countDownLatch.await(2, TimeUnit.SECONDS)).isTrue();
+ final Intent intent = receiver.mIntent;
+ assertThat(intent).isNotNull();
+ assertThat(intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1)).isEqualTo(0);
+ assertThat(intent.getIntExtra(AuthenticationStatsCollector.EXTRA_MODALITY,
+ BiometricsProtoEnums.MODALITY_UNKNOWN))
+ .isEqualTo(BiometricsProtoEnums.MODALITY_FINGERPRINT);
+ }
+
private void showHideOverlay(
Consumer<FingerprintEnrollClient> block) throws RemoteException {
final FingerprintEnrollClient client = createClient();
@@ -409,4 +439,19 @@ public class FingerprintEnrollClientTest {
.setEnrollReason(ENROLL_SOURCE).build()
);
}
+
+ static final class EnrollmentTimeReceiver extends BroadcastReceiver {
+ final CountDownLatch mLatch;
+ Intent mIntent;
+
+ EnrollmentTimeReceiver(CountDownLatch latch) {
+ mLatch = latch;
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ mIntent = intent;
+ mLatch.countDown();
+ }
+ }
}
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTestable.java b/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTestable.java
index abd39b0bb963..33b91ec967cd 100644
--- a/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTestable.java
+++ b/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTestable.java
@@ -203,7 +203,7 @@ public class LockSettingsServiceTestable extends LockSettingsService {
}
@Override
- protected boolean isCredentialSharableWithParent(int userId) {
+ protected boolean isCredentialShareableWithParent(int userId) {
UserInfo userInfo = mUserManager.getUserInfo(userId);
return userInfo.isCloneProfile() || userInfo.isManagedProfile();
}
diff --git a/services/tests/servicestests/src/com/android/server/media/LegacyDeviceRouteControllerTest.java b/services/tests/servicestests/src/com/android/server/media/LegacyDeviceRouteControllerTest.java
index aed68a5dc7b5..113e039d9a43 100644
--- a/services/tests/servicestests/src/com/android/server/media/LegacyDeviceRouteControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/media/LegacyDeviceRouteControllerTest.java
@@ -30,12 +30,17 @@ import android.media.AudioRoutesInfo;
import android.media.IAudioRoutesObserver;
import android.media.MediaRoute2Info;
import android.os.RemoteException;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
import android.text.TextUtils;
import com.android.internal.R;
+import com.android.media.flags.Flags;
import com.android.server.audio.AudioService;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.experimental.runners.Enclosed;
import org.junit.runner.RunWith;
@@ -48,6 +53,7 @@ import org.mockito.MockitoAnnotations;
import java.util.Arrays;
import java.util.Collection;
+import java.util.List;
@RunWith(Enclosed.class)
public class LegacyDeviceRouteControllerTest {
@@ -70,6 +76,11 @@ public class LegacyDeviceRouteControllerTest {
@RunWith(JUnit4.class)
public static class DefaultDeviceRouteValueTest {
+
+ @Rule
+ public final CheckFlagsRule mCheckFlagsRule =
+ DeviceFlagsValueProvider.createCheckFlagsRule();
+
@Mock
private Context mContext;
@Mock
@@ -136,6 +147,23 @@ public class LegacyDeviceRouteControllerTest {
.isTrue();
assertThat(actualMediaRoute.getVolume()).isEqualTo(VOLUME_DEFAULT_VALUE);
}
+
+ @RequiresFlagsEnabled(Flags.FLAG_ENABLE_FIX_FOR_EMPTY_SYSTEM_ROUTES_CRASH)
+ @Test
+ public void getAvailableRoutes_matchesSelectedRoute() {
+ when(mResources.getText(R.string.default_audio_route_name))
+ .thenReturn(DEFAULT_ROUTE_NAME);
+
+ when(mAudioService.startWatchingRoutes(any())).thenReturn(null);
+
+ LegacyDeviceRouteController deviceRouteController =
+ new LegacyDeviceRouteController(
+ mContext, mAudioManager, mAudioService, mOnDeviceRouteChangedListener);
+
+ MediaRoute2Info selectedRoute = deviceRouteController.getSelectedRoute();
+ assertThat(deviceRouteController.getAvailableRoutes())
+ .isEqualTo(List.of(selectedRoute));
+ }
}
@RunWith(Parameterized.class)
diff --git a/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserInfoTest.java b/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserInfoTest.java
index 1fb84113e278..1bc2242c9d9c 100644
--- a/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserInfoTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserInfoTest.java
@@ -15,14 +15,11 @@
*/
package com.android.server.pm;
-
import static android.content.pm.UserInfo.FLAG_DEMO;
-import static android.content.pm.UserInfo.FLAG_DISABLED;
import static android.content.pm.UserInfo.FLAG_EPHEMERAL;
import static android.content.pm.UserInfo.FLAG_FULL;
import static android.content.pm.UserInfo.FLAG_GUEST;
import static android.content.pm.UserInfo.FLAG_INITIALIZED;
-import static android.content.pm.UserInfo.FLAG_MAIN;
import static android.content.pm.UserInfo.FLAG_MANAGED_PROFILE;
import static android.content.pm.UserInfo.FLAG_PROFILE;
import static android.content.pm.UserInfo.FLAG_RESTRICTED;
@@ -35,9 +32,7 @@ import static android.os.UserManager.USER_TYPE_FULL_SYSTEM;
import static android.os.UserManager.USER_TYPE_PROFILE_MANAGED;
import static android.os.UserManager.USER_TYPE_SYSTEM_HEADLESS;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
+import static com.google.common.truth.Truth.assertWithMessage;
import android.annotation.UserIdInt;
import android.app.PropertyInvalidatedCache;
@@ -46,7 +41,6 @@ import android.content.pm.UserInfo.UserInfoFlag;
import android.content.res.Resources;
import android.multiuser.Flags;
import android.os.Looper;
-import android.os.Parcel;
import android.os.UserHandle;
import android.os.UserManager;
import android.platform.test.annotations.Presubmit;
@@ -55,15 +49,16 @@ import android.util.Xml;
import androidx.test.InstrumentationRegistry;
import androidx.test.filters.MediumTest;
-import androidx.test.runner.AndroidJUnit4;
import com.android.frameworks.servicestests.R;
import com.android.server.LocalServices;
import com.android.server.pm.UserManagerService.UserData;
+import com.google.common.truth.Expect;
+
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
-import org.junit.runner.RunWith;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlSerializer;
@@ -74,14 +69,16 @@ import java.nio.charset.StandardCharsets;
import java.util.List;
/**
- * <p>Run with:<pre>
- * runtest -c com.android.server.pm.UserManagerServiceUserInfoTest frameworks-services
- * </pre>
+ * Run with
+ * {@code atest FrameworksServicesTests:com.android.server.pm.UserManagerServiceUserInfoTest}.
*/
@Presubmit
-@RunWith(AndroidJUnit4.class)
@MediumTest
-public class UserManagerServiceUserInfoTest {
+@SuppressWarnings("deprecation")
+public final class UserManagerServiceUserInfoTest {
+
+ @Rule public final Expect expect = Expect.create();
+
private UserManagerService mUserManagerService;
private Resources mResources;
@@ -100,9 +97,10 @@ public class UserManagerServiceUserInfoTest {
// The tests assume that the device has one user and its the system user.
List<UserInfo> users = mUserManagerService.getUsers(/* excludeDying */ false);
- assertEquals("Multiple users so this test can't run.", 1, users.size());
- assertEquals("Only user present isn't the system user.",
- UserHandle.USER_SYSTEM, users.get(0).id);
+ assertWithMessage("initial users").that(users).isNotNull();
+ assertWithMessage("initial users").that(users).hasSize(1);
+ expect.withMessage("only user present initially is the system user.").that(users.get(0).id)
+ .isEqualTo(UserHandle.USER_SYSTEM);
mResources = InstrumentationRegistry.getTargetContext().getResources();
}
@@ -164,118 +162,57 @@ public class UserManagerServiceUserInfoTest {
new ByteArrayInputStream(systemUserBytes), userVersion);
}
- assertTrue(mUserManagerService.hasUserRestrictionOnAnyUser(globalRestriction));
- assertTrue(mUserManagerService.hasUserRestrictionOnAnyUser(localRestriction));
+ expect.withMessage("hasUserRestrictionOnAnyUser(%s)", globalRestriction)
+ .that(mUserManagerService.hasUserRestrictionOnAnyUser(globalRestriction)).isTrue();
+ expect.withMessage("hasUserRestrictionOnAnyUser(%s)", localRestriction)
+ .that(mUserManagerService.hasUserRestrictionOnAnyUser(localRestriction)).isTrue();
}
/** Sets a global and local restriction and verifies they were set properly **/
private void setUserRestrictions(int id, String global, String local, boolean enabled) {
mUserManagerService.setUserRestrictionInner(UserHandle.USER_ALL, global, enabled);
- assertEquals(mUserManagerService.hasUserRestrictionOnAnyUser(global), enabled);
+ expect.withMessage("hasUserRestrictionOnAnyUser(%s)", global)
+ .that(mUserManagerService.hasUserRestrictionOnAnyUser(global)).isEqualTo(enabled);
mUserManagerService.setUserRestrictionInner(id, local, enabled);
- assertEquals(mUserManagerService.hasUserRestrictionOnAnyUser(local), enabled);
- }
-
- @Test
- public void testParcelUnparcelUserInfo() throws Exception {
- UserInfo info = createUser();
-
- Parcel out = Parcel.obtain();
- info.writeToParcel(out, 0);
- byte[] data = out.marshall();
- out.recycle();
-
- Parcel in = Parcel.obtain();
- in.unmarshall(data, 0, data.length);
- in.setDataPosition(0);
- UserInfo read = UserInfo.CREATOR.createFromParcel(in);
- in.recycle();
-
- assertUserInfoEquals(info, read, /* parcelCopy= */ true);
- }
-
- @Test
- public void testCopyConstructor() throws Exception {
- UserInfo info = createUser();
-
- UserInfo copy = new UserInfo(info);
-
- assertUserInfoEquals(info, copy, /* parcelCopy= */ false);
+ expect.withMessage("hasUserRestrictionOnAnyUser(%s)", local)
+ .that(mUserManagerService.hasUserRestrictionOnAnyUser(local)).isEqualTo(enabled);
}
@Test
public void testGetUserName() throws Exception {
- assertFalse("System user name shouldn't be set",
- mUserManagerService.isUserNameSet(UserHandle.USER_SYSTEM));
+ expect.withMessage("System user name is set")
+ .that(mUserManagerService.isUserNameSet(UserHandle.USER_SYSTEM)).isFalse();
UserInfo userInfo = mUserManagerService.getUserInfo(UserHandle.USER_SYSTEM);
- assertFalse("A system provided name should be returned for primary user",
- TextUtils.isEmpty(userInfo.name));
+ expect.withMessage("A system provided name returned for primary user is empty")
+ .that(TextUtils.isEmpty(userInfo.name)).isFalse();
userInfo = createUser();
userInfo.partial = false;
final int TEST_ID = 100;
userInfo.id = TEST_ID;
mUserManagerService.putUserInfo(userInfo);
- assertTrue("Test user name must be set", mUserManagerService.isUserNameSet(TEST_ID));
- assertEquals("A Name", mUserManagerService.getUserInfo(TEST_ID).name);
+ expect.withMessage("user name is set").that(mUserManagerService.isUserNameSet(TEST_ID))
+ .isTrue();
+ expect.withMessage("name").that(mUserManagerService.getUserInfo(TEST_ID).name)
+ .isEqualTo("A Name");
}
/** Test UMS.isUserOfType(). */
@Test
public void testIsUserOfType() throws Exception {
- assertTrue("System user was of invalid type",
+ expect.withMessage("System user type is valid").that(
mUserManagerService.isUserOfType(UserHandle.USER_SYSTEM, USER_TYPE_SYSTEM_HEADLESS)
- || mUserManagerService.isUserOfType(UserHandle.USER_SYSTEM, USER_TYPE_FULL_SYSTEM));
+ || mUserManagerService.isUserOfType(UserHandle.USER_SYSTEM,
+ USER_TYPE_FULL_SYSTEM))
+ .isTrue();
final int testId = 100;
final String typeName = "A type";
UserInfo userInfo = createUser(testId, 0, typeName);
mUserManagerService.putUserInfo(userInfo);
- assertTrue(mUserManagerService.isUserOfType(testId, typeName));
- }
-
- /** Test UserInfo.supportsSwitchTo() for partial user. */
- @Test
- public void testSupportSwitchTo_partial() throws Exception {
- UserInfo userInfo = createUser(100, FLAG_FULL, null);
- userInfo.partial = true;
- assertFalse("Switching to a partial user should be disabled",
- userInfo.supportsSwitchTo());
- }
-
- /** Test UserInfo.supportsSwitchTo() for disabled user. */
- @Test
- public void testSupportSwitchTo_disabled() throws Exception {
- UserInfo userInfo = createUser(100, FLAG_DISABLED, null);
- assertFalse("Switching to a DISABLED user should be disabled",
- userInfo.supportsSwitchTo());
- }
-
- /** Test UserInfo.supportsSwitchTo() for precreated users. */
- @Test
- public void testSupportSwitchTo_preCreated() throws Exception {
- UserInfo userInfo = createUser(100, FLAG_FULL, null);
- userInfo.preCreated = true;
- assertFalse("Switching to a precreated user should be disabled",
- userInfo.supportsSwitchTo());
-
- userInfo.preCreated = false;
- assertTrue("Switching to a full, real user should be allowed", userInfo.supportsSwitchTo());
- }
-
- /** Test UserInfo.supportsSwitchTo() for profiles. */
- @Test
- public void testSupportSwitchTo_profile() throws Exception {
- UserInfo userInfo = createUser(100, FLAG_PROFILE, null);
- assertFalse("Switching to a profiles should be disabled", userInfo.supportsSwitchTo());
- }
-
- /** Test UserInfo.canHaveProfile for main user */
- @Test
- public void testCanHaveProfile() throws Exception {
- UserInfo userInfo = createUser(100, FLAG_FULL | FLAG_MAIN, null);
- assertTrue("Main users can have profile", userInfo.canHaveProfile());
+ expect.withMessage("isUserOfType()")
+ .that(mUserManagerService.isUserOfType(testId, typeName)).isTrue();
}
/** Tests upgradeIfNecessaryLP (but without locking) for upgrading from version 8 to 9+. */
@@ -296,22 +233,32 @@ public class UserManagerServiceUserInfoTest {
mUserManagerService.upgradeIfNecessaryLP(versionToTest - 1, userTypeVersion);
- assertTrue(mUserManagerService.isUserOfType(100, USER_TYPE_PROFILE_MANAGED));
- assertTrue((mUserManagerService.getUserInfo(100).flags & FLAG_PROFILE) != 0);
+ expect.withMessage("isUserOfType(100, USER_TYPE_PROFILE_MANAGED)")
+ .that(mUserManagerService.isUserOfType(100, USER_TYPE_PROFILE_MANAGED)).isTrue();
+ expect.withMessage("getUserInfo(100).flags & FLAG_PROFILE)")
+ .that(mUserManagerService.getUserInfo(100).flags & FLAG_PROFILE).isNotEqualTo(0);
- assertTrue(mUserManagerService.isUserOfType(101, USER_TYPE_FULL_GUEST));
+ expect.withMessage("isUserOfType(101, USER_TYPE_FULL_GUEST)")
+ .that(mUserManagerService.isUserOfType(101, USER_TYPE_FULL_GUEST)).isTrue();
- assertTrue(mUserManagerService.isUserOfType(102, USER_TYPE_FULL_RESTRICTED));
- assertTrue((mUserManagerService.getUserInfo(102).flags & FLAG_PROFILE) == 0);
+ expect.withMessage("isUserOfType(102, USER_TYPE_FULL_RESTRICTED)")
+ .that(mUserManagerService.isUserOfType(102, USER_TYPE_FULL_RESTRICTED)).isTrue();
+ expect.withMessage("getUserInfo(102).flags & FLAG_PROFILE)")
+ .that(mUserManagerService.getUserInfo(102).flags & FLAG_PROFILE).isEqualTo(0);
- assertTrue(mUserManagerService.isUserOfType(103, USER_TYPE_FULL_SECONDARY));
- assertTrue((mUserManagerService.getUserInfo(103).flags & FLAG_PROFILE) == 0);
+ expect.withMessage("isUserOfType(103, USER_TYPE_FULL_SECONDARY)")
+ .that(mUserManagerService.isUserOfType(103, USER_TYPE_FULL_SECONDARY)).isTrue();
+ expect.withMessage("getUserInfo(103).flags & FLAG_PROFILE)")
+ .that(mUserManagerService.getUserInfo(103).flags & FLAG_PROFILE).isEqualTo(0);
- assertTrue(mUserManagerService.isUserOfType(104, USER_TYPE_SYSTEM_HEADLESS));
+ expect.withMessage("isUserOfType(104, USER_TYPE_SYSTEM_HEADLESS)")
+ .that(mUserManagerService.isUserOfType(104, USER_TYPE_SYSTEM_HEADLESS)).isTrue();
- assertTrue(mUserManagerService.isUserOfType(105, USER_TYPE_FULL_SYSTEM));
+ expect.withMessage("isUserOfType(105, USER_TYPE_FULL_SYSTEM)")
+ .that(mUserManagerService.isUserOfType(105, USER_TYPE_FULL_SYSTEM)).isTrue();
- assertTrue(mUserManagerService.isUserOfType(106, USER_TYPE_FULL_DEMO));
+ expect.withMessage("isUserOfType(106, USER_TYPE_FULL_DEMO)")
+ .that(mUserManagerService.isUserOfType(106, USER_TYPE_FULL_DEMO)).isTrue();
}
/** Tests readUserLP upgrading from version 9 to 10+. */
@@ -329,8 +276,11 @@ public class UserManagerServiceUserInfoTest {
mUserManagerService.putUserInfo(data.info);
for (String restriction : localRestrictions) {
- assertFalse(mUserManagerService.hasBaseUserRestriction(restriction, userId));
- assertFalse(mUserManagerService.hasUserRestriction(restriction, userId));
+ expect.withMessage("hasBaseUserRestriction(%s, %s)", restriction, userId)
+ .that(mUserManagerService.hasBaseUserRestriction(restriction, userId))
+ .isFalse();
+ expect.withMessage("hasUserRestriction(%s, %s)", restriction, userId)
+ .that(mUserManagerService.hasUserRestriction(restriction, userId)).isFalse();
}
// Convert the xml resource to the system storage xml format.
@@ -348,8 +298,11 @@ public class UserManagerServiceUserInfoTest {
userVersion);
for (String restriction : localRestrictions) {
- assertFalse(mUserManagerService.hasBaseUserRestriction(restriction, userId));
- assertTrue(mUserManagerService.hasUserRestriction(restriction, userId));
+ expect.withMessage("hasBaseUserRestriction(%s, %s)", restriction, userId)
+ .that(mUserManagerService.hasBaseUserRestriction(restriction, userId))
+ .isFalse();
+ expect.withMessage("hasUserRestriction(%s, %s)", restriction, userId)
+ .that(mUserManagerService.hasUserRestriction(restriction, userId)).isTrue();
}
}
@@ -375,24 +328,24 @@ public class UserManagerServiceUserInfoTest {
}
private void assertUserInfoEquals(UserInfo one, UserInfo two, boolean parcelCopy) {
- assertEquals("Id not preserved", one.id, two.id);
- assertEquals("Name not preserved", one.name, two.name);
- assertEquals("Icon path not preserved", one.iconPath, two.iconPath);
- assertEquals("Flags not preserved", one.flags, two.flags);
- assertEquals("UserType not preserved", one.userType, two.userType);
- assertEquals("profile group not preserved", one.profileGroupId,
- two.profileGroupId);
- assertEquals("restricted profile parent not preserved", one.restrictedProfileParentId,
- two.restrictedProfileParentId);
- assertEquals("profile badge not preserved", one.profileBadge, two.profileBadge);
- assertEquals("partial not preserved", one.partial, two.partial);
- assertEquals("guestToRemove not preserved", one.guestToRemove, two.guestToRemove);
- assertEquals("preCreated not preserved", one.preCreated, two.preCreated);
+ expect.withMessage("Id").that(two.id).isEqualTo(one.id);
+ expect.withMessage("Name").that(two.name).isEqualTo(one.name);
+ expect.withMessage("Icon path").that(two.iconPath).isEqualTo(one.iconPath);
+ expect.withMessage("Flags").that(two.flags).isEqualTo(one.flags);
+ expect.withMessage("User type").that(two.userType).isEqualTo(one.userType);
+ expect.withMessage("Profile group").that(two.profileGroupId).isEqualTo(one.profileGroupId);
+ expect.withMessage("Restricted profile parent").that(two.restrictedProfileParentId)
+ .isEqualTo(one.restrictedProfileParentId);
+ expect.withMessage("Profile badge").that(two.profileBadge).isEqualTo(one.profileBadge);
+ expect.withMessage("Partial").that(two.partial).isEqualTo(one.partial);
+ expect.withMessage("Guest to remove").that(two.guestToRemove).isEqualTo(one.guestToRemove);
+ expect.withMessage("Pre created").that(two.preCreated).isEqualTo(one.preCreated);
if (parcelCopy) {
- assertFalse("convertedFromPreCreated should not be set", two.convertedFromPreCreated);
+ expect.withMessage("convertedFromPreCreated").that(two.convertedFromPreCreated)
+ .isFalse();
} else {
- assertEquals("convertedFromPreCreated not preserved", one.convertedFromPreCreated,
- two.convertedFromPreCreated);
+ expect.withMessage("convertedFromPreCreated").that(two.convertedFromPreCreated)
+ .isEqualTo(one.convertedFromPreCreated);
}
}
@@ -428,10 +381,16 @@ public class UserManagerServiceUserInfoTest {
mUserManagerService.upgradeProfileToTypeLU(userInfo, newUserType);
- assertTrue(mUserManagerService.isUserOfType(userId, newUserTypeName));
- assertTrue((mUserManagerService.getUserInfo(userId).flags & FLAG_PROFILE) != 0);
- assertTrue((mUserManagerService.getUserInfo(userId).flags & FLAG_MANAGED_PROFILE) == 0);
- assertTrue((mUserManagerService.getUserInfo(userId).flags & FLAG_INITIALIZED) != 0);
+ expect.withMessage("isUserOfType(%s)", newUserTypeName)
+ .that(mUserManagerService.isUserOfType(userId, newUserTypeName)).isTrue();
+ expect.withMessage("flags(FLAG_PROFILE)")
+ .that(mUserManagerService.getUserInfo(userId).flags & FLAG_PROFILE).isNotEqualTo(0);
+ expect.withMessage("flags(FLAG_MANAGED_PROFILE)")
+ .that(mUserManagerService.getUserInfo(userId).flags & FLAG_MANAGED_PROFILE)
+ .isEqualTo(0);
+ expect.withMessage("flags(FLAG_FLAG_INITIALIZED")
+ .that(mUserManagerService.getUserInfo(userId).flags & FLAG_INITIALIZED)
+ .isNotEqualTo(0);
}
@Test
@@ -470,11 +429,11 @@ public class UserManagerServiceUserInfoTest {
mUserManagerService.upgradeProfileToTypeLU(userInfo, newUserType);
- assertTrue(mUserManagerService.getUserRestrictions(userId).getBoolean(
- UserManager.DISALLOW_PRINTING));
- assertTrue(mUserManagerService.getUserRestrictions(userId).getBoolean(
- UserManager.DISALLOW_CAMERA));
- assertTrue(mUserManagerService.getUserRestrictions(userId).getBoolean(
- UserManager.DISALLOW_WALLPAPER));
+ expect.withMessage("getUserRestrictions(DISALLOW_PRINTING)").that(mUserManagerService
+ .getUserRestrictions(userId).getBoolean(UserManager.DISALLOW_PRINTING)).isTrue();
+ expect.withMessage("getUserRestrictions(DISALLOW_CAMERA)").that(mUserManagerService
+ .getUserRestrictions(userId).getBoolean(UserManager.DISALLOW_CAMERA)).isTrue();
+ expect.withMessage("getUserRestrictions(DISALLOW_WALLPAPER)").that(mUserManagerService
+ .getUserRestrictions(userId).getBoolean(UserManager.DISALLOW_WALLPAPER)).isTrue();
}
}
diff --git a/services/tests/servicestests/src/com/android/server/supervision/SupervisionServiceTest.kt b/services/tests/servicestests/src/com/android/server/supervision/SupervisionServiceTest.kt
index c59f0a05c619..02b97442b218 100644
--- a/services/tests/servicestests/src/com/android/server/supervision/SupervisionServiceTest.kt
+++ b/services/tests/servicestests/src/com/android/server/supervision/SupervisionServiceTest.kt
@@ -29,9 +29,15 @@ import android.content.Intent
import android.content.IntentFilter
import android.content.pm.PackageManager
import android.content.pm.UserInfo
+import android.content.pm.UserInfo.FLAG_FOR_TESTING
+import android.content.pm.UserInfo.FLAG_FULL
+import android.content.pm.UserInfo.FLAG_MAIN
+import android.content.pm.UserInfo.FLAG_SYSTEM
import android.os.Handler
import android.os.PersistableBundle
import android.os.UserHandle
+import android.os.UserHandle.MIN_SECONDARY_USER_ID
+import android.os.UserHandle.USER_SYSTEM
import android.platform.test.annotations.RequiresFlagsEnabled
import android.platform.test.flag.junit.DeviceFlagsValueProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -49,6 +55,7 @@ import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.junit.MockitoJUnit
import org.mockito.junit.MockitoRule
+import org.mockito.kotlin.any
import org.mockito.kotlin.whenever
/**
@@ -289,6 +296,36 @@ class SupervisionServiceTest {
assertThat(service.createConfirmSupervisionCredentialsIntent()).isNull()
}
+ fun shouldAllowBypassingSupervisionRoleQualification_returnsTrue() {
+ assertThat(service.isSupervisionEnabledForUser(USER_ID)).isFalse()
+ assertThat(service.shouldAllowBypassingSupervisionRoleQualification()).isTrue()
+
+ addDefaultAndTestUsers()
+ assertThat(service.shouldAllowBypassingSupervisionRoleQualification()).isTrue()
+ }
+
+ @Test
+ fun shouldAllowBypassingSupervisionRoleQualification_returnsFalse() {
+ assertThat(service.isSupervisionEnabledForUser(USER_ID)).isFalse()
+ assertThat(service.shouldAllowBypassingSupervisionRoleQualification()).isTrue()
+
+ addDefaultAndTestUsers()
+ assertThat(service.shouldAllowBypassingSupervisionRoleQualification()).isTrue()
+
+ // Enabling supervision on any user will disallow bypassing
+ service.setSupervisionEnabledForUser(USER_ID, true)
+ assertThat(service.isSupervisionEnabledForUser(USER_ID)).isTrue()
+ assertThat(service.shouldAllowBypassingSupervisionRoleQualification()).isFalse()
+
+ // Adding non-default users should also disallow bypassing
+ addDefaultAndFullUsers()
+ assertThat(service.shouldAllowBypassingSupervisionRoleQualification()).isFalse()
+
+ // Turning off supervision with non-default users should still disallow bypassing
+ service.setSupervisionEnabledForUser(USER_ID, false)
+ assertThat(service.isSupervisionEnabledForUser(USER_ID)).isFalse()
+ }
+
private val systemSupervisionPackage: String
get() = context.getResources().getString(R.string.config_systemSupervision)
@@ -310,10 +347,31 @@ class SupervisionServiceTest {
context.sendBroadcastAsUser(intent, UserHandle.of(userId))
}
+ private fun addDefaultAndTestUsers() {
+ val userInfos = userData.map { (userId, flags) ->
+ UserInfo(userId, "user" + userId, USER_ICON, flags, USER_TYPE)
+ }
+ whenever(mockUserManagerInternal.getUsers(any())).thenReturn(userInfos)
+ }
+
+ private fun addDefaultAndFullUsers() {
+ val userInfos = userData.map { (userId, flags) ->
+ UserInfo(userId, "user" + userId, USER_ICON, flags, USER_TYPE)
+ } + UserInfo(USER_ID, "user" + USER_ID, USER_ICON, FLAG_FULL, USER_TYPE)
+ whenever(mockUserManagerInternal.getUsers(any())).thenReturn(userInfos)
+ }
+
private companion object {
const val USER_ID = 100
const val APP_UID = USER_ID * UserHandle.PER_USER_RANGE
const val SUPERVISING_USER_ID = 10
+ const val USER_ICON = "user_icon"
+ const val USER_TYPE = "fake_user_type"
+ val userData: Map<Int, Int> = mapOf(
+ USER_SYSTEM to FLAG_SYSTEM,
+ MIN_SECONDARY_USER_ID to FLAG_MAIN,
+ (MIN_SECONDARY_USER_ID + 1) to (FLAG_FULL or FLAG_FOR_TESTING)
+ )
}
}
diff --git a/services/tests/servicestests/src/com/android/server/theming/FieldColorBothTests.java b/services/tests/servicestests/src/com/android/server/theming/FieldColorBothTests.java
new file mode 100644
index 000000000000..38cbcf37f88c
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/theming/FieldColorBothTests.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.theming;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.theming.FieldColorBoth;
+import android.content.theming.ThemeSettings;
+import android.content.theming.ThemeSettingsUpdater;
+import android.content.theming.ThemeStyle;
+
+import com.google.common.truth.Truth;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class FieldColorBothTests {
+ static final ThemeSettings DEFAULTS = new ThemeSettings(1, 0xFF123456, 0xFF654321,
+ "home_wallpaper", ThemeStyle.VIBRANT, true);
+ private FieldColorBoth mFieldColorBoth;
+
+ @Before
+ public void setup() {
+ mFieldColorBoth = new FieldColorBoth("colorBoth", ThemeSettingsUpdater::colorBoth,
+ ThemeSettings::colorBoth, DEFAULTS);
+ }
+
+ @Test
+ public void parse_validColorBoth_returnsTrue() {
+ Boolean parsedValue = mFieldColorBoth.parse("1");
+ assertThat(parsedValue).isTrue();
+ }
+
+ @Test
+ public void parse_validColorBoth_returnsFalse() {
+ Boolean parsedValue = mFieldColorBoth.parse("0");
+ assertThat(parsedValue).isFalse();
+ }
+
+ @Test
+ public void parse_invalidColorBoth_returnsNull() {
+ Boolean parsedValue = mFieldColorBoth.parse("invalid");
+ assertThat(parsedValue).isNull();
+ }
+
+ @Test
+ public void serialize_true_returnsTrueString() {
+ String serializedValue = mFieldColorBoth.serialize(true);
+ assertThat(serializedValue).isEqualTo("1");
+ }
+
+ @Test
+ public void serialize_false_returnsFalseString() {
+ String serializedValue = mFieldColorBoth.serialize(false);
+ assertThat(serializedValue).isEqualTo("0");
+ }
+
+ @Test
+ public void validate_true_returnsTrue() {
+ assertThat(mFieldColorBoth.validate(true)).isTrue();
+ }
+
+ @Test
+ public void validate_false_returnsTrue() {
+ assertThat(mFieldColorBoth.validate(false)).isTrue();
+ }
+
+ @Test
+ public void getFieldType_returnsBooleanClass() {
+ Truth.assertThat(mFieldColorBoth.getFieldType()).isEqualTo(Boolean.class);
+ }
+
+ @Test
+ public void getJsonType_returnsStringClass() {
+ Truth.assertThat(mFieldColorBoth.getJsonType()).isEqualTo(String.class);
+ }
+
+ @Test
+ public void get_returnsDefaultValue() {
+ Truth.assertThat(mFieldColorBoth.getDefaultValue()).isEqualTo(DEFAULTS.colorBoth());
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/theming/FieldColorIndexTests.java b/services/tests/servicestests/src/com/android/server/theming/FieldColorIndexTests.java
new file mode 100644
index 000000000000..32df3684a81d
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/theming/FieldColorIndexTests.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.theming;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.theming.FieldColorIndex;
+import android.content.theming.ThemeSettings;
+import android.content.theming.ThemeSettingsUpdater;
+import android.content.theming.ThemeStyle;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class FieldColorIndexTests {
+ static final ThemeSettings DEFAULTS = new ThemeSettings(1, 0xFF123456, 0xFF654321,
+ "home_wallpaper", ThemeStyle.VIBRANT, true);
+
+ private FieldColorIndex mFieldColorIndex;
+
+ @Before
+ public void setup() {
+ mFieldColorIndex = new FieldColorIndex("colorIndex", ThemeSettingsUpdater::colorIndex,
+ ThemeSettings::colorIndex, DEFAULTS);
+ }
+
+ @Test
+ public void parse_validColorIndex_returnsCorrectInteger() {
+ Integer parsedValue = mFieldColorIndex.parse("10");
+ assertThat(parsedValue).isEqualTo(10);
+ }
+
+ @Test
+ public void parse_negativeColorIndex_returnsCorrectInteger() {
+ Integer parsedValue = mFieldColorIndex.parse("-1");
+ assertThat(parsedValue).isEqualTo(-1);
+ }
+
+ @Test
+ public void parse_invalidColorIndex_returnsNull() {
+ Integer parsedValue = mFieldColorIndex.parse("invalid");
+ assertThat(parsedValue).isNull();
+ }
+
+ @Test
+ public void serialize_validColorIndex_returnsCorrectString() {
+ String serializedValue = mFieldColorIndex.serialize(15);
+ assertThat(serializedValue).isEqualTo("15");
+ }
+
+ @Test
+ public void serialize_negativeColorIndex_returnsCorrectString() {
+ String serializedValue = mFieldColorIndex.serialize(-1);
+ assertThat(serializedValue).isEqualTo("-1");
+ }
+
+ @Test
+ public void validate_validColorIndex_returnsTrue() {
+ assertThat(mFieldColorIndex.validate(5)).isTrue();
+ }
+
+ @Test
+ public void validate_negativeColorIndex_returnsTrue() {
+ assertThat(mFieldColorIndex.validate(-1)).isTrue();
+ }
+
+ @Test
+ public void validate_invalidColorIndex_returnsFalse() {
+ assertThat(mFieldColorIndex.validate(-2)).isFalse();
+ }
+
+ @Test
+ public void getFieldType_returnsIntegerClass() {
+ assertThat(mFieldColorIndex.getFieldType()).isEqualTo(Integer.class);
+ }
+
+ @Test
+ public void getJsonType_returnsStringClass() {
+ assertThat(mFieldColorIndex.getJsonType()).isEqualTo(String.class);
+ }
+
+ @Test
+ public void get_returnsDefaultValue() {
+ assertThat(mFieldColorIndex.getDefaultValue()).isEqualTo(DEFAULTS.colorIndex());
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/theming/FieldColorSourceTests.java b/services/tests/servicestests/src/com/android/server/theming/FieldColorSourceTests.java
new file mode 100644
index 000000000000..06edfa862d9c
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/theming/FieldColorSourceTests.java
@@ -0,0 +1,94 @@
+/*
+ * 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.theming;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.theming.FieldColorSource;
+import android.content.theming.ThemeSettings;
+import android.content.theming.ThemeSettingsUpdater;
+import android.content.theming.ThemeStyle;
+
+import com.google.common.truth.Truth;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+
+@RunWith(JUnit4.class)
+public class FieldColorSourceTests {
+ static final ThemeSettings DEFAULTS = new ThemeSettings(1, 0xFF123456, 0xFF654321,
+ "home_wallpaper", ThemeStyle.VIBRANT, true);
+ private FieldColorSource mFieldColorSource;
+
+ @Before
+ public void setup() {
+ mFieldColorSource = new FieldColorSource("colorSource", ThemeSettingsUpdater::colorSource,
+ ThemeSettings::colorSource, DEFAULTS);
+ }
+
+ @Test
+ public void parse_validColorSource_returnsSameString() {
+ String validColorSource = "home_wallpaper";
+ String parsedValue = mFieldColorSource.parse(validColorSource);
+ assertThat(parsedValue).isEqualTo(validColorSource);
+ }
+
+ @Test
+ public void serialize_validColorSource_returnsSameString() {
+ String validColorSource = "lock_wallpaper";
+ String serializedValue = mFieldColorSource.serialize(validColorSource);
+ assertThat(serializedValue).isEqualTo(validColorSource);
+ }
+
+ @Test
+ public void validate_preset_returnsTrue() {
+ assertThat(mFieldColorSource.validate("preset")).isTrue();
+ }
+
+ @Test
+ public void validate_homeWallpaper_returnsTrue() {
+ assertThat(mFieldColorSource.validate("home_wallpaper")).isTrue();
+ }
+
+ @Test
+ public void validate_lockWallpaper_returnsTrue() {
+ assertThat(mFieldColorSource.validate("lock_wallpaper")).isTrue();
+ }
+
+ @Test
+ public void validate_invalidColorSource_returnsFalse() {
+ assertThat(mFieldColorSource.validate("invalid")).isFalse();
+ }
+
+ @Test
+ public void getFieldType_returnsStringClass() {
+ Truth.assertThat(mFieldColorSource.getFieldType()).isEqualTo(String.class);
+ }
+
+ @Test
+ public void getJsonType_returnsStringClass() {
+ Truth.assertThat(mFieldColorSource.getJsonType()).isEqualTo(String.class);
+ }
+
+ @Test
+ public void get_returnsDefaultValue() {
+ Truth.assertThat(mFieldColorSource.getDefaultValue()).isEqualTo(DEFAULTS.colorSource());
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/theming/FieldColorTests.java b/services/tests/servicestests/src/com/android/server/theming/FieldColorTests.java
new file mode 100644
index 000000000000..54c4b29a5063
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/theming/FieldColorTests.java
@@ -0,0 +1,107 @@
+/*
+ * 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.theming;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.theming.FieldColor;
+import android.content.theming.ThemeSettings;
+import android.content.theming.ThemeSettingsUpdater;
+import android.content.theming.ThemeStyle;
+
+import com.google.common.truth.Truth;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class FieldColorTests {
+ static final ThemeSettings DEFAULTS = new ThemeSettings(1, 0xFF123456, 0xFF654321,
+ "home_wallpaper", ThemeStyle.VIBRANT, true);
+
+ private FieldColor mFieldColor;
+
+ @Before
+ public void setup() {
+ // Default to blue
+ mFieldColor = new FieldColor("accentColor", ThemeSettingsUpdater::accentColor,
+ ThemeSettings::accentColor, DEFAULTS);
+ }
+
+ @Test
+ public void parse_validColor_returnsCorrectColor() {
+ Integer parsedValue = mFieldColor.parse("FF0000FF");
+ assertThat(parsedValue).isEqualTo(0xFF0000FF);
+ } @Test
+ public void parse_validColorLowercase_returnsCorrectColor() {
+ Integer parsedValue = mFieldColor.parse("ff0000ff");
+ assertThat(parsedValue).isEqualTo(0xFF0000FF);
+ }
+
+ @Test
+ public void parse_validColorNoAlpha_returnsCorrectColor() {
+ Integer parsedValue = mFieldColor.parse("0000ff");
+ assertThat(parsedValue).isEqualTo(0xFF0000FF);
+ }
+
+
+ @Test
+ public void parse_invalidColor_returnsNull() {
+ Integer parsedValue = mFieldColor.parse("invalid");
+ assertThat(parsedValue).isNull();
+ }
+
+ @Test
+ public void parse_nullColor_returnsNull() {
+ Integer parsedValue = mFieldColor.parse(null);
+ assertThat(parsedValue).isNull();
+ }
+
+ @Test
+ public void serialize_validColor_returnsCorrectString() {
+ String serializedValue = mFieldColor.serialize(0xFFFF0000); // Red
+ assertThat(serializedValue).isEqualTo("ffff0000");
+ }
+
+ @Test
+ public void serialize_zeroColor_returnsZeroString() {
+ String serializedValue = mFieldColor.serialize(0);
+ assertThat(serializedValue).isEqualTo("0");
+ }
+
+ @Test
+ public void validate_validColor_returnsTrue() {
+ assertThat(mFieldColor.validate(0xFF00FF00)).isTrue(); // Green
+ }
+
+ @Test
+ public void getFieldType_returnsIntegerClass() {
+ Truth.assertThat(mFieldColor.getFieldType()).isEqualTo(Integer.class);
+ }
+
+ @Test
+ public void getJsonType_returnsStringClass() {
+ Truth.assertThat(mFieldColor.getJsonType()).isEqualTo(String.class);
+ }
+
+ @Test
+ public void get_returnsDefaultValue() {
+ Truth.assertThat(mFieldColor.getDefaultValue()).isEqualTo(DEFAULTS.accentColor());
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/theming/FieldThemeStyleTests.java b/services/tests/servicestests/src/com/android/server/theming/FieldThemeStyleTests.java
new file mode 100644
index 000000000000..09d71292fcf6
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/theming/FieldThemeStyleTests.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.theming;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.theming.FieldThemeStyle;
+import android.content.theming.ThemeSettings;
+import android.content.theming.ThemeSettingsUpdater;
+import android.content.theming.ThemeStyle;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class FieldThemeStyleTests {
+ static final ThemeSettings DEFAULTS = new ThemeSettings(1, 0xFF123456, 0xFF654321,
+ "home_wallpaper", ThemeStyle.VIBRANT, true);
+
+ private FieldThemeStyle mFieldThemeStyle;
+
+ @Before
+ public void setup() {
+ mFieldThemeStyle = new FieldThemeStyle("themeStyle", ThemeSettingsUpdater::themeStyle,
+ ThemeSettings::themeStyle, DEFAULTS);
+ }
+
+ @Test
+ public void parse_validThemeStyle_returnsCorrectStyle() {
+ Integer parsedValue = mFieldThemeStyle.parse("EXPRESSIVE");
+ assertThat(parsedValue).isEqualTo(ThemeStyle.EXPRESSIVE);
+ }
+
+ @Test
+ public void parse_invalidThemeStyle_returnsNull() {
+ Integer parsedValue = mFieldThemeStyle.parse("INVALID");
+ assertThat(parsedValue).isNull();
+ }
+
+ @Test
+ public void serialize_validThemeStyle_returnsCorrectString() {
+ String serializedValue = mFieldThemeStyle.serialize(ThemeStyle.SPRITZ);
+ assertThat(serializedValue).isEqualTo("SPRITZ");
+ }
+
+ @Test
+ public void validate_validThemeStyle_returnsTrue() {
+ assertThat(mFieldThemeStyle.validate(ThemeStyle.TONAL_SPOT)).isTrue();
+ }
+
+ @Test
+ public void validate_invalidThemeStyle_returnsFalse() {
+ assertThat(mFieldThemeStyle.validate(-1)).isFalse();
+ }
+
+ @Test
+ public void getFieldType_returnsIntegerClass() {
+ assertThat(mFieldThemeStyle.getFieldType()).isEqualTo(Integer.class);
+ }
+
+ @Test
+ public void getJsonType_returnsStringClass() {
+ assertThat(mFieldThemeStyle.getJsonType()).isEqualTo(String.class);
+ }
+
+ @Test
+ public void get_returnsDefaultValue() {
+ assertThat(mFieldThemeStyle.getDefaultValue()).isEqualTo(DEFAULTS.themeStyle());
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/theming/TEST_MAPPING b/services/tests/servicestests/src/com/android/server/theming/TEST_MAPPING
new file mode 100644
index 000000000000..d8d73444f6ce
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/theming/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+ "presubmit": [
+ {
+ "name": "FrameworksServicesTests_theme"
+ }
+ ]
+} \ No newline at end of file
diff --git a/services/tests/servicestests/src/com/android/server/theming/ThemeSettingsFieldTests.java b/services/tests/servicestests/src/com/android/server/theming/ThemeSettingsFieldTests.java
new file mode 100644
index 000000000000..0dc267a8059f
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/theming/ThemeSettingsFieldTests.java
@@ -0,0 +1,169 @@
+/*
+ * 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.theming;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.theming.ThemeSettings;
+import android.content.theming.ThemeSettingsField;
+import android.content.theming.ThemeSettingsUpdater;
+import android.content.theming.ThemeStyle;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.function.BiConsumer;
+import java.util.function.Function;
+
+@RunWith(JUnit4.class)
+public class ThemeSettingsFieldTests {
+ static final ThemeSettings DEFAULTS = new ThemeSettings(1, 0xFF123456, 0xFF654321,
+ "home_wallpaper", ThemeStyle.VIBRANT, true);
+ private ThemeSettingsUpdater mUpdater;
+
+ @Before
+ public void setup() {
+ mUpdater = ThemeSettings.updater();
+ }
+
+ @Test
+ public void testFromJSON_validValue_setsValue() throws Exception {
+ TestThemeSettingsFieldInteger field = getSampleField();
+
+ JSONObject json = new JSONObject();
+ json.put("testKey", "5");
+
+ field.fromJSON(json, mUpdater);
+
+ assertThat(mUpdater.getColorIndex()).isEqualTo(5);
+ }
+
+ @Test
+ public void testFromJSON_nullValue_setsDefault() throws Exception {
+ TestThemeSettingsFieldInteger field = getSampleField();
+
+ JSONObject json = new JSONObject();
+ json.put("testKey",
+ JSONObject.NULL); // Using JSONObject.NULL is how you should indicate null in JSON
+
+ field.fromJSON(json, mUpdater);
+
+ assertThat(mUpdater.getColorIndex()).isEqualTo(DEFAULTS.colorIndex());
+ }
+
+ @Test
+ public void testFromJSON_invalidValue_setsDefault() throws Exception {
+ TestThemeSettingsFieldInteger field = getSampleField();
+
+ JSONObject json = new JSONObject();
+ json.put("testKey", "abc"); // Invalid value
+
+ field.fromJSON(json, mUpdater);
+
+ assertThat(mUpdater.getColorIndex()).isEqualTo(DEFAULTS.colorIndex());
+ }
+
+ @Test
+ public void testToJSON_validValue_writesValue() throws JSONException {
+ TestThemeSettingsFieldInteger field = getSampleField();
+ ThemeSettings settings = new ThemeSettings(10, 0xFF123456, 0xFF654321, "home_wallpaper",
+ 0, true);
+ JSONObject json = new JSONObject();
+
+ field.toJSON(settings, json);
+
+ assertThat(json.getString("testKey")).isEqualTo("10");
+ }
+
+ @Test
+ public void testDefaultValue_returnsGetDefault() {
+ TestThemeSettingsFieldInteger field = getSampleField();
+
+ assertThat(field.getDefaultValue()).isEqualTo(DEFAULTS.colorIndex());
+ }
+
+ @Test
+ public void test_String_validValue_returnsParsedValue() throws JSONException {
+ TestThemeSettingsFieldInteger field = getSampleField();
+
+ JSONObject json = new JSONObject();
+ json.put("testKey", "123");
+
+ field.fromJSON(json, mUpdater);
+
+ assertThat(mUpdater.getColorIndex()).isEqualTo(123);
+ }
+
+ @Test
+ public void test_String_invalidValue_returnsDefaultValue() throws JSONException {
+ TestThemeSettingsFieldInteger field = getSampleField();
+
+ JSONObject json = new JSONObject();
+ // values < 0 are invalid
+ json.put("testKey", "-123");
+ field.fromJSON(json, mUpdater);
+
+ assertThat(mUpdater.getColorIndex()).isEqualTo(DEFAULTS.colorIndex());
+ }
+
+ private TestThemeSettingsFieldInteger getSampleField() {
+ return new TestThemeSettingsFieldInteger("testKey", ThemeSettingsUpdater::colorIndex,
+ ThemeSettings::colorIndex, DEFAULTS);
+ }
+
+
+ // Helper class for testing
+ private static class TestThemeSettingsFieldInteger extends ThemeSettingsField<Integer, String> {
+ TestThemeSettingsFieldInteger(String key, BiConsumer<ThemeSettingsUpdater, Integer> setter,
+ Function<ThemeSettings, Integer> getter, ThemeSettings defaults) {
+ super(key, setter, getter, defaults);
+ }
+
+ @Override
+ public Integer parse(String primitive) {
+ try {
+ return Integer.parseInt(primitive);
+ } catch (NumberFormatException e) {
+ return null;
+ }
+ }
+
+ @Override
+ public String serialize(Integer value) throws RuntimeException {
+ return value.toString();
+ }
+
+ @Override
+ public boolean validate(Integer value) {
+ return value > 0;
+ }
+
+ @Override
+ public Class<Integer> getFieldType() {
+ return Integer.class;
+ }
+
+ @Override
+ public Class<String> getJsonType() {
+ return String.class;
+ }
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/theming/ThemeSettingsManagerTests.java b/services/tests/servicestests/src/com/android/server/theming/ThemeSettingsManagerTests.java
new file mode 100644
index 000000000000..44f8c73dec84
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/theming/ThemeSettingsManagerTests.java
@@ -0,0 +1,114 @@
+/*
+ * 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.theming;
+
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.ContentResolver;
+import android.content.theming.ThemeSettings;
+import android.content.theming.ThemeStyle;
+import android.provider.Settings;
+import android.testing.TestableContext;
+
+import org.json.JSONObject;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class ThemeSettingsManagerTests {
+ private final int mUserId = 0;
+ public static final ThemeSettings DEFAULTS = new ThemeSettings(
+ /* colorIndex= */ 1,
+ /* systemPalette= */ 0xFF123456,
+ /* accentColor= */ 0xFF654321,
+ /* colorSource= */ "home_wallpaper",
+ /* themeStyle= */ ThemeStyle.VIBRANT,
+ /* colorBoth= */ true);
+
+ @Rule
+ public final TestableContext mContext = new TestableContext(
+ getInstrumentation().getTargetContext(), null);
+
+ @Rule
+ public TemporaryFolder temporaryFolder = new TemporaryFolder();
+ private ContentResolver mContentResolver;
+
+
+ @Before
+ public void setup() {
+ mContentResolver = mContext.getContentResolver();
+ }
+
+ @Test
+ public void loadSettings_emptyJSON_returnsDefault() {
+ Settings.Secure.putStringForUser(mContentResolver,
+ Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES, "{}", mUserId);
+
+ ThemeSettingsManager manager = new ThemeSettingsManager(DEFAULTS);
+ ThemeSettings settings = manager.loadSettings(mUserId, mContentResolver);
+
+ assertThat(settings.colorIndex()).isEqualTo(DEFAULTS.colorIndex());
+ assertThat(settings.systemPalette()).isEqualTo(DEFAULTS.systemPalette());
+ assertThat(settings.accentColor()).isEqualTo(DEFAULTS.accentColor());
+ assertThat(settings.colorSource()).isEqualTo(DEFAULTS.colorSource());
+ assertThat(settings.themeStyle()).isEqualTo(DEFAULTS.themeStyle());
+ assertThat(settings.colorBoth()).isEqualTo(DEFAULTS.colorBoth());
+ }
+
+ @Test
+ public void replaceSettings_writesSettingsToProvider() throws Exception {
+
+ ThemeSettingsManager manager = new ThemeSettingsManager(DEFAULTS);
+
+ ThemeSettings newSettings = new ThemeSettings(3, 0xFF112233, 0xFF332211, "preset",
+ ThemeStyle.MONOCHROMATIC, false);
+ manager.replaceSettings(mUserId, mContentResolver, newSettings);
+
+ String settingsString = Settings.Secure.getStringForUser(mContentResolver,
+ Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES, mUserId);
+ JSONObject settingsJson = new JSONObject(settingsString);
+ assertThat(settingsJson.getString("android.theme.customization.color_index")).isEqualTo(
+ "3");
+ assertThat(settingsJson.getString("android.theme.customization.system_palette"))
+ .isEqualTo("ff112233");
+ assertThat(settingsJson.getString("android.theme.customization.accent_color"))
+ .isEqualTo("ff332211");
+ assertThat(settingsJson.getString("android.theme.customization.color_source"))
+ .isEqualTo("preset");
+ assertThat(settingsJson.getString("android.theme.customization.theme_style"))
+ .isEqualTo("MONOCHROMATIC");
+ assertThat(settingsJson.getString("android.theme.customization.color_both")).isEqualTo("0");
+ }
+
+ @Test
+ public void updatesSettings_writesSettingsToProvider() throws Exception {
+ ThemeSettingsManager manager = new ThemeSettingsManager(DEFAULTS);
+
+ ThemeSettings newSettings = new ThemeSettings(3, 0xFF112233, 0xFF332211, "preset",
+ ThemeStyle.MONOCHROMATIC, false);
+ manager.updateSettings(mUserId, mContentResolver, newSettings);
+
+ ThemeSettings loadedSettings = manager.loadSettings(mUserId, mContentResolver);
+ assertThat(loadedSettings.equals(newSettings)).isTrue();
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/theming/ThemeSettingsTests.java b/services/tests/servicestests/src/com/android/server/theming/ThemeSettingsTests.java
new file mode 100644
index 000000000000..c417a4b571cb
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/theming/ThemeSettingsTests.java
@@ -0,0 +1,108 @@
+/*
+ * 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.theming;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertNull;
+
+import android.content.theming.ThemeSettings;
+import android.content.theming.ThemeSettingsUpdater;
+import android.content.theming.ThemeStyle;
+import android.os.Parcel;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class ThemeSettingsTests {
+ public static final ThemeSettings DEFAULTS = new ThemeSettings(
+ /* colorIndex= */ 1,
+ /* systemPalette= */ 0xFF123456,
+ /* accentColor= */ 0xFF654321,
+ /* colorSource= */ "home_wallpaper",
+ /* themeStyle= */ ThemeStyle.VIBRANT,
+ /* colorBoth= */ true);
+
+ /**
+ * Test that the updater correctly sets all fields when they are provided.
+ */
+ @Test
+ public void testUpdater_allFieldsSet() {
+ ThemeSettingsUpdater updater = ThemeSettings.updater()
+ .colorIndex(2)
+ .systemPalette(0xFFFF0000)
+ .accentColor(0xFF00FF00)
+ .colorSource("preset")
+ .themeStyle(ThemeStyle.MONOCHROMATIC)
+ .colorBoth(false);
+
+ ThemeSettings settings = updater.toThemeSettings(DEFAULTS);
+
+ assertThat(settings.colorIndex()).isEqualTo(2);
+ assertThat(settings.systemPalette()).isEqualTo(0xFFFF0000);
+ assertThat(settings.accentColor()).isEqualTo(0xFF00FF00);
+ assertThat(settings.colorSource()).isEqualTo("preset");
+ assertThat(settings.themeStyle()).isEqualTo(ThemeStyle.MONOCHROMATIC);
+ assertThat(settings.colorBoth()).isEqualTo(false);
+ }
+
+ /**
+ * Test that the updater uses null values when no fields are explicitly set.
+ */
+ @Test
+ public void testUpdater_noFieldsSet() {
+ ThemeSettingsUpdater updater = ThemeSettings.updater();
+
+ assertNull(updater.getColorIndex());
+ assertNull(updater.getSystemPalette());
+ assertNull(updater.getAccentColor());
+ assertNull(updater.getColorSource());
+ assertNull(updater.getThemeStyle());
+ assertNull(updater.getColorBoth());
+ }
+
+ /**
+ * Test that the ThemeSettings object can be correctly parceled and restored.
+ */
+ @Test
+ public void testParcel_roundTrip() {
+ ThemeSettingsUpdater updater = ThemeSettings.updater()
+ .colorIndex(2)
+ .systemPalette(0xFFFF0000)
+ .accentColor(0xFF00FF00)
+ .colorSource("preset")
+ .themeStyle(ThemeStyle.MONOCHROMATIC)
+ .colorBoth(false);
+
+ ThemeSettings settings = updater.toThemeSettings(DEFAULTS);
+
+ Parcel parcel = Parcel.obtain();
+ settings.writeToParcel(parcel, 0);
+ parcel.setDataPosition(0);
+ ThemeSettings fromParcel = ThemeSettings.CREATOR.createFromParcel(parcel);
+
+ assertThat(settings.colorIndex()).isEqualTo(fromParcel.colorIndex());
+ assertThat(settings.systemPalette()).isEqualTo(fromParcel.systemPalette());
+ assertThat(settings.accentColor()).isEqualTo(fromParcel.accentColor());
+ assertThat(settings.colorSource()).isEqualTo(fromParcel.colorSource());
+ assertThat(settings.themeStyle()).isEqualTo(fromParcel.themeStyle());
+ assertThat(settings.colorBoth()).isEqualTo(fromParcel.colorBoth());
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/theming/ThemeSettingsUpdaterTests.java b/services/tests/servicestests/src/com/android/server/theming/ThemeSettingsUpdaterTests.java
new file mode 100644
index 000000000000..7ce32da7b713
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/theming/ThemeSettingsUpdaterTests.java
@@ -0,0 +1,154 @@
+/*
+ * 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.theming;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.theming.ThemeSettings;
+import android.content.theming.ThemeSettingsUpdater;
+import android.content.theming.ThemeStyle;
+import android.os.Parcel;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class ThemeSettingsUpdaterTests {
+ public static final ThemeSettings DEFAULTS = new ThemeSettings(
+ /* colorIndex= */ 1,
+ /* systemPalette= */ 0xFF123456,
+ /* accentColor= */ 0xFF654321,
+ /* colorSource= */ "home_wallpaper",
+ /* themeStyle= */ ThemeStyle.VIBRANT,
+ /* colorBoth= */ true);
+ private ThemeSettingsUpdater mUpdater;
+
+ @Before
+ public void setUp() {
+ mUpdater = ThemeSettings.updater();
+ }
+
+ @Test
+ public void testSetAndGetColorIndex() {
+ mUpdater.colorIndex(5);
+ assertThat(mUpdater.getColorIndex()).isEqualTo(5);
+ }
+
+ @Test
+ public void testSetAndGetSystemPalette() {
+ mUpdater.systemPalette(0xFFABCDEF);
+ assertThat(mUpdater.getSystemPalette()).isEqualTo(0xFFABCDEF);
+ }
+
+ @Test
+ public void testSetAndGetAccentColor() {
+ mUpdater.accentColor(0xFFFEDCBA);
+ assertThat(mUpdater.getAccentColor()).isEqualTo(0xFFFEDCBA);
+ }
+
+ @Test
+ public void testSetAndGetColorSource() {
+ mUpdater.colorSource("lock_wallpaper");
+ assertThat(mUpdater.getColorSource()).isEqualTo("lock_wallpaper");
+ }
+
+ @Test
+ public void testSetAndGetThemeStyle() {
+ mUpdater.themeStyle(ThemeStyle.EXPRESSIVE);
+ assertThat(mUpdater.getThemeStyle()).isEqualTo(ThemeStyle.EXPRESSIVE);
+ }
+
+ @Test
+ public void testSetAndGetColorBoth() {
+ mUpdater.colorBoth(false);
+ assertThat(mUpdater.getColorBoth()).isFalse();
+ }
+
+
+ @Test
+ public void testToThemeSettings_allFieldsSet() {
+ mUpdater.colorIndex(5)
+ .systemPalette(0xFFABCDEF)
+ .accentColor(0xFFFEDCBA)
+ .colorSource("lock_wallpaper")
+ .themeStyle(ThemeStyle.EXPRESSIVE)
+ .colorBoth(false);
+ ThemeSettings settings = mUpdater.toThemeSettings(DEFAULTS);
+
+ assertThat(settings.colorIndex()).isEqualTo(5);
+ assertThat(settings.systemPalette()).isEqualTo(0xFFABCDEF);
+ assertThat(settings.accentColor()).isEqualTo(0xFFFEDCBA);
+ assertThat(settings.colorSource()).isEqualTo("lock_wallpaper");
+ assertThat(settings.themeStyle()).isEqualTo(ThemeStyle.EXPRESSIVE);
+ assertThat(settings.colorBoth()).isFalse();
+ }
+
+ @Test
+ public void testToThemeSettings_someFieldsNotSet_usesDefaults() {
+ mUpdater.colorIndex(5)
+ .systemPalette(0xFFABCDEF);
+
+ ThemeSettings settings = mUpdater.toThemeSettings(DEFAULTS);
+
+ assertThat(settings.colorIndex()).isEqualTo(5);
+ assertThat(settings.systemPalette()).isEqualTo(0xFFABCDEF);
+ assertThat(settings.accentColor()).isEqualTo(DEFAULTS.accentColor());
+ assertThat(settings.colorSource()).isEqualTo(DEFAULTS.colorSource());
+ assertThat(settings.themeStyle()).isEqualTo(DEFAULTS.themeStyle());
+ assertThat(settings.colorBoth()).isEqualTo(DEFAULTS.colorBoth());
+ }
+
+ @Test
+ public void testParcel_roundTrip_allFieldsSet() {
+ mUpdater.colorIndex(5)
+ .systemPalette(0xFFABCDEF)
+ .accentColor(0xFFFEDCBA)
+ .colorSource("lock_wallpaper")
+ .themeStyle(ThemeStyle.EXPRESSIVE)
+ .colorBoth(false);
+
+ Parcel parcel = Parcel.obtain();
+ mUpdater.writeToParcel(parcel, 0);
+ parcel.setDataPosition(0);
+ ThemeSettingsUpdater fromParcel = ThemeSettingsUpdater.CREATOR.createFromParcel(parcel);
+
+ assertThat(fromParcel.getColorIndex()).isEqualTo(5);
+ assertThat(fromParcel.getSystemPalette()).isEqualTo(0xFFABCDEF);
+ assertThat(fromParcel.getAccentColor()).isEqualTo(0xFFFEDCBA);
+ assertThat(fromParcel.getColorSource()).isEqualTo("lock_wallpaper");
+ assertThat(fromParcel.getThemeStyle()).isEqualTo(ThemeStyle.EXPRESSIVE);
+ assertThat(fromParcel.getColorBoth()).isFalse();
+ }
+
+ @Test
+ public void testParcel_roundTrip_noFieldsSet() {
+ Parcel parcel = Parcel.obtain();
+ mUpdater.writeToParcel(parcel, 0);
+ parcel.setDataPosition(0);
+ ThemeSettingsUpdater fromParcel = ThemeSettingsUpdater.CREATOR.createFromParcel(parcel);
+
+ assertThat(fromParcel.getColorIndex()).isNull();
+ assertThat(fromParcel.getSystemPalette()).isNull();
+ assertThat(fromParcel.getAccentColor()).isNull();
+ assertThat(fromParcel.getColorSource()).isNull();
+ assertThat(fromParcel.getThemeStyle()).isNull();
+ assertThat(fromParcel.getColorBoth()).isNull();
+ }
+}
diff --git a/services/tests/wmtests/src/com/android/server/policy/PowerKeyGestureTests.java b/services/tests/wmtests/src/com/android/server/policy/PowerKeyGestureTests.java
index 53e82bad818d..8d717bc19e72 100644
--- a/services/tests/wmtests/src/com/android/server/policy/PowerKeyGestureTests.java
+++ b/services/tests/wmtests/src/com/android/server/policy/PowerKeyGestureTests.java
@@ -18,16 +18,12 @@ package com.android.server.policy;
import static android.view.KeyEvent.KEYCODE_POWER;
import static android.view.KeyEvent.KEYCODE_VOLUME_UP;
-import static com.android.hardware.input.Flags.FLAG_OVERRIDE_POWER_KEY_BEHAVIOR_IN_FOCUSED_WINDOW;
import static com.android.server.policy.PhoneWindowManager.LONG_PRESS_POWER_ASSISTANT;
import static com.android.server.policy.PhoneWindowManager.LONG_PRESS_POWER_GLOBAL_ACTIONS;
import static com.android.server.policy.PhoneWindowManager.POWER_MULTI_PRESS_TIMEOUT_MILLIS;
import static com.android.server.policy.PhoneWindowManager.SHORT_PRESS_POWER_DREAM_OR_SLEEP;
import static com.android.server.policy.PhoneWindowManager.SHORT_PRESS_POWER_GO_TO_SLEEP;
-import static org.junit.Assert.assertEquals;
-
-import android.platform.test.annotations.EnableFlags;
import android.platform.test.annotations.Presubmit;
import android.provider.Settings;
import android.view.Display;
@@ -153,143 +149,4 @@ public class PowerKeyGestureTests extends ShortcutKeyTestBase {
sendKey(KEYCODE_POWER);
mPhoneWindowManager.assertNoPowerSleep();
}
-
-
- /**
- * Double press of power when the window handles the power key events. The
- * system double power gesture launch should not be performed.
- */
- @Test
- @EnableFlags(FLAG_OVERRIDE_POWER_KEY_BEHAVIOR_IN_FOCUSED_WINDOW)
- public void testPowerDoublePress_windowHasOverridePermissionAndKeysHandled() {
- mPhoneWindowManager.overrideCanWindowOverridePowerKey(true);
- setDispatchedKeyHandler(keyEvent -> true);
-
- sendKey(KEYCODE_POWER);
- sendKey(KEYCODE_POWER);
-
- mPhoneWindowManager.assertDidNotLockAfterAppTransitionFinished();
-
- mPhoneWindowManager.assertNoDoublePowerLaunch();
- }
-
- /**
- * Double press of power when the window doesn't handle the power key events.
- * The system default gesture launch should be performed and the app should receive both events.
- */
- @Test
- @EnableFlags(FLAG_OVERRIDE_POWER_KEY_BEHAVIOR_IN_FOCUSED_WINDOW)
- public void testPowerDoublePress_windowHasOverridePermissionAndKeysUnHandled() {
- mPhoneWindowManager.overrideCanWindowOverridePowerKey(true);
- setDispatchedKeyHandler(keyEvent -> false);
-
- sendKey(KEYCODE_POWER);
- sendKey(KEYCODE_POWER);
-
- mPhoneWindowManager.assertDidNotLockAfterAppTransitionFinished();
- mPhoneWindowManager.assertDoublePowerLaunch();
- assertEquals(getDownKeysDispatched(), 2);
- assertEquals(getUpKeysDispatched(), 2);
- }
-
- /**
- * Triple press of power when the window handles the power key double press gesture.
- * The system default gesture launch should not be performed, and the app only receives the
- * first two presses.
- */
- @Test
- @EnableFlags(FLAG_OVERRIDE_POWER_KEY_BEHAVIOR_IN_FOCUSED_WINDOW)
- public void testPowerTriplePress_windowHasOverridePermissionAndKeysHandled() {
- mPhoneWindowManager.overrideCanWindowOverridePowerKey(true);
- setDispatchedKeyHandler(keyEvent -> true);
-
- sendKey(KEYCODE_POWER);
- sendKey(KEYCODE_POWER);
- sendKey(KEYCODE_POWER);
-
- mPhoneWindowManager.assertDidNotLockAfterAppTransitionFinished();
- mPhoneWindowManager.assertNoDoublePowerLaunch();
- assertEquals(getDownKeysDispatched(), 2);
- assertEquals(getUpKeysDispatched(), 2);
- }
-
- /**
- * Tests a single press, followed by a double press when the window can handle the power key.
- * The app should receive all 3 events.
- */
- @Test
- @EnableFlags(FLAG_OVERRIDE_POWER_KEY_BEHAVIOR_IN_FOCUSED_WINDOW)
- public void testPowerTriplePressWithDelay_windowHasOverridePermissionAndKeysHandled() {
- mPhoneWindowManager.overrideCanWindowOverridePowerKey(true);
- setDispatchedKeyHandler(keyEvent -> true);
-
- sendKey(KEYCODE_POWER);
- mPhoneWindowManager.moveTimeForward(POWER_MULTI_PRESS_TIMEOUT_MILLIS);
- sendKey(KEYCODE_POWER);
- sendKey(KEYCODE_POWER);
-
- mPhoneWindowManager.assertNoDoublePowerLaunch();
- assertEquals(getDownKeysDispatched(), 3);
- assertEquals(getUpKeysDispatched(), 3);
- }
-
- /**
- * Tests single press when window doesn't handle the power key. Phone should go to sleep.
- */
- @Test
- @EnableFlags(FLAG_OVERRIDE_POWER_KEY_BEHAVIOR_IN_FOCUSED_WINDOW)
- public void testPowerSinglePress_windowHasOverridePermissionAndKeyUnhandledByApp() {
- mPhoneWindowManager.overrideCanWindowOverridePowerKey(true);
- setDispatchedKeyHandler(keyEvent -> false);
- mPhoneWindowManager.overrideShortPressOnPower(SHORT_PRESS_POWER_GO_TO_SLEEP);
-
- sendKey(KEYCODE_POWER);
-
- mPhoneWindowManager.assertPowerSleep();
- }
-
- /**
- * Tests single press when the window handles the power key. Phone should go to sleep after a
- * delay of {POWER_MULTI_PRESS_TIMEOUT_MILLIS}
- */
- @Test
- @EnableFlags(FLAG_OVERRIDE_POWER_KEY_BEHAVIOR_IN_FOCUSED_WINDOW)
- public void testPowerSinglePress_windowHasOverridePermissionAndKeyHandledByApp() {
- mPhoneWindowManager.overrideCanWindowOverridePowerKey(true);
- setDispatchedKeyHandler(keyEvent -> true);
- mPhoneWindowManager.overrideDisplayState(Display.STATE_ON);
- mPhoneWindowManager.overrideShortPressOnPower(SHORT_PRESS_POWER_GO_TO_SLEEP);
-
- sendKey(KEYCODE_POWER);
-
- mPhoneWindowManager.moveTimeForward(POWER_MULTI_PRESS_TIMEOUT_MILLIS);
-
- mPhoneWindowManager.assertPowerSleep();
- }
-
-
- /**
- * Tests 5x press when the window handles the power key. Emergency gesture should still be
- * launched.
- */
- @Test
- @EnableFlags(FLAG_OVERRIDE_POWER_KEY_BEHAVIOR_IN_FOCUSED_WINDOW)
- public void testPowerFiveTimesPress_windowHasOverridePermissionAndKeyHandledByApp() {
- mPhoneWindowManager.overrideCanWindowOverridePowerKey(true);
- setDispatchedKeyHandler(keyEvent -> true);
- mPhoneWindowManager.overrideDisplayState(Display.STATE_ON);
- mPhoneWindowManager.overrideShortPressOnPower(SHORT_PRESS_POWER_GO_TO_SLEEP);
-
- int minEmergencyGestureDurationMillis = mContext.getResources().getInteger(
- com.android.internal.R.integer.config_defaultMinEmergencyGestureTapDurationMillis);
- int durationMillis = minEmergencyGestureDurationMillis / 4;
- for (int i = 0; i < 5; ++i) {
- sendKey(KEYCODE_POWER);
- mPhoneWindowManager.moveTimeForward(durationMillis);
- }
-
- mPhoneWindowManager.assertEmergencyLaunch();
- assertEquals(getDownKeysDispatched(), 2);
- assertEquals(getUpKeysDispatched(), 2);
- }
}
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 7059c41898f3..2097d15658a6 100644
--- a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java
+++ b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java
@@ -37,7 +37,6 @@ import static com.android.dx.mockito.inline.extended.ExtendedMockito.spy;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.times;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
-import static com.android.hardware.input.Flags.overridePowerKeyBehaviorInFocusedWindow;
import static com.android.server.policy.PhoneWindowManager.LONG_PRESS_POWER_ASSISTANT;
import static com.android.server.policy.PhoneWindowManager.LONG_PRESS_POWER_GLOBAL_ACTIONS;
import static com.android.server.policy.PhoneWindowManager.LONG_PRESS_POWER_GO_TO_VOICE_ASSIST;
@@ -210,8 +209,6 @@ class TestPhoneWindowManager {
private int mKeyEventPolicyFlags = FLAG_INTERACTIVE;
- private int mProcessPowerKeyDownCount = 0;
-
private class TestTalkbackShortcutController extends TalkbackShortcutController {
TestTalkbackShortcutController(Context context) {
super(context);
@@ -424,7 +421,7 @@ class TestPhoneWindowManager {
doNothing().when(mContext).startActivityAsUser(any(), any());
doNothing().when(mContext).startActivityAsUser(any(), any(), any());
- KeyInterceptionInfo interceptionInfo = new KeyInterceptionInfo(0, 0, null, 0, 0);
+ KeyInterceptionInfo interceptionInfo = new KeyInterceptionInfo(0, 0, null, 0);
doReturn(interceptionInfo)
.when(mWindowManagerInternal).getKeyInterceptionInfoFromToken(any());
@@ -442,9 +439,6 @@ class TestPhoneWindowManager {
eq(TEST_BROWSER_ROLE_PACKAGE_NAME));
doReturn(mSmsIntent).when(mPackageManager).getLaunchIntentForPackage(
eq(TEST_SMS_ROLE_PACKAGE_NAME));
- mProcessPowerKeyDownCount = 0;
- captureProcessPowerKeyDownCount();
-
Mockito.reset(mContext);
}
@@ -715,12 +709,6 @@ class TestPhoneWindowManager {
.when(mButtonOverridePermissionChecker).canAppOverrideSystemKey(any(), anyInt());
}
- void overrideCanWindowOverridePowerKey(boolean granted) {
- doReturn(granted)
- .when(mButtonOverridePermissionChecker).canWindowOverridePowerKey(any(), anyInt(),
- anyInt());
- }
-
void overrideKeyEventPolicyFlags(int flags) {
mKeyEventPolicyFlags = flags;
}
@@ -800,10 +788,6 @@ class TestPhoneWindowManager {
verify(mGestureLauncherService, atMost(4))
.interceptPowerKeyDown(any(), anyBoolean(), valueCaptor.capture());
- if (overridePowerKeyBehaviorInFocusedWindow()) {
- assertTrue(mProcessPowerKeyDownCount >= 2 && mProcessPowerKeyDownCount <= 4);
- }
-
List<Boolean> capturedValues = valueCaptor.getAllValues().stream()
.map(mutableBoolean -> mutableBoolean.value)
.toList();
@@ -832,10 +816,6 @@ class TestPhoneWindowManager {
verify(mGestureLauncherService, atLeast(1))
.interceptPowerKeyDown(any(), anyBoolean(), valueCaptor.capture());
- if (overridePowerKeyBehaviorInFocusedWindow()) {
- assertEquals(mProcessPowerKeyDownCount, 5);
- }
-
List<Boolean> capturedValues = valueCaptor.getAllValues().stream()
.map(mutableBoolean -> mutableBoolean.value)
.toList();
@@ -1063,12 +1043,4 @@ class TestPhoneWindowManager {
verify(mContext, never()).startActivityAsUser(any(), any(), any());
verify(mContext, never()).startActivityAsUser(any(), any());
}
-
- private void captureProcessPowerKeyDownCount() {
- doAnswer((Answer<Void>) invocation -> {
- invocation.callRealMethod();
- mProcessPowerKeyDownCount++;
- return null;
- }).when(mGestureLauncherService).processPowerKeyDown(any());
- }
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
index ed00a9e8e74b..b7c325878a78 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
@@ -2983,9 +2983,45 @@ public class DisplayContentTests extends WindowTestsBase {
assertTrue(dc.mWmService.mDisplayWindowSettings.shouldShowSystemDecorsLocked(dc));
}
+ @EnableFlags(FLAG_ENABLE_DISPLAY_CONTENT_MODE_MANAGEMENT)
+ @Test
+ public void testRemove_displayWithSystemDecorations_emitRemoveSystemDecorations() {
+ final DisplayInfo displayInfo = new DisplayInfo(mDisplayInfo);
+ displayInfo.displayId = DEFAULT_DISPLAY + 1;
+ displayInfo.flags = (FLAG_ALLOWS_CONTENT_MODE_SWITCH | FLAG_TRUSTED);
+ final DisplayContent dc = createNewDisplay(displayInfo);
+ spyOn(dc.mDisplay);
+ doReturn(true).when(dc.mDisplay).canHostTasks();
+ dc.onDisplayInfoChangeApplied();
+ final DisplayPolicy displayPolicy = dc.getDisplayPolicy();
+ spyOn(displayPolicy);
+
+ dc.remove();
+
+ verify(displayPolicy).notifyDisplayRemoveSystemDecorations();
+ }
+
+ @EnableFlags(FLAG_ENABLE_DISPLAY_CONTENT_MODE_MANAGEMENT)
+ @Test
+ public void testRemove_displayWithoutSystemDecorations_dontEmitRemoveSystemDecorations() {
+ final DisplayInfo displayInfo = new DisplayInfo(mDisplayInfo);
+ displayInfo.displayId = DEFAULT_DISPLAY + 1;
+ displayInfo.flags = (FLAG_ALLOWS_CONTENT_MODE_SWITCH | FLAG_TRUSTED);
+ final DisplayContent dc = createNewDisplay(displayInfo);
+ spyOn(dc.mDisplay);
+ doReturn(false).when(dc.mDisplay).canHostTasks();
+ dc.onDisplayInfoChangeApplied();
+ final DisplayPolicy displayPolicy = dc.getDisplayPolicy();
+ spyOn(displayPolicy);
+
+ dc.remove();
+
+ verify(displayPolicy, never()).notifyDisplayRemoveSystemDecorations();
+ }
+
@EnableFlags(FLAG_ENABLE_PERSISTING_DISPLAY_SIZE_FOR_CONNECTED_DISPLAYS)
@Test
- public void testForcedDensityRatioSetForExternalDisplays_persistDensityScaleFlagEnabled() {
+ public void testForcedDensityRatioSet_persistDensityScaleFlagEnabled() {
final DisplayInfo displayInfo = new DisplayInfo(mDisplayInfo);
displayInfo.displayId = DEFAULT_DISPLAY + 1;
displayInfo.type = Display.TYPE_EXTERNAL;
@@ -3003,19 +3039,20 @@ public class DisplayContentTests extends WindowTestsBase {
baseYDpi);
final int forcedDensity = 640;
-
- // Verify that forcing the density is honored and the size doesn't change.
- displayContent.setForcedDensity(forcedDensity, 0 /* userId */);
- verifySizes(displayContent, baseWidth, baseHeight, forcedDensity);
+ displayContent.setForcedDensityRatio(
+ (float) forcedDensity / baseDensity, 0 /* userId */);
// Verify that density ratio is set correctly.
assertEquals((float) forcedDensity / baseDensity,
- displayContent.mExternalDisplayForcedDensityRatio, 0.01);
+ displayContent.mForcedDisplayDensityRatio, 0.01);
+ // Verify that density is set correctly.
+ assertEquals(forcedDensity,
+ displayContent.mBaseDisplayDensity);
}
@EnableFlags(FLAG_ENABLE_PERSISTING_DISPLAY_SIZE_FOR_CONNECTED_DISPLAYS)
@Test
- public void testForcedDensityUpdateForExternalDisplays_persistDensityScaleFlagEnabled() {
+ public void testForcedDensityUpdateWithRatio_persistDensityScaleFlagEnabled() {
final DisplayInfo displayInfo = new DisplayInfo(mDisplayInfo);
displayInfo.displayId = DEFAULT_DISPLAY + 1;
displayInfo.type = Display.TYPE_EXTERNAL;
@@ -3033,14 +3070,12 @@ public class DisplayContentTests extends WindowTestsBase {
baseYDpi);
final int forcedDensity = 640;
-
- // Verify that forcing the density is honored and the size doesn't change.
- displayContent.setForcedDensity(forcedDensity, 0 /* userId */);
- verifySizes(displayContent, baseWidth, baseHeight, forcedDensity);
+ displayContent.setForcedDensityRatio(
+ (float) forcedDensity / baseDensity, 0 /* userId */);
// Verify that density ratio is set correctly.
- assertEquals((float) 2.0f,
- displayContent.mExternalDisplayForcedDensityRatio, 0.001);
+ assertEquals(2.0f,
+ displayContent.mForcedDisplayDensityRatio, 0.001);
displayContent.mInitialDisplayDensity = 160;
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayWindowSettingsTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayWindowSettingsTests.java
index 449ca867b987..9ab20d15acc8 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayWindowSettingsTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayWindowSettingsTests.java
@@ -280,18 +280,24 @@ public class DisplayWindowSettingsTests extends WindowTestsBase {
@EnableFlags(Flags.FLAG_ENABLE_PERSISTING_DISPLAY_SIZE_FOR_CONNECTED_DISPLAYS)
@Test
public void testSetForcedDensityRatio() {
- mDisplayWindowSettings.setForcedDensity(mSecondaryDisplay.getDisplayInfo(),
- 300 /* density */, 0 /* userId */);
+ DisplayInfo info = new DisplayInfo(mDisplayInfo);
+ info.logicalDensityDpi = 300;
+ info.type = Display.TYPE_EXTERNAL;
+ mSecondaryDisplay = createNewDisplay(info);
mDisplayWindowSettings.setForcedDensityRatio(mSecondaryDisplay.getDisplayInfo(),
2.0f /* ratio */);
mDisplayWindowSettings.applySettingsToDisplayLocked(mSecondaryDisplay);
- assertEquals(mSecondaryDisplay.mInitialDisplayDensity * 2.0f,
- mSecondaryDisplay.mBaseDisplayDensity, 0.01);
+ assertEquals((int) (mSecondaryDisplay.mInitialDisplayDensity * 2.0f),
+ mSecondaryDisplay.mBaseDisplayDensity);
+
+ mWm.clearForcedDisplayDensityForUser(mSecondaryDisplay.getDisplayId(),
+ 0 /* userId */);
- mWm.clearForcedDisplayDensityForUser(mSecondaryDisplay.getDisplayId(), 0 /* userId */);
assertEquals(mSecondaryDisplay.mInitialDisplayDensity,
mSecondaryDisplay.mBaseDisplayDensity);
+ assertEquals(mSecondaryDisplay.mForcedDisplayDensityRatio,
+ 0.0f, 0.001);
}
@Test
diff --git a/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java b/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java
index 2c6884e7a35a..4458b7330a68 100644
--- a/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java
@@ -81,6 +81,7 @@ import android.window.TaskSnapshot;
import androidx.test.filters.MediumTest;
import com.android.server.wm.RecentTasks.Callbacks;
+import com.android.window.flags.Flags;
import org.junit.Before;
import org.junit.Rule;
@@ -931,6 +932,20 @@ public class RecentTasksTest extends WindowTestsBase {
}
@Test
+ public void testVisibleTask_forceExcludedFromRecents() {
+ final Task forceExcludedFromRecentsTask = mTasks.getFirst();
+ forceExcludedFromRecentsTask.setForceExcludedFromRecents(true);
+
+ final boolean visible = mRecentTasks.isVisibleRecentTask(forceExcludedFromRecentsTask);
+
+ if (Flags.excludeTaskFromRecents()) {
+ assertFalse(visible);
+ } else {
+ assertTrue(visible);
+ }
+ }
+
+ @Test
public void testFreezeTaskListOrder_reorderExistingTask() {
// Add some tasks
mRecentTasks.add(mTasks.get(0));
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 b617f0285606..e57f1144e6e9 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskTests.java
@@ -86,6 +86,7 @@ import android.graphics.Color;
import android.graphics.Point;
import android.graphics.Rect;
import android.os.IBinder;
+import android.platform.test.annotations.EnableFlags;
import android.platform.test.annotations.Presubmit;
import android.util.DisplayMetrics;
import android.util.Xml;
@@ -99,6 +100,7 @@ import androidx.test.filters.MediumTest;
import com.android.modules.utils.TypedXmlPullParser;
import com.android.modules.utils.TypedXmlSerializer;
+import com.android.window.flags.Flags;
import org.junit.Assert;
import org.junit.Before;
@@ -2161,6 +2163,36 @@ public class TaskTests extends WindowTestsBase {
}
+ @Test
+ public void testIsForceExcludedFromRecents_defaultFalse() {
+ final Task task = createTask(mDisplayContent);
+ assertFalse(task.isForceExcludedFromRecents());
+ }
+
+ @Test
+ public void testSetForceExcludedFromRecents() {
+ final Task task = createTask(mDisplayContent);
+
+ task.setForceExcludedFromRecents(true);
+
+ if (Flags.excludeTaskFromRecents()) {
+ assertTrue(task.isForceExcludedFromRecents());
+ } else {
+ assertFalse(task.isForceExcludedFromRecents());
+ }
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_EXCLUDE_TASK_FROM_RECENTS)
+ public void testSetForceExcludedFromRecents_resetsTaskForceExcludedFromRecents() {
+ final Task task = createTask(mDisplayContent);
+ task.setForceExcludedFromRecents(true);
+
+ task.setForceExcludedFromRecents(false);
+
+ assertFalse(task.isForceExcludedFromRecents());
+ }
+
private Task getTestTask() {
return new TaskBuilder(mSupervisor).setCreateActivity(true).build();
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java
index 5427dc22e700..7836ca7d1b4d 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java
@@ -29,7 +29,6 @@ import static android.view.Display.FLAG_OWN_FOCUS;
import static android.view.Display.INVALID_DISPLAY;
import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
import static android.view.WindowManager.LayoutParams.FLAG_SECURE;
-import static android.view.WindowManager.LayoutParams.INPUT_FEATURE_RECEIVE_POWER_KEY_DOUBLE_PRESS;
import static android.view.WindowManager.LayoutParams.INPUT_FEATURE_SENSITIVE_FOR_PRIVACY;
import static android.view.WindowManager.LayoutParams.INPUT_FEATURE_SPY;
import static android.view.WindowManager.LayoutParams.INVALID_WINDOW_TYPE;
@@ -49,7 +48,6 @@ import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.never;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
-import static com.android.hardware.input.Flags.FLAG_OVERRIDE_POWER_KEY_BEHAVIOR_IN_FOCUSED_WINDOW;
import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND;
import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND_FLOATING;
import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_BACKGROUND_SOLID_COLOR;
@@ -100,6 +98,8 @@ import android.provider.Settings;
import android.util.ArraySet;
import android.util.MergedConfiguration;
import android.view.ContentRecordingSession;
+import android.view.Display;
+import android.view.DisplayInfo;
import android.view.IWindow;
import android.view.InputChannel;
import android.view.InputDevice;
@@ -1154,53 +1154,6 @@ public class WindowManagerServiceTests extends WindowTestsBase {
}
@Test
- @RequiresFlagsEnabled(FLAG_OVERRIDE_POWER_KEY_BEHAVIOR_IN_FOCUSED_WINDOW)
- public void testUpdateInputChannel_sanitizeWithoutPermission_ThrowsError() {
- final Session session = mock(Session.class);
- final int callingUid = Process.FIRST_APPLICATION_UID;
- final int callingPid = 1234;
- final SurfaceControl surfaceControl = mock(SurfaceControl.class);
- final IBinder window = new Binder();
- final InputTransferToken inputTransferToken = mock(InputTransferToken.class);
-
-
- final InputChannel inputChannel = new InputChannel();
-
- assertThrows(IllegalArgumentException.class, () ->
- mWm.grantInputChannel(session, callingUid, callingPid, DEFAULT_DISPLAY,
- surfaceControl, window, null /* hostInputToken */, FLAG_NOT_FOCUSABLE,
- 0 /* privateFlags */,
- INPUT_FEATURE_RECEIVE_POWER_KEY_DOUBLE_PRESS,
- TYPE_APPLICATION, null /* windowToken */, inputTransferToken,
- "TestInputChannel", inputChannel));
- }
-
-
- @Test
- @RequiresFlagsEnabled(FLAG_OVERRIDE_POWER_KEY_BEHAVIOR_IN_FOCUSED_WINDOW)
- public void testUpdateInputChannel_sanitizeWithPermission_doesNotThrowError() {
- final Session session = mock(Session.class);
- final int callingUid = Process.FIRST_APPLICATION_UID;
- final int callingPid = 1234;
- final SurfaceControl surfaceControl = mock(SurfaceControl.class);
- final IBinder window = new Binder();
- final InputTransferToken inputTransferToken = mock(InputTransferToken.class);
-
- doReturn(PackageManager.PERMISSION_GRANTED).when(mWm.mContext).checkPermission(
- android.Manifest.permission.OVERRIDE_SYSTEM_KEY_BEHAVIOR_IN_FOCUSED_WINDOW,
- callingPid,
- callingUid);
-
- final InputChannel inputChannel = new InputChannel();
-
- mWm.grantInputChannel(session, callingUid, callingPid, DEFAULT_DISPLAY, surfaceControl,
- window, null /* hostInputToken */, FLAG_NOT_FOCUSABLE, 0 /* privateFlags */,
- INPUT_FEATURE_RECEIVE_POWER_KEY_DOUBLE_PRESS,
- TYPE_APPLICATION, null /* windowToken */, inputTransferToken, "TestInputChannel",
- inputChannel);
- }
-
- @Test
public void testUpdateInputChannel_allowSpyWindowForInputMonitorPermission() {
final Session session = mock(Session.class);
final int callingUid = Process.SYSTEM_UID;
@@ -1643,6 +1596,60 @@ public class WindowManagerServiceTests extends WindowTestsBase {
});
}
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_PERSISTING_DISPLAY_SIZE_FOR_CONNECTED_DISPLAYS)
+ public void setForcedDisplayDensityRatio_forExternalDisplay_setsRatio() {
+ final DisplayInfo displayInfo = new DisplayInfo(mDisplayInfo);
+ displayInfo.displayId = DEFAULT_DISPLAY + 1;
+ displayInfo.type = Display.TYPE_EXTERNAL;
+ displayInfo.logicalDensityDpi = 100;
+ mDisplayContent = createNewDisplay(displayInfo);
+ final int currentUserId = ActivityManager.getCurrentUser();
+ final float forcedDensityRatio = 2f;
+
+ mWm.setForcedDisplayDensityRatio(displayInfo.displayId, forcedDensityRatio,
+ currentUserId);
+
+ verify(mDisplayContent).setForcedDensityRatio(forcedDensityRatio,
+ currentUserId);
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_PERSISTING_DISPLAY_SIZE_FOR_CONNECTED_DISPLAYS)
+ public void setForcedDisplayDensityRatio_forInternalDisplay_setsRatio() {
+ final DisplayInfo displayInfo = new DisplayInfo(mDisplayInfo);
+ displayInfo.displayId = DEFAULT_DISPLAY + 1;
+ displayInfo.type = Display.TYPE_INTERNAL;
+ mDisplayContent = createNewDisplay(displayInfo);
+ final int currentUserId = ActivityManager.getCurrentUser();
+ final float forcedDensityRatio = 2f;
+
+ mWm.setForcedDisplayDensityRatio(displayInfo.displayId, forcedDensityRatio,
+ currentUserId);
+
+ verify(mDisplayContent).setForcedDensityRatio(forcedDensityRatio,
+ currentUserId);
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_PERSISTING_DISPLAY_SIZE_FOR_CONNECTED_DISPLAYS)
+ public void clearForcedDisplayDensityRatio_clearsRatioAndDensity() {
+ final DisplayInfo displayInfo = new DisplayInfo(mDisplayInfo);
+ displayInfo.displayId = DEFAULT_DISPLAY + 1;
+ displayInfo.type = Display.TYPE_INTERNAL;
+ mDisplayContent = createNewDisplay(displayInfo);
+ final int currentUserId = ActivityManager.getCurrentUser();
+
+ mWm.clearForcedDisplayDensityForUser(displayInfo.displayId, currentUserId);
+
+ verify(mDisplayContent).setForcedDensityRatio(0.0f,
+ currentUserId);
+
+ assertEquals(mDisplayContent.mBaseDisplayDensity,
+ mDisplayContent.getInitialDisplayDensity());
+ assertEquals(mDisplayContent.mForcedDisplayDensityRatio, 0.0f, 0.001);
+ }
+
/**
* Simulates IPC transfer by writing the setting to a parcel and reading it back.
*
diff --git a/telecomm/java/android/telecom/Connection.java b/telecomm/java/android/telecom/Connection.java
index ebe00782319a..68216b2dbd8a 100644
--- a/telecomm/java/android/telecom/Connection.java
+++ b/telecomm/java/android/telecom/Connection.java
@@ -912,6 +912,16 @@ public abstract class Connection extends Conferenceable {
public static final String EVENT_CALL_HOLD_FAILED = "android.telecom.event.CALL_HOLD_FAILED";
/**
+ * Connection event used to inform Telecom when a resume operation on a call has failed.
+ * <p>
+ * Sent via {@link #sendConnectionEvent(String, Bundle)}. The {@link Bundle} parameter is
+ * expected to be null when this connection event is used.
+ */
+ @FlaggedApi(Flags.FLAG_CALL_SEQUENCING_CALL_RESUME_FAILED)
+ public static final String EVENT_CALL_RESUME_FAILED =
+ "android.telecom.event.CALL_RESUME_FAILED";
+
+ /**
* Connection event used to inform Telecom when a switch operation on a call has failed.
* <p>
* Sent via {@link #sendConnectionEvent(String, Bundle)}. The {@link Bundle} parameter is
diff --git a/tests/AppJankTest/src/android/app/jank/tests/IntegrationTests.java b/tests/AppJankTest/src/android/app/jank/tests/IntegrationTests.java
index 229c7bfb53e9..9c176cfe45e5 100644
--- a/tests/AppJankTest/src/android/app/jank/tests/IntegrationTests.java
+++ b/tests/AppJankTest/src/android/app/jank/tests/IntegrationTests.java
@@ -60,6 +60,7 @@ import java.util.HashMap;
@RunWith(AndroidJUnit4.class)
public class IntegrationTests {
public static final int WAIT_FOR_TIMEOUT_MS = 5000;
+ public static final int WAIT_FOR_PENDING_JANKSTATS_MS = 1000;
@Rule
public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
@@ -116,18 +117,8 @@ public class IntegrationTests {
editText.reportAppJankStats(JankUtils.getAppJankStats());
- // reportAppJankStats performs the work on a background thread, check periodically to see
- // if the work is complete.
- for (int i = 0; i < 10; i++) {
- try {
- Thread.sleep(100);
- if (jankTracker.getPendingJankStats().size() > 0) {
- break;
- }
- } catch (InterruptedException exception) {
- //do nothing and continue
- }
- }
+ // wait until pending results are available.
+ JankUtils.waitForResults(jankTracker, WAIT_FOR_PENDING_JANKSTATS_MS);
pendingStats = jankTracker.getPendingJankStats();
@@ -222,4 +213,36 @@ public class IntegrationTests {
assertTrue(jankTracker.shouldTrack());
}
+
+ /*
+ When JankTracker is first instantiated it gets passed the apps UID the same UID should be
+ passed when reporting AppJankStats. To make sure frames and metrics are all associated with
+ the same app these UIDs need to match. This test confirms that mismatched IDs are not
+ counted.
+ */
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_DETAILED_APP_JANK_METRICS_API)
+ public void reportJankStats_statNotMerged_onMisMatchedAppIds() {
+ Activity jankTrackerActivity = mJankTrackerActivityRule.launchActivity(null);
+ mDevice.wait(Until.findObject(
+ By.text(jankTrackerActivity.getString(R.string.continue_test))),
+ WAIT_FOR_TIMEOUT_MS);
+
+ EditText editText = jankTrackerActivity.findViewById(R.id.edit_text);
+ JankTracker jankTracker = editText.getJankTracker();
+
+ HashMap<String, JankDataProcessor.PendingJankStat> pendingStats =
+ jankTracker.getPendingJankStats();
+ assertEquals(0, pendingStats.size());
+
+ int mismatchedAppUID = 25;
+ editText.reportAppJankStats(JankUtils.getAppJankStats(mismatchedAppUID));
+
+ // wait until pending results should be available.
+ JankUtils.waitForResults(jankTracker, WAIT_FOR_PENDING_JANKSTATS_MS);
+
+ pendingStats = jankTracker.getPendingJankStats();
+
+ assertEquals(0, pendingStats.size());
+ }
}
diff --git a/tests/AppJankTest/src/android/app/jank/tests/JankUtils.java b/tests/AppJankTest/src/android/app/jank/tests/JankUtils.java
index 9640a84eb9ca..302cad11bbb9 100644
--- a/tests/AppJankTest/src/android/app/jank/tests/JankUtils.java
+++ b/tests/AppJankTest/src/android/app/jank/tests/JankUtils.java
@@ -17,17 +17,20 @@
package android.app.jank.tests;
import android.app.jank.AppJankStats;
+import android.app.jank.JankTracker;
import android.app.jank.RelativeFrameTimeHistogram;
+import android.os.Process;
+
public class JankUtils {
- private static final int APP_ID = 25;
+ private static final int APP_ID = Process.myUid();
/**
* Returns a mock AppJankStats object to be used in tests.
*/
- public static AppJankStats getAppJankStats() {
+ public static AppJankStats getAppJankStats(int appUID) {
AppJankStats jankStats = new AppJankStats(
- /*App Uid*/APP_ID,
+ /*App Uid*/appUID,
/*Widget Id*/"test widget id",
/*navigationComponent*/null,
/*Widget Category*/AppJankStats.WIDGET_CATEGORY_SCROLL,
@@ -39,6 +42,10 @@ public class JankUtils {
return jankStats;
}
+ public static AppJankStats getAppJankStats() {
+ return getAppJankStats(APP_ID);
+ }
+
/**
* Returns a mock histogram to be used with an AppJankStats object.
*/
@@ -50,4 +57,26 @@ public class JankUtils {
overrunHistogram.addRelativeFrameTimeMillis(25);
return overrunHistogram;
}
+
+ /**
+ * When JankStats are reported they are processed on a background thread. This method checks
+ * every 100 ms up to the maxWaitTime to see if the pending stat count is greater than zero.
+ * If the pending stat count is greater than zero it will return or keep trying until
+ * maxWaitTime has elapsed.
+ */
+ public static void waitForResults(JankTracker jankTracker, int maxWaitTimeMs) {
+ int currentWaitTimeMs = 0;
+ int threadSleepTimeMs = 100;
+ while (currentWaitTimeMs < maxWaitTimeMs) {
+ try {
+ Thread.sleep(threadSleepTimeMs);
+ if (!jankTracker.getPendingJankStats().isEmpty()) {
+ return;
+ }
+ currentWaitTimeMs += threadSleepTimeMs;
+ } catch (InterruptedException exception) {
+ // do nothing and continue.
+ }
+ }
+ }
}
diff --git a/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt b/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt
index 4737d19acde1..1858b1da916b 100644
--- a/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt
+++ b/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt
@@ -714,18 +714,16 @@ class InputManagerServiceTests {
)
}
- val info =
- KeyInterceptionInfo(
- /* type = */ 0,
- if (hasPrivateFlag) {
- WindowManager.LayoutParams.PRIVATE_FLAG_ALLOW_ACTION_KEY_EVENTS
- } else {
- 0
- },
- "title",
- /* uid = */ 0,
- /* inputFeatureFlags = */ 0,
- )
+ val info = KeyInterceptionInfo(
+ /* type = */0,
+ if (hasPrivateFlag) {
+ WindowManager.LayoutParams.PRIVATE_FLAG_ALLOW_ACTION_KEY_EVENTS
+ } else {
+ 0
+ },
+ "title",
+ /* uid = */0
+ )
whenever(windowManagerInternal.getKeyInterceptionInfoFromToken(any())).thenReturn(info)
}
}
diff --git a/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt b/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt
index c64578e4638f..cdc4256a5fd4 100644
--- a/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt
+++ b/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt
@@ -980,7 +980,7 @@ class KeyGestureControllerTests {
TestData(
"META -> Open Apps Drawer",
intArrayOf(KeyEvent.KEYCODE_META_LEFT),
- KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_ALL_APPS,
+ KeyGestureEvent.KEY_GESTURE_TYPE_ALL_APPS,
intArrayOf(KeyEvent.KEYCODE_META_LEFT),
0,
intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE),
diff --git a/tests/Input/src/com/android/server/input/KeyboardBacklightControllerTests.kt b/tests/Input/src/com/android/server/input/KeyboardBacklightControllerTests.kt
index cf09b54753b0..a0cf88809af4 100644
--- a/tests/Input/src/com/android/server/input/KeyboardBacklightControllerTests.kt
+++ b/tests/Input/src/com/android/server/input/KeyboardBacklightControllerTests.kt
@@ -26,7 +26,6 @@ import android.hardware.input.IKeyboardBacklightState
import android.hardware.input.InputManager
import android.hardware.lights.Light
import android.os.SystemProperties
-import android.os.UEventObserver
import android.os.test.TestLooper
import android.platform.test.annotations.Presubmit
import android.util.TypedValue
@@ -98,14 +97,12 @@ class KeyboardBacklightControllerTests {
@get:Rule val inputManagerRule = MockInputManagerRule()
@Mock private lateinit var native: NativeInputManagerService
- @Mock private lateinit var uEventManager: UEventManager
@Mock private lateinit var resources: Resources
private lateinit var keyboardBacklightController: KeyboardBacklightController
private lateinit var context: Context
private lateinit var testLooper: TestLooper
private var lightColorMap: HashMap<Int, Int> = HashMap()
private var lastBacklightState: KeyboardBacklightState? = null
- private var sysfsNodeChanges = 0
private var lastAnimationValues = IntArray(2)
@Before
@@ -126,7 +123,6 @@ class KeyboardBacklightControllerTests {
lightColorMap.getOrDefault(args[1] as Int, 0)
}
lightColorMap.clear()
- `when`(native.sysfsNodeChanged(any())).then { sysfsNodeChanges++ }
}
private fun setupConfig() {
@@ -158,13 +154,7 @@ class KeyboardBacklightControllerTests {
private fun setupController() {
keyboardBacklightController =
- KeyboardBacklightController(
- context,
- native,
- testLooper.looper,
- FakeAnimatorFactory(),
- uEventManager,
- )
+ KeyboardBacklightController(context, native, testLooper.looper, FakeAnimatorFactory())
}
@Test
@@ -318,77 +308,6 @@ class KeyboardBacklightControllerTests {
}
@Test
- fun testKeyboardBacklightSysfsNodeAdded_AfterInputDeviceAdded() {
- setupController()
- var counter = sysfsNodeChanges
- keyboardBacklightController.onKeyboardBacklightUEvent(
- UEventObserver.UEvent(
- "ACTION=add\u0000SUBSYSTEM=leds\u0000DEVPATH=/xyz/leds/abc::no_backlight\u0000"
- )
- )
- assertEquals(
- "Should not reload sysfs node if UEvent path doesn't contain kbd_backlight",
- counter,
- sysfsNodeChanges,
- )
-
- keyboardBacklightController.onKeyboardBacklightUEvent(
- UEventObserver.UEvent(
- "ACTION=add\u0000SUBSYSTEM=power\u0000DEVPATH=/xyz/leds/abc::kbd_backlight\u0000"
- )
- )
- assertEquals(
- "Should not reload sysfs node if UEvent doesn't belong to subsystem LED",
- counter,
- sysfsNodeChanges,
- )
-
- keyboardBacklightController.onKeyboardBacklightUEvent(
- UEventObserver.UEvent(
- "ACTION=remove\u0000SUBSYSTEM=leds\u0000DEVPATH=/xyz/leds/abc::kbd_backlight\u0000"
- )
- )
- assertEquals(
- "Should not reload sysfs node if UEvent doesn't have ACTION(add)",
- counter,
- sysfsNodeChanges,
- )
-
- keyboardBacklightController.onKeyboardBacklightUEvent(
- UEventObserver.UEvent(
- "ACTION=add\u0000SUBSYSTEM=leds\u0000DEVPATH=/xyz/pqr/abc::kbd_backlight\u0000"
- )
- )
- assertEquals(
- "Should not reload sysfs node if UEvent path doesn't belong to leds/ directory",
- counter,
- sysfsNodeChanges,
- )
-
- keyboardBacklightController.onKeyboardBacklightUEvent(
- UEventObserver.UEvent(
- "ACTION=add\u0000SUBSYSTEM=leds\u0000DEVPATH=/xyz/leds/abc::kbd_backlight\u0000"
- )
- )
- assertEquals(
- "Should reload sysfs node if a valid Keyboard backlight LED UEvent occurs",
- ++counter,
- sysfsNodeChanges,
- )
-
- keyboardBacklightController.onKeyboardBacklightUEvent(
- UEventObserver.UEvent(
- "ACTION=add\u0000SUBSYSTEM=leds\u0000DEVPATH=/xyz/leds/abc:kbd_backlight:red\u0000"
- )
- )
- assertEquals(
- "Should reload sysfs node if a valid Keyboard backlight LED UEvent occurs",
- ++counter,
- sysfsNodeChanges,
- )
- }
-
- @Test
@UiThreadTest
fun testKeyboardBacklightAnimation_onChangeLevels() {
ExtendedMockito.doReturn("true").`when` {
diff --git a/tests/Tracing/src/com/android/internal/protolog/ProcessedPerfettoProtoLogImplTest.java b/tests/Tracing/src/com/android/internal/protolog/ProcessedPerfettoProtoLogImplTest.java
index ed256e72b415..d30ec1699d03 100644
--- a/tests/Tracing/src/com/android/internal/protolog/ProcessedPerfettoProtoLogImplTest.java
+++ b/tests/Tracing/src/com/android/internal/protolog/ProcessedPerfettoProtoLogImplTest.java
@@ -552,7 +552,7 @@ public class ProcessedPerfettoProtoLogImplTest {
}
final ResultReader reader = new ResultReader(mWriter.write(), mTraceConfig);
- assertThrows(IllegalStateException.class, reader::readProtoLogTrace);
+ assertThrows(java.net.SocketException.class, reader::readProtoLogTrace);
}
@Test